位对齐空间和性能提升

时间:2012-01-31 20:44:54

标签: c++ performance visual-studio-2010 optimization compiler-construction

在书Game Coding Complete, 3rd Edition,中,作者提到了一种减少数据结构大小以提高访问性能的技术。从本质上讲,它依赖于当成员变量与内存对齐时获得性能的事实。这是编译器可以利用的明显潜在优化,但通过确保每个变量对齐,它们最终会膨胀数据结构的大小。

或者至少这是他的主张。

他说,真正的性能提升是通过使用你的大脑并确保你的结构设计合理,以利用速度增加,同时防止编译器膨胀。他提供了以下代码段:

#pragma pack( push, 1 )

struct SlowStruct
{
    char c;
    __int64 a;
    int b;
    char d;
};

struct FastStruct
{
    __int64 a;
    int b;
    char c;
    char d;  
    char unused[ 2 ]; // fill to 8-byte boundary for array use
};

#pragma pack( pop )

在未指定的测试中使用上述struct个对象时,他会报告15.6%222ms192ms相比)的性能提升,{{1 }}。这对我来说都是有意义的,但在我的测试中它无法阻止:

enter image description here

同时结果大小(计算FastStruct)!

现在,如果char unused[ 2 ]仅与#pragma pack( push, 1 )隔离(或完全删除),我们确实看到了不同之处:

enter image description here

所以,最后,这里有一个问题:现代编译器(特别是VS2010)已经针对位对齐进行了优化,因此缺乏性能提升(但是像Mike Mcshaffry所说的那样增加了结构尺寸作为副作用)?或者我的测试不够密集/不确定以返回任何重要结果?

对于测试,我在未对齐的FastStruct成员上执行了数学运算,列主要多维数组遍历/检查,矩阵运算等各种任务。这两种结构都没有产生不同的结果。

最后,即使它们没有性能提升,这仍然是一个有用的消息,要记住将内存使用量降至最低。但是,如果我没有看到性能提升(无论多么轻微),我会很高兴。

7 个答案:

答案 0 :(得分:13)

它高度依赖于硬件。

让我演示:

#pragma pack( push, 1 )

struct SlowStruct
{
    char c;
    __int64 a;
    int b;
    char d;
};

struct FastStruct
{
    __int64 a;
    int b;
    char c;
    char d;  
    char unused[ 2 ]; // fill to 8-byte boundary for array use
};

#pragma pack( pop )

int main (void){

    int x = 1000;
    int iterations = 10000000;

    SlowStruct *slow = new SlowStruct[x];
    FastStruct *fast = new FastStruct[x];



    //  Warm the cache.
    memset(slow,0,x * sizeof(SlowStruct));
    clock_t time0 = clock();
    for (int c = 0; c < iterations; c++){
        for (int i = 0; i < x; i++){
            slow[i].a += c;
        }
    }
    clock_t time1 = clock();
    cout << "slow = " << (double)(time1 - time0) / CLOCKS_PER_SEC << endl;

    //  Warm the cache.
    memset(fast,0,x * sizeof(FastStruct));
    time1 = clock();
    for (int c = 0; c < iterations; c++){
        for (int i = 0; i < x; i++){
            fast[i].a += c;
        }
    }
    clock_t time2 = clock();
    cout << "fast = " << (double)(time2 - time1) / CLOCKS_PER_SEC << endl;



    //  Print to avoid Dead Code Elimination
    __int64 sum = 0;
    for (int c = 0; c < x; c++){
        sum += slow[c].a;
        sum += fast[c].a;
    }
    cout << "sum = " << sum << endl;


    return 0;
}

Core i7 920 @ 3.5 GHz

slow = 4.578
fast = 4.434
sum = 99999990000000000

好的,差别不大。但它在多次运行中仍然保持一致。
因此,对齐在Nehalem Core i7上产生了一点点差异。


英特尔至强X5482 Harpertown @ 3.2 GHz (核心2代Xeon)

slow = 22.803
fast = 3.669
sum = 99999990000000000

现在看看......

快6.2倍!!!


结论:

您会看到结果。您决定是否值得花时间进行这些优化。


编辑:

相同的基准但没有#pragma pack

Core i7 920 @ 3.5 GHz

slow = 4.49
fast = 4.442
sum = 99999990000000000

Intel Xeon X5482 Harpertown @ 3.2 GHz

slow = 3.684
fast = 3.717
sum = 99999990000000000
  • Core i7号码没有变化。显然它可以处理 这个基准没有问题,没有错位。
  • Core 2 Xeon现在显示两个版本的相同时间。这证实了Core 2架构上的错位是一个问题。

取自我的评论:

