严格别名和内存对齐

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

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

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

short history_[HIST_SIZE + 32]; short * history = (short*)((((uintptr_t)history_) + 31) & (~31)); 

这样, 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)); 

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

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

编辑:

  • 我在多个操作系统/编译器上使用了这个代码,但是当我切换到基于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 ,然后将指针对齐到该堆栈空间并重新解释这块内存,因为某些结构具有一些定义良好的布局只要结构本身正确对齐,就可以保证某些变量的对齐。我正在尝试使用各种方法来转换内存,我将大堆栈分配移动到一个单独的函数,仍然会导致输出错误和堆栈损坏,我真的开始越来越多地想到这个巨大的function会在gcc中遇到某种错误。这很奇怪,通过这样做,无论我尝试什么,我都无法完成这件事。顺便说一下,我禁用所有需要任何对齐的优化,它现在是纯C风格的代码,但仍然会得到不好的结果(非bitexact输出和偶尔的堆栈损坏崩溃)。修复它的简单修复,我编写而不是:

     char buf[sizeof(tmp) + 32]; tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31)); 

    这段代码:

     tmp buf; tmp * X = &buf; 

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

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

     tmp buf; tmp * X = &buf; 

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

     struct tmp * to_struct_tmp(void * buffer32) { return (struct tmp *)buffer32; } 

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

     tmp buf; tmp * X = to_struct_tmp(&buf); 

    然后我得到了同样糟糕的结果! 哇,我还能说什么? 也许,基于严格别名规则gcc假设tmp * Xtmp buf无关,并且在从to_struct_tmp返回后立即将tmp buf删除为未使用的变量? 或者做一些奇怪的事情会产生意外的结果 我也尝试检查生成的程序集,然而,更改tmp * X = &buf; to tmp * X = to_struct_tmp(&buf); 为函数生成极其不同的代码,因此,不知何故,别名规则会影响代码生成的大量时间。

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

    只需禁用基于别名的优化并将其称为一天

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

    Praetorian的好点。 我记得一位开发人员在gcc中引入别名分析引起的歇斯底里症。 某个Linux内核作者想要(A)别名,并且(B)仍然得到优化。 (这是一个过于简单化,但似乎-fno-strict-aliasing可以解决问题,而不是花费太多,而且他们都必须有其他的鱼来煎炸。)

    首先,我想说,当你要求不要关注“标准违规”,“依赖于实现”等问题时,我肯定会和你在一起。你的问题绝对合法恕我直言。

    你在一个struct包装所有数组的方法也是有意义的,这就是我要做的。

    从问题的表述中不清楚你观察到哪些“文物”。 是否生成了不需要的代码? 还是数据错位? 如果是后者 – 您可能(希望)使用STATIC_ASSERT类的STATIC_ASSERT来确保在编译时事情正确对齐。 或者至少在调试版本中有一些运行时ASSERT

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

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

    此外,如果您不需要同时使用所有40个arrays,则可以在union安排其中一些arrays。 也就是说,你将拥有一个大struct ,其中一些子structs将被分组为union

    这里有很多问题。

    对齐:几乎没有需要32字节对齐。 16字节对齐有利于当前Intel和ARM处理器上的SIMD类型。 在当前的英特尔处理器上使用AVX,使用16字节对齐但不是32字节对齐的地址的性能成本通常很低。 跨越高速缓存行的32字节存储可能会有很大的损失,因此32字节对齐可能会有所帮助。 否则,16字节对齐可能没问题。 (在OS X和iOS上, malloc返回16字节对齐的内存。)

    关键代码中的分配:您应该避免在性能关键代码中分配内存。 通常,应在程序开始时或在性能关键工作开始之前分配内存,并在性能关键代码期间重用内存。 如果在性能关键代码开始之前分配内存,那么分配和准备内存所花费的时间基本上是无关紧要的。

    堆栈上有大量的数组:堆栈不适用于大内存分配,并且它的使用受到限制。 即使您现在没有遇到问题,未来代码中显然无关的更改也可能与堆栈上的大量内存交互并导致堆栈溢出。

    众多arrays: 40个arrays很多。 除非这些数据同时用于不同的数据,并且必然如此,否则您应该寻求为不同的数据和目的重用一些相同的空间。 不必要地使用不同的arrays会导致比必要的更多缓存抖动。

    优化:目前尚不清楚你的意思是说“对齐混乱会使优化器混淆,不同的寄存器分配会使function大大减慢”。 如果函数内部有多个自动数组,我通常希望优化器知道它们是不同的,即使你通过地址算法从数组派生指针。 例如,给定代码如a[i] = 3; b[i] = c[i]; a[i] = 4; a[i] = 3; b[i] = c[i]; a[i] = 4; ,我希望优化器知道abc是不同的数组,因此c[i]不能与a[i]相同,所以可以消除a[i] = 3; 。 也许你遇到的问题是,有40个数组,你有40个指向数组的指针,所以编译器最终会将指针移入和移出寄存器?

    在这种情况下,为多种目的重用较少的数组可能有助于减少这种情况。 如果您的算法实际上一次使用40个arrays,那么您可能会考虑重构算法,以便一次使用更少的arrays。 如果一个算法必须指向内存中的40个不同的位置,那么你基本上需要40个指针,无论它们在何处或如何分配,并且40个指针比可用的寄存器更多。

    如果您对优化和注册使用有其他顾虑,您应该更加具体。

    别名和工件:您报告存在一些别名和工件问题,但是您没有提供足够的细节来理解它们。 如果你有一个大的char数组,你重新解释为包含所有数组的结构,那么结构中没有别名。 所以目前尚不清楚你遇到了什么问题。

    32字节对齐的声音就好像你按下按钮太远了。 没有CPU指令应该要求这样大的对齐。 基本上,与架构的最大数据类型一样宽的对齐应该足够了。

    C11具有fo maxalign_t的概念,这是架构的最大对齐的虚拟类型。 如果您的编译器没有它,那么您可以通过类似的方式轻松地模拟它

     union maxalign0 { long double a; long long b; ... perhaps a 128 integer type here ... }; typedef union maxalign1 maxalign1; union maxalign1 { unsigned char bytes[sizeof(union maxalign0)]; union maxalign0; } 

    现在,您的数据类型具有平台的最大对齐,并且默认情况下将所有字节设置为0初始化。

     maxalign1 history_[someSize]; short * history = history_.bytes; 

    这避免了您当前执行的糟糕地址计算,您只需要采用someSize来考虑您总是分配sizeof(maxalign1)倍数。

    还要确保这没有别名问题。 首先是C中的联合为此做的,然后字符指针(任何版本)总是被允许别名任何其他指针。