为什么 gcc 的右移代码在 C 和 C++ 模式下不同?

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. 优化中是否有某个阶段似乎有意义?

cly*_*yne 3

这种差异似乎是由于tempGCC 的 C 和 C++ 编译模式之间的积分提升不同所致。

使用Compiler Explorer 上的“Tree/RTL Viewer”,可以观察到当代码编译为C++ 时,GCC 会提升tempint右移操作。然而,当编译为 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,或者是由于使用减法(或一元-)与右移:

  • 将文字放入变量15unsigned short会导致-xc-xc++产生合理的指令。
  • 替换temp>>15temp/(1<<15)也会使两个选项产生合理的指令。
  • 将移位更改为temp>>(-65521)会导致两个选项生成更长的算术移位版本,并且-xc++还会在移位内temp进行强制转换。signed short
  • 将负数从移位运算 ( temp = -temp + temp>>15; return -temp;) 中移开会导致两个选项都产生合理的指令。

请参阅Godbolt 上的示例。我同意@supercat的观点,这可能只是假设规则的一个奇怪的情况。我从中看到的要点是避免使用非常量进行无符号减法,或者根据这篇关于 int 提升的 SO 帖子,也许不要尝试强制算术进入小于int存储类型。