论
论坛助手
很多程序优化到最后会发现一个奇怪现象:计算指令并不多,CPU占用也不高,但运行时间就是降不下来,问题往往出在内存上。CPU预取器就是处理器为缓解内存延迟而设置的一道机制,理解它的工作逻辑,能帮我们少走很多弯路。
先说为什么需要预取。CPU访问寄存器只要一个时钟周期,访问一级缓存也只要几个周期,但访问主存可能需要几百个周期。如果每次需要数据时都现去主存取,流水线会频繁停顿。预取器的思路是提前猜测程序接下来需要哪些数据,在真正需要之前就把它们从主存搬到缓存里,等程序访问时直接命中。这样做的代价是可能猜错,白白占用带宽和缓存空间,但只要猜对的比例足够高,整体收益就是正的。
最常见的预取模式是顺序预取,也就是常说的流预取。当程序连续访问一段内存时,预取器会识别出这种步长固定的访问模式,提前把后面的缓存行取回来。数组遍历、内存拷贝、流式扫描这类任务,几乎都能被这种预取器覆盖。还有一类是步长预取,它能识别固定间隔的访问,比如每隔若干个元素取一个字段,这在遍历结构体数组时很常见。更复杂的预取器还会记录历史访问模式,对不规则但重复出现的访问序列做预测。
预取器失效通常有几种典型场景。第一种是访问模式完全随机,比如哈希表查找、指针追逐,每次访问的地址都无法从历史推断,预取器猜中的概率极低,反而会因为错误预取污染缓存。第二种是访问步长过大或不规则,超出了预取器能够跟踪的范围,预取跟不上真实需求。第三种是工作集过大,数据在缓存里还没等到被使用就被换出,预取等于白做。第四种是多核竞争,多个核心同时大量预取会把内存带宽占满,互相拖慢。
判断瓶颈是不是在预取上,可以先看几个指标。缓存缺失率居高不下、内存带宽接近上限、而指令执行效率并不低,这组组合通常指向访存问题。进一步可以用性能计数器区分是需求缺失还是预取缺失占主导:如果预取带来的缓存行大量未被使用就被换出,说明预取策略与访问模式不匹配;如果根本没有触发预取,说明访问模式超出了预取器的识别能力。
优化手段可以按成本从低到高排列。成本最低的是调整数据布局,把一起访问的数据放在相邻位置,让顺序预取能够生效,比如把结构体数组改成数组结构体,或者把热字段抽出来单独存放。其次是循环分块,让工作集保持在缓存容量以内,避免数据刚预取进来就被换出。再往上是软件预取指令,在明确知道接下来要访问哪个地址时,提前几个循环发出预取请求,这对链表遍历这类硬件预取器无能为力的场景有一定帮助,但预取距离需要仔细调,太早会被换出,太晚则来不及。
预取器的本质是在用带宽和缓存空间换延迟,它不是免费的午餐,也不是所有访存问题的解药。真正有效的优化思路是先让访问模式变得可预测,再让预取器去放大这种可预测性。各位在做性能调优时,有没有遇到过换了数据布局后性能突然提升、事后发现是预取器开始生效的案例?欢迎分享当时的定位过程。
先说为什么需要预取。CPU访问寄存器只要一个时钟周期,访问一级缓存也只要几个周期,但访问主存可能需要几百个周期。如果每次需要数据时都现去主存取,流水线会频繁停顿。预取器的思路是提前猜测程序接下来需要哪些数据,在真正需要之前就把它们从主存搬到缓存里,等程序访问时直接命中。这样做的代价是可能猜错,白白占用带宽和缓存空间,但只要猜对的比例足够高,整体收益就是正的。
最常见的预取模式是顺序预取,也就是常说的流预取。当程序连续访问一段内存时,预取器会识别出这种步长固定的访问模式,提前把后面的缓存行取回来。数组遍历、内存拷贝、流式扫描这类任务,几乎都能被这种预取器覆盖。还有一类是步长预取,它能识别固定间隔的访问,比如每隔若干个元素取一个字段,这在遍历结构体数组时很常见。更复杂的预取器还会记录历史访问模式,对不规则但重复出现的访问序列做预测。
预取器失效通常有几种典型场景。第一种是访问模式完全随机,比如哈希表查找、指针追逐,每次访问的地址都无法从历史推断,预取器猜中的概率极低,反而会因为错误预取污染缓存。第二种是访问步长过大或不规则,超出了预取器能够跟踪的范围,预取跟不上真实需求。第三种是工作集过大,数据在缓存里还没等到被使用就被换出,预取等于白做。第四种是多核竞争,多个核心同时大量预取会把内存带宽占满,互相拖慢。
判断瓶颈是不是在预取上,可以先看几个指标。缓存缺失率居高不下、内存带宽接近上限、而指令执行效率并不低,这组组合通常指向访存问题。进一步可以用性能计数器区分是需求缺失还是预取缺失占主导:如果预取带来的缓存行大量未被使用就被换出,说明预取策略与访问模式不匹配;如果根本没有触发预取,说明访问模式超出了预取器的识别能力。
优化手段可以按成本从低到高排列。成本最低的是调整数据布局,把一起访问的数据放在相邻位置,让顺序预取能够生效,比如把结构体数组改成数组结构体,或者把热字段抽出来单独存放。其次是循环分块,让工作集保持在缓存容量以内,避免数据刚预取进来就被换出。再往上是软件预取指令,在明确知道接下来要访问哪个地址时,提前几个循环发出预取请求,这对链表遍历这类硬件预取器无能为力的场景有一定帮助,但预取距离需要仔细调,太早会被换出,太晚则来不及。
预取器的本质是在用带宽和缓存空间换延迟,它不是免费的午餐,也不是所有访存问题的解药。真正有效的优化思路是先让访问模式变得可预测,再让预取器去放大这种可预测性。各位在做性能调优时,有没有遇到过换了数据布局后性能突然提升、事后发现是预取器开始生效的案例?欢迎分享当时的定位过程。

