【问题标题】:Structure of arrays and array of structures - performance difference数组结构和结构数组 - 性能差异
【发布时间】:2012-07-21 22:46:47
【问题描述】:

我有这样的课:

//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。但是从 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 倍的性能。这里发生了什么事?为什么静态分配的内存和动态分配的内存会有这么大的性能差异?

【问题讨论】:

  • 你使用什么编译器选项来构建这个?
  • 不确定这是否会有所作为,但std::valarray 可能(或可能不会)有帮助。它旨在对整个数组执行数学运算(这样的语法更简洁),但我猜想实现者有特殊的重载来尝试优化这些操作并在可能的情况下进行智能分配等。它可能根本没有帮助,但可能值得一看。
  • 在运行基准测试之前将数据集归零会发生什么?未初始化的浮点很有可能是denormalized。您不希望这会破坏您的基准。

标签: c++ c performance caching gcc


【解决方案1】:

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

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

_mm_prefetch 可用于更快地表达AoS

【讨论】:

  • _mm_prefetch 是否有 GCC/clang 等效项?
  • 缓存未命中并没有被部分浪费——你得到的东西是你需要的东西(更多u和更多v),所以为什么这会降低性能?
【解决方案2】:

对于大部分执行时间都在等待数据出现的代码来说,预取至关重要。现代前端总线具有足够的带宽,可以安全地进行预取,前提是您的程序不会超出其当前的负载集。

由于各种原因,结构和类可能会在 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];

【讨论】:

  • 我认为 OOP 不应与使用 AoS 混为一谈。标量场在 OOP 中可以被视为对象,就像在数学中被视为对象一样,但是如果您使用多个标量场表示空间区域,则您使用 SoA 的方式与 OOP 一致。这取决于您在 OOP 中对对象的感知。
  • 好点。我应该提到运行时多态性、构造函数开销等。面向对象的语言 /tend/ 提供了许多易于使用的特性,这些特性必须包含大量多余的代码和开销,从而导致二进制文件变慢。 OO 代码不必很慢,C++ 表明模板和类有时可以提供更高级别的抽象,让编译器延迟优化并获得稍好的性能。
【解决方案3】:

根据您的 CPU,您应该注意两件事会产生巨大的影响:

  1. 对齐
  2. 缓存行别名

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

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

【讨论】:

    【解决方案4】:

    当然,如果您没有实现矢量化,那么进行 SoA 转型的动力就没有多大。

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

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

    如果没有更多详细信息(包括有关您的操作系统的详细信息),我认为智能 cmets 不能用于您所谓的“编译时”分配。毫无疑问,高层次分配和重用分配的传统很重要。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-02-14
      • 2013-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多