如果省略#pragma pack,编译器会将所有内容保持一致,这样您就不会看到此问题。因此,如果您 误用 #pragma pack,这实际上就是一个例子。

答案 1 :(得分:6)

这种手部优化通常已经很久了。如果您正在打包空间,或者如果您具有类似SSE类型的强制对齐类型,则对齐只是一个重要的考虑因素。编译器的默认对齐和打包规则是有意设计的,以显着提高性能,虽然手动调整它们可能是有益的,但通常不值得。

可能在你的测试程序中,编译器从不在堆栈中存储任何结构,只是将成员保存在寄存器中,这些寄存器没有对齐,这意味着它与结构大小或对齐方式完全无关。

这就是:子字访问可能存在别名和其他恶意,访问整个字而不是访问子字的速度并不慢。因此,一般来说,如果你只是访问一个成员,那么打包比单词大小更紧密并不是更有效率。

答案 2 :(得分:3)

Visual Studio在优化方面是一个很棒的编译器。但是,请记住,目前游戏开发中的“优化战争”不在PC领域。虽然PC上的这种优化很可能已经消失,但在控制台平台上却是一双完全不同的鞋子。

也就是说,您可能希望在专业版gamedev stackexchange site上重新发布此问题,您可能会直接从“字段”中获得一些答案。

最后,你的结果完全相同高达微秒,这在现代多线程系统中是不可能的 - 我很确定你要么使用非常低分辨率的计时器,要么你的计时代码坏了。

答案 3 :(得分:2)

现代编译器根据成员的大小在不同的字节边界上对齐成员。请参阅this的底部。

通常你真的不应该关心结构填充,但如果你有一个对象将有1000000个实例或者其他东西,那么拇指的规则就是从大到小对你的成员进行排序。我不建议使用#pragma指令来填充填充。

答案 4 :(得分:1)

编译器要么针对大小或速度进行优化,除非你明确告诉它你不会知道你得到了什么。但如果你遵循那本书的建议,你将在大多数编译器上双赢。把最大的,对齐的东西放在你的结构中然后是一半大小的东西,然后是单字节的东西,如果有的话,添加一些虚拟变量来对齐。对于不必要的事情使用字节可能会成为一个性能损失,作为妥协使用ints for everything(必须知道这样做的优点和缺点)

x86已经为许多糟糕的程序员和编译器做了准备,因为它允许未对齐的访问。使许多人难以转移到其他平台(即接管)。虽然未对齐的访问在x86上运行,但会受到严重的性能影响。这就是为什么重要的是要知道编译器如何在一般情况下以及您正在使用的特定编译器上工作。

拥有缓存,并且与依赖缓存获得任何性能的现代计算机平台一样,您希望对齐和打包。一般来说,正在教授的简单规则给你们两个......这是非常好的建议。添加编译器特定的pragma并不是那么好,使得代码不可移植,并且不需要通过SO或谷歌搜索来查找编译器忽略编译指示的频率或者不执行您真正想要的操作。

答案 5 :(得分:1)

在某些平台上,编译器没有选项:类型大于char的对象通常具有严格的要求,以处于适当对齐的地址。通常,对齐要求与对象的大小相同,直到CPU本身支持的最大单词的大小。这是short通常需要处于偶数地址,long通常需要处于可被4整除的地址,double处于可被8整除的地址,例如, SIMD矢量位于可被16整除的地址。

由于C和C ++需要按照声明的顺序对成员进行排序,因此结构的大小在相应的平台上会有很大差异。由于更大的结构有效地导致更多的缓存未命中,页面未命中等,因此在创建更大的结构时会出现性能下降。

因为我看到声称它无关紧要:它对我正在使用的大多数(如果不是全部)系统都很重要。有一个显示不同尺寸的简单示例。这对性能的影响显然取决于结构的使用方式。

#include <iostream>

struct A
{
    char a;
    double b;
    char c;
    double d;
};

struct B
{
    double b;
    double d;
    char a;
    char c;
};

int main()
{
    std::cout << "sizeof(A) = " << sizeof(A) << "\n";
    std::cout << "sizeof(B) = " << sizeof(B) << "\n";
}

./alignment.tsk 
sizeof(A) = 32
sizeof(B) = 24

答案 6 :(得分:1)

C标准指定必须在增加的地址中分配结构中的字段。一个结构有八个变量类型'int8'和七个变量类型'int64',按顺序存储,将占用64个字节(几乎与机器的对齐要求无关)。如果字段是'int8','int64','int8',...'int64','int8',那么结构将在平台上占用120个字节,其中'int64'字段在8字节边界上对齐。自己重新排序这些字段可以让它们更紧密地打包。但是,编译器不会在没有明确许可的情况下对结构中的字段重新排序,因为这样做会改变程序语义。