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

In function branch removal when a member is known at compile-time

考虑以下代码:

// 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) 被调用时 ab 整数(在这种情况下调用隐式单参数构造函数,并且 option 始终为真)?当我说 "generally" 时,我的意思是在复杂的现实世界程序中,但是在两个 int.

上调用 f

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

一般来说,常量传播(换句话说,"converting something that was passed into a function as a constant, into the constant itself" 对编译器来说不是一件很难的事情。Clang/LLVM 在编译过程中很早的时候就通过将 类 for "values we know are constants" 和 "values we don't know the value of" 在生成 LLVM-IR 时("intermediate representation",从源代码构建的代码层,不代表实际的机器代码)。其他编译器也会有类似的构造,既通过使用 IR,又通过跟踪与 non-constant 值分开的常量。

因此,假设编译器可以 "follow" 代码(例如,如果 f 和对 f 的调用位于不同的源文件中,则不太可能得到优化)。

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

// 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 位值分成两部分,并根据值决定是否 return 乘法或加法的结果。