【问题标题】:Why does the C++ compiler fail to optimize "if(test) --foo" to "foo -= test"?为什么 C++ 编译器无法将“if(test) --foo”优化为“foo -= test”?
【发布时间】:2017-03-30 12:12:29
【问题描述】:

我有一个函数可以找到给定整数的二的下一个幂。如果整数是 2 的幂,则返回幂。

非常简单:

char nextpow2if(int a)
{
    char foo = char(32 - __builtin_clz(a));
    bool ispow2 = !(a & a-1);
    if (ispow2) --foo;
    return foo;
}

但是,在使用 gcc 6 和 -O2 编译后,在检查生成的程序集后,我发现这是在计算 foo-1 后使用看似无用的指令 cmovne 编译的。更糟糕的是,使用 gcc5 和更早版本时,我在代码中得到了一个实际的 jne 分支。

编译它的更快方法就像我编写了以下函数:

char nextpow2sub(int a)
{
    char foo = char(32 - __builtin_clz(a));
    bool ispow2 = !(a & a-1);
    return foo - ispow2;
}

此代码已被所有编译器正确编译为最短(和最快)可能的程序集,带有 sete 和布尔减法。

为什么编译器无法优化第一个?这似乎是一个非常容易识别的案例。为什么 gcc 5 和更早版本将其编译为实际的 jne 分支?两个版本之间是否存在我看不到的边缘情况,这可能导致它们的行为不同?

PS:现场演示here

编辑:我还没有测试过 gcc 6 的性能,但 gcc 5 的性能大约快了两倍(至少在合成性能测试中)。这就是我真正提出这个问题的原因。

【问题讨论】:

  • 不要为不相关的语言发送垃圾标签!
  • “编译这个更快的方法就像我编写了以下函数一样:” 你测量了吗?速度快多少?
  • 您是按生成的汇编代码数量来比较性能吗?这不是一个好方法(尽管在某些情况下可能是这样)。
  • 在 GCC 5 上,后者大约快两倍。
  • 这不是一个抱怨,而是一个问题(这可能会变成向适当的人抱怨:))也许代码中有一些东西使它无法优化。我看不到,但也许有人可以。或者具有适当知识的人可能会说这是一个实际已知的编译器问题

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


【解决方案1】:

我认为原因可能是bool 通常存储在一个字节内。因此,编译器可能无法安全地假设实际内存完全等于 1。true/false 检查可能只是与零进行比较。但是,减法可能是另一回事,但有副作用。

example code on Ideone:

#include <iostream>
using namespace std;

union charBool
{
    unsigned char aChar;
    bool aBool;
};

int main() 
{
    charBool var;
    charBool* varMemory = &var;

    var.aBool = 65;
    std::cout << "a boolean = " << var.aBool << std::endl;
    std::cout << "a char = " << var.aChar << std::endl;
    std::cout << "varMemory = " << (*(reinterpret_cast<unsigned char*>(varMemory))) << std::endl;

    var.aChar = 98;   // note: Ideone C++ compiler resolves this to zero, hence bit0 seems to be the only checked
    std::cout << "a boolean = " << var.aBool << std::endl;
    std::cout << "a char = " << var.aChar << std::endl;
    std::cout << "varMemory = " << (*(reinterpret_cast<unsigned char*>(varMemory))) << std::endl;

    return 0;
}

结果:

a boolean = 1
a char = 
varMemory = 
a boolean = 0
a char = b
varMemory = b

(注意:前两个字符不可打印)

【讨论】:

  • 我不太明白这如何回答任何问题。不将其优化为更快的代码的唯一实际原因是在 as-if 规则下发现两种变体之间的一些差异。
  • @BaummitAugen 简要:假设布尔值中除零以外的任何值似乎很便宜是true。如果您想启用此“精确减 1”优化,执行第一个 - 您必须始终检查副作用(即,如果有人基于 =1)。上面的代码是一个修改布尔内部存储器的游戏(因为赋值可能会将其设置为 0/1)。
  • @hauron "你必须总是检查副作用" 但是有问题的 bool 不是天知道在哪里的任意字节,编译器可以看到它的初始化和它的整个生命周期只需检查 4 loc。这不是不合理的。
  • 这里的问题是 GCC没有将该 bool 存储在一个字节中。 Clang 可以,然后它可以减去。
  • @harold:这似乎把事情弄清楚了。所以罪魁祸首似乎是优化顺序。听起来很合理。所以它可能是一个错误,但不是主要错误。很高兴将其视为答案而不是晦涩的评论。特别是如果您有链接来支持声明。
【解决方案2】:

好吧,编译器确实可以在不违反标准的情况下在这种特定情况下执行这种优化。但请考虑以下稍微不同的情况:

char nextpow2sub(int a)
{
    char foo = char(32 - __builtin_clz(a));
    bool ispow2 = !(a & a-1);
    return foo - (5 * ispow2);
}

char nextpow2if(int a)
{
    char foo = char(32 - __builtin_clz(a));
    bool ispow2 = !(a & a-1);
    if (ispow2) foo = foo - 5;
    return foo;
}

我在这里所做的唯一更改是减去 5 而不是 1。如果您使用 gcc 6.x 进行编译并进行比较,您会发现生成的二进制代码对于两个函数具有相同的大小。我也希望它们具有或多或少相同的性能。

这表明编译器使用的优化算法是为处理一般情况而设计的。也就是说,即使是减 1,我预计(使用 gcc 6.x)在任何支持指令级并行和寄存器重命名的现代处理器上的性能都会有微小的差异。

此代码被所有编译器正确编译为最短(并且 最快)可能的汇编与setebool 的减法。

您怎么知道这是最短和最快的代码?是的,它更短更快,但你有证据证明这是最短和最快的吗?如果不指定特定的架构和微架构,您也不能给出这样的声明。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-29
    • 2017-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多