在成员在编译时已知时删除函数分支

时间:2016-01-17 15:43:30

标签: c++ c++11 constructor compiler-optimization constexpr

请考虑以下代码:

// Class definition
class myclass
{
    public:
    constexpr myclass() noexcept: _value{0}, _option{true} {}
    constexpr myclass(int value) noexcept: _value{value}, _option{true} {}
    constexpr myclass(int value, bool option) noexcept: _value{value}, _option{option} {}
    constexpr int get_value() const noexcept {return _value;}
    constexpr int get_option() const noexcept {return _option;}
    private:
    int _value;
    bool _option;
};

// Some function that should be super-optimized
int f(myclass x, myclass y) 
{
    if (x.get_option() && y.get_option()) {
        return x.get_value() + y.get_value();
    } else {
        return x.get_value() * y.get_value();
    }
}

我的问题如下:在这种模式中,编译器通常能够在编译时知道该选项时避免测试,例如,使用f(a, b)调用a时{ {1}}整数(在这种情况下调用隐式单参数构造函数,b总是为真)?当我说"一般"时,我指的是复杂的真实世界计划,但option在两个f上被调用。

1 个答案:

答案 0 :(得分:1)

简短的回答是"取决于"。这取决于很多东西,包括代码的复杂性,使用的编译器等。

一般来说,常量传播(换句话说,"将作为常量传递给函数的内容转换为常量本身"对于编译器来说并不是一件非常困难的事情.Clang / LLVM这样做在编译过程中很早就有了单独的类,我们知道的值是常量"和#34;我们在生成LLVM-IR时不知道"的值(&#34) 34;中间表示",从源代码构建的代码层,它不代表实际的机器代码。)其他编译器也将有类似的结构,通过使用IR,并通过跟踪常量分离非 - 恒定值。

所以,假设编译器可以"跟随"代码(例如ff的调用位于不同的源文件中,它不太可能得到优化)。

当然,如果您想确定您的特定编译器对您的特定代码所做的事情,您将必须检查编译器生成的代码。

// Class definition
class myclass
{
    public:
    constexpr myclass() noexcept: _value{0}, _option{true} {}
    constexpr myclass(int value) noexcept: _value{value}, _option{true} {}
    constexpr myclass(int value, bool option) noexcept: _value{value}, _option{option} {}
    constexpr int get_value() const noexcept {return _value;}
    constexpr int get_option() const noexcept {return _option;}
    private:
    int _value;
    bool _option;
};

// Some function that should be super-optimized
int f(myclass x, myclass y) 
{
    if (x.get_option() && y.get_option()) {
        return x.get_value() + y.get_value();
    } else {
        return x.get_value() * y.get_value();
    }
}

int main()
{
    myclass a;
    myclass b(1);
    myclass c(2, false);

    int x = f(a, b);
    int y = f(b, c);

    return x + y;
}

这将生成与以下内容相同的代码:

int main()
{
    return 3;
}

但是,如果我们将代码更改为:

#include "myclass.h"

extern int f(myclass x, myclass y);

int main()
{
    myclass a;
    myclass b(1);
    myclass c(2, false);

    int x = f(a, b);
    int y = f(b, c);

    return x + y;
}

并在单独的文件中声明f-O2优化),结果代码为

define i32 @_Z1f7myclassS_(i64 %x.coerce, i64 %y.coerce) #0 {
entry:
  %x.sroa.0.0.extract.trunc = trunc i64 %x.coerce to i32
  %y.sroa.0.0.extract.trunc = trunc i64 %y.coerce to i32
  %conv.i = and i64 %x.coerce, 1095216660480
  %tobool = icmp eq i64 %conv.i, 0
  %conv.i12 = and i64 %y.coerce, 1095216660480
  %tobool2 = icmp eq i64 %conv.i12, 0
  %or.cond = or i1 %tobool, %tobool2
  %add = add nsw i32 %y.sroa.0.0.extract.trunc, %x.sroa.0.0.extract.trunc
  %mul = mul nsw i32 %y.sroa.0.0.extract.trunc, %x.sroa.0.0.extract.trunc
  %retval.0 = select i1 %or.cond, i32 %mul, i32 %add
  ret i32 %retval.0
}

和主要:

define i32 @main() #0 {
entry:
  %call = tail call i32 @_Z1f7myclassS_(i64 4294967296, i64 4294967297)
  %call4 = tail call i32 @_Z1f7myclassS_(i64 4294967297, i64 2)
  %add = add nsw i32 %call4, %call
  ret i32 %add
}

如您所见,f的参数转换为两个64位整数,option的值存储在64位值的上半部分。函数f然后将64位值分成两部分,并根据值决定是否返回乘法或加法的结果。