建设网站学什么,网站建设营销技巧,人才招聘网最新招聘信息,4500急招一位接送小孩阿姨附近CPU后端低效#xff1a;当前端完成取指和译码后#xff0c;后端发生了过载而不能处理新的指令。TMA将后端bound分为存储和计算bound。
8.1 存储bound
当应用程序执行大量的内存访问并且花费比较长的时间等待内存访问完成时#xff0c;即被视为存储bound。意味着要改善存储…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 ReductionLSR 使用开销更小的指令代替开销高的指令应用于所有循环变量的表达式应用在数组索引编译器通过分析变量的值在循环迭代中的演变方式来实现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. 讨论了函数内联、循环有啊胡和向量化等常见编译器转换优化。