为什么C++编译器无法优化"if(test) --foo"以"foo -= test"?

Why does the C++ compiler fail to optimize "if(test) --foo" to "foo -= test"?

本文关键字:test --foo foo 编译器 C++ if 优化 为什么      更新时间:2023-10-16

我有一个函数,它求给定整数2的下一个幂。如果整数是2的幂,则返回该整数的幂

非常直接:

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

然而,在用-O2编译gcc 6之后,在检查生成的程序集之后,我看到这是在计算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和bool的减法。

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

PS:现场演示在这里

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

我认为这样做的原因可能是bool通常存储在一个字节内。因此,编译器可能无法安全地假设实际内存恰好等于1。true/false检查可能只是与零比较。然而,减法可能是一个不同的故事,有副作用。

参见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

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

编译器确实可以在不违反标准的情况下执行此优化。但是考虑以下稍微不同的情况:

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的减法组装。

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