g ++(4.7.2)和类似版本似乎在编译期间以惊人的速度评估constexpr.事实上在我的机器上比运行时编译的程序快得多.
这种行为有合理的解释吗?是否存在仅在编译时适用的优化技术,可以比实际编译的代码更快地执行?如果是这样,哪个?
这是我的测试程序和观察结果.
#include <iostream>
constexpr int mc91(int n)
{
return (n > 100)? n-10 : mc91(mc91(n+11));
}
constexpr double foo(double n)
{
return (n>2)? (0.9999)*((unsigned int)(foo(n-1)+foo(n-2))%100):1;
}
constexpr unsigned ack( unsigned m, unsigned n )
{
return m == 0
? n + 1
: n == 0
? ack( m - 1, 1 )
: ack( m - 1, ack( m, n - 1 ) );
}
constexpr unsigned slow91(int n) {
return mc91(mc91(foo(n))%100);
}
int main(void)
{
constexpr unsigned int compiletime_ack=ack(3,14);
constexpr int compiletime_91=slow91(49);
static_assert( compiletime_ack == 131069, "Must be evaluated at compile-time" );
static_assert( compiletime_91 == 91, "Must be evaluated at compile-time" );
std::cout << compiletime_ack << std::endl;
std::cout << compiletime_91 << std::endl;
std::cout << ack(3,14) << std::endl;
std::cout << slow91(49) << std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
编译时:
time g++ constexpr.cpp -std=c++11 -fconstexpr-depth=10000000 -O3
real 0m0.645s
user 0m0.600s
sys 0m0.032s
Run Code Online (Sandbox Code Playgroud)
运行:
time ./a.out
131069
91
131069
91
real 0m43.708s
user 0m43.567s
sys 0m0.008s
Run Code Online (Sandbox Code Playgroud)
这里mc91是通常的mac carthy f91(可以在wikipedia上找到),而foo只是一个无用的函数,返回大约1到100之间的实际值,具有fib运行时复杂性.
编译器和编译程序使用相同的参数来评估91的慢速计算和ackermann函数.
令人惊讶的是,程序甚至可以运行得更快,只需生成代码并通过编译器运行它,而不是执行代码本身.
Dre*_*ann 14
在编译时,constexpr可以记忆冗余(相同)调用,而运行时递归行为不提供此功能.
如果你改变每个递归函数,如...
constexpr unsigned slow91(int n) {
return mc91(mc91(foo(n))%100);
}
Run Code Online (Sandbox Code Playgroud)
...的形式,是不是constexpr,但不记得在运行时过去算了一笔账:
std::unordered_map< int, boost::optional<unsigned> > results4;
// parameter(s) ^^^ result ^^^^^^^^
unsigned slow91(int n) {
boost::optional<unsigned> &ret = results4[n];
if ( !ret )
{
ret = mc91(mc91(foo(n))%100);
}
return *ret;
}
Run Code Online (Sandbox Code Playgroud)
你会得到不那么令人惊讶的结果.
编译时:
time g++ test.cpp -std=c++11 -O3
real 0m1.708s
user 0m1.496s
sys 0m0.176s
Run Code Online (Sandbox Code Playgroud)
运行:
time ./a.out
131069
91
131069
91
real 0m0.097s
user 0m0.064s
sys 0m0.032s
Run Code Online (Sandbox Code Playgroud)
这是一个非常有趣的"发现",但答案可能比你想象的更简单.
如果在编译时所有涉及的值都是已知的(并且如果值应该结束的变量也被声明为constexpr),则可以在声明编译时评估某些内容,并且可以想象下面的伪代码:constexpr
f(x) = g(x)
g(x) = x + h(x,x)
h(x,y) = x + y
Run Code Online (Sandbox Code Playgroud)
因为每个值在编译时都是已知的,所以编译器可以将上面的内容重写为等效的,如下:
f(x) = x + x + x
Run Code Online (Sandbox Code Playgroud)
换句话说,每个函数调用都已被删除,并替换为表达式本身的函数调用.同样适用的是一种称为memoization的方法,其中存储的计算表达式的结果被存储起来,因此您只需要进行一次艰苦的工作.
如果你知道g(5) = 15为什么再次计算它?而只需g(5)在15每次需要时替换,这是可能的,因为声明的函数constexpr不允许有副作用.
在运行时,这没有发生(因为我们没有告诉代码以这种方式运行).通过您的代码运行的小家伙将需要跳转f到g向h,然后跳回g从h它跳跃之前g到f,而他存储每个函数的返回值,并将其传递给下一个个都.
即使这个人非常小,并且他不需要跳得很远,他仍然不喜欢一直来回跳跃,他需要做很多事情并且这样做; 这需要时间.
是的,对于那些不相信编译器实际计算并将其作为常量二进制文件中的常量的人,我将从下面的OP代码中提供相关的汇编指令(输出g++ -S -Wall -pedantic -fconstexpr-depth=1000000 -std=c++11)
main:
.LFB1200:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $131069, -4(%rbp)
movl $91, -8(%rbp)
movl $131069, %esi # one of the values from constexpr
movl $_ZSt4cout, %edi
call _ZNSolsEj
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E
movl $91, %esi # the other value from our constexpr
movl $_ZSt4cout, %edi
call _ZNSolsEi
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
movq %rax, %rdi
# ...
# a lot of jumping is taking place down here
# see the full output at http://codepad.org/Q8D7c41y
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1527 次 |
| 最近记录: |