【问题标题】:g++ c++11 constexpr evaluation performanceg++ c++11 constexpr 评估性能
【发布时间】:2013-04-25 18:08:29
【问题描述】:

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;
}

编译时间:

time g++ constexpr.cpp -std=c++11 -fconstexpr-depth=10000000 -O3 

real    0m0.645s
user    0m0.600s
sys     0m0.032s

运行时:

time ./a.out 

131069
91
131069
91

real    0m43.708s
user    0m43.567s
sys     0m0.008s

这里的 mc91 是通常的 mac carthy f91(可以在 wikipedia 上找到),而 foo 只是一个返回大约 1 到 100 之间的实际值的无用函数,具有 fib 运行时复杂性。

91 的慢速计算和 ackermann 函数都由编译器和编译程序使用相同的参数进行评估。

令人惊讶的是,该程序甚至会运行得更快,只需生成代码并通过编译器运行它,而不是执行代码本身。

【问题讨论】:

  • 你怎么知道编译器计算了这些表达式?
  • this 基准测试期间的相同问题。阅读最后一段。
  • 编辑:按照 Drew Dormann 的建议添加了 static_assert 以使示例更具说服力(不会改变结果。)

标签: c++ constexpr


【解决方案1】:

在编译时,冗余(相同)constexpr 调用可以是 memoized,而运行时递归行为不提供此功能。

如果您更改每个递归函数,例如...

constexpr unsigned slow91(int n) {
   return mc91(mc91(foo(n))%100);
}

...转换为不是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;
}

你会得到不那么令人惊讶的结果。

编译时间:

time g++ test.cpp -std=c++11 -O3

real    0m1.708s
user    0m1.496s
sys     0m0.176s

运行时:

time ./a.out

131069
91
131069
91

real    0m0.097s
user    0m0.064s
sys     0m0.032s

【讨论】:

  • 分配给constexpr 变量是不允许运行时评估的情况之一。
  • @bames53 是的,我想知道 gcc 在这种情况下是否绕过标准。我怀疑在这个答案中测试这两行会告诉我们。
  • 查看我的回答,这些值实际上是在编译时评估的(正如我的汇编器输出所证明的那样)。当然,static_assert 会证明同样的事情。
  • 是的,使用相同的参数评估表达式一次(记忆化)是我的理论首先,因此这种技术不适用于实数斐波那契类似 foo 函数。关于递归深度:没错,我们在这里不标准,但使用如此大的值更好地展示了观察到的效果。
  • 此外,由于可以排除诸如记忆化之类的简单技术用于实值函数,在编译时仍然可以更快地进行评估,这类问题可能会成为支持源代码级转换的解释语言的一个点在运行时可以吗?
【解决方案2】:

记忆

这是一个非常有趣的“发现”,但答案可能比你想象的要简单。

如果所有涉及的值在编译时都是已知的(并且如果声明了值应该结束的变量constexpr),则可以在声明constexpr 时评估某些东西 以及)说想象以下伪代码:

f(x)   = g(x)
g(x)   = x + h(x,x)
h(x,y) = x + y

由于每个值在编译时都是已知的,编译器可以将上面的内容重写为等价的下面:

f(x) = x + x + x

换句话说,每个函数调用都已被删除并替换为表达式本身的调用。同样适用的是一种称为memoization 的方法,其中将传递的计算表达式的结果存储起来,因此您只需执行一次繁重的工作。

如果你知道g(5) = 15 为什么还要计算呢?而是在每次需要时将g(5) 替换为15,这是可能的,因为声明为constexpr 的函数不允许有副作用


运行时

在运行时这不会发生(因为我们没有告诉代码以这种方式运行)。运行您的代码的小家伙需要从f 跳转到gh,然后在从g 跳转到f 之前从h 跳转回g。每个函数的返回值并将其传递给下一个函数。

即使这家伙很小很小,他不需要跳得很远,他也不喜欢一直来回跳,他做这个做那个需要很多;这需要时间。


但是在 OPs 的例子中,真的是计算出来的compile-time吗?

是的,对于那些不相信编译器实际计算并把它作为常量放入完成的二进制文件的人,我将从下面的 OPs 代码中提供相关的汇编指令(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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-27
    • 1970-01-01
    • 2019-08-12
    • 2021-02-13
    • 1970-01-01
    • 2012-10-25
    • 1970-01-01
    • 2020-01-13
    相关资源
    最近更新 更多