当给 ARM gcc 9.2.1 提供命令行选项时 -O3 -xc++ -mcpu=cortex-m0
[编译为 C++] 和以下代码:
unsigned short adjust(unsigned short *p)
{
unsigned short temp = *p;
temp -= temp>>15;
return temp;
}
它产生合理的机器代码:
ldrh r0, [r0]
lsrs r3, r0, #15
subs r0, r0, r3
uxth r0, r0
bx lr
这相当于:
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.
}
非常合理。在这种特殊情况下实际上可以省略最后一个“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.
}
我知道左移定义的极端情况在 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
相当于:
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.
}
不像 9.2.1 版本那么糟糕,但仍然比直接翻译代码更长的指令。使用 9.2.1 时,将参数声明为
unsigned short volatile *p
将消除 p
的冗余负载,但我很好奇为什么 gcc 9.2.1 需要 volatile
限定符来帮助它避免冗余负载,或者为什么这种奇怪的“优化”只发生在 C 模式而不是 C++ 模式。我也有点好奇为什么 gcc 会考虑添加 ((short)temp) >> 15
而不是减去 temp >> 15
.优化中是否有某个阶段似乎有意义?
最佳答案
差异似乎是由于temp
积分提升的差异所致。 GCC 的 C 和 C++ 编译模式之间。
使用Compiler Explorer上的“Tree/RTL Viewer”,可以观察到当代码被编译为C++时,GCC提升temp
到 int
用于右移操作。但是,当编译为 C 时 temp
仅提升为 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;
}
与 -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;
}
投到signed short
仅在移动 temp
时才明确比其 16 位大小少一位;当移位少于 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
更新:因此,这种“优化”似乎是由于文字
15
,或使用减法(或一元 -
)与右移:15
在 unsigned short
变量导致两者 -xc
和 -xc++
产生合理的指示。 temp>>15
与 temp/(1<<15)
也会使两个选项产生合理的指令。 temp>>(-65521)
导致两个选项产生更长的算术移位版本,-xc++
还类型转换temp
至 signed short
换类之内。 temp = -temp + temp>>15; return -temp;
)中移开会导致两个选项产生合理的指令。 查看这些示例 on Godbolt .我同意@supercat 的观点,这可能只是 as-if rule 的一个奇怪案例。 .我从中看到的要点是避免使用非常量进行无符号减法,或者按照 this SO post关于 int 提升,也许不要试图强制算术小于-
int
存储类型。
关于c++ - 为什么 gcc 的右移代码在 C 和 C++ 模式下不同?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62473454/