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

标签 c++ c gcc optimization compiler-optimization

当给 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提升tempint用于右移操作。但是,当编译为 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 ,或使用减法(或一元 - )与右移:
  • 放置文字 15unsigned short变量导致两者 -xc-xc++产生合理的指示。
  • 更换 temp>>15temp/(1<<15)也会使两个选项产生合理的指令。
  • 将类次更改为 temp>>(-65521)导致两个选项产生更长的算术移位版本,-xc++还类型转换tempsigned 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/

    相关文章:

    c++ - 不同语言的 OpenCV 性能

    c - OpenGL Glut C - 想要渲染弯曲的圆柱体

    c - 了解使用 for (;clock() - now < CLOCKS_PER_SEC;) 时的程序顺序

    c++ - 基类复制构造函数的可见性问题

    c++ - 可以创建 N 个方法的类接口(interface)

    c++ - 跨窗

    c++ - 退出码3(不是我的返回值,找源码)

    C++ 如何从父方法调用子方法

    c++ - G++ 链接到 Cygwin 中的 psapi

    c - 0..9 约束在 GCC 内联汇编中有什么作用?