第8章 CPU后端优化

CPU后端低效:当前端完成取指和译码后,后端发生了过载而不能处理新的指令。TMA将后端bound分为存储和计算bound。

8.1 存储bound

当应用程序执行大量的内存访问并且花费比较长的时间等待内存访问完成时,即被视为存储bound。意味着要改善存储访问情况,减少存储访问次数或者升级存储子系统。

在TMA中,存储bound会统计CPU流水线由于按需加载或者存储指令而阻塞的部分槽位。解决此类性能问题第一步是,定位导致高“存储bound”指标的访存操作。识别出具体访存操作之后,开始调优。

8.1.1 缓存友好的数据类型

关于编写缓存友好算法和数据结构是性能关键要素之一,重点在于时间和空间局部性原则,其目标是从缓存中高效地读取所需的数据。

8.1.1.1 按顺序访问数据

利用缓存空间局部性的最佳方法是按顺序访问内存。

二分搜索的标准实现不会利用空间局部性,解决该问题的著名方法是Eytzinger布局存储数组元素。它的思想是维护一个隐式二叉搜索树,使用类似广度优先搜索的布局把二叉搜索数打包到一个数组中。

8.1.1.2 使用适当容器

几乎在任何语言中都有各种各样的现成容器,了解它们的底层存储机制和性能影响很重要。结合代码如何处理数据,才能选择数据存储方式。

8.1.1.3 打包数据

可通过使数据更紧凑来提高内存层次利用率。打包数据的一个经典例子就是使用位存储。大大减少来回传输的内存数量,同时节省缓存空间。不过b位和a与c共享一个机器字,编译器需要执行移位操作。在额外计算的开销比低效内存转移开销低的情景下,打包数据是有意义的。

struct S {unsigned a;unsigned b;unsigned c;
};// optimal
struct S {unsigned a:4;unsigned b:2;unsigned c:2;
};

程序员可以通过重新排布结构体或类中字段来减少内存的使用,同时避免由编译器添加结构体填充。

struct S {bool a;int b;short c;
};// optimal
struct S {int b;short c;bool a;
};

8.1.1.4 对齐与填充

如果变量被存储在能被变量大小整除的内存地址中,那么访问这个地址的效率最高。

alignas(16) int16_t a[N];

对齐可能会导致未使用的字节出现空位,可能会降低内存带宽利用率。

有时需要填充数据结构成员以避免边缘情况,例如缓存争用和伪共享。例如两个线程访问同一结构体的不同字段。缓存一致性问题可能会明显降低程序的运行速度。使用填充方法使得结构体的不同字段处于不同的缓存行。

struct S {int a;int b;
};// optimal
struct S {int a;alignas(64) int b;
};

当通过malloc进行动态分配时,保证返回的内存地址满足平台目标的最小对齐要求。对齐注意事项中最重要的一个是SIMD代码。当使用编译器向量化内建函数时,通常地址要被16、/3或64整除。

8.1.1.5 动态内存分配

有许多malloc替代品,它们更快、更具有可扩展性,能更好地动态解决内存碎片化问题。动态内存分配的一个经典问题是在启动时,线程之间试图同时分配它们的内存区域。

其次,可以使用自定义分配器来加速分配,这类分配器的主要优点是开销低,因为它们不会对每个内存分配执行系统调用。另一个优点是高灵活性。开发者可以基于操作系统提供的内存区域实现自己的分配策略。最简单的策略是维护2个不同的分配器,它们有各自的内存区域:一个用于热数据;一个用于冷数据。将热数据放在一起可以让它们共享高速缓存行,从而提高内存带宽利用率和空间局部性。它还可以提高TLB利用率,因为热数据占用的内存页更少。此外,自定义内存分配器可以使用线程本地存储实现每个线程的分配,从而消除线程之间的同步问题。

8.1.1.6 针对存储器层次调优代码

些应用程序的性能取决于特定层缓存的大小,最著名的例子是使用循环分块来改进矩阵乘法。 

8.1.2 显式内存预取

当arr数组足够大时,硬件预取功能将无法捕获它的访存模式,也无法提前预取所需的数据。在计算j和请求元素arrp[j]之间某个时间窗口,可以使用__builtin_prefetch手动显式添加预取指令,如下所示:
 

for (int i = 0; i < N; ++i) {int j = calNextIndex();// ...doSomeExtensiveComputation();// ...x = arr[j];
}// optimal
for (int i = 0; i < N; ++i) {int j = calNextIndex();__builtin_prefetch(arr + j, 0, 1);// ...doSomeExtensiveComputation();// ...x = arr[j];
}

