严格别名和内存对齐

Pav*_*l P 17 c c++ casting type-punning

我有性能关键代码,并且有一个巨大的函数,在函数开始时在堆栈上分配40个不同大小的数组.这些阵列中的大多数必须具有一定的对齐性(因为这些阵列是使用需要内存对齐的cpu指令(对于Intel和arm CPU)在链中的其他位置访问的.

由于某些版本的gcc无法正确对齐堆栈变量(特别是对于arm代码),或者甚至有时它表示目标体系结构的最大对齐小于我的代码实际请求的对齐,我别无选择,只能分配这些数组在堆栈上并手动对齐它们.

所以,对于每个数组,我需要做类似的事情才能使它正确对齐:

short history_[HIST_SIZE + 32];
short * history = (short*)((((uintptr_t)history_) + 31) & (~31));
Run Code Online (Sandbox Code Playgroud)

这样,history现在在32字节边界上对齐.对所有40个数组执行相同的操作非常繁琐,而且这部分代码实际上是cpu密集型的,我根本无法对每个数组执行相同的对齐技术(这种对齐混乱会使优化器和不同的寄存器分配混淆,从而减慢函数的使用时间,为了更好的解释,请参阅问题末尾的解释).

所以......显然,我只想做一次手动对齐,并假设这些数组一个接着一个.我还为这些数组添加了额外的填充,以便它们总是32个字节的倍数.那么,我只需在堆栈上创建一个jumbo char数组并将其转换为具有所有这些对齐数组的结构:

struct tmp
{
   short history[HIST_SIZE];
   short history2[2*HIST_SIZE];
   ...
   int energy[320];
   ...
};


char buf[sizeof(tmp) + 32];
tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));
Run Code Online (Sandbox Code Playgroud)

这样的事情.也许不是最优雅的,但它产生了非常好的结果,并且对生成的组件的手动检查证明生成的代码或多或少是足够的和可接受的.构建系统已更新为使用更新的GCC,突然我们开始在生成的数据中有一些工件(例如,即使在具有禁用的asm代码的纯C构建中,验证测试套件的输出也不再精确).调试问题花了很长时间,它似乎与别名规则和更新版本的GCC有关.

那么,我该怎么做呢?请不要浪费时间试图解释它不是标准的,不是可移植的,未定义的等等(我已经阅读过很多关于此的文章).此外,我无法改变代码(我可能会考虑修改GCC以解决问题,但不能重构代码)...基本上,我想要的是应用一些黑魔法咒语以便更新的GCC为这种类型的代码生成功能相同的代码而不禁用优化?

