2012-07-23 49 views
13

我有一个类是这样的:结构 - 性能差

//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 
    } 
}; 

创建单元类型的对象的数组。并致电更新。

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 
    } 
    } 
}; 

当循环失败autovectorize,我得到一个非常糟糕的性能的数组结构。对于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倍。这里发生了什么事?为什么静态和动态分配内存之间存在性能差异?

+0

你用什么编译器选项来构建它? – 2012-07-23 16:57:23

+0

不知道这是否会有所作为,但[std :: valarray](http://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a00738.html)可能会(或可能不会) ) 帮帮我。它被设计用于在整个数组上执行数学运算(比较干净的语法),但是我猜测实现者有特殊的重载来尝试优化这些操作并在可能时进行智能分配等。它可能根本没有帮助,但可能值得一看。 – pstrjds 2012-07-23 17:16:54

+1

在运行基准测试之前将数据集清零会发生什么?未初始化的浮点很有可能被[非规范化](http://stackoverflow.com/a/9314926/922184)。你不想那样搞砸你的基准。 – Mysticial 2012-07-25 03:27:28

回答

9

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

您同时使用uv,但是如果两个不同的阵列对应它们,它们将不会同时加载到一个缓存行中,并且缓存未命中会造成巨大的性能损失。

_mm_prefetch可用于使AoS表示更快。

+0

是否有'_mm_prefetch'的GCC/clang等价物? – 2012-07-23 17:02:40

+0

http://gcc.gnu.org/ml/gcc-patches/2008-01/msg01425.html – 2012-07-23 17:08:30

+2

然而,缓存未命中并没有被部分浪费 - 你得到的东西是你将*需要的东西(更多的“u” 's和更多'v''s),那为什么会降低性能? – harold 2012-07-23 17:23:07

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]; 
+1

我不认为OOP不应该与使用AoS混淆。标量场可以被认为是OOP中的一个对象,就像它在数学中被认为是一个对象一样,但是如果使用多个标量场表示一个空间区域,那么您将以与OOP一致的方式使用SoA。这归结于你认为OOP中的对象。 – 16807 2017-03-02 18:18:08

0

当然,如果你没有实现向量化,那么没有太多的动机来进行SoA转换。

除了__RESTRICT事实上接受的相当宽泛,gcc 4.9已采用#pragma GCC ivdep来打破假定的别名依赖关系。对于显式预取的使用,如果它是有用的,当然你可能需要更多的SoA。主要的观点可能是通过提前提取页面来加速DTLB未命中的解决方案,因此您的算法可能会变得更加缓慢。

我不认为可以对任何你称为“编译时”分配的智能注释做出更多细节,包括关于你的操作系统的细节。毫无疑问,高水平分配和重新使用分配的传统非常重要。