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

问题描述

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

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

constexpr

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

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

但如果我将 $ 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) 使它在运行时被评估.我真的很困惑。

我的系统:Ubuntu 18.04

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

解决方法

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

    movl    $2178309,%esi

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

记忆化将斐波那契计算复杂度降低到 O(N) 复杂度,从而完全消除了它。

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

,

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

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

,

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

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

因此,使用 Binet 公式,我们可以计算出只有很少 Fibonacci 数适合 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,... 并返回一个 std::array<unsigned long long,...> 和相应的斐波那契数列。

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

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

现在,终于,

constexpr auto FIB = generateArray();

会给我们一个名为 FIB 的编译时 std::array<unsigned long long,93>,其中包含所有的斐波那契数。如果我们需要第 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,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(),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