本文是对谷歌进行严格别名的首批结果之一
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也没有优化它。
答案 0 :(得分:9)
GCC开发人员付出了一些努力,使编译器在这些情况下“按预期”运行。 (我希望我可以为你提供一个适当的参考 - 我记得它会出现在邮件列表上,或者某些时候会出现这种情况)。
无论如何,你说的是:
...没有使用严格别名
来实现该功能
...意味着可能对严格别名规则的含义有轻微的误解。您的代码示例调用未定义的行为 - 因此任何编译在技术上都是有效的,包括普通ret
或生成陷阱指令,甚至根本没有(假设永远不会调用该方法是合理的。较新版本的GCC产生更长/更慢的代码几乎不是缺陷,因为生成任何特定事物的代码都不会违反标准。实际上,较新的版本通过生成代码来改进这种情况,这些代码执行程序员可能希望代码执行的操作,而不是默默地执行不同的操作。
您更愿意 - 编译器生成的快速代码不能达到您想要的效果,或者代码稍微慢一些,可以做您想做的事情吗?
话虽如此,我坚信你不应该编写破坏严格别名规则的代码。依靠编译器做“正确”的事情,当它“明显”是什么意图是走钢丝。优化已经足够困难,编译器不必猜测 - 并且允许程序员想要的东西。此外,可以编写符合规则的代码,并且编译器可以将其转换为非常有效的目标代码。确实可以提出进一步的问题:
为什么早期版本的GCC表现得像他们那样,并依靠严格的别名规则来“优化”这个功能?
这有点复杂,但对于这个讨论很有意思(特别是考虑到编译器只是打破代码的建议)。严格别名是一个名为别名分析的过程的组成部分(或者更确切地说是一个帮助规则的组件)。此过程决定两个指针是否为别名。在任何两个指针之间基本上有三种可能的条件:
对于您的问题中的代码,严格别名意味着&acopy
和ptr
之间必须不是ALIAS条件(做出此决定是微不足道的,因为这两个值具有不兼容的类型,不允许别名)。这个条件允许你看到的优化:*ptr
值的所有操作都可以被丢弃,因为它们在理论上不能影响acopy
的值,否则它们不会逃避函数(可以是通过逃逸分析确定)。
需要进一步努力确定两个指针之间的MUST ALIAS条件。此外,在这样做时,编译器需要忽略(至少暂时)先前确定的MUST NOT ALIAS条件,这意味着它必须花时间试图确定条件的真实性,如果一切都是应该的话,必须是假的。
当两者都不能确定并且必须确定ALIAS条件时,我们会遇到代码必须调用未定义行为的情况(我们可以发出警告)。然后我们必须决定要保留哪个条件以及丢弃哪个条件。因为在这种情况下,MUST NOT ALIAS来自于一个可以(实际上已经被)破坏的约束,它是丢弃的最佳选择。
因此,较旧版本的GCC要么不进行必要的分析以确定必须ALIAS条件(可能是因为已经建立了相反的MUST ALIAS条件),或者旧的GCC版本选择丢弃必须ALIAS条件优先于MUST NOT ALIAS条件,这导致更快的代码,而不是程序员最想要的。在任何一种情况下,似乎新版本都提供了改进。
答案 1 :(得分:7)
在这个other related question中,有@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
只是因为标准允许它这样做?
答案 2 :(得分:3)
编译器的目标通常应尽可能与代码的意图相匹配。在这种情况下,代码调用UB,但意图应该非常清楚。我的猜测是,最近编译器一直专注于正确而不是利用UB进行优化。
严格别名本质上是一种假设,即代码不会试图破坏类型系统,正如@rodrigo所述,它为编译器提供了可用于优化的更多信息。如果编译器不能假设严格别名,则排除了许多非平凡的优化,这就是C甚至添加了 library(XML)
library(RCurl)
# Read and parse HTML file
forbe = 'http://www.forbes.com/powerful-brands/list/#tab:rank'
data <- getURL('http://www.forbes.com/powerful-brands/list/#tab:rank')
data
htmldata <- readHTMLTable(data)
htmldata
限定符(C99)的原因。
对于我能想到的任何优化,都不需要打破严格的别名。事实上,在这种特定情况下,根据原始意图的不同,您可以在不调用UB的情况下获得正确/优化的代码......
restrict
编译为......
uint32_t wswap(uint32_t ws) {
return (ws << 16) | (ws >> 16);
}