Pav*_*l P 17 c c++ casting type-punning
我有性能关键代码,并且有一个巨大的函数,在函数开始时在堆栈上分配40个不同大小的数组.这些阵列中的大多数必须具有一定的对齐性(因为这些阵列是使用需要内存对齐的cpu指令(对于Intel和arm CPU)在链中的其他位置访问的.
由于某些版本的gcc无法正确对齐堆栈变量(特别是对于arm代码),或者甚至有时它表示目标体系结构的最大对齐小于我的代码实际请求的对齐,我别无选择,只能分配这些数组在堆栈上并手动对齐它们.
所以,对于每个数组,我需要做类似的事情才能使它正确对齐:
short history_[HIST_SIZE + 32];
short * history = (short*)((((uintptr_t)history_) + 31) & (~31));
Run Code Online (Sandbox Code Playgroud)
这样,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));
Run Code Online (Sandbox Code Playgroud)
这样的事情.也许不是最优雅的,但它产生了非常好的结果,并且对生成的组件的手动检查证明生成的代码或多或少是足够的和可接受的.构建系统已更新为使用更新的GCC,突然我们开始在生成的数据中有一些工件(例如,即使在具有禁用的asm代码的纯C构建中,验证测试套件的输出也不再精确).调试问题花了很长时间,它似乎与别名规则和更新版本的GCC有关.
那么,我该怎么做呢?请不要浪费时间试图解释它不是标准的,不是可移植的,未定义的等等(我已经阅读过很多关于此的文章).此外,我无法改变代码(我可能会考虑修改GCC以解决问题,但不能重构代码)...基本上,我想要的是应用一些黑魔法咒语以便更新的GCC为这种类型的代码生成功能相同的代码而不禁用优化?
编辑:
简而言之,问题的关键点......我如何分配随机数量的堆栈空间(使用char数组或者alloca
,然后将指针对齐到该堆栈空间并重新解释这个内存块,因为某些结构具有一些定义良好的布局,只要结构本身正确对齐,我就会保证某些变量的对齐.我正在尝试使用各种方法来转换内存,我将大堆栈分配移动到一个单独的函数,仍然会导致输出错误和堆栈损坏,我我真的开始越来越多地认为这个巨大的功能会在gcc中遇到某种错误.这很奇怪,通过这样做,无论我尝试什么,我都无法完成这件事.顺便说一下,我禁用了所有需要任何对齐的优化,现在都是纯C风格的代码,我仍然得到不好的结果(非bitexact输出和偶尔的堆栈损坏崩溃).修复它的简单修复,我写而不是:
char buf[sizeof(tmp) + 32];
tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));
Run Code Online (Sandbox Code Playgroud)
这段代码:
tmp buf;
tmp * X = &buf;
Run Code Online (Sandbox Code Playgroud)
然后所有的bug都消失了!唯一的问题是这个代码没有为数组做正确的对齐,并且在启用优化时会崩溃.
有趣的观察:
我提到这种方法运作良好并产生预期的输出:
tmp buf;
tmp * X = &buf;
Run Code Online (Sandbox Code Playgroud)
在其他一些文件中,我添加了一个独立的noinline函数,它只是将一个void指针强制转换为该struct tmp*:
struct tmp * to_struct_tmp(void * buffer32)
{
return (struct tmp *)buffer32;
}
Run Code Online (Sandbox Code Playgroud)
最初,我认为如果我使用to_struct_tmp转换alloc'ed内存,它会欺骗gcc产生我期望获得的结果,但是,它仍然产生无效输出.如果我尝试以这种方式修改工作代码:
tmp buf;
tmp * X = to_struct_tmp(&buf);
Run Code Online (Sandbox Code Playgroud)
然后我得到了同样糟糕的结果!哇,我还能说什么呢?也许,基于严格别名规则,gcc假定在从to_struct_tmp返回后,它与未使用的变量tmp * X
无关tmp buf
并被删除tmp buf
?或做一些奇怪的事情会产生意想不到的结 我也试图检查生成的程序集,但是,改变tmp * X = &buf;
到tmp * X = to_struct_tmp(&buf);
对功能产生非常不同的代码,因此,在某种程度上是混淆规则影响的代码生成大的时间.
结论:
经过各种测试后,我知道为什么不管我尝试什么,我都不能让它工作.基于严格类型别名,GCC认为静态数组未使用,因此不为其分配堆栈.然后,也使用堆栈的局部变量被写入tmp
存储结构的同一位置; 换句话说,我的jumbo struct与函数的其他变量共享相同的堆栈内存.只有这可以解释为什么它总会导致同样糟糕的结果.-fno-strict-aliasing修复了该问题,正如本案例所预期的那样.
首先,当您要求不要谈论“违反标准”、“依赖于实施”等问题时,我想说我绝对支持您。恕我直言,您的问题绝对合法。
您将所有数组打包成一个的方法struct
也很有意义,这就是我要做的。
从问题表述中不清楚您观察到哪些“人工制品”。是否生成了不需要的代码?还是数据错位?如果是后者 - 您可以(希望)使用诸如STATIC_ASSERT
确保在编译时正确对齐的东西。或者至少ASSERT
在调试版本中有一些运行时。
正如 Eric Postpischil 所提议的,您可以考虑将此结构声明为全局(如果这适用于这种情况,我的意思是多线程和递归不是一种选择)。
我想注意的另一点是所谓的堆栈探测。当您在单个函数中从堆栈中分配大量内存(准确地说是超过 1 页)时 - 在某些平台(例如 Win32)上,编译器会添加额外的初始化代码,称为堆栈探测器。这也可能对性能产生一些影响(尽管可能很小)。
此外,如果您不需要同时需要所有 40 个数组,您可以将其中一些排列在union
. 也就是说,您将拥有一个 big struct
,其中一些子structs
将被分组到union
.
归档时间: |
|
查看次数: |
2405 次 |
最近记录: |