【问题标题】:Why is initialization of variable with constexpr evaluated at runtime instead of at compile time为什么在运行时而不是在编译时使用 constexpr 初始化变量
【发布时间】:2021-05-05 11:42:48
【问题描述】:

据我了解,关键字constexpr 告诉编译器表达式的计算可以在编译时发生。具体来说,变量上的constexpr 意味着可以在编译时评估变量的值,而函数上的constexpr 意味着可以在编译时调用该函数并评估其返回值。如果函数在运行时被调用,它只是作为一个通用函数。

今天写了一段代码尝试使用constexpr

#include <iostream>

using namespace std;

constexpr long int fib(int n)
{
    return (n <= 1)? n : fib(n-1) + fib(n-2);
}

int main ()
{
    constexpr long int res = fib(32);
    // const long int res = fib(32);
    
    cout << res << endl;
    return 0;
}

我原以为代码的编译会花费很多时间,但我错了。编译只用了 0.0290s:

$ time g++ test.cpp
real    0m0.290s
user    0m0.252s
sys     0m0.035s

但如果我将constexpr long int res = fib(32); 更改为const long int res = fib(32);,令我惊讶的是,它在编译上花费了更多时间:

$ time g++ test.cpp
real    0m5.830s
user    0m5.568s
sys     0m0.233s

总之,似乎 const 使函数fib(32) 在编译时被评估,但constexpr 使它在运行时被评估。我真的很困惑。

我的系统:Ubuntu 18.04

我的 gcc:g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

【问题讨论】:

  • 你真的要检查生成的程序集。 Godbolt 需要更多时间来使用 GCC 编译 const 版本,但两者都生成相同的程序集 (godbolt.org/z/rsq94a)。它绝对在运行时执行。
  • 编译时间并不能很好地表明编译器是否在编译期间评估了表达式/函数。编译时间 - 对于任何版本的代码 - 也不一定是可重复的,因为其他执行进程可能会影响编译执行所需的时间。在得出函数调用已被内联的结论之前 - 无论哪种情况 - 尝试检查编译器输出。
  • 您应该删除问题的最后 2 段。你关于更长编译时间的结论是错误的,但constconstexpr 花费更长的时间这一事实很有趣。请添加有关您正在使用的编译器和版本的详细信息。
  • 启用优化后,两个版本的结果代码没有任何差异:godbolt.org/z/zTneae
  • @cigien 谢谢,你是对的。我已经删除了它们。

标签: c++ c++11 compilation constants constexpr


【解决方案1】:

通过inspecting 生成的程序集,我们可以确认在两种情况 G++ 7.5 在编译时计算了fib(32) 值:

    movl    $2178309, %esi

G++ 如此快速地评估constexpr 上下文的原因是由于它在评估constexprtemplate 上下文时执行的memoization

Memoization 通过将斐波那契计算复杂度降低到 O(N) 复杂度,完全消除了它。

那么为什么非constexpr 的评估会慢很多呢?我认为这是优化器中的错误/缺点。如果我尝试使用 G++ 8.1 或更高版本,编译时间没有区别,所以大概已经解决了。

