sup*_*cat 10 c c++ optimization gcc compiler-optimization
当给 ARM gcc 9.2.1 提供命令行选项-O3 -xc++ -mcpu=cortex-m0[compile as C++] 和以下代码时:
unsigned short adjust(unsigned short *p)
{
unsigned short temp = *p;
temp -= temp>>15;
return temp;
}
Run Code Online (Sandbox Code Playgroud)
它产生合理的机器代码:
ldrh r0, [r0]
lsrs r3, r0, #15
subs r0, r0, r3
uxth r0, r0
bx lr
Run Code Online (Sandbox Code Playgroud)
这相当于:
unsigned short adjust(unsigned short *p)
{
unsigned r0,r3;
r0 = *p;
r3 = temp >> 15;
r0 -= r3;
r0 &= 0xFFFFu; // Returning an unsigned short requires...
return r0; // computing a 32-bit unsigned value 0-65535.
}
Run Code Online (Sandbox Code Playgroud)
非常合理。在这种特殊情况下实际上可以省略最后一个“uxtw”,但是对于无法证明此类优化安全性的编译器而言,谨慎起见比冒险返回 0-65535 范围之外的值要好可以完全下沉下游代码。
但是,当使用-O3 -xc -mcpu=cortex-m0[相同的选项,除了编译为 C 而不是 C++] 时,代码会发生变化:
ldrh r3, [r0]
movs r2, #0
ldrsh r0, [r0, r2]
asrs r0, r0, #15
adds r0, r0, r3
uxth r0, r0
bx lr
unsigned short adjust(unsigned short *p)
{
unsigned r0,r2,r3;
r3 = *p;
r2 = 0;
r0 = ((unsigned short*)p)[r2];
r0 = ((int)r0) >> 15; // Effectively computes -((*p)>>15) with redundant load
r0 += r3
r0 &= 0xFFFFu; // Returning an unsigned short requires...
return temp; // computing a 32-bit unsigned value 0-65535.
}
Run Code Online (Sandbox Code Playgroud)
我知道左移定义的极端情况在 C 和 C++ 中是不同的,但我认为右移是相同的。C 和 C++ 中右移的工作方式有什么不同,会导致编译器使用不同的代码来处理它们吗?9.2.1 之前的版本在 C 模式下生成的不良代码略少:
ldrh r3, [r0]
sxth r0, r3
asrs r0, r0, #15
adds r0, r0, r3
uxth r0, r0
bx lr
Run Code Online (Sandbox Code Playgroud)
相当于:
unsigned short adjust(unsigned short *p)
{
unsigned r0,r3;
r3 = *p;
r0 = (short)r3;
r0 = ((int)r0) >> 15; // Effectively computes -(temp>>15)
r0 += r3
r0 &= 0xFFFFu; // Returning an unsigned short requires...
return temp; // computing a 32-bit unsigned value 0-65535.
}
Run Code Online (Sandbox Code Playgroud)
没有 9.2.1 版本那么糟糕,但仍然比直接翻译代码更长的指令。在使用 9.2.1 时,声明参数 asunsigned short volatile *p将消除 的冗余负载p,但我很好奇为什么 gcc 9.2.1 需要一个volatile限定符来帮助它避免冗余负载,或者为什么这种奇怪的“优化”只发生在C 模式而不是 C++ 模式。我也有点好奇为什么 gcc 甚至会考虑添加((short)temp) >> 15而不是减去temp >> 15. 优化中是否有某个阶段似乎有意义?
这种差异似乎是由于tempGCC 的 C 和 C++ 编译模式之间的积分提升不同所致。
使用Compiler Explorer 上的“Tree/RTL Viewer”,可以观察到当代码编译为C++ 时,GCC 会提升temp为int右移操作。然而,当编译为 C 时temp,仅提升为 a signed short( On godbolt ):
GCC 树-xc++:
{
short unsigned int temp = *p;
# DEBUG BEGIN STMT;
short unsigned int temp = *p;
# DEBUG BEGIN STMT;
<<cleanup_point <<< Unknown tree: expr_stmt
(void) (temp = temp - (short unsigned int) ((int) temp >> 15)) >>>>>;
# DEBUG BEGIN STMT;
return <retval> = temp;
}
Run Code Online (Sandbox Code Playgroud)
和-xc:
{
short unsigned int temp = *p;
# DEBUG BEGIN STMT;
short unsigned int temp = *p;
# DEBUG BEGIN STMT;
temp = (short unsigned int) ((signed short) temp >> 15) + temp;
# DEBUG BEGIN STMT;
return temp;
}
Run Code Online (Sandbox Code Playgroud)
仅当移位比其 16 位大小少一位signed short时,才进行显式转换;temp当移位少于 15 位时,转换就会消失,并且代码会编译以匹配-xc++生成的“合理”指令。unsigned char使用s 并移位 7 位时也会出现意外行为。
有趣的是,armv7-a clang 不会产生相同的行为;两者-xc都会-xc++产生“合理”的结果:
ldrh r0, [r0]
sxth r0, r0
lsrs r1, r0, #15
adds r0, r1, r0
uxth r0, r0
bx lr
Run Code Online (Sandbox Code Playgroud)
更新:所以看来这种“优化”是由于文字15,或者是由于使用减法(或一元-)与右移:
15中unsigned short会导致-xc和-xc++产生合理的指令。temp>>15为temp/(1<<15)也会使两个选项产生合理的指令。temp>>(-65521)会导致两个选项生成更长的算术移位版本,并且-xc++还会在移位内temp进行强制转换。signed shorttemp = -temp + temp>>15; return -temp;) 中移开会导致两个选项都产生合理的指令。请参阅Godbolt 上的示例。我同意@supercat的观点,这可能只是假设规则的一个奇怪的情况。我从中看到的要点是避免使用非常量进行无符号减法,或者根据这篇关于 int 提升的 SO 帖子,也许不要尝试强制算术进入小于int存储类型。