关于c ++:数组结构和结构数组 – 性能差异

Structure of arrays and array of structures - performance difference

我有一个这样的班级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//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
    }
};

我创建了一个单位类型的对象数组。并调用更新。

1
2
3
4
5
6
7
8
9
int NUM_UNITS = 10000;
void ProcessUpdate()
{
  Unit *units = new Unit[NUM_UNITS];
  for(int i = 0; i < NUM_UNITS; i++)
  {
    units[i].update();
  }
}

为了加快速度,并可能使循环自动向量化,我将AOS转换为数组结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//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稍快,但从100个单元开始,SOA比AOS慢。在300个单元的情况下,SOA的情况几乎是前者的两倍。在100k单位时,SOA比AOS慢4倍。虽然缓存可能是SOA的一个问题,但我没想到性能差异会如此之大。cacheGrind上的分析显示两种方法的未命中次数相似。单位对象的大小为48字节。一级缓存256K,二级缓存1MB,三级缓存8MB。我这里缺什么?这真的是缓存问题吗?

编辑:我使用的是GCC4.5.2。编译器选项是-o3-msse4-ftree矢量化。

我在SOA中做了另一个实验。我没有动态地分配数组,而是在编译时分配了"v"和"u"。当有10万个单元时,这样的性能比具有动态分配数组的SOA快10倍。这里发生了什么?为什么静态和动态分配内存之间存在这样的性能差异?


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

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

使用_mm_prefetch可以使AoS表示更快。


根据CPU的不同,有两件事你应该知道,这会产生巨大的影响:

  • 对齐
  • 缓存线别名
  • 由于您使用的是SSE4,因此使用一个专门的内存分配函数,该函数返回一个在16字节边界上对齐的地址,而不是new,可能会给您带来一个提升,因为您或编译器可以使用对齐的加载和存储。我没有注意到新的CPU有什么不同,但是在旧的CPU上使用未对齐的负载和存储可能会慢一点。

    至于缓存线别名,Intel Explicit在其参考手册中提到过(搜索"英特尔"?64和IA-32体系结构优化参考手册")。英特尔说这是你应该知道的,特别是在使用SOA时。所以,你可以尝试的一件事是填充你的数组,这样它们的低6位地址就不同了。这样做的目的是避免让他们为同一条缓存线而战。


    预取对于花费大部分执行时间等待数据显示的代码至关重要。现代前端总线有足够的带宽来保证预取的安全性,前提是您的程序不会超过当前的负载集。

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

    1
    2
    3
    4
    5
    6
    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];


    当然,如果您没有实现矢量化,就没有太多的动机来进行SOA转换。

    除了相当广泛的事实上接受"限制"之外,GCC4.9还采用了#pragma GCC ivdep来打破假定的别名依赖关系。

    至于显式预取的使用,如果有用的话,当然您可能需要在SOA中使用更多的显式预取。主要的一点可能是通过提前提取页面来加速DTLB未命中解析,这样您的算法就会变得更需要缓存。

    我不认为在没有更多细节(包括操作系统的细节)的情况下,可以对您所称的"编译时"分配做出明智的评论。毫无疑问,高层次分配和重新使用分配的传统是很重要的。