数组结构和结构数组 – 性能差异

我有一个这样的课:

//Array of Structures class Unit { public: float v; float u; //And similarly many other variables of float type, upto 10-12 of them. void update() { v+=u; v=v*i*t; //And many other equations } }; 

我创建了一个Unit类型的对象数组。 并调用它们的更新。

 int NUM_UNITS = 10000; void ProcessUpdate() { Unit *units = new Unit[NUM_UNITS]; for(int i = 0; i < NUM_UNITS; i++) { units[i].update(); } } 

为了加快速度,并可能对循环进行自动调整,我将AoS转换为数组结构。

 //Structure of Arrays: class Unit { public: Unit(int NUM_UNITS) { v = new float[NUM_UNITS]; } float *v; float *u; //Mnay other variables void update() { for(int i = 0; i < NUM_UNITS; i++) { v[i]+=u[i]; //Many other equations } } }; 

当循环无法自动向量化时,我对数组结构的性能非常糟糕。 对于50个单位,SoA的更新速度略快于AoS.But然后从100个单位开始,SoA比AoS慢。 在300个单位,SoA差不多两倍。 在100K单位,SoA比AoS慢4倍。 虽然缓存可能是SoA的一个问题,但我并不认为性能差异如此之高。 对于两种方法,cachegrind上的分析显示了相似数量的未命中。 Unit对象的大小为48个字节。 L1缓存为256K,L2为1MB,L3为8MB。 我在这里缺少什么? 这真的是缓存问题吗?

编辑:我正在使用gcc 4.5.2。 编译器选项是-o3 -msse4 -ftree-vectorize。

我在SoA做了另一个实验。 我没有动态分配数组,而是在编译时分配了“v”和“u”。 当存在100K单元时,这提供的性能比具有动态分配的arrays的SoA快10倍。 这里发生了什么事? 为什么静态和动态分配的内存之间存在这样的性能差异?

在这种情况下,数组的结构不是缓存友好的。

您同时使用uv ,但是如果它们有2个不同的数组,它们将不会同时加载到一个缓存行中,并且缓存未命中将导致巨大的性能损失。

_mm_prefetch可用于使AoS表示更快。

预取对于花费大部分执行时间等待数据显示的代码至关重要。 现代前端总线具有足够的带宽,预备应该是安全的,只要您的程序不会超出其当前的负载组。

由于各种原因,结构和类可能会在C ++中产生许多性能问题,并且可能需要进行更多调整才能获得可接受的性能级别。 当代码很大时,使用面向对象的编程。 当数据很大(性能很重要)时,请不要。

 float v[N]; float u[N]; //And similarly many other variables of float type, up to 10-12 of them. //Either using an inlined function or just adding this text in main() v[j] += u[j]; v[j] = v[j] * i[j] * t[j]; 

您应该注意的两件事可以产生巨大的差异,具体取决于您的CPU:

  1. 对准
  2. 缓存行别名

由于您使用的是SSE4,因此使用专门的内存分配函数可以返回一个以16字节边界而不是new边界对齐的地址,这可能会提升,因为您或编译器将能够使用对齐的加载和存储。 我没有注意到较新的CPU存在很大差异,但在较旧的CPU上使用未对齐的加载和存储可能会稍微慢一些。

至于缓存行别名,英特尔明确在其参考手册中提到它(搜索“英特尔®64和IA-32架构优化参考手册”)。 英特尔说,这是你应该注意的事情,特别是在使用SoA时。 因此,您可以尝试的一件事是填充数组,使其地址的低6位不同。 我们的想法是避免让他们争夺同一个缓存行。

当然,如果你没有实现矢量化,那么进行SoA转换的动力并不大。

除了事实上接受__RESTRICT之外,gcc 4.9还采用了#pragma GCC ivdep来打破假定的别名依赖关系。

至于使用显式预取,如果它有用,当然你可能需要更多的SoA。 主要观点可能是通过提取页面来加速DTLB未命中分辨率,因此您的算法可能会变得更加缓存。

我不认为可以在没有更多细节的情况下对你所谓的“编译时”分配进行智能评论,包括有关你的操作系统的细节。 毫无疑问,高水平分配和重新使用分配的传统很重要。