c-将指针存储在数组中受益于CPU缓存速度的提高



据我所知,理论上,如果我将数据存储在连续内存中,例如数组,当cpu读取数组的第一个元素时,由于跨步,cpu可以将整个数组加载到其一级缓存中(如果数组可以放入一级缓存),从而减少读取其他数组元素时的缓存未命中。

但是,如果数组存储指针,cpu必须取消对指针的引用并对其执行一些操作,这将导致内存的其他部分加载到L1 cahce。例如,如果我的一级数据缓存是32KB,并且我有一个8K元素的数组:

MyObject* myarray[8192];
//...
for (int i=0; i < 8192; ++i) {
    MyObject& obj = *myarray[i];
    obj.doSomethingComplicated();
}

在这种情况下,myarray的总大小将是4x8=32KB,我说得对吗?在执行doSomethingComplicated()时,至少一部分L1缓存可能会被丢弃?因此,试图限制myarray的大小以使其能够容纳在一级缓存中是毫无意义的吗?

事实上,当为myarray选择相对较小的尺寸时,我仍然可以看到一个小但可重复测量的性能提高,但我真的无法解释为什么。

我只关心x86_64,因为我对其他平台完全不熟悉。

我认为没有一个简单的答案。

  1. 缓存使用称为缓存线的内存区域。对于现代的英特尔CPU,缓存线有64个字节,这就是缓存的操作(加载、存储)。因此,它可能不会将所有数组加载到缓存中,即使它适合。

  2. 数据和指令有一个单独的一级缓存。在您的情况下,myarray将加载到数据缓存中,但函数的代码将加载到指令缓存中。函数的代码在类的所有对象之间共享,因此除非使用多态性,否则代码将被加载到缓存中一次。

  3. 班上当然有人。doSomethingComplicated方法可能对它们进行操作,因此数据必须再次以64B块的形式加载到(数据)缓存中。因此,该方法所操作的数据的布局将很重要。

总而言之,我的建议是:根据上面的信息,做一些实验并衡量你的情况。

如果"doSomethingComplicated"触及任何内存,您几乎可以确定指针数组的部分会被挤出L1。逐出使用伪LRU算法,伪部分是它发生在一组基上,即共享地址最低部分的缓存行。

你的指针数组很好(如果你喜欢指针的话),在这种情况下,它将以接近最佳的方式运行,因为它将按顺序使用,你的问题是它也指向什么,也应该按顺序访问,以获得最佳性能。

这意味着"MyObject"将最佳地位于数组本身中,在这种情况下,原始指针将变得毫无意义!因为指数会一样好,或者更好。

MyObject MyObjectArray[8192];

如果它们不是按顺序访问的,那么您可能会陷入指针追逐的领域,这很糟糕。但至少它们有一些空间局部性,因为它们占据了一个连续的记忆区域。

1级缓存使用2个预取器,使连续内存访问性能非常好,有效地使所有内存访问都以L1延迟通过它们。在最新的英特尔二级缓存中,一级缓存可以服务两个内存请求,但每个周期只能从三级缓存获得一个,三级缓存为所有二级缓存提供服务,因此如果每个周期发出多个请求,它们必须排队。L3只能由其他L3(或L4)高速缓存或存储器在每X个周期的一个请求下提供服务。

因此,只要myarray和MyObjectArray所有其他内存"doSomethingComplicated"或它调用的函数都能适应L2,你的程序就会运行得很快。一旦你需要3个预取器或进入L3/内存,所有的速度都会减慢。

因此,一切都取决于"doSomethingComplicated"的行为,这不足为奇。

最新更新