编辑:

  • 我在多个操作系统/编译器上使用了这个代码,但是当我切换到基于GCC 4.6的更新的NDK时开始出现问题.我用GCC 4.7得到了同样糟糕的结果(来自NDK r8d)
  • 我提到32字节对齐.如果它伤害了你的眼睛,用你喜欢的任何其他数字代替它,例如666,如果它有帮助.毫无疑问,大多数架构都不需要这种对齐.如果我在堆栈上对齐8KB的本地数组,则为16字节对齐松散15个字节,而对于32字节对齐,我松散31个字节.我希望我的意思很清楚.
  • 我说在性能关键代码中堆栈上有40个阵列.我可能还需要说它是一个运行良好的第三方旧代码,我不想搞砸它.没有必要说它是好还是坏,没有意义.
  • 此代码/函数具有经过良好测试和定义的行为.我们对该代码的要求具有确切的数量,例如它分配Xkb或RAM,使用Y kb的静态表,并且消耗多达Z kb的堆栈空间并且它不能改变,因为代码不会被改变.
  • 通过说"对齐混乱混淆优化器"我的意思是,如果我尝试单独对齐每个数组代码优化器为对齐代码分配额外的寄存器和性能关键代码部分突然没有足够的寄存器并开始垃圾堆栈而不是导致代码速度减慢.在ARM CPU上观察到这种行为(顺便说一句,我根本不担心英特尔).
  • 通过工件,我的意思是输出变为非bitexact,增加了一些噪音.要么是因为这种类型的别名问题,要么编译器中存在一些错误,最终导致函数输出错误.

    简而言之,问题的关键点......我如何分配随机数量的堆栈空间(使用char数组或者alloca,然后将指针对齐到该堆栈空间并重新解释这个内存块,因为某些结构具有一些定义良好的布局,只要结构本身正确对齐,我就会保证某些变量的对齐.我正在尝试使用各种方法来转换内存,我将大堆栈分配移动到一个单独的函数,仍然会导致输出错误和堆栈损坏,我我真的开始越来越多地认为这个巨大的功能会在gcc中遇到某种错误.这很奇怪,通过这样做,无论我尝试什么,我都无法完成这件事.顺便说一下,我禁用了所有需要任何对齐的优化,现在都是纯C风格的代码,我仍然得到不好的结果(非bitexact输出和偶尔的堆栈损坏崩溃).修复它的简单修复,我写而不是:

    char buf[sizeof(tmp) + 32];
    tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));
    
    Run Code Online (Sandbox Code Playgroud)

    这段代码:

    tmp buf;
    tmp * X = &buf;
    
    Run Code Online (Sandbox Code Playgroud)

    然后所有的bug都消失了!唯一的问题是这个代码没有为数组做正确的对齐,并且在启用优化时会崩溃.

    有趣的观察:
    我提到这种方法运作良好并产生预期的输出:

    tmp buf;
    tmp * X = &buf;
    
    Run Code Online (Sandbox Code Playgroud)

    在其他一些文件中,我添加了一个独立的noinline函数,它只是将一个void指针强制转换为该struct tmp*:

    struct tmp * to_struct_tmp(void * buffer32)
    {
        return (struct tmp *)buffer32;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    最初,我认为如果我使用to_struct_tmp转换alloc'ed内存,它会欺骗gcc产生我期望获得的结果,但是,它仍然产生无效输出.如果我尝试以这种方式修改工作代码:

    tmp buf;
    tmp * X = to_struct_tmp(&buf);
    
    Run Code Online (Sandbox Code Playgroud)

    然后我得到了同样糟糕的结果!哇,我还能说什么呢?也许,基于严格别名规则,gcc假定在从to_struct_tmp返回后,它与未使用的变量tmp * X无关tmp buf并被删除tmp buf?或做一些奇怪的事情会产生意想不到的结 我也试图检查生成的程序集,但是,改变tmp * X = &buf;tmp * X = to_struct_tmp(&buf);对功能产生非常不同的代码,因此,在某种程度上是混淆规则影响的代码生成大的时间.

    结论:
    经过各种测试后,我知道为什么不管我尝试什么,我都不能让它工作.基于严格类型别名,GCC认为静态数组未使用,因此不为其分配堆栈.然后,也使用堆栈的局部变量被写入tmp存储结构的同一位置; 换句话说,我的jumbo struct与函数的其他变量共享相同的堆栈内存.只有这可以解释为什么它总会导致同样糟糕的结果.-fno-strict-aliasing修复了该问题,正如本案例所预期的那样.

  • val*_*ldo 5

    首先,当您要求不要谈论“违反标准”、“依赖于实施”等问题时,我想说我绝对支持您。恕我直言,您的问题绝对合法。

    您将所有数组打包成一个的方法struct也很有意义,这就是我要做的。

    从问题表述中不清楚您观察到哪些“人工制品”。是否生成了不需要的代码?还是数据错位?如果是后者 - 您可以(希望)使用诸如STATIC_ASSERT确保在编译时正确对齐的东西。或者至少ASSERT在调试版本中有一些运行时。

    正如 Eric Postpischil 所提议的,您可以考虑将此结构声明为全局(如果这适用于这种情况,我的意思是多线程和递归不是一种选择)。

    我想注意的另一点是所谓的堆栈探测。当您在单个函数中从堆栈中分配大量内存(准确地说是超过 1 页)时 - 在某些平台(例如 Win32)上,编译器会添加额外的初始化代码,称为堆栈探测器。这也可能对性能产生一些影响(尽管可能很小)。

    此外,如果您不需要同时需要所有 40 个数组,您可以将其中一些排列在union. 也就是说,您将拥有一个 big struct,其中一些子structs将被分组到union.


    Dig*_*oss 3

    只需禁用基于别名的优化即可

    如果您的问题实际上是由与严格别名相关的优化引起的,那么-fno-strict-aliasing将解决该问题。此外,在这种情况下,您无需担心失去优化,因为根据定义,这些优化对您的代码来说是不安全的,您无法使用它们。

    禁卫军的观点很好。我记得一位开发人员由于在 gcc 中引入别名分析而感到歇斯底里。某个 Linux 内核作者想要 (A) 给一些东西起别名,并且 (B) 仍然得到优化。(这过于简单化了,但似乎-fno-strict-aliasing可以解决问题,而且花费不多,而且他们肯定还有其他的鱼要煎。)