GCC:溢出的未定义行为是否应该保持逻辑一致性?

GCC: Should undefined behavior of overflows preserve logical consistency?

以下代码在我的系统上产生了奇怪的东西:

#include <stdio.h>

void f (int x) {
  int y = x + x;
  int v = !y;
  if (x == (1 << 31))
    printf ("y: %d, !y: %d\n", y, !y);
}

int main () {
  f (1 << 31);
  return 0;
}

使用 -O1 编译,打印 y: 0, !y: 0.

除了删除 int vif 行会产生预期结果这一令人费解的事实之外,我对将溢出转化为逻辑不一致的未定义行为感到不舒服。

这应该被认为是一个错误,还是 GCC 团队的哲学,即一个意外的行为可以级联成逻辑矛盾?

调用未定义的行为时,任何事情都可能发生。毕竟,它被称为未定义行为是有原因的。

Should this be considered a bug, or is the GCC team philosophy that one unexpected behavior can cascade into logical contradiction?

这不是错误。我不太了解 GCC 团队的哲学,但一般来说,未定义的行为是 "useful" 编译器开发人员实现某些优化:假设某事永远不会发生可以更容易地优化代码。之所以UB之后什么事都能发生,就是因为这个。编译器做了很多假设,如果其中任何一个被破坏,那么发出的代码就不能被信任。

正如我在 another answer of mine 中所说:

Undefined behavior means that anything can happen. There is no explanation as to why anything strange happens after invoking undefined behavior, nor there needs to be. The compiler could very well emit 16-bit Real Mode x86 assembly, produce a binary that deletes your entire home folder, emit the Apollo 11 Guidance Computer assembly code, or whatever else. It is not a bug. It's perfectly conforming to the standard.

2018 C 标准在第 3.4.3 条第 1 段中将“未定义行为”定义为:

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this document imposes no requirements

这很简单。 标准中没有要求。所以,不,标准不要求行为“一致”。没有要求。

此外,编译器、操作系统以及构建和 运行 程序中涉及的其他事物通常不会在这个问题中提出的意义上强加任何“一致性”要求。

附录

请注意,“任何事情都可能发生”的答案是不正确的。 C 标准只说 it 在存在它认为“未定义”的行为时不强加任何要求。它不会取消其他要求,也无权取消它们。编译器、操作系统、机器架构或消费品法律的任何规范;或物理定律;逻辑法则;或其他限制仍然适用。这很重要的一种情况是简单地链接到不是用 C 编写的软件库:C 标准没有定义会发生什么,但是会发生什么仍然受到所使用的其他编程语言和库规范的限制,以及作为链接器、操作系统等。

出于某种原因,出现了一个神话,即该标准的作者使用短语 "Undefined Behavior" 来描述其发明者对该语言的早期描述描述为 "machine dependent" 的行为是允许编译器推断各种事情不会发生。虽然标准确实不要求实现有意义地处理此类操作,即使在存在自然 "machine-dependent" 行为的平台上也是如此,但标准也不要求任何实现能够处理 任何有意义的有用程序;一个实现可能是合规的,但除了一个人为的和无用的程序之外,不能有意义地处理任何东西。这并不是对标准意图的扭曲:“虽然有缺陷的实施可能会想方设法 一个满足这个要求的程序,但仍然成功无用,C89 委员会认为,这种独创性可能需要更多的工作而不是做出有用的东西。"

在讨论将短无符号值提升为有符号 int 的决定时,该标准的作者观察到大多数当前实现使用安静环绕整数溢出语义,并且将值提升为有符号 int 不会对行为产生不利影响,如果该值用于高位无关紧要的溢出场景。

从实际的角度来看,保证干净的环绕语义比允许整数计算表现得像在未指定的时间对较大类型执行的操作要多一点。即使没有 "optimization",甚至像 long1 = int1*int2+long2; 这样的表达式的直接代码生成在许多平台上也会受益于能够直接使用 16x16->32 或 32x32->64 乘法指令的结果,而不必对结果的下半部分进行符号扩展。此外,允许编译器在方便时将 x+1 评估为大于 x 的类型将允许它用 x >= y 替换 x+1 > y——通常是有用且安全的优化。

然而,像 gcc 这样的编译器走得更远。尽管该标准的作者在评估类似以下内容时观察到:

unsigned mul(unsigned short x, unsigned short y) { return x*y; }

标准决定将 xy 提升为 signed int 与使用 [=18 相比不会对行为产生不利影响=] ("Both schemes give the same answer in the vast majority of cases, and both give the same effective result in even more cases in implementations with two’s-complement arithmetic and quiet wraparound on signed overflow—that is, in most current implementations."),gcc 有时会使用上述函数在 调用 代码中推断 x 不可能超过 INT_MAX/y。我没有看到任何证据表明该标准的作者预料到了这种行为,更不用说鼓励这种行为了。虽然 gcc 的作者声称在这种情况下会调用溢出的任何代码都是 "broken",但我认为标准的作者不会同意,因为在讨论一致性时,他们指出:"The goal is to give the programmer a fighting chance to make powerful C programs that are also highly portable, without seeming to demean perfectly useful C programs that happen not to be portable, thus the adverb strictly."

因为标准的作者未能禁止 gcc 的作者在整数溢出的情况下无意义地处理代码,即使在 quiet-wraparound 平台上,他们也坚持在这种情况下应该跳转 rails .没有一个试图赢得付费客户的编译器编写者会持这种态度,但标准的作者没有意识到编译器编写者可能更看重聪明而不是客户满意度。

Marco Bonelli 给出了允许这种行为的原因;我想尝试解释为什么它可能实用。

根据定义,优化编译器应该做各种事情以使程序 运行 更快。他们可以删除未使用的代码、展开循环、重新排列操作等。

拿你的代码来说,真的可以期望编译器在调用 printf() 之前严格执行 !y 操作吗?我会说如果你强加这样的规则,就没有任何优化的余地。因此,编译器应该可以自由地将代码重写为

void f (int x) {
  int y = x + x;
  int notY = !(x + x);
  if (x == (1 << 31))
    printf ("y: %d, !y: %d\n", y, notY);
}

现在,显而易见的是,对于任何不会导致溢出的输入,其行为都是相同的。但是,在溢出的情况下,ynotY分别经历了UB的影响,有可能都变成0,为什么不呢