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

Why compilers no longer optimize this UB with strict aliasing

google 上严格别名的第一个结果是这篇文章 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 也没有优化它。

在此 other related question 中,@DanMoulding 发表了评论。让我抄袭一下:

The intent of the standard's strict aliasing rules are to allow the compiler to optimize in situations where it doesn't and cannot know whether an object is being aliased. The rules permit the optimizer to not make worst-case aliasing assumptions in those situations. However, when it is clear from the context that an object is being aliased, then the compiler should treat the object as being aliased, no matter what types are being used to access it. Doing otherwise is not in line with the intent of the language's aliasing rules.

在您的代码中,*ptracopy 的别名很明显,因为它们都是局部变量,所以任何 健全的编译器 都应该将它们视为别名。从这个角度来看,GCC 4.4 的行为虽然符合对标准的严格阅读,但会被大多数 real-world 程序员视为一个错误。

你首先要考虑为什么会有别名规则。它们使编译器可以在 可能 存在别名但最有可能存在 none 的情况下利用优化。所以语言禁止别名,编译器可以自由优化。例如:

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

仅仅是因为标准允许这样做吗?

GCC 开发人员付出了一些努力使编译器在这些情况下表现 "as expected"。 (我希望我能给你一个适当的参考 - 我记得它在某个时候出现在邮件列表或类似的东西上)。

好歹有你说的:

... does not implement the function as it could using strict aliasing

... 暗示可能对严格的别名规则的用途有轻微的误解。您的代码示例调用 undefined behavior - 所以 any 编译在技术上是有效的,包括简单的 ret 或陷阱指令的生成,甚至什么都没有(假设该方法永远不会被调用是合理的)。较新版本的 GCC 生成 longer/slower 代码几乎不是缺陷,因为生成执行任何特定操作的代码根本不会违反标准。事实上,较新的版本通过生成代码来完成程序员可能想要代码要做的事情而不是默默地做一些不同的事情来改善这种情况。

您更希望编译器生成的代码运行速度快但不符合您的要求,还是生成速度稍慢但符合您要求的代码?

话虽如此,我坚信您不应该编写违反严格别名规则的代码。依靠编译器在 "obvious" 时做 "right" 的事情是在走钢丝。优化已经够难了,编译器不必猜测 - 并考虑 - 程序员的意图。此外,可以编写遵守规则的代码,并且可以由编译器将其转换为非常有效的目标代码。确实可以提出进一步的问题:

为什么早期版本的 GCC 和 "optimize" 函数依赖于严格的别名规则?

这有点复杂,但是对于这个讨论来说很有趣(特别是考虑到编译器为了破坏代码而竭尽全力的建议)。严格别名是一个组成部分(或者更确切地说,是辅助规则)称为 别名分析 的过程。这个过程决定两个指针是否别名。本质上,任意两个指针之间有 3 种可能的情况:

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

对于您问题中的代码,严格别名意味着 &acopyptr 之间的 MUST NOT ALIAS 条件(做出此决定很简单,因为这两个值不兼容不允许别名的类型)。此条件允许您随后看到的优化:可以丢弃对 *ptr 值的所有操作,因为它们在理论上不能影响 acopy 的值并且它们不会以其他方式逃避函数(可以是通过逃逸分析)确定。

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

当同时确定 MUST NOT ALIAS 和 MUST ALIAS 条件时,我们会遇到代码必须调用未定义行为的情况(我们可以发出警告)。然后我们必须决定保留哪些条件,丢弃哪些条件。因为 MUST NOT ALIAS,在这种情况下,来自可以(并且确实已经)被用户破坏的约束,所以它是丢弃的最佳选择。

因此,旧版本的 GCC 要么不进行必要的分析来确定 MUST ALIAS 条件(可能是因为相反的 MUST NOT ALIAS 条件已经成立),要么旧版本的 GCC 选择丢弃MUST ALIAS 条件优先于 MUST NOT ALIAS 条件,这导致更快的代码不会执行程序员最可能想要的。无论哪种情况,新版本似乎都提供了改进。

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

严格别名本质上是一种假设,即代码不会试图颠覆类型系统,正如@rodrigo 所指出的那样,它为编译器提供了更多可用于优化的信息。如果编译器不能假设严格的别名,它会排除一些 non-trivial 优化,这就是为什么 C 甚至添加了 restrict 限定符(C99)。

我能想到的任何优化都不需要打破严格的别名。事实上,在这种特定情况下,根据最初的意图,您可以在不调用 UB 的情况下获得 correct/optimized 代码...

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

编译为...

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