谷歌严格别名的第一个结果之一就是这篇文章
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开发人员付出了一些努力使编译器"按预期"运行.(我希望我能为你提供一个适当的参考 - 我记得它会出现在邮件列表中,或者某些时候出现在某些邮件列表中).
无论如何,你说的话:
...没有实现该功能,因为它可以使用严格的别名
...意味着可能会对严格别名规则的含义产生轻微的误解.您的代码示例调用未定义的行为 - 因此任何编译在技术上都是有效的,包括只是普通ret
或生成陷阱指令,甚至根本没有(假设永远不会调用该方法是合法的).较新版本的GCC产生更长/更慢的代码几乎不是缺陷,因为生成任何特定事物的代码都不会违反标准.实际上,较新的版本通过生成代码来改进这种情况,这些代码执行程序员可能希望代码执行的操作,而不是默默地执行不同的操作.
你还想要什么 - 编译器产生的快速代码不能达到你想要的效果,或者稍微慢一点的代码可以做你想做的事情?
话虽如此,我坚信你不应该编写破坏严格别名规则的代码.依靠编译器做"正确"的事情,当它"明显"是什么意图是走钢丝.优化已经足够困难,编译器不必猜测 - 并且允许程序员想要的东西.此外,可以编写符合规则的代码,并且编译器可以将其转换为非常有效的目标代码.确实可以提出进一步的问题:
为什么早期版本的GCC表现得像他们那样,并依靠严格的别名规则"优化"功能?
这有点复杂,但对于这个讨论很有意思(特别是考虑到编译器只是打破代码的建议).严格别名是称为别名分析的过程的一部分(或者更确切地说,是一种协助规则).此过程决定两个指针是否为别名.基本上,任何两个指针之间有三种可能的条件:
它们绝不可能(严格的别名规则可以很容易地推断出这种情况,尽管有时可以通过其他方式推断出来).
它们必须是ALIAS(这需要分析;值传播可能会检测到这种情况)
他们可能是ALIAS.当其他两个条件都不能建立时,这是默认条件.
对于您的问题中的代码,严格别名意味着在&acopy
和之间必须不是ALIAS条件ptr
(这是很容易做出这个决定,因为这两个值具有不兼容的类型,不允许别名).这个条件允许你看到的优化:所有对*ptr
值的操作都可以被丢弃,因为它们在理论上不能影响它的值,acopy
否则它们不会逃避函数(可以通过转义分析确定).
需要进一步努力来确定两个指针之间的MUST ALIAS条件.此外,在这样做时,编译器需要忽略(至少暂时)先前确定的MUST NOT ALIAS条件,这意味着它必须花时间试图确定条件的真实性,如果一切都是应该的话,必须是假.
当两者都不能确定并且必须确定ALIAS条件时,我们会遇到代码必须调用未定义行为的情况(我们可以发出警告).然后我们必须决定要保留哪个条件以及丢弃哪个条件.因为在这种情况下,必须不是ALIAS,而是来自用户可能(实际上已经)打破的约束,它是丢弃的最佳选择.
因此,较旧版本的GCC要么不进行必要的分析以确定必须ALIAS条件(可能因为已经建立了相反的MUST ALIAS条件),或者旧的GCC版本选择放弃MUST ALIAS条件.优先选择MUST NOT ALIAS条件,这会导致更快的代码,而这些代码不会达到程序员最想要的程度.在任何一种情况下,似乎新版本都提供了改进.
在这个其他相关问题中,有@DanMoulding的评论.让我抄袭它:
标准的严格别名规则的目的是允许编译器在不存在的情况下进行优化,并且无法知道对象是否是别名.规则允许优化器在这些情况下不做出最坏情况下的混叠假设.但是,当从上下文中清楚地知道对象是别名时,编译器应该将该对象视为别名,无论使用何种类型来访问它.否则不符合语言别名规则的意图.
在你的代码中,别名*ptr
和acopy
显而易见,因为它们都是局部变量,因此任何理智的编译器都应将它们视为别名.从这个角度来看,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
仅仅因为标准允许它这样做?