要使预取生效,请务必提前插入预取指示,确保被加载的值在被用于计算时已经在缓存中。也不要过早插入预取提示,因为它可能会在预取的那段时间内用不到的数据,污染缓存。

显式内存预取不可移植,即使它在一个平台上实现了性能提升,也不能保证在另一个平台也有类似的提升效果。

最后,显式预取指令会增加代码大小并增加CPU前端的压力。

8.1.3 针对DTLB优化

TBL在L1分为ITLB和DTLB,在L2上是统一TLB。L1 ITLB的未命中有非常小的时延,通常会被乱序执行隐藏。统一TLB的未命中会调用页遍历器,可能会明显损失性能。

Linux中默认页面大小为4KB,当页大小增大时,TLB条目减少,TLB未命中次数减少。Intel 64和AMD 64都支持2MB和1GB巨型页。

使用大页的TLB本身更紧凑,通常需要更少的页遍历,所以在TLB未命中的情况下遍历内核页表的代价会减少。

在Linux系统中,在应用程序中使用大页的方法有2种:显式大页和透明大页。

有一个选项可利用libhugetlbfs库在大页头部动态分配内存,该库重写了现有动态链接二进制可执行文件中使用malloc调用。不需要修改代码,甚至不需要重新链接二进制文件,最终用户只需要修改几个环境变量。

为了更细粒度地通过代码控制对大页的访问,开发者有以下选择:
        1. 带MAP_HUGETLB参数使用mmap;
        2. 对挂载hugetlbfs文件系统中的文件使用mmap;
        3. 对SHM_HUGETLB参数使用shmget。

8.1.3.2 透明大页

linux支持的透明大页(Transparent Huge Page,THP)会自动管理大页。THP功能有2种操作模式:针对系统范围和针对进程范围。当在系统范围内启用THP时,内核会尝试将大页分配给任何可能分配的进程,因此不需要手动保留大页。当在进程范围内启用THP时,内核只会将大页分配给单个进程使用了madvise系统调用的内存区域。使用cat /sys/kernel/mm/transparent_hugepage/enabled查看系统是否启用了THP。

8.1.3.3 显式大页和透明大页对比

显式大页预先保留在虚拟内存,透明大页不会。透明大页分配失败,将默认分配4KB页。

透明大页的后台维护需要管理不可避免的内存碎片化和内存交换问题,从而导致内核产生不确定的延迟开销。而显式大页不受内存碎片化的影响,也无法交换到磁盘。

显式大页可用于应用程序的所有段,包括文本段(DTLB和ITLB都会收益),而透明大页仅可用于动态内存分配的内存区域。

透明大页的优势之一,与显式大页相比所需的操作系统配置更少,可以更快地进行实验。

8.2 计算bound

主要有2种:
        1. 硬件计算资源短缺,表示某些执行单元过载(执行端口争用),在负载频繁执行大量繁重的指令时发生。
        2. 软件指令之间的依赖关系,表示程序数据流或指令流中的依赖关系限制了性能。

本节讨论常见的优化手段,比如函数内联、向量化和循环优化,优化目标是减少执行指令的总量。

8.2.1 函数内联

内联不仅能消除调用函数的开销,还支持其他优化手段。当编译器内联某个函数时,编译器的分析范围会扩大到更大的代码块。内联缺点是可能会增加编译结果(二进制文件)大小和编译时间。

大多数编译器基于成本模型来决定函数是否内联。例如LLVM基于计算成本和每个函数调用次数的阈值。一般而言:
        1. 小函数(封装)几乎总是内联;
        2. 具有单个调用点的函数更适合内联;
        3. 大型函数基本不会被内联,因为这会使调用方函数的代码膨胀;
        4. 递归函数不能内联自己;
        5. 通过指针调用的函数可以用内联来代替直接调用,但必须保存在二进制文件中。

除了让编译器根据成本模型来决定是否内联函数,开发者也可以给编译器一些特殊提示(C++ 11 gnu::always_inline)。在程序中寻找潜在的内联对象的一种方法是剖析数据,尤其是分析函数的“传参”和“返回”有多频繁。

8.2.2 循环优化

由于循环代表着一段会被执行很多次的代码,因此大部分的执行时间都耗在循环中。通常循环的性能被内存时延、内存带宽或机器的计算能力的一种或多种限制。屋顶线模型是很好的基于硬件理论最大值评估不同循环的入手点,TMA分析是另一种处理这种瓶颈的方法。

首先,讨论只会在循环内部移动代码的低层优化,目的是使循环内部的计算更高效。然后讨论重构循环的高层优化,通常会影响多个循环,旨在提升内存访问,消除内存带宽和内存时延的问题。已发现的循环转换参考文献Cooper & Torczon 2012。

