【问题标题】:strict aliasing and memory alignment严格的别名和内存对齐
【发布时间】:2012-12-19 15:37:39
【问题描述】:

我有性能关键代码,并且有一个巨大的函数,它在函数开始时在堆栈上分配了 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);
    

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

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

  • 【问题讨论】:

    • 也许stackoverflow.com/questions/98650/… 会有所帮助?
    • 您可以通过将-fno-strict-aliasing 开关传递给gcc 来禁用严格的别名规则。也许这对你来说是一个选择?
    • @JonathonReinhart:可能是出于 TLDR 的原因
    • 我会说它应该作为 NARQ 关闭,因为您在特定情况下显然遇到了特定编译器的问题,但您没有说您使用的是什么编译器版本,也没有您给出了一个无法在该编译器版本上运行的代码示例。这是一个非常低级的特定于编译器的问题;这意味着我们需要确切地知道哪个编译器有问题,在什么架构上,我们需要确切地了解哪些代码生成了有问题的代码。当然,这样做会使问题过于本地化。
    • @Pavel:“其他人也会有同样的问题,这对他们会有帮助。什么同样的问题?我不明白问题是什么,因为您没有提供足够的信息来实际了解您必须编写哪些代码才能实现它,它发生在哪些编译器版本上,以及哪些编译的体系结构存在问题。我不是在指责这个问题不好。我指责它是不完整的。这是 NARQ 的定义之一。给我看一个展示问题的完全可编译的测试示例。

    标签: c++ c casting type-punning


    【解决方案1】:

    只需禁用基于别名的优化就可以了

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

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

    【讨论】:

    • 我明白了混叠问题的意义,但令我惊讶的是,无论我如何尝试投射它,我从来没有得到好的结果。我在不同的功能中使用相同的技术,只有一个地方导致了问题。我也将它发布到 gcc 列表中。问题是,如果我分配堆栈内存,然后使用独立函数将该内存“投射”到对齐的结构,那么它应该可以正常工作,因为没有更多的别名......但是,我仍然得到无效的结果
    • 看来我知道为什么它对我不起作用了。我将其添加到问题的末尾。
    • 看来-fno-strict-aliasing 是最好的“解决方案”,但我仍然必须首先了解为什么会出现问题,以便将来即使其他人不这样做,我也可以避免它不要使用 -fno-strict-aliasing。在我更新的问题的底部查看我为什么要进行一些更改并测试输出的有趣观察。
    • 太糟糕了 gcc 不支持以有限的方式应用基于类型的别名的模式,因为在许多情况下,大多数好处都可以在不破坏大量现有代码的情况下实现,并且使多种代码无法安全有效地编写。
    【解决方案2】:

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

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

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

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

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

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

    【讨论】:

    • 我没有说要声明它是全局的。我说要提前分配内存。内存可以通过指针分配和传递。
    • Eric,这是另一种可能性,但我需要更改大量代码。另外,这将需要更改我不允许更改的代码。
    • @valdo:我所说的人工制品是指它会产生失真的声音输出,如果使用 Gcc4.6 编译并启用了优化和堆栈保护,则偶尔会出现“检测到堆栈损坏:中止”。
    • 是的,我有各种静态断言来验证不同结构成员的偏移量和大小。问题是,如果我使用 char 数组或 alloca 分配堆栈内存,然后将其转换为结构,我会得到不好的结果(非 bitexact 输出)。我用更多细节更新了原始问题。
    【解决方案3】:

    这里有很多问题。

    对齐:很少需要 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; 之类的代码,我希望优化器知道abc 是不同的数组,因此c[i] 不能与a[i] 相同,所以它可以消除a[i] = 3;。也许您遇到的一个问题是,对于 40 个数组,您有 40 个指向数组的指针,所以编译器最终会将指针移入和移出寄存器?

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

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

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

    【讨论】:

    • 视环境而定,该栈可以随意用于大数组。您应该只了解后果:保留足够的堆栈(在线程创建期间)。另外,编译器可能会生成堆栈探针来初始化堆栈使用情况。
    • 很难用简短的评论回复。我了解对齐问题和最大对齐大小。 32byte alignemtn 没有理由。可能是 256 或其他任何值,这根本不是重点。该函数堆栈上的 40 个数组占用大约 8K,我不在乎它们是否占用 256 个字节。这是一个叶函数,不会被递归调用。我的问题是,无论我尝试什么,我都无法让它与 GCC 一起工作,我开始认为它暴露了编译器/binutils 中的一些错误。
    • 性能关键代码中的堆栈分配并不是真正的“分配”。就像只有 3-5 条指令 :) 如果堆栈需要扩大,可能会有一次惩罚,例如当访问超出分配的堆栈页面时,会触发操作系统为程序堆栈分配额外的内存,但我并不担心。顺便说一句,我编辑了原始问题以解决您的大部分观点。
    【解决方案4】:

    32 字节对齐听起来好像您将按钮推得太远了。没有 CPU 指令需要这么大的对齐。基本上,与架构的最大数据类型一样宽的对齐方式就足够了。

    C11 具有 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 语言中的unions 是为此而生的,然后总是允许字符指针(任何版本的)为任何其他指针起别名。

    【讨论】:

    • DMA 要求页面对齐的访问,可以是 2K 对齐或更多。 32 字节对齐远非不寻常......毕竟它只有 256 位。英特尔 AVX 的 vmovaps 需要 32 字节对齐。
    • 确实,由于 AVX,32 字节对齐非常普遍。 64 字节对齐也很常见,因为如果您尝试对此进行优化,这是一个常见的缓存线大小。
    • 不要关注细节,关注问题本身。我在没有实际检查代码的情况下放置了 32 字节对齐。 ARM(手机的 CPU)有可以指定 256 位对齐的指令。所以,我只是将所有数据对齐到最大对齐。
    猜你喜欢
    • 2012-11-08
    • 2014-06-28
    • 2021-10-21
    • 2018-01-18
    • 2013-03-27
    • 1970-01-01
    • 2017-10-23
    • 2019-02-09
    • 1970-01-01
    相关资源
    最近更新 更多