优化器能否消除从 64 位到 32 位整数类型的赋值?
Can the optimizer elide assignment from 64bit to 32bit integer types?
鉴于此:
uint64_t truncate(uint64_t num) {
uint32_t lessBits = static_cast<uint32_t>(num);
return static_cast<uint64_t>(lessBits);
}
是否允许编译器优化静态转换并保留完整的 64 位值,还是必须保留 32 位“截断”?
快速 godbold check, it would appear a mov
eax
使用 , ...
,这意味着 32 位“截断”未被优化。
语言如何涵盖这种情况?我目前的猜测是,这只是 Integer Conversions 被完全定义并且编译器观察到这一点的情况,因此它不能省略 assignment/32bit 转换。
在C语言中,整数类型之间的转换是强制性的(as-if规则除外,这意味着如果没有任何区别,编译器可以做任何它喜欢的事情。所以如果每个return函数的值被忽略或赋值给一个32位整数,那么编译器将被允许不转换。)
浮点类型之间的转换可能不是强制性的;编译器可以自由使用比要求更高的精度。但是,由于强制转换或由于分配给变量而导致的转换是强制性的。此外,还有一些预定义的宏,可让您检测编译器的作用。
所以
float x = 3.1;
float y = 3.2;
double d = x * y;
double e = (float) (x * y);
float z = x * y;
必须将 3.1f 和 3.2f 分配给 x 和 y。 x * y 可以用浮点数或双精度计算,所以 d 可能有不同的结果。 e 和 z 不能。 (这是因为 double 的尾数位多于 float 的两倍,因此以无限精度计算 x*y,先四舍五入到 double 再到 float,产生与仅四舍五入到 float 相同的结果。如果涉及 long double,事情可能会有所不同)。
由于优化绝不能影响定义的程序行为的原则,以及为了允许优化的必然结果,标准必须将其行为可能影响其行为的所有行为描述为未定义行为,因此没有定义的情况该标准将允许实现进行您描述的那种替换。如果一个实现作为“一致语言扩展”的一种形式,指定除 divide/remainder 以外的整数计算将永远不会产生任何副作用,除了产生可能截断的值,这些值可能在正常范围内,也可能不在正常范围内他们的类型,然后是 32 位 int
和 64 位 long
的实现,给出类似的东西:
long test(int *arr, int count)
{
long total = 0;
int subtotal = 0;
for (int i=0; i<count; i++)
{
subtotal += arr[i];
total += subtotal;
}
return total;
}
可能会在闲暇时将小计保持为 32 位 int
并在循环的每次传递中将其符号扩展为 64 位,然后再将其添加到 total
,或者它可以签名- 在从内存加载时扩展 arr[i]
的每个项目并将 subtotal
保持为 64 位值,并且在溢出的情况下可能会观察到这种行为差异,因为实现的“符合语言扩展”。
请注意,不幸的是,无法邀请编译器对小于 int
的类型执行此类优化,因为实现需要记录 consistent 规则将 int
和 unsigned
转换为较小的有符号类型,因此不允许在不截断的情况下简单地将较小的类型存储在 32 位寄存器中,并允许读出它们的全部值,即使这样做会比截断更有效。
鉴于此:
uint64_t truncate(uint64_t num) {
uint32_t lessBits = static_cast<uint32_t>(num);
return static_cast<uint64_t>(lessBits);
}
是否允许编译器优化静态转换并保留完整的 64 位值,还是必须保留 32 位“截断”?
快速 godbold check, it would appear a mov
eax
使用 , ...
,这意味着 32 位“截断”未被优化。
语言如何涵盖这种情况?我目前的猜测是,这只是 Integer Conversions 被完全定义并且编译器观察到这一点的情况,因此它不能省略 assignment/32bit 转换。
在C语言中,整数类型之间的转换是强制性的(as-if规则除外,这意味着如果没有任何区别,编译器可以做任何它喜欢的事情。所以如果每个return函数的值被忽略或赋值给一个32位整数,那么编译器将被允许不转换。)
浮点类型之间的转换可能不是强制性的;编译器可以自由使用比要求更高的精度。但是,由于强制转换或由于分配给变量而导致的转换是强制性的。此外,还有一些预定义的宏,可让您检测编译器的作用。
所以
float x = 3.1;
float y = 3.2;
double d = x * y;
double e = (float) (x * y);
float z = x * y;
必须将 3.1f 和 3.2f 分配给 x 和 y。 x * y 可以用浮点数或双精度计算,所以 d 可能有不同的结果。 e 和 z 不能。 (这是因为 double 的尾数位多于 float 的两倍,因此以无限精度计算 x*y,先四舍五入到 double 再到 float,产生与仅四舍五入到 float 相同的结果。如果涉及 long double,事情可能会有所不同)。
由于优化绝不能影响定义的程序行为的原则,以及为了允许优化的必然结果,标准必须将其行为可能影响其行为的所有行为描述为未定义行为,因此没有定义的情况该标准将允许实现进行您描述的那种替换。如果一个实现作为“一致语言扩展”的一种形式,指定除 divide/remainder 以外的整数计算将永远不会产生任何副作用,除了产生可能截断的值,这些值可能在正常范围内,也可能不在正常范围内他们的类型,然后是 32 位 int
和 64 位 long
的实现,给出类似的东西:
long test(int *arr, int count)
{
long total = 0;
int subtotal = 0;
for (int i=0; i<count; i++)
{
subtotal += arr[i];
total += subtotal;
}
return total;
}
可能会在闲暇时将小计保持为 32 位 int
并在循环的每次传递中将其符号扩展为 64 位,然后再将其添加到 total
,或者它可以签名- 在从内存加载时扩展 arr[i]
的每个项目并将 subtotal
保持为 64 位值,并且在溢出的情况下可能会观察到这种行为差异,因为实现的“符合语言扩展”。
请注意,不幸的是,无法邀请编译器对小于 int
的类型执行此类优化,因为实现需要记录 consistent 规则将 int
和 unsigned
转换为较小的有符号类型,因此不允许在不截断的情况下简单地将较小的类型存储在 32 位寄存器中,并允许读出它们的全部值,即使这样做会比截断更有效。