我想学习如何编写利用CPU缓存的更好代码。使用连续记忆似乎是理想的情况。话虽如此,我很好奇是否可以使用非连续内存进行类似的改进,但是需要遵循一系列指针,例如:
struct Position {
int32_t x,y,z;
}
...
std::vector<Position*> posPointers;
...
updatePosition () {
for (uint32_t i = 0; i < posPointers.size(); i++) {
Position& nextPos = *posPointers[i];
nextPos.x++;
nextPos.y++;
nextPos.z++;
}
}
这只是一些粗略的模拟代码,为了正确学习这一点,我们只是说所有的位置结构都是在整个堆中随机创建的。
像英特尔i7这样的现代智能处理器是否可以提前看到它很快就会需要X_ptr
的数据?以下代码行会有帮助吗?
... // for loop
Position& nextPos1 = *posPointers[i];
Position& nextPos2 = *posPointers[i+1];
Position& nextPos3 = *posPointers[i+2];
Position& nextPos4 = *posPointers[i+3];
... // Work on data here
我读过一些演示幻灯片,似乎表明这样的代码会导致处理器预取一些数据。真的吗?我知道有一些非标准的,特定于平台的方法来调用像__builtin_prefetch
这样的预取,但是把它扔到这个地方看起来就像是一个丑陋的过早优化。我正在寻找一种可以下意识地编写缓存效率代码的方法。
答案 0 :(得分:6)
我知道你没有问过(并且可能不需要正确处理缓存的布道,但我认为无论如何我都会贡献两分钱。请注意,这一切只适用于 hot 记住,过早的优化是所有邪恶的根源。
正如评论中指出的那样,最好的方法是拥有实际数据的容器。一般来说,平面数据结构比“指针意大利面”更受欢迎,即使您必须复制某些数据和/或为调整数据结构的大小/移动/碎片整理付出代价。
如您所知,平面数据结构(例如数据数组)只有在大部分时间线性和顺序访问它们时才能获得回报。
但这种策略可能并不总是可用。代替实际的线性数据,您可以使用其他策略,例如使用池分配器,并迭代池本身,而不是使用包含指针的向量。这当然有其自身的缺点,可能会有点复杂。
我确定你已经知道了这一点,但是再次提到,从缓存中获取最大收益的最有效技术之一就是拥有更小的数据!在上面的代码中,如果您可以使用int16_t
代替int32_t
,那么您肯定应该这样做。您应该将许多bool
和标志和枚举打包到位字段中,使用索引而不是指针(特别是在64位系统上),在数据结构中使用固定大小的散列值而不是字符串等。
现在,关于你的主要问题是处理器是否可以跟随随机指针并在需要之前将数据带入缓存。在非常有限的程度上,这确实发生了。您可能知道,现代CPU采用了许多技巧来提高速度(即提高其指令退出率。)如存储缓冲区,无序执行,超标量流水线,各种功能单元,分支等技巧大多数时候,这些技巧都只是帮助CPU 继续执行指令,即使当前指令已经停止或需要很长时间才能完成。对于内存加载(这是最慢的事情,如果数据不在缓存中),这意味着CPU应该尽快到达指令,计算地址,并从内存控制器请求数据。然而,内存控制器只能有非常有限数量的未完成请求(通常是两天,但我不确定。)这意味着即使CPU做了非常复杂的事情来展望其他内存位置(例如你的posPointers
向量的元素,并推断这些是你的代码将需要的新数据的地址,它不可能走得很远,因为内存控制器只有很多待处理的请求。
无论如何,AFAIK,我认为CPU实际上并没有这样做。请注意,这是一个很难的情况,因为随机分布的内存位置的地址本身就在内存中(而不是在寄存器中,也可以从寄存器的内容中计算出来。)如果CPU做到了,它就不会由于存储器接口的限制,无论如何都会产生很大的影响。
你提到的预取技术似乎对我有用,而且我已经看过它的使用,但是如果你的CPU在等待未来的数据到达时有事可做,它只会产生明显的效果。增加三个整数比从内存加载12个字节(实际上加载一个高速缓存行)所花费的时间少得多,因此对于执行时间而言并不意味着什么。但是如果你有一些有价值的东西和更重量级的东西叠加在内存预取之上(例如计算一个不需要内存数据的复杂函数!)那么你可以获得非常好的加速。你看,经历上述循环的时间基本上是所有缓存未命中时间的总和;并且您将获得免费的坐标增量和循环簿记。所以,如果免费的东西更有价值,你会赢得更多!
答案 1 :(得分:4)
现代处理器具有硬件预取机制:Intel Hardware prefetcher。他们推断出对内存的跨步访问模式以及在不久的将来可能访问的预取内存位置。
然而,在完全随机指针追逐这种技术的情况下无法帮助。处理器不知道正在执行的程序正在执行指针追踪,因此它无法相应地预取。在这种情况下,硬件机制对性能是有害的,因为它们会预取不太可能使用的值。您可以做的最好的事情就是尝试在内存中组织数据结构,以便更有可能访问连续的内存部分。