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

时间:2016-11-16 10:27:07

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

我有一个函数,它为给定的整数找到下一个2的幂。如果整数是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和bool的减法。

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

PS:现场演示here

编辑:我没有使用gcc 6测试性能,但是使用gcc 5,后者的速度提高了约两倍(至少在合成性能测试中)。这才是让我提出这个问题的原因。

2 个答案:

答案 0 :(得分:0)

我认为原因可能是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

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

答案 1 :(得分:0)

嗯,编译器确实可以在这种特定情况下执行此优化而不违反标准。但请考虑以下略有不同的情况:

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的减法进行汇编。

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