简体   繁体   English

为什么在运行时而不是在编译时使用 constexpr 初始化变量

[英]Why is initialization of variable with constexpr evaluated at runtime instead of at compile time

As my understanding, the keyword constexpr is telling the compiler that the evaluation of expression can happen at compile time.据我了解,关键字constexpr告诉编译器表达式的评估可以在编译时发生。 Specifically, constexpr on a variable means that the value of the variable can be evaluated at compile time, whereas constexpr on a function means that this function may be invoked and evaluated its return value at compile time.具体来说,变量上的constexpr意味着可以在编译时评估变量的值,而 function 上的constexpr意味着可以在编译时调用此 function 并评估其返回值。 If the funcion is invoked at runtime, it just acts as a common function.如果函数在运行时被调用,它只是作为一个普通的 function。

Today, I wrote a piece of code to try to use 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;
}

I was expecting that the compilation of the code would spend much time but I'm wrong.我原以为代码的编译会花费很多时间,但我错了。 It only spent 0.0290s to do the compilation:编译只用了 0.0290 秒:

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

But if I change constexpr long int res = fib(32);但是如果我改变constexpr long int res = fib(32); into const long int res = fib(32);进入const long int res = fib(32); , to my surprise, it spent much more time on the compilation: ,令我惊讶的是,它在编译上花费了更多时间:

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

In a word, it seems that const makes the function fib(32) to be evaluated at compile time, but constexpr makes it to be evaluated at runtime.总之,似乎const使 function fib(32)在编译时被评估,但constexpr使它在运行时被评估。 I'm really confused.我真的很困惑。

My system: Ubuntu 18.04我的系统:Ubuntu 18.04

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

By inspecting the generated assembly, we can confirm that in both cases G++ 7.5 computed the fib(32) value at compile time:通过检查生成的程序集,我们可以确认在这两种情况下G++ 7.5 在编译时计算了fib(32)值:

    movl    $2178309, %esi

The reason G++ evaluates constexpr context so fast is due to memoization which it performs when evaluating constexpr and template contexts. G++ 如此快速地评估constexpr上下文的原因是由于它在评估constexprtemplate上下文时执行的记忆

Memoization completely kills fibonacci computational complexity by reducing it to O(N) complexity.记忆化通过将其降低到 O(N) 复杂度来完全消除斐波那契计算复杂度。

So why then is non- constexpr evaluation so much slower?那么为什么非constexpr评估要慢得多呢? I presume that's a bug/shortcoming in the optimizer.我认为这是优化器中的错误/缺点。 If I try with G++ 8.1 or later, there's no difference in compilation times, so presumably it had already been addressed.如果我尝试使用 G++ 8.1 或更高版本,编译时间没有区别,所以大概已经解决了。

A short compilation time is not proof that the calls weren't evaluated at compile time.较短的编译时间并不能证明调用未在编译时进行评估。 You can look at the compiled assembly to see that it was in fact evaluated at compile time using constexpr in my test here: https://godbolt.org/z/vbWaxe您可以查看已编译的程序集,看看它实际上是在我的测试中使用constexpr在编译时评估的: https://godbolt.org/z/vbWaxe

In my tests with a newer compiler, const wasn't measurably slower than constexpr .在我使用更新的编译器进行的测试中, const并不比constexpr慢得多。 It could be a quality of implementation issue with your version of the compiler.这可能是您的编译器版本的实现质量问题。

The secret for the fast compile time evaluation is that there are only very very few Fibonacci numbers that would fit into the nowadays maximum data type unsigned long long.快速编译时评估的秘诀在于,只有极少数的斐波那契数可以适合当今最大的数据类型 unsigned long long。

The basic message for Fibonacci number calculation is: Any calculation at runtime is not necessary at all.斐波那契数计算的基本信息是:根本不需要在运行时进行任何计算。 Everything can and should be done at compile time, And then.一切都可以而且应该在编译时完成,然后。 a simple lookup mechanism can be used.可以使用简单的查找机制。 That will always be the most efficient and fastest solution.这将永远是最有效和最快的解决方案。

