TL:DR:你是对的,这样的优化并不被禁止signed int
, 仅适用于float
/double
,而不仅仅是因为这种情况下的例外。
UB 的原因之一是某些不起眼的机器可能会引发异常。但是打UB是不保证在所有机器上引发异常(除非您使用gcc -fsanitize=undefined
,对于 UB 类型,它或 clang 可以可靠地检测到,或者gcc -ftrapv https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html将signed int 溢出的行为定义为捕获)。当编译器通过假设某些事情不会发生而将 UB 视为优化机会时,情况就大不相同了:UB 不是“错误”或“陷阱”的同义词。
有些操作可能会在普通 CPU 上陷入困境,例如未知指针的 deref 以及某些 ISA 上的整数除法(例如 x86,但不是 ARM)。如果您正在寻找编译器可能需要小心的操作,以避免在需要发生的副作用之前或在可能导致抽象机无法到达未定义的分支之前引入异常,那么这些可以作为示例根本没有操作。
有符号整数溢出是 UB,因此在程序执行过程中的任何点(在 C++ 中,根据 C 标准的某些解释),任何事情都可能发生,即使是在为非陷阱机器进行编译时也是如此add
指令(像所有现代 ISA 一样)。
某些实现可能会将行为定义为引发异常。如果他们定义where引发异常,那么它将阻止优化;每个加法都需要按照编写的方式进行,这样如果抽象机中的操作溢出,它就会陷入其中。但这将定义行为,与 UB 的含义完全相反绝对零保证 http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html关于你的程序实际上做了什么。
In C, if n3128 is accepted1, any visible side-effects sequenced before the abstract machine encounters UB must happen. But after UB is encountered, literally anything is allowed, including doing I/O. UB doesn't have to fault and stop execution. If a compiler was compiling the +=
operations with MIPS signed-overflow-trapping add
instructions instead of the usual addu
, it would be legal to optimize to x+=12
after the intervening code even if it contained I/O operations or other visible side-effects (like a volatile
read or write). Even if the x+=5
caused signed overflow UB in the abstract machine, it's fine if the actual behaviour is to trap later (for example when the abstract machine would have done the x+=7
part). As long as it's at or after the abstract machine hit UB, literally anything is allowed. (In C++, it would also be legal to do the possibly-trapping addi $s0, $s0, 12
even before a printf
or something, due to the explicit lack of requirements on behaviour even before the first undefined operation, for an execution that does encounter UB. But only if the compiler can prove that printf definitely returns, so in practice this optimization can usually only happen for volatile
accesses if at all. But even without retroactive effects, we can either do x+=5
before and x+=7
after, or x+=12
after. Not faulting is valid behaviour for signed overflow, but the abstract machine has done an undefined operation so anything that happens later, like printing and then trapping, or just having the addition wrap, is allowed.)
编译器只需避免在不应该有任何异常的执行路径上引入异常。 (这对于主流 ISA 上的整数加法来说不是问题;大多数甚至没有捕获有符号加法指令,并且针对 MIPS 使用的编译器addu
即使是有符号的数学,这样他们就可以自由优化,而且因为历史上程序员不希望陷入困境int
math.)
脚注 1:C 与 C++,以及 C UB 应该是“具体”还是“抽象”
See 未定义的行为追溯是否意味着不能保证早期可见的副作用? https://stackoverflow.com/questions/77146088/does-undefined-behaviour-retroactively-mean-that-earlier-visible-side-effects-ar and n3128:驯服恶魔——未定义的行为和部分程序正确性 https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3128.pdf,建议 ISO C 明确指定在抽象机达到未定义操作之前可见的副作用(如 I/O)仍然必须发生。 (当前 ISO C 标准的常见解释将 UB 视为 C++,其中C++ 标准明确允许 https://eel.is/c++draft/intro.abstract#5沿着通往 UB 的必然之路“破坏”东西。)
编译器执行此操作的实际示例
Both int
and unsigned
可以进行此优化,只有 FP 类型不能,但这也是因为舍入,即使您使用gcc -fno-trapping-math
(FP 数学选项)。查看它的实际应用Godbolt https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1AB9U8lJL6yAngGVG6AMKpaAVxYM9DgDJ4GmADl3ACNMYj0AB1QFQlsGZzcPSOjYgV9/IJZQ8K4LTCsbASECJmICBPdPXMtMazji0oJ0wJCwvQUSsoqk6s6mvxastq4ASgtUV2Jkdg4/AgBqGIYAawBSAGYAIVWNAEE5%2Bf5UYxjgf3QIA9UR%2BdWAdm29%2Bef51VuAJm31gBF5gFYNo9di9Fn5lrcfvMuICdsCXm9Vp8Nr87jCni9iJgCJMGK80bt7t9YbDXAxTudDqhjqTyZgLjS8Gc6a8bvcgSCEUjIQCtrCQUtwciofiOR8viiRRisTi8by9oTiXsqLRUEwFkdjMrVQRqCq1Szbg8%2BfCxUKeeyXgKIb9oXK4c9OeL5qi7SDMdjiLjVPiFXsOGNaJw/rxPBwtKRUJxHIsJlNMB91jxSARNP6xssQOt3gA6O4ANkkfw0AA4NFxi3m/gBOSTrfScSQh1MRzi8BQgDTJ1NjOCwJBoFgROhhciUAdD%2BjhZDALhZ0hYABueGmADU8JgAO4AeQijE4SZotAIYXbEGCzeCflKAE997xL8xiNet8FtLUU9xeAO2IItwxaLeYa8FgwSuMAjhiLQ7afvOmAsIYwDiEB854JidQLpg0Hhpgqi1K4x53uQgj5M2tB4MExA3s4WDNgQxB4Cwd5jMqTDAAoa6bjue4wfwggiGI7BSDIgiKCo6jIbouQGEYICmMY5hkcE7aQGMqARIUZKcAAtFu6zzFpVAMKgWl0UwERDkYWnwQQCC8KgGHEPRWDKRAYw1HUdgQA43RVKQPgDJk2TJDEGk%2BcFqQMM0gXDHkBT1H0YXVPk77xY0UWtDkFgJS4lTtH06VDDkbmxtMEgBkGTbIZGHCvBWWkFvMwDIMgULrNm7zzBAuCECQCajLwH5aCM6aZpI2Z/JIebFvmXAaHmU3FlWdaBhwjakKG4bVW2HZdkBPYwIgKCoIOw5kBQEDjqdKDINJM55honaLsumAcduu6hgedDHsQp7nshD43oRANPi%2Bb7WIR36MAQf4Ac2IFgRBtBQYRWDwUYSHhvgaE2BhWG8DheEETBcwkchimUU%2B1EzOGdEMUxfAGGxr1cR9vC8cIojiEJ7OiWoza6NIt2yWY%2BjkS5qnqXE0E6esdkOU5mHwG5yUeZ4XkME4OU9H5GsFUFuRRCFcSJaQhsRXrMXuRpDRdFrvlW6lZQW5lHSNCbrtOwFGVleMkylaM9YcMG63NtVtV5vVkjzDdCFQnm2YaAnnXdUQxB9SMA3dmMCCYEwWDhK5pAZlm2YLXmXB/NNk1/FwlaB2tjGzZ2G12a2Fg7YNaaB%2B8lWbW3nfDaQDkxHYkhAA与 GCC13 和 Clang 16
int sink; // volatile int sink doesn't make a difference
int foo_signed(int x) {
x += 5;
sink = 1;
x += 7;
return x;
}
// also unsigned and float versions
# GCC -O3 -fno-trapping-math
foo_signed: # input in EDI, retval in EAX
mov DWORD PTR sink[rip], 1
lea eax, [rdi+12] # x86 can use LEA as a copy-and-add
ret
foo_unsigned:
mov DWORD PTR sink[rip], 1
lea eax, [rdi+12]
ret
foo_float: # first arg and retval in XMM0
addss xmm0, DWORD PTR .LC0[rip] # add Scalar Single-precision
mov DWORD PTR sink[rip], 1
addss xmm0, DWORD PTR .LC1[rip] # two separate 5.0f and 7.0f adds
ret
答案的早期版本,对同一结论提出了一些不同的观点
你是对的;假设x
是一个局部变量,所以实际上没有什么可以使用x += 5
结果,优化是安全的x+=5; ... ; x+=7
to x+=12
对彼此而言signed
and unsigned
整数类型。
无符号整数数学当然没问题。
在抽象机的任何情况下,有符号整数数学都必须产生正确的结果doesn't遇到UB。x+=12
这样做。不能保证有符号溢出会在程序中的任何特定点引发异常,这就是现代 C 中基于不会发生未定义行为的假设的优化的全部要点。对于将遇到 UB 的执行,实际上任何事情都可能在该点之前或之后发生(但请参阅上面的脚注 1:沿着通往 UB 的不可避免的路径“破坏”东西)。
这种优化即使对于转弯也是安全的x-=5; x+=7
into x+=2
,其中抽象机可以换行两次(遇到 UB),但 asm 不会换行,因为“碰巧工作”是允许的行为,并且在实践中很常见。 (即使使用 MIPS 捕获add
例如,说明。)
如果您使用类似的编译器选项gcc -fwrapv
,它将有符号整数数学的行为定义为 2 的补码换行,删除 UB 并使情况与无符号相同。
GCC 有时确实会错过带符号整数数学的优化,因为 GCC 内部不太愿意在汇编中临时创建有符号溢出,而抽象机中不存在这种溢出。当为允许非陷阱整数数学(即所有现代 ISA)的机器进行编译时,这是一个错过的优化。例如,GCC 将优化a+b+c+d+e+f
into (a+b)+(c+d)+(e+f)
for unsigned int
但不是为了signed int
没有-fwrapv
。 Clang 对于 AArch64 和 RISC-V 都适用,但选择不针对 x86。 (Godbolt https://godbolt.org/#z:OYLghAFBqd5TKALEBjA9gEwKYFFMCWALugE4A0BIEAZgQDbYB2AhgLbYgDkAjF%2BTXRMiAZVQtGIHgBYBQogFUAztgAKAD24AGfgCsp5eiyagA%2BudTkVjVEQJDqzTAGF09AK5smBpwBkCTNgAcp4ARtikIABM5AAO6ErE9kyuHl4G8Yl2Qv6BIWzhkTHW2LbJIkQspESpnt48Vtg22UwVVUS5wWER0VaV1bXpDUr9HQFdBT1RAJRW6O6kqJxcAPQrANQA4s7O6%2BixdmwEAF7YmOuJwIHnAUTrhDQ0EcxE9ACe64Kk60zo66TYFhKITkdbuJiMJRKdZveZglTrAC0NAA7qQWLEAG7rCAopAEVBIdZIIE/P7YR6lO5CdYYUgA2yBKF7GjrIhIbDrIFsaYAUi0AEE1rSjCZaUh0AlsNChJz6EJgOKWAEWVzMJhoV91qF0OzaUDpaC8QSiSpsGxobFSOhQixQu8Lu5Qoj9ocJPyBR7eVEAMw4OiBdYAFXWtw9IcE6AgIZYoJDoTjtMTmET2ETNETwETSGm615AHYAEIe9al/7YIgLJhc72F0K11C1zC17C1mi14C1pC8n3FwUFgAiXv7vvB/uDXt9/vGwbBTEu13D2qq0a5iYTs8ss5Ts7Ts4zs6zs5zeaLJbLAMrpGrLFr9aihcbD%2BbD9bD/bD87D%2B7va9%2BaHgpcLM9DcAArPw3hcDo5DoNwuzAgsSx5r6fDkEQ2hAbMADWIDSFoAB0oH5gAbDIAAcZE%2BqBWgUVEsggVw0j8GwIDUeQkHQbBXD8EoIBaGhGGzHAsCIBAKAYGwsQMBElDUBJUmMJEqCoKKwA8MRWj8TgmIEtgABqBDYCiADysTMNwqF0PQRARLxEChBh5ChAEVRvBZ/DOawpBvMZoS6FS7nkBJHDCMZEJuVB/A4KE7jAM4Ej0LxvBReaxjAJIkXkIQDJ2Ji0qOdg6ilO4NmBbcTSOfQBChOi3muDgjlEKQBAsclsw0EYwBKAZRmmeZyVyMIYgSJwMiDYoKgaI5%2Bj0WlIDmKYlhVaEvGQLMrrJEliLGT6/DoHl9IEDgq0QLMJRlA4EBOIM3igeQfjjPkhQgMRcQJEkQg3axb1ZMknRPT0r3nS0bQDG4dTfcD5SjP93SREDoxfXdIztLDkzw2d8yLKNwFgRBjlces6hkcRiLEdIIppes6n4QRWg4vgxBkMhPo8NM/DoZF0yzByLA4JEp3kDhbEMUx5AsWxHF7dwPF8QJXPkMJyBoOgknSRQVAQPJ6sgFUbCYmRiLKap6maVl2A6UsPUmWZkGWQwNmkHZDmZZ5rmBW73m%2Bf5tiBcFLxhe8jnRbF8X0IlgU4GwaUZdB2VUgQeVJdBhXFaVA3lQx0HLbVbz1cs0FNS17ntZ13WGTb/WWfIw2SGNgjCMoaiaJl%2Bg%2BoYc0LUt1UnetBybdw227TBB3Ncd8BnU0CeXdd4PpFE%2Bb3UwmBo89MSZB9KRz94C8/Zvq9TI0zTQ%2B0X271DQig2MeRw70KNg2kO%2BL/f18TGvmOITjhh4%2BxBPcFypB9aGxYJTMUNM6YM0ICQb43pWbs3ljobm5Beb82oNhVi/FRb40ylxWW/FOaINxlwKIzEMG/xwTLBBmEha4QIkRUi0gKJURomROi38uDDyljBShBCgKK1EuJVWCkZKa21opNAakfQxG0rpa2fU7b8Cso7Z2jlPYRVQmo72AUBr%2B1CuFYO2AYpxQSklVCUcY4FyigQHKid8qZVTqgEqyxUKZ0qtVXO%2BdGrNVanwUuLAupyNtoFBuohxB11kCEpuU1W4GFUvNCwhge4Txgv3IQW0drrD2qPI6%2BU1pH2nt4K6y8voNAejfdGGR3otBKXvFoB9IjDCnhdVoiNt4GAvi01Gj1b7DFaY/dpMNukVLZnMT%2BUgiHgXIZxf%2BxNSbk3WMAZS1MfT4SiJApmMCULwN4UgnCUQyL4QotIaQREZBaGkKzMiWhXpYKmdLbiVg5Y7P4fAMSKs1aKVklrIROtUCSOkRbWRFd5HBIdrZagLtoJqI9i5L2fltGoV0UQQOEU46GNDiYyOqUTCxysTYpOBUiqOPTi44QFVMo51cp4zKRcfF8I6v48uvUgkDRCbXUaET5BRJbtBfQMQ4ld0SStZJG00mDwyciNEGJMRZIiGPXJgsOmOGKW00py96mVN%2Bp9VVtS/pDOeo04%2Bl8%2BkQ0NQUq%2BGremnx1S/S1H9sbjPYZMrhhNZlkwpos1AyzVnrOgSzNmHNBLoJ9HQn0ABOcNeE6LSHzGGsNd1bkusobxfBQb2EkLudwh5OzZgHUSA4aQQA%3D%3D)。同样,这是由于 GCC 由于某种未知原因过于谨慎而错过的优化;这是完全有效的。 2 的补码有符号数学与无符号二进制数学相同,因此是结合律;例如,在优化计算来回绕行但抽象机没有绕行的情况下,最终结果将是正确的。
有符号溢出UB只是抽象机中的东西,不是asm;大多数主流 ISA 甚至没有在溢出时陷入困境的整数加法指令。 (MIPS 确实如此,但编译器不将它们用于int
数学,因此他们可以进行优化,产生抽象机中不存在的值。)
半相关:为什么 GCC 不将 a*a*a*a*a*a 优化为 (a*a*a)*(a*a*a)? https://stackoverflow.com/questions/6430448/why-doesnt-gcc-optimize-aaaaaa-to-aaaaaa(答案表明,编译器确实优化了整数数学的三乘,即使是有符号的int
.)
FP 异常并不是 float/double 的唯一问题
浮点数学无法进行此优化,因为由于舍入不同,它可能在非溢出情况下给出不同的结果。两个较小的数字都可以向下舍入,而一个较大的数字则可以克服阈值。
例如对于一个数字足够大,最接近的可表示double值是16彼此分开 https://en.wikipedia.org/wiki/Double-precision_floating-point_format#Precision_limitations_on_integer_values, 8
将得到一半并舍入到最近的偶数(假设默认舍入模式)。但再少一点,比如7
or 5
,将始终向下舍入;x + 7 == x
,所以两者5
和7
会丢失,但是x+12
一次性完成所有操作将跨越驼峰到达下一个可表示的浮点或双精度,产生x+16
.
(The magnitude of 1 unit-in-the-last-place (of the mantissa) depends on the exponent of a float/double. For large enough FP values, it's 1.0. For even larger values, e.g. double
from 253 to 254 only even numbers are representable, and so on with larger exponents.)
如果您使用 GCC 有缺陷的默认值进行编译-ftrapping-math
,它将尝试尊重 FP 异常语义。如果溢出发生两次,它不会可靠地生成 2 个 FP 异常,因此它可能不关心这一点。
但是,是的,与#pragma STDC FENV_ACCESS ON
,每个单独的 FP 操作都应该具有可观察到的效果。 (https://en.cppreference.com/w/c/numeric/fenv https://en.cppreference.com/w/c/numeric/fenv)。但如果你不打电话fegetexcept
为了实际观察两个操作之间的 FP 异常标志,理论上它们仍然可以被优化if我们可以证明舍入是相同的,因为我认为即使是 ISO CFENV_ACCESS ON
应该支持每个捕获操作实际运行的异常/信号处理程序。
例如两个身份操作,例如x *= 1.0;
可以折叠为 1,这将引发 NaN 异常。或者x *= 2; x *= 2;
可以优化为x *= 4;
因为乘以 2 的精确幂不会改变尾数,因此不会导致四舍五入。第一个或第二个乘法是否溢出并不重要+-Inf
,这仍然是最终的结果。 (除非Inf * 2
引发溢出乘法不会已经引发的异常标志?我不这么认为。)
他们都在同一个方向改变指数,这与x *= 4; x *= 0.5;
对于大数可能会溢出到 +Inf,因此不等于x *= 2
。另外,如果x *= 0.5; x *= 0.5;
产生低于正常的结果,它实际上could尾数右移时舍入两次; IEEE FP 具有逐渐下溢(对指数进行特殊编码的次正规),但非逐渐溢出到 +Inf。
确定优化是否安全x * 0.5 * 0.5
to x *= 0.25
超出了这个答案的范围。 GCC 和 clang 没有优化x *= 2.0f; x *= 2.0f;
into x *= 4.0f;
即使-fno-trapping-math
,但我认为这是一个错过的优化。