19

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

由于某些版本的 gcc 根本无法正确对齐堆栈变量(特别是对于 arm 代码),甚至有时它说目标架构的最大对齐比我的代码实际要求的要少,我别无选择,只能分配这些数组在堆栈上并手动对齐它们。

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

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

这样,history现在在 32 字节边界上对齐。对所有 40 个数组做同样的事情是很乏味的,而且这部分代码真的是 cpu 密集型的,我根本无法为每个数组做相同的对齐技术(这种对齐混乱使优化器感到困惑,不同的寄存器分配会大大减慢函数的速度,为了更好的解释,请参阅问题末尾的解释)。

所以......显然,我只想进行一次手动对齐,并假设这些数组一个接一个。我还为这些数组添加了额外的填充,以便它们始终是 32 字节的倍数。所以,然后我只需在堆栈上创建一个巨型字符数组并将其转换为具有所有这些对齐数组的结构:

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

类似的东西。也许不是最优雅的,但它产生了非常好的结果,并且手动检查生成的程序集证明生成的代码或多或少是足够的和可接受的。构建系统已更新为使用更新的 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 上观察到了这种行为(顺便说一下,我根本不担心英特尔)。
  • 我所说的伪像是指输出变得不精确,添加了一些噪声。要么是因为这种类型的别名问题,要么是编译器中存在一些错误,最终导致函数输出错误。

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

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

    这段代码:

    tmp buf;
    tmp * X = &buf;
    

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

    有趣的观察:
    我提到这种方法效果很好并产生了预期的输出:

    tmp buf;
    tmp * X = &buf;
    

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

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

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

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

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

    结论:
    经过各种测试,我知道为什么无论我尝试什么都无法让它工作。基于严格的类型别名,GCC 认为静态数组是未使用的,因此不会为其分配堆栈。tmp然后,也使用堆栈的局部变量被写入存储我的结构的同一位置;换句话说,我的巨型结构与函数的其他变量共享相同的堆栈内存。只有这样才能解释为什么它总是导致同样的坏结果。-fno-strict-aliasing 解决了这个问题,正如在这种情况下所预期的那样。

  • 4

    4 回答 4

    5

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

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

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

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

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

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

    于 2013-01-05T13:31:40.497 回答
    4

    这里有很多问题。

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

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

    堆栈上的大而多的数组:堆栈不适用于大内存分配,并且对其使用有限制。即使您现在没有遇到问题,将来代码中明显不相关的更改也可能与使用堆栈上的大量内存进行交互并导致堆栈溢出。

    阵列众多: 40 个阵列很多。除非这些都同时用于不同的数据,而且必然如此,否则您应该寻求将一些相同的空间重新用于不同的数据和目的。不必要地使用不同的数组可能会导致更多的缓存抖动。

    优化:不清楚您所说的“对齐混乱使优化器感到困惑,不同的寄存器分配会大大减慢函数的速度”是什么意思。如果您在一个函数中有多个自动数组,我通常希望优化器知道它们是不同的,即使您通过地址算术从数组中派生指针。例如,给定诸如 之类的代码a[i] = 3; b[i] = c[i]; a[i] = 4;,我希望优化器知道a,bc是不同的数组,因此c[i]不能与 相同a[i],因此可以消除a[i] = 3;。也许您遇到的一个问题是,对于 40 个数组,您有 40 个指向数组的指针,所以编译器最终会将指针移入和移出寄存器?

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

    如果您对优化和寄存器使用有其他顾虑,您应该更具体地了解它们。

    混叠和伪影:您报告存在一些混叠和伪影问题,但您没有提供足够的细节来理解它们。如果您有一个大型char数组,您将其重新解释为包含所有数组的结构,则该结构内没有别名。所以不清楚你遇到了什么问题。

    于 2013-01-05T13:04:11.837 回答
    3

    只需禁用基于别名的优化并收工

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

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

    于 2013-01-06T00:40:59.000 回答
    2

    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).

    还要确保这没有混叠问题。首先unions在 C 中为此而生,然后总是允许字符指针(任何版本)为任何其他指针起别名。

    于 2013-01-05T08:54:05.183 回答