理解给定循环进行那些转换及编译器的那些优化无法取得相应效果,是性能调优成功的关键。

8.2.2.1 低层优化

转换循环内部的代码,有助于提高算术强度的性能:
        1. 循环不变量外提Loop Invariant Code Motion:循环中永远不会改变的表达式移到循环外;
        2. 循环展开Loop Unrolling:好处是每次迭代都要执行更多的操作,提升指令级并行,同时减少循环开销;不建议开发者手动展开任何循环。编译器非常擅长展开循环,并且通常会以最佳方式来展开。其次,借助乱序执行。处理器具有“内嵌的展开器“。
        3. 循环强度折叠Loop Strength Reduction,LSR, 使用开销更小的指令代替开销高的指令,应用于所有循环变量的表达式,应用在数组索引,编译器通过分析变量的值在循环迭代中的演变方式来实现LSR;
        4. 循环判断外提Loop Unswitching, 如果循环内部有不变的判断条件,则可以将它移到循环外。

8.2.2.2 高层优化

此类优化会改变循环的结构并经常会影响多个嵌套循环,旨在提升内存访问性能,消除内存带宽和时延瓶颈,而编译器很难自动且合法地实现高层优化转换:
        1. 循环交换,交换嵌套循环顺序的过程,目的是对多维数组的元素执行顺序内存访问,消除内存带宽和内存时延瓶颈;
        2. 循环分块, 将多维循环执行范围拆分为若干个循环块,使得每块访问的数据可以与CPU缓存大小适配,优化跨步幅访存算法的内存带宽和内存时延;
        3. 循环合并及拆分,当多个独立循环在相同范围内迭代,并且不互相引用彼此的数据时,它们可以融合在一起。循环合并有助于减少循环开销,还可以改善内存访问的时间局部性。然而循环合并并不总能提高性能,有时将循环拆分为多条路径、预过滤数据、对数据进行排序和重组等可能更好。循环拆分有助于解决在大循环中发生的缓存高度争用的问题,还可以减少寄存器压力,借助编译器对小循环进一步单独优化;

8.2.2.3 发现循环优化的机会

编译优化报告高速我们失败的转换,查看基于应用程序的剖析文件生成的汇编代码的热点部分。

合理的优化策略时先尝试容易得优化方案。然后,开发者明确循环中的瓶颈,并基于硬件理论最大值评估性能。可以先使用屋顶线模型指出要分析的瓶颈点,之后尝试各种变换。

本书作者建议尽量依赖编译器做编译优化,仅手动做些必要的转换作为补充。

8.2.2.4 使用循环优化框架

多面体框架检查循环转换的合法性并自动转换循环。Polly是基于 LLVM的高层循环和数据局部性优化器及优化基础设施,它使用基于整数多面体的抽象数学表示来分析和优化程序的内存访问模式。

LLVM基础设施的标准流水线没有启用Polly,需要用户通过显式的编译器选项(-mllvm -polly)来启用它。

8.2.3 向量化

SIMD指令的使用可以大幅提升常规的未向量化代码的运行速度。在性能分析时,最高优先级的工作之一就是确保热点代码被编译器向量化。

本书作者建议让编译器完成向量化工作,仅在需要时根据编译器或者剖析数据获得的反馈进行手动干预。

如果无法让编译器生成所需的汇编指令,则可以使用编译器内建函数重写代码片段。

本书作者观点:使用编译器内建函数的代码类似于内联后的汇编代码,代码很快变得不可读。通常使用编译注解等来调整编译器自动向量化。

编译器会进行3种向量化:内循环自动向量化、外循环向量化和超字向量化。

8.2.3.1 编译器自动向量化

阻碍编译器自动向量化的因素有很多:
        1. 编程语言的固有语义导致,例如编译器必须假设无符号循环索引可能溢出,和C语言中指针可能指向重叠的内存区域。
        2. 处理器向量操作支持,例如大多数处理无法执行预测(掩码控制)的加载和存储操作,和带符号整数到双精度浮点数的向量范围格式支持。

