【问题标题】:Why is gcc's right-shift code different in C and C++ mode?为什么gcc的右移代码在C和C++模式下不同?
【发布时间】:2020-10-09 21:30:23
【问题描述】:

当 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 时,将参数声明为 unsigned short volatile *p 将消除 p 的冗余负载,但我很好奇为什么 gcc 9.2.1 需要一个 volatile 限定符来帮助它避免冗余负载,或者为什么这种奇怪的“优化”只发生在 C 模式而不是 C++ 模式。我也有点好奇为什么 gcc 甚至会考虑添加((short)temp) >> 15 而不是减去temp >> 15。优化中是否有某个阶段似乎有意义?

【问题讨论】:

  • 我发现 C 代码添加移位值而不是按照源代码的意图减去它真的很奇怪。
  • @MarkRansom:在优化版本中执行移位的方式会产生 0 或 -1 而不是 0 或 1,因此将减法更改为加法是正确性所必需的。与我发现的大多数 gcc 怪癖不同,这个怪癖仅产生比直接翻译慢的代码,而不是产生损坏的代码。尽管添加*p 的冗余负载会带来不必要的出错机会,但在某些情况下,如果冗余负载在语义上是可接受的,它们可以提高代码效率。不过,这似乎不是其中之一。
  • 今天早上我的大脑一定是异常的慢,我没有想到将一个 16 位的数量移动 15 位只会导致两种结果。
  • @supercat:不完全确定原因,但如果将temp 更改为unsigned int,编译器会生成相同的“合理机器码”。
  • 您可以尝试一些-fopt-info options 来更深入地了解优化器正在做什么而不是推测。

标签: c++ c gcc optimization compiler-optimization


【解决方案1】:

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

使用 Compiler Explorer 上的“Tree/RTL Viewer”,可以观察到当代码编译为 C++ 时,GCC 将 temp 提升为 int 以进行右移操作。但是,当编译为 C 时,temp 只会提升为 signed shortOn godbolt):

带有-xc++的GCC树:

{
  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;
}

只有在将temp 移动到比其 16 位大小少一位时,才会显式转换为 signed short;当移位少于 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&gt;&gt;15 替换为temp/(1&lt;&lt;15) 也会导致这两个选项产生合理的指令。
  • 将 shift 更改为 temp&gt;&gt;(-65521) 会导致两个选项生成更长的算术移位版本,-xc++ 还会在 shift 中将 temp 转换为 signed short
  • 将负数从移位操作中移开 (temp = -temp + temp&gt;&gt;15; return -temp;) 会使这两个选项产生合理的指令。

查看这些on Godbolt 的示例。我同意@supercat 的观点,这可能只是as-if rule 的一个奇怪案例。我从中看到的要点是避免使用非常数进行无符号减法,或者根据this SO post 关于 int 提升,也许不要尝试将算术强制为小于int 的存储类型。

【讨论】:

  • 这是 GCC 的(优化器的)错误吗?我在C standard 中看不到任何内容,允许转换为(signed short)。请注意,当15 也显式转换为无符号短时,似乎也会出现同样的问题。
  • @JérômeRichard:替换是将(temp&gt;&gt;15) 替换为-((signed short)(temp &gt;&gt; 15)) [注意负号]。这种替换在 C 中的 as-if 规则下是允许的,并且可能在 C++ 中也是如此,但是适应运算符重载的需要可能会使在那里应用相同的逻辑变得更加困难。
  • 我刚刚发现了一些有趣的东西:gcc 10.1 的 x86-64 版本,即使在 -O0 时,也将运算转换为算术右移值的减法,无论它是在 C 语言还是C++ 模式,但之前的版本 9.3 仅在 C 模式下进行更改。这仍然让我对为什么在编译中首先应用转换感到困惑,因为它有时会降低性能,并且不足以提高性能而不值得麻烦实施它。
  • 也许在某些平台上,将数字的 MSB 转换为 0 或 1 会比右移更快,但这种转换应该作为特定平台优化的一部分应用。
猜你喜欢
  • 2017-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-08
  • 2020-05-28
  • 2017-03-02
  • 1970-01-01
相关资源
最近更新 更多