在编译时已知成员时在函数分支删除中
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)
被调用时 a
和 b
整数(在这种情况下调用隐式单参数构造函数,并且 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 乘法或加法的结果。
考虑以下代码:
// 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
和 b
整数(在这种情况下调用隐式单参数构造函数,并且 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 乘法或加法的结果。