为什么优化杀死这个function?

我们最近在大学做了关于几种语言的编程特辑的讲座。

讲师写下了以下function:

inline u64 Swap_64(u64 x) { u64 tmp; (*(u32*)&tmp) = Swap_32(*(((u32*)&x)+1)); (*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x); return tmp; } 

虽然我完全理解这在可读性方面也是非常糟糕的风格,但他的主要观点是这部分代码在生产代码中工作良好,直到他们启用了高优化级别。 那么,代码就什么都不做。

他说,编译器会优化variablestmp所有赋值。 但为什么会发生?

我明白,在某些情况下,variables需要被声明为volatile,这样编译器即使认为它们从来没有读过或写过,也不会碰到它们,但我不知道为什么会在这里发生。

此代码违反了严格的别名规则 ,通过不同types的指针访问对象是非法的,尽pipe允许通过* char **进行访问。 允许编译器假定不同types的指针不指向相同的内存并相应地进行优化。 这也意味着代码调用未定义的行为 ,可以做任何事情。

这个主题的最好的参考之一是理解严格别名 ,我们可以看到第一个例子与OP的代码类似:

 uint32_t swap_words( uint32_t arg ) { uint16_t* const sp = (uint16_t*)&arg; uint16_t hi = sp[0]; uint16_t lo = sp[1]; sp[1] = hi; sp[0] = lo; return (arg); } 

这篇文章解释了这个代码违反了严格的别名规则,因为sparg的别名,但是它们有不同的types,并且说虽然它会编译,但是在swap_words返回之后, arg可能会保持不变。 尽pipe通过简单的testing,我无法重现上述代码或OP代码的结果,但这并不意味着什么,因为这是未定义的行为 ,因此不可预测。

文章继续讨论许多不同的案例,并提出了几个工作的解决scheme,包括在C99 1中明确定义的联合types戳 ,可能在C ++中是未定义的,但在实践中得到大多数主要编译器的支持,例如这里是海湾合作委员会关于打字的参考资料 。 上一个线程C和C ++中的联合的目的进入了血淋淋的细节。 虽然这个话题有很多线索,但这似乎做得最好。

该解决scheme的代码如下所示:

 typedef union { uint32_t u32; uint16_t u16[2]; } U32; uint32_t swap_words( uint32_t arg ) { U32 in; uint16_t lo; uint16_t hi; in.u32 = arg; hi = in.u16[0]; lo = in.u16[1]; in.u16[0] = lo; in.u16[1] = hi; return (in.u32); } 

C99关于严格别名 标准草案的相关部分参见6.5 expression式7段,内容为:

对象的存储值只能由具有以下types之一的左值expression式访问:

– 与对象的有效types兼容的types,

– 与对象的有效types兼容的types的限定版本,

– 与对象的有效types对应的有符号或无符号types,

– 与对象的有效types的限定版本对应的有符号或无符号types,

– 在其成员中包括上述types之一的集合或联合types(包括recursion地,子集合的成员或包含的联合),或者

– 一个字符types。

脚注76说:

这个列表的目的是指定一个对象可能被别名或不被别名的情况。

而C ++草案标准中的相关部分是3.10 左值和右值10

Type-punning和strict-aliasing这个文章给出了一个比较温和但不太全面的介绍, C99重新给出了对C99和别名的深入分析,而不是轻读。 这个答案访问不活动的联盟成员 – undefined? 在C ++中通过一个联合来查看types戳的泥泞细节,并且也不是轻量级阅读。


脚注:

  1. 引用帕斯卡·库克(Pascal Cuoq)的评论 : C99起初言辞拙劣,似乎是通过未定义的工会来打字。 事实上,工会打字虽然在C89中是合法的,在C11中是合法的,而在C99中一直是合法的,尽pipe直到2004年才由委员会修正了错误的措词,随后又发布了TC3。 open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

在C ++中,如果指向基本上不同的types( “严格别名”规则 ),则指针参数不会被假定为别名(除char* )。 这允许一些优化。

在这里, u64 tmp永远不会被修改为u64
u32*的内容被修改,但可能与' u64 tmp '无关,因此可能被视为u64 tmp nop

g ++(Ubuntu / Linaro 4.8.1-10ubuntu9)4.8.1:

 > g++ -Wall -std=c++11 -O0 -o sample sample.cpp > g++ -Wall -std=c++11 -O3 -o sample sample.cpp sample.cpp: In function 'uint64_t Swap_64(uint64_t)': sample.cpp:10:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] (*(uint32_t*)&tmp) = Swap_32(*(((uint32_t*)&x)+1)); ^ sample.cpp:11:54: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] (*(((uint32_t*)&tmp)+1)) = Swap_32(*(uint32_t*) &x); ^ 

铿锵3.4不警告任何优化级别, 这是好奇的 …