So, with Binet's formula, we can calculate that there are only very few Fibonacci numbers that will fit in a C++ unsigned long long data type, which has usually 64 bit now in 2021 and is the "biggest" available data type.因此,通过比内特的公式,我们可以计算出只有极少数斐波那契数适合 C++ unsigned long long数据类型,该数据类型现在通常在 2021 年为 64 位,是“最大”的可用数据类型。 Roundabout 93 .环岛 93 . That is nowadays a really low number.这在当今是一个非常低的数字。

With modern C++ 17 (and above) features, we can easily create a std::array of all Fibonacci numbers for a 64bit data type at compile time .借助现代 C++ 17(及以上)功能,我们可以在编译时轻松创建 64 位数据类型的所有斐波那契数的std::array Because there are, as mentioned above, only 93 numbers.因为如上所述,只有 93 个数字。

So, we will spend only 93*8= 744 BYTE of none-runtime memory for our lookup array.因此,我们将只花费 93*8= 744 BYTE的非运行时 memory 用于我们的查找数组。 That is really negligible.这实在是微不足道。 And, the compiler can get those values fast.而且,编译器可以快速获取这些值。

After the compile time calculation, we can easily get the Fibonacci number n by writing FIB[n] .在编译时间计算之后,我们可以通过编写FIB[n]轻松获得斐波那契数 n。 For detailed explanation, please see below.详细解释请看下文。

And, if we want to know, if a number is Fibonacci, then we use std::binary_search for finding the value.而且,如果我们想知道一个数字是否是斐波那契,那么我们使用std::binary_search来查找该值。 So, this function will be for example:因此,这个 function 将是例如:

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

FIB (of course any other name possible) is a compile time, constexpr std::array . FIB(当然任何其他可能的名称)是一个编译时间, constexpr std::array So, how to build that array?那么,如何构建该数组?

We will first define the default approach for calculation a Fibonacci number as a constexpr function (non-recursive):我们首先将计算斐波那契数的默认方法定义为constexpr function(非递归):

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

With that, Fibonacci numbers can easily be calculated at compile time as constexpr values .这样,斐波那契数可以在编译时轻松计算为constexpr values Then, we fill a std::array with all Fibonacci numbers.然后,我们用所有斐波那契数填充std::array We use also a constexpr and make it a template with a variadic parameter pack.我们还使用constexpr并使其成为带有可变参数包的模板。

We use std::integer_sequence to create a Fibonacci number for indices 0,1,2,3,4,5, ....我们使用std::integer_sequence为索引 0,1,2,3,4,5, ... 创建一个斐波那契数。

That is straigtforward and not complicated:这很简单,并不复杂:

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

This function will be fed with an integer sequence 0,1,2,3,4,... and return a std::array<unsigned long long, ...> with the corresponding Fibonacci numbers.这个 function 将被输入一个 integer 序列 0,1,2,3,4,... 并返回一个std::array<unsigned long long, ...>与相应的斐波那契数。

We know that we can store maximum 93 values.我们知道我们最多可以存储 93 个值。 And therefore we make a next function, that will call the above with the integer sequence 1,2,3,4,...,92,93, like so:因此我们制作了下一个 function,它将使用 integer 序列 1、2、3、4、...、92、93 调用上述代码,如下所示:

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

And now, finally,而现在,终于,

constexpr auto FIB = generateArray();

will give us a compile-time std::array<unsigned long long, 93> with the name FIB containing all Fibonacci numbers.将给我们一个编译时std::array<unsigned long long, 93>名称 FIB 包含所有斐波那契数。 And if we need the i'th Fibonacci number, then we can simply write FIB[i] .如果我们需要第 i 个斐波那契数,那么我们可以简单地写FIB[i] There will be no calculation at runtime.运行时不会进行计算。


The whole example program will look like this:整个示例程序将如下所示:

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

Developed and tested with Microsoft Visual Studio Community 2019, Version 16.8.2使用 Microsoft Visual Studio Community 2019 版本 16.8.2 开发和测试

Additionally tested with gcc 10.2 and clang 11.0.1使用 gcc 10.2 和 clang 11.0.1 进行了额外测试

Language: C++ 17语言:C++ 17

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM