c++ - 为什么gcc的右移代码在C和C++模式下不同?
问题描述
当 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;
}
它产生合理的机器代码:
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 时,将参数声明为 asunsigned 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;
}
仅当移位比其 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
更新:所以看起来这种“优化”是由于文字15
,或者是由于使用了减法(或一元-
)和右移:
- 将文字
15
放在unsigned short
变量中会导致-xc
并-xc++
产生合理的指令。 - 替换
temp>>15
为temp/(1<<15)
也会导致两个选项都产生合理的指令。 - 将移位更改为
temp>>(-65521)
会导致两个选项产生更长的算术移位版本,并且-xc++
还会在移位内转换temp
为signed short
。 - 将负数从移位操作 (
temp = -temp + temp>>15; return -temp;
) 中移开会导致两个选项都产生合理的指令。
请参阅Godbolt 上的这些示例。我同意@supercat 的观点,这可能只是as-if 规则的一个奇怪情况。我从中看到的要点是避免使用非常数进行无符号减法,或者根据这篇关于 int 提升的 SO 帖子,也许不要尝试将算术强制转换为小于int
存储的类型。
推荐阅读
- html - 为什么按钮会向下移动,尽管它被指定为内联?
- python - tKinter 和 PySimpleGUI 中的帧大小问题
- vue.js - 如果 API 变量发生变化,如何更新 vue 变量?
- c# - 在 Selected 事件之后访问 SQLDataSource 数据
- javascript - 将类型文件的输入更改为图标 - 如何删除路径预览并更改样式?反应,JS
- c# - 是否有一种有效的方法可以使用 Entity Framework Core、通用方法和复合键将本地数据集软同步到 SQL Server 表?
- prometheus - 转换 Grafana Label 并在 prometheus 查询中使用
- android - 井字游戏按钮阵列横向重力不起作用
- php - 在 SELECT 语句之后标记所有选定的列
- ios - iOS Core Data 和标准差 NSExpression 崩溃