【讨论】:

    【解决方案2】:

    编译时间短并不能证明调用未在编译时进行评估。您可以查看已编译的程序集,看看它实际上是在编译时使用 constexpr 在我的测试中评估的:https://godbolt.org/z/vbWaxe

    在我使用较新编译器的测试中,const 的速度并不比constexpr 慢。这可能是您的编译器版本的实施质量问题。

    【讨论】:

      【解决方案3】:

      快速编译时评估的秘诀在于,只有极少数的斐波那契数可以适合当今最大的数据类型 unsigned long long。

      斐波那契数计算的基本信息是:根本不需要在运行时进行任何计算!一切都可以而且应该在编译时完成。然后,可以使用简单的查找机制。这将永远是最有效和最快的解决方案。

      因此,使用比内特的公式,我们可以计算出只有 很少 斐波那契数可以适合 C++ unsigned long long 数据类型,该数据类型现在通常在 2021 年为 64 位,并且是“最大”的可用数据类型。 93 号环岛。现在这个数字真的很低。

      借助现代 C++ 17(及更高版本)功能,我们可以在编译时轻松创建 64 位数据类型的所有斐波那契数的 std::array。因为如上所述,只有 93 个数字。

      因此,我们将只为查找数组花费 93*8= 744 BYTE 的非运行时内存。这实在是微不足道。而且,编译器可以快速获取这些值。

      编译时间计算后,我们可以很容易的通过写FIB[n]得到斐波那契数n。详细解释见下文。

      而且,如果我们想知道,如果一个数字是斐波那契,那么我们使用std::binary_search 来查找值。所以,这个函数将是例如:

      bool isFib(const unsigned long long numberToBeChecked) {
          return std::binary_search(FIB.begin(), FIB.end(), numberToBeChecked);
      }
      

      FIB(当然任何其他可能的名称)是一个编译时间,constexpr std::array。那么,如何构建这个数组呢?

      我们首先将计算斐波那契数的默认方法定义为constexpr 函数(非递归):

      // Constexpr function to calculate the nth Fibonacci number
      constexpr unsigned long long getFibonacciNumber(size_t index) noexcept {
          // Initialize first two even numbers 
          unsigned long long f1{ 0 }, f2{ 1 };
      
          // Calculating Fibonacci value 
          while (index--) {
              // get next value of Fibonacci sequence 
              unsigned long long f3 = f2 + f1;
              // Move to next number
              f1 = f2;
              f2 = f3;
          }
          return f2;
      }
      

      这样,斐波那契数可以在编译时轻松计算为constexpr values。然后,我们用所有斐波那契数填充std::array。我们还使用了constexpr 并使其成为带有可变参数包的模板。

      我们使用std::integer_sequence 为索引0、1、2、3、4、5、...创建一个斐波那契数。

      这很简单,并不复杂:

      template <size_t... ManyIndices>
      constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept {
          return std::array<unsigned long long, sizeof...(ManyIndices)>{ { getFibonacciNumber(ManyIndices)... } };
      };
      

      这个函数将输入一个整数序列 0,1,2,3,4,... 并返回一个 std::array&lt;unsigned long long, ...&gt; 和相应的斐波那契数。

      我们知道我们最多可以存储 93 个值。因此我们创建了一个 next 函数,它将使用整数序列 1,2,3,4,...,92,93 调用上述函数,如下所示:

      constexpr auto generateArray() noexcept {
          return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
      }
      

      现在,终于,

      constexpr auto FIB = generateArray();
      

      将给我们一个编译时std::array&lt;unsigned long long, 93&gt;,名称为 FIB,包含所有斐波那契数。如果我们需要第 i 个斐波那契数,那么我们可以简单地写 FIB[i]。运行时不会进行计算。


      整个示例程序如下所示:

      #include <iostream>
      #include <array>
      #include <utility>
      #include <algorithm>
      #include <iomanip>
      // ----------------------------------------------------------------------
      // All the following will be done during compile time
      
      // Constexpr function to calculate the nth Fibonacci number
      constexpr unsigned long long getFibonacciNumber(size_t index) noexcept {
          // Initialize first two even numbers 
          unsigned long long f1{ 0 }, f2{ 1 };
      
          // calculating Fibonacci value 
          while (index--) {
              // get next value of Fibonacci sequence 
              unsigned long long f3 = f2 + f1;
              // Move to next number
              f1 = f2;
              f2 = f3;
          }
          return f2;
      }
      // We will automatically build an array of Fibonacci numbers at compile time
      // Generate a std::array with n elements 
      template <size_t... ManyIndices>
      constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept {
          return std::array<unsigned long long, sizeof...(ManyIndices)>{ { getFibonacciNumber(ManyIndices)... } };
      };
      
      // Max index for Fibonaccis that for an 64bit unsigned value (Binet's formula)
      constexpr size_t MaxIndexFor64BitValue = 93;
      
      // Generate the required number of elements
      constexpr auto generateArray()noexcept {
          return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
      }
      
      // This is an constexpr array of all Fibonacci numbers
      constexpr auto FIB = generateArray();
      
      // All the above was compile time
      // ----------------------------------------------------------------------
      
      
      // Check, if a number belongs to the Fibonacci series
      bool isFib(const unsigned long long numberToBeChecked) {
          return std::binary_search(FIB.begin(), FIB.end(), numberToBeChecked);
      }
      
      // Test
      int main() {
      
          const unsigned long long testValue{ 498454011879264ull };
      
          std::cout << std::boolalpha << "Does '" <<testValue << "' belong to Fibonacci series?  --> " << isFib(testValue) << "\n\n";
      
          for (size_t i{}; i < 10u; ++i)
              std::cout << i << '\t' << FIB[i] << '\n';
      
          return 0;
      }
      

      使用 Microsoft Visual Studio Community 2019 版本 16.8.2 开发和测试

      使用 gcc 10.2 和 clang 11.0.1 进行了额外测试

      语言:C++ 17

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-08-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-11-25
        • 2020-03-06
        相关资源
        最近更新 更多