我有性能关键代码,并且有一个巨大的函数,它在函数开始时在堆栈上分配了 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);
然后我得到同样糟糕的结果!哇,我还能说什么?也许,基于严格的别名规则 gcc 假设它tmp * X
不相关tmp buf
并tmp buf
在从 to_struct_tmp 返回后立即作为未使用的变量删除?或者做了一些奇怪的事情,产生了意想不到的结果。我也尝试检查生成的程序集,但是,更改tmp * X = &buf;
为tmp * X = to_struct_tmp(&buf);
为函数生成截然不同的代码,因此,该别名规则不知何故影响了代码生成。
结论:
经过各种测试,我知道为什么无论我尝试什么都无法让它工作。基于严格的类型别名,GCC 认为静态数组是未使用的,因此不会为其分配堆栈。tmp
然后,也使用堆栈的局部变量被写入存储我的结构的同一位置;换句话说,我的巨型结构与函数的其他变量共享相同的堆栈内存。只有这样才能解释为什么它总是导致同样的坏结果。-fno-strict-aliasing 解决了这个问题,正如在这种情况下所预期的那样。