我有一个这样的课程:
//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单元时,这提供的性能比具有动态分配的阵列的SoA快10倍。这里发生了什么事?为什么静态和动态分配的内存之间存在性能差异?
答案 0 :(得分:10)
在这种情况下,数组的结构不是缓存友好的。
您同时使用u
和v
,但如果有2个不同的数组,它们将不会同时加载到一个缓存行中,并且缓存未命中将会造成巨大的性能损失。
_mm_prefetch
可用于更快地进行AoS
表示。
答案 1 :(得分:1)
预取对于花费大部分执行时间等待数据显示的代码至关重要。现代前端总线具有足够的带宽,预备应该是安全的,只要您的程序没有超出其当前的负载范围。
由于各种原因,结构和类可能会在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];
答案 2 :(得分:1)
您应该注意的两件事可能会产生很大的不同,具体取决于您的CPU:
由于您使用的是SSE4,因此使用专门的内存分配函数返回以16字节边界而不是new
对齐的地址可能会有所帮助,因为您或编译器将能够使用aligned加载和存储。我没有注意到较新的CPU有什么区别,但是在未使用的负载和较旧的CPU上进行存储可能会稍慢一些。
对于高速缓存行别名,英特尔在其参考手册中明确提及 (搜索“英特尔®64和IA-32架构优化参考手册”)。英特尔表示,这是您应该意识到的,特别是在使用SoA时。因此,您可以尝试的一件事就是填充数组,以使它们地址的低6位不同。这样做的目的是避免让它们争夺相同的缓存行。
答案 3 :(得分:0)
当然,如果你没有实现矢量化,那么进行SoA转换的动力并不大。
除了事实上接受__RESTRICT之外,gcc 4.9已采用#pragma GCC ivdep
来打破假定的别名依赖。
至于使用显式预取,如果它有用,当然你可能需要更多的SoA。主要观点可能是通过提前获取页面来加速DTLB未命中分辨率,因此您的算法可能会变得更加缓存。
我认为没有任何你称之为“编译时”分配的智能评论,而没有更多细节,包括有关你的操作系统的细节。毫无疑问,高分配和重新使用分配的传统很重要。