为什么编译器不再使用严格的别名来优化此UB

谷歌严格别名的第一个结果之一就是这篇文章http://dbp-consulting.com/tutorials/StrictAliasing.html
我注意到的一件有趣的事情是: http : //goo.gl/lPtIa5

uint32_t swaphalves(uint32_t a) { uint32_t acopy = a; uint16_t* ptr = (uint16_t*)&acopy; uint16_t tmp = ptr[0]; ptr[0] = ptr[1]; ptr[1] = tmp; return acopy; } 

被编译为

 swaphalves(unsigned int): mov eax, edi ret 

由GCC 4.4.7。 任何比这更新的编译器(文章中提到的4.4所以文章没有错)都没有实现该函数,因为它可以使用严格别名。 这是什么原因? 它实际上是GCC中的错误还是GCC决定放弃它,因为许多行代码是以产生UB的方式编写的,或者它只是一个持续多年的编译器回归…而Clang也没有优化它。

在这些情况下,GCC开发人员付出了一些努力使编译器“按预期”运行。 (我希望我能为你提供一个适当的参考 – 我记得它会出现在邮件列表中或某些时候出现在某些时候)。

无论如何,你说的话:

…没有实现该function,因为它可以使用严格的别名

…意味着可能会对严格别名规则的含义产生轻微的误解。 您的代码示例调用未定义的行为 – 因此任何编译在技术上都是有效的,包括只是简单的ret或生成陷阱指令,甚至根本没有(假设永远不会调用该方法是合法的)。 较新版本的GCC产生更长/更慢的代码几乎不是缺陷,因为生成任何特定事物的代码都不会违反标准。 实际上,较新的版本通过生成代码来改进这种情况,代码可以执行程序员可能希望代码执行的操作,而不是默默地执行不同的操作。

你还想要什么 – 编译器产生的快速代码不能达到你想要的效果,或者稍微慢一点的代码可以做你想做的事情?

话虽如此,我坚信你不应该编写破坏严格别名规则的代码。 依靠编译器做“正确”的事情,当它“显而易见”时,目的是走钢丝。 优化已经足够困难了,编译器不必猜测 – 并且允许程序员想要的东西。 此外,可以编写遵循规则的代码,并且可以由编译器转换为非常有效的目标代码。 确实可以提出进一步的问题:

为什么早期版本的GCC表现得像他们那样 ,并依靠严格的别名规则“优化”function?

这有点复杂,但对于这个讨论很有意思(特别是考虑到编译器只是为了打破代码而需要一些长度的建议)。 严格别名是称为别名分析的过程的一部分(或更确切地说,是一种协助规则)。 此过程决定两个指针是否为别名。 在任何两个指针之间基本上有3种可能的条件:

  • 它们绝不可能(严格的别名规则可以很容易地推断出这种情况,尽管有时可以通过其他方式推断出来)。
  • 它们必须是ALIAS(这需要分析;值传播可能会检测到这种情况)
  • 他们可能是ALIAS。 当其他两个条件都不能建立时,这是默认条件。

对于您的问题中的代码,严格别名意味着&acopyptr之间必须不是ALIAS条件(进行此确定是微不足道的,因为这两个值具有不兼容的类型,不允许别名)。 这个条件允许您随后看到的优化: *ptr值的所有操作都可以被丢弃,因为它们在理论上不能影响acopy的值,否则它们不会逃避函数(可以通过转义分析确定)。

需要进一步努力来确定两个指针之间的MUST ALIAS条件。 此外,在这样做时,编译器将需要忽略(至少暂时)先前确定的MUST NOT ALIAS条件,这意味着它必须花时间试图确定条件的真实性,如果一切都是应该的话,必须是假。

如果两者都不能确定并且必须确定ALIAS条件,我们就会遇到代码必须调用未定义行为的情况(我们可以发出警告)。 然后我们必须决定要保留哪个条件以及丢弃哪个条件。 因为在这种情况下,必须不是ALIAS,而是来自用户可以(实际上已经)打破的约束,它是丢弃的最佳选择。

因此,较旧版本的GCC要么不进行必要的分析以确定必须ALIAS条件(可能因为已经建立了相反的MUST ALIAS条件),或者旧的GCC版本选择放弃MUST ALIAS条件。偏向于MUST NOT ALIAS条件,这会导致更快的代码,而这些代码不会达到程序员最想要的程度。 在任何一种情况下,似乎新版本都提供了改进。

在这个其他相关问题中 ,有@DanMoulding的评论。 让我抄袭它:

标准的严格别名规则的目的是允许编译器在不存在的情况下进行优化,并且无法知道对象是否是别名。 规则允许优化器在这些情况下不做出最坏情况的混叠假设。 但是,当从上下文中清楚地看到对象是别名时,编译器应该将该对象视为别名,无论使用何种类型来访问它。 否则不符合语言别名规则的意图。

在你的代码中, *ptracopy的别名是显而易见的,因为它们都是局部变量,因此任何理智的编译器都应将它们视为别名。 从这个角度来看,GCC 4.4行为虽然符合严格的标准读取,但大多数现实世界的程序员都会被认为是一个错误。

您必须首先考虑为什么存在别名规则。 它们是这样的,编译器可以在可能存在别名的情况下利用优化,但很可能没有。 所以语言禁止别名,编译器可以自由优化。 例如:

 void foo(int *idx, float *data) { /* idx and data do not overlap */ } 

但是,当别名涉及局部变量时,不会丢失优化:

 void foo() { uint32_t x; uint16_t *p = (uint16_t *)&x; //x and p do overlap! } 

编译器试图尽可能地完成它的工作,而不是试图在某个地方找到一个UB来借口格式化你的硬盘!

有很多代码在技术上是UB但被所有编译器忽略。 例如,您如何看待将此视为空文件的编译器:

 #ifndef _FOO_H_ #define _FOO_H_ void foo(void); #endif 

或者忽略这个宏的编译器呢:

 #define new DEBUG_NEW 

仅仅因为标准允许它这样做?

编译器的目标通常应尽可能与代码的意图相匹配。 在这种情况下,代码调用UB,但意图应该非常清楚。 我的猜测是,最近编译器一直专注于正确而不是利用UB进行优化。

严格别名本质上是一种假设,即代码不会试图破坏类型系统,如@rodrigo所述,它为编译器提供了可用于优化的更多信息。 如果编译器不能假设严格别名,则排除了许多非平凡的优化,这就是C甚至添加了restrict限定符(C99)的原因。

我能想到的任何优化都不需要打破严格的别名。 实际上,在这种特定情况下,根据原始意图的不同,您可以在不调用UB的情况下获得正确/优化的代码…

 uint32_t wswap(uint32_t ws) { return (ws << 16) | (ws >> 16); } 

编译成……

 wswap: # @wswap .cfi_startproc # BB#0: roll $16, %edi movl %edi, %eax retq