向量化程序通常包含3个阶段:合法性检查、收益检查和转换:
        1. 合法性检查阶段会收集满足循环向量化合法性的需求清单:
                a.循环向量化程序检查循环的迭代是否连续;
                b.循环中的所有内存和算术操作是否都可扩展为连续操作;
                c.所有路径上控制流是否一致;
                d.确保生成的代码不会访问不该访问的内存,并且操作的顺序被保留;
                e.分析可能得指针范围。
        2. 收益检查:
                a. 比较不同的向量化因子识别出让程序运算速度最快的向量化因子。成本包括添加将数据转移到寄存器的指令,预测寄存器压力并估计循环保护成本。
                b. 检查收益的算法很简单,将代码中所有操作的成本相加,比较每个版本的成本,将成本除以预期的执行次数。
        3. 转换:
                a. 在确定转换合法且有收益后,就会转换代码。还会插入启用向量化的保护代码,比如迭代次数除不断。

8.2.3.2 探索向量化的机会

首先分析程序中热点循环,检查编译器已经做了那些优化。最简单的方法是检查编译器向量化标记。当无法向量化循环时,编译器会给出失败原因。其他办法是检查程序的汇编输出,最好是分析剖析工具的输出。经过查看汇编费时,但是该技能是高回报的,因为从汇编代码中可以发现次优代码、如缺乏向量化、次优向量化因子,执行不必要的计算等。

向量化标记可以很清晰地解释出了什么问题以及为什么编译器不能对代码进行向量化。

gcc 10.2输出优化报告(使用参数-fopt-info启用)。

开发者应该意识到向量化代码的隐藏成本,尤其是AVX512会导致大幅度地降频。

对于小循环次数的循环而言,强制向量化程序使用较小的向量化因子或展开计数以减少循环处理的元素数量。

8.3 本章小结

        1. 缓存友好型数据结构、内存预取和利用大内存来提高DTLB性能的常见优化方法。
        2. 现代编译器很擅长通过执行许多不同的代码转换来消除不必要的计算开销。
        3. 讨论了函数内联、循环有啊胡和向量化等常见编译器转换优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/48559.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue静态html加载外部组件

当我们在开发vue应用时, 使用的是html页面开发, 需要引用外部vue组件, 怎么办呢, 首先我们引用http-vue-loader.js文件, 像下面这样: <script src"/assets/javascript/vue.min.js"></script> <script src"/assets/javascript/http-vue-loader.j…

每日一学——Vlan配置

VLAN&#xff08;Virtual Local Area Network&#xff09;是虚拟局域网的缩写&#xff0c;它是一种将多台主机和网络设备逻辑上划分成不同的局域网的技术。VLAN的实施可以基于端口、MAC地址、协议等多种方式进行。 VLAN的主要功能包括&#xff1a; 分割网络&#xff1a;VLAN可…

Pytorch-day06-复杂模型构建-checkpoint

1、PyTorch 复杂模型构建 1、模型截图2、模型部件实现3、模型组装 2、模型定义 2.1、Sequential 1、当模型的前向计算为简单串联各个层的计算时&#xff0c; Sequential 类可以通过更加简单的方式定义模型。2、可以接收一个子模块的有序字典(OrderedDict) 或者一系列子模块…

Android学习之路(9) Intent

Intent 是一个消息传递对象&#xff0c;您可以用来从其他应用组件请求操作。尽管 Intent 可以通过多种方式促进组件之间的通信&#xff0c;但其基本用例主要包括以下三个&#xff1a; 启动 Activity Activity 表示应用中的一个屏幕。通过将 Intent 传递给 startActivity()&…

简单计算器的实现(含转移表实现)

文章目录 计算器的一般实现使⽤函数指针数组的实现&#xff08;转移表&#xff09; 计算器的一般实现 通过函数的调用&#xff0c;实现加减乘除 # define _CRT_SECURE_NO_WARNINGS#include<stdio.h>int Add(int x, int y) {return x y; }int Sub(int x, int y) {retur…

【OpenCV】OpenCV环境搭建,Mac系统,C++开发环境

OpenCV环境搭建&#xff0c;Mac系统&#xff0c;C开发环境 一、步骤VSCode C环境安装运行CMake安装运行OpenCV 安装CMakeList 一、步骤 VSCode C环境安装CMake 安装OpenCV 安装CmakeList.txt VSCode C环境安装运行 访问官网 CMake安装运行 CMake官网 参考文档 OpenCV 安…

服务注册中心 Eureka

服务注册中心 Eureka Spring Cloud Eureka 是 Netflix 公司开发的注册发现组件&#xff0c;本身是一个基于 REST 的服务。提供注册与发现&#xff0c;同时还提供了负载均衡、故障转移等能力。 Eureka 有 3 个角色 服务中心&#xff08;Eureka Server&#xff09;&#xff1a;…

vue项目配置git提交规范

vue项目配置git提交规范 一、背景介绍二、husky、lint-staged、commitlint/cli1.husky2.lint-staged3.commitlint/cli 三、具体使用1.安装依赖2.运行初始化脚本3.在package.json中配置lint-staged4.根目录新增 commitlint.config.js 4.提交测试1.提示信息格式错误时2.eslint校验…

