首页 文章

位对齐空间和性能提升

提问于
浏览
9

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

或者这至少是他的主张 .

他指出,真正的性能提升是通过使用你的大脑并确保你的结构设计得恰当,以利用速度提升同时防止编译器臃肿 . 他提供了以下代码段:

#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 相比),并且 FastStruct 的大小更小 . 这对我来说都是有意义的,但它在我的测试中无法阻止:

enter image description here

同时结果和大小(计算 char unused[ 2 ] )!

现在,如果 #pragma pack( push, 1 ) 仅被隔离到 FastStruct (或完全删除),我们确实看到了一个区别:

enter image description here

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

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

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

7 回答

  • 1

    它高度依赖于硬件 .

    Let me demonstrate:

    #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
    

    好的,差别不大 . 但它在多次运行中仍然保持一致 .
    So the alignment makes a small difference on Nehalem Core i7.


    Intel Xeon X5482 Harpertown @ 3.2 GHz (核心2代Xeon)

    slow = 22.803
    fast = 3.669
    sum = 99999990000000000
    

    现在来看看......

    6.2 x快!!!


    结论:

    You see the results. You decide whether or not it's worth your time to do these optimizations.


    EDIT :

    相同的基准但没有 #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 ,编译器会将所有内容保持一致,这样您就不会看到此问题 . 所以这实际上是一个例子,如果你 misuse #pragma pack 会发生什么 .

  • 1

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

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

    事情就是:可能存在别名和其他有子字访问的恶意,并且访问整个单词比访问子字的速度慢 . 所以一般来说,如果你只是访问一个成员,那么在时间上打包比单词大小更有效率 .

  • 3

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

    也就是说,你可能想在专门的gamedev stackexchange site上重新发布这个问题,你可能会直接从"the field"得到一些答案 .

    最后,您的结果完全相同 up to the microsecond 这在现代多线程系统中是不可能的 - 我很确定您要么使用非常低分辨率的计时器,要么您的计时代码被破坏 .

  • 6

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

    通常你不应该建议使用 #pragma 指令搞乱填充 .

  • 1

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

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

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

  • 2

    在某些平台上,编译器没有选项:类型大于 char 的对象通常具有严格的要求,以处于适当对齐的地址 . 通常,对齐要求与对象的大小相同,直到CPU本身支持的最大单词的大小 . 那就是 short 通常需要处于偶数地址, long 通常需要处于可被4整除的地址,地址可被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
    
  • 13

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

相关问题