【发布时间】: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在不禁用优化的情况下为此类代码生成功能相同的代码?
编辑:
简而言之,问题的重点......我如何分配随机数量的堆栈空间(使用 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 * X 与 tmp 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