java:解析XML文件

文章目录 XML组成部分约束 解析解析xml的方式xml常见的解析器Jsoup详解Jsoup 相关对象的使用&#xff1a;快捷查询方式 XML 概念&#xff1a;Extensible Markup Language 可扩展标记语言。 功能&#xff1a;存储数据 配置文件在网络中传输 xml与html的区别&#xff1a; 3. …

elementPlus-tree 自定义展开,收起图标

elementPlus-tree 自定义展开&#xff0c;收起图标 <template><div><el-tree:data"treeData"default-expand-allhighlight-current:expand-on-click-node"false"><template #default"{ node, data }"><span><…

数据结构好题总结

Cut Inequality Down 题解 https://blog.csdn.net/lzh_naive/article/details/103340568 概括&#xff1a;st表倍增类st表 考虑如果没有UL限制的话&#xff0c;相当于是前缀和 我们发现&#xff0c;如果某次到了U/L&#xff08;相当于是一次碰壁&#xff09;那么这个值已知…

线程池的实现v2.0(可伸缩线程池)

目录 前言 可伸缩线程池原理 可伸缩线程池实现 完整程序 前言 本篇可伸缩线程池的实现是在静态线程池上拓展而来&#xff0c;对于静态线程池的实现&#xff0c;请参考&#xff1a; 线程池的实现全过程v1.0版本&#xff08;手把手创建&#xff0c;看完必掌握&#xff01;&…

Java课题笔记~Element UI

Element&#xff1a;是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库&#xff0c;用于快速构建网页。 Element 提供了很多组件&#xff08;组成网页的部件&#xff09;供我们使用。例如 超链接、按钮、图片、表格等等~ 如下图左边的是我们编写页面看到的按钮&#…

5G与4G的RRC协议之异同

什么是无线资源控制&#xff08;RRC&#xff09;&#xff1f; 我们知道&#xff0c;在移动通信中&#xff0c;无线资源管理是非常重要的一个环节&#xff0c;首先介绍一下什么是无线资源控制&#xff08;RRC&#xff09;。 手机和网络通过无线信道相互通信&#xff0c;彼此交…

【踩坑日记】STM32 USART 串口与 FreeRTOS 冲突

文章目录 问题描述问题出现的环境问题解决过程第一步第二步第三步第四步第五步第六步第七步第八步 后续验证一些思考类似的问题后记 问题描述 笔者使用 FreeRTOS 创建了两个任务&#xff0c;使两颗 LED 以不同频率闪烁&#xff0c;但是在加入串口 USART 部分代码后&#xff0c…

【官方中文文档】Mybatis-Spring #简介

简介 什么是 MyBatis-Spring&#xff1f; MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中&#xff0c;创建映射器 mapper 和 SqlSession 并注入到 bean 中&#xff0c;以及将 Mybatis 的异常转换为 Spring 的…

先进封装技术:满足5G、AI和高性能计算的关键

先进封装技术&#xff1a;满足5G、AI和高性能计算的关键 目录 引言&#xff1a;先进封装技术的重要性先进封装技术的定义和种类为何先进封装技术至关重要先进封装技术在高性能计算中的应用先进封装技术在人工智能中的应用先进封装技术在5G技术中的应用先进封装技术的挑战和未…

Mysql 开窗函数(窗口函数)

文章目录 全部数据示例1&#xff08;说明&#xff09;开窗函数可以比groupby多查出条件列外的字段&#xff0c;开窗函数主要是为了跟聚合函数一起使用&#xff0c;达到分组统计效果&#xff0c;并且开窗函数的结果集基本都是跟总行数一样示例2示例3示例4错误示例1错误示例2错误…

Flink源码之Checkpoint执行流程

Checkpoint完整流程如上图所示&#xff1a; JobMaster的CheckpointCoordinator向所有SourceTask发送RPC触发一次CheckPointSourceTask向下游广播CheckpointBarrierSouceTask完成状态快照后向JobMaster发送快照结果非SouceTask在Barrier对齐后完成状态快照向JobMaster发送快照结…

LION AI 大模型落地,首搭星纪元 ES

自新能源汽车蓬勃发展以来&#xff0c;随着潮流不断进步和变革的“四大件”有着明显变化。其中有&#xff1a;平台、智能驾驶、配置、以及车机。方方面面都有着不同程度的革新。 而车机方面&#xff0c;从以前老旧的媒体机、 CD 机发展至如今具有拓展性、开放性、智能化的车机…