首页 > 解决方案 > 为什么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。优化中是否有某个阶段似乎有意义?

标签: c++cgccoptimizationcompiler-optimization

解决方案


差异似乎是由于tempGCC 的 C 和 C++ 编译模式之间的整体提升不同。

使用 Compiler Explorer 上的“Tree/RTL Viewer”,可以观察到当代码编译为 C++ 时,GCC 会提升tempint右移操作。但是,当编译为 C 时temp,仅提升为signed shortOn 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 chars 并移位 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>>15temp/(1<<15)也会导致两个选项都产生合理的指令。
  • 将移位更改为temp>>(-65521)会导致两个选项产生更长的算术移位版本,并且-xc++还会在移位内转换tempsigned short
  • 将负数从移位操作 ( temp = -temp + temp>>15; return -temp;) 中移开会导致两个选项都产生合理的指令。

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


推荐阅读