为什么编译时执行明显快于运行时执行



与这个问题相反,这段代码表现出了一些奇怪的行为:

long long int fibonacci(int num) {
if (num <= 2) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}
int main() {
auto t1 = std::chrono::high_resolution_clock::now();
long long int x = fibonacci(45);
auto t2 = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> time(t2 - t1);
std::cout << "Time taken: " << time.count() << "ms";
}

在我的机器上,它用-O3(GCC)在约700ms内编译,输出为:

Time taken: 2667.55ms

我用constexpr重写了上面的代码,如下所示:

constexpr long long int fibonacci(int num) {
if (num <= 2) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}
int main() {
auto t1 = std::chrono::high_resolution_clock::now();
constexpr long long int x = fibonacci(45);
auto t2 = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> time(t2 - t1);
std::cout << "Time taken: " << time.count() << "ms";
}

它在大致相同的时间内编译,但输出为:

Time taken: 0ms

目前,在编译时评估fibonacci(45)比在运行时评估要快得多。为了消除多核编译的原因(这肯定不是),我用fibonacci(60)重新运行了上面的第二个块。同样,代码在相同的时间内编译,但输出为:

Time taken: 29499.6ms

是什么导致了这种巨大的性能差距?

基本上,对于这段短代码来说,编译时间并不重要。

甚至,如果您进行编译时评估。

这里的主要问题是这里使用的最糟糕的算法。使用2个递归调用,然后再次调用2个递归函数等等,对于这样一个简单的算法来说,时间复杂度是最差的。

这个问题可以而且必须用迭代方法来解决。

类似的东西:

unsigned long long fibonacci(size_t index) noexcept {
unsigned long long f1{ 0ull }, f2{ 1ull }, f3{};
while (index--) { f3 = f2 + f1; f1 = f2; f2 = f3; }
return f2;
}

如果你使用这个函数,那么,无论是否重新优化,你的main都将在1ms以下运行。

在你原来的主函数中,如果你不使用计算结果,那么变量"X〃;,那么优化编译器可以消除完整的函数调用。没有必要调用该函数。未使用结果。

进行以下实验。

添加std::cout << x << 'n';作为主函数中的最后一条语句。你会大吃一惊的。启用优化后,函数将在最后被调用。而你的时间测量没有任何意义。

现在转到编译器时版本。此外,编译器将在内部使用优化代码。它将把非必要的递归方法转换为迭代方法。并且计算这些值将比编译器开销函数花费更少的时间。

所以,这才是真正的原因。

斐波那契数总是可以在编译时完全编译的。对于64位的结果值,只有93个可能的结果。

请参阅以下方法,它创建了一个包含所有Fibonacci数字的编译时数组。如果你想要第n个数字,它只是一个查找值。

这将在非优化或优化模式下工作得非常快。

这是解决那个问题的最快捷的可能办法。

#include <iostream>
#include <utility>
#include <array>
#include <algorithm>
#include <iterator>
// All done during compile time -------------------------------------------------------------------
constexpr unsigned long long getFibonacciNumber(size_t index) noexcept {
unsigned long long f1{ 0ull }, f2{ 1ull }, f3{};
while (index--) { f3 = f2 + f1; f1 = f2; f2 = f3; }
return f2;
}
// Some helper to create a constexpr std::array initilized by a generator function
template <typename Generator, size_t ... Indices>
constexpr auto generateArrayHelper(Generator generator, std::index_sequence<Indices...>) {
return std::array<decltype(std::declval<Generator>()(size_t{})), sizeof...(Indices) > { generator(Indices)... };
}
template <size_t Size, typename Generator>
constexpr auto generateArray(Generator generator) {
return  generateArrayHelper(generator, std::make_index_sequence<Size>());
}
constexpr size_t MaxIndexFor64BitFibonacci = 93;
// This is the definition of a std::array<unsigned long long, 93> with all Fibonacci numbers in it
constexpr auto Fib = generateArray<MaxIndexFor64BitFibonacci>(getFibonacciNumber);
// End of: All done during compile time -----------------------------------------------------------

// The only code executed during runtime
int main() {
// Show all Fibonacci numbers up to border value
for (const auto fib : Fib) std::cout << fib << 'n';
}

我甚至不知道为什么会出现这个问题,除非你没有完全阅读constexpr的描述。你不能靠";猜测;用C++编写的程序中发生了什么,除非行为是UB或平台定义的一部分。

C++标准没有以任何方式描述内存化,因此我们没有理由怀疑可能会使用如此复杂的缓存过程。支持记忆(或称为"输入"、"tombstoning"等)的语言通常与在某个虚拟机上运行中间代码的语言相同,并且事实上不提供对任意函数进行编译时评估的机制,因为这在未知平台上是不可能实现的。

CCD_ 6允许创建";常数";它们是通过编译器端执行的代码初始化的(具有所有可能的平台定义的效果和相关标准中应用的所有限制)。

如果没有它,就好像规则适用一样,这包括编译器对更改代码不做任何事情的自由。如果结果不可观察,并且很明显函数调用没有副作用,编译器也可以抛出整个过程,这在使用constexpr时更容易检查,即定义中没有副作用。

constexpr版本中,由于您明确地告诉编译器这是可能的,所以整个执行堆栈都将被取消。标准对constexpr函数的限制保证了该函数适用于此类评估。

对于行为与主机不同的平台进行交叉编译时,可能会出现意外结果:编译器端平台可能会得到constexpr值。

使用递归constexpr函数,您有可能超出实现定义的递归限制。例如,您的案例根本不会使用g++的某些构建进行编译,因为由于非最优递归,您在那里有超过3200万次迭代。按照递归的方式,在编译时不可能进行内存化。什么时候可能?如果f(x2)正在调用f(x1),则在调用f(x1)之后,对f(x2)的求值可能使用f(x1)的已知值。在您的情况下,对于任何大于2的给定xfibonacci(x)调用fibonacci(x-1)fibonacci(x-2)。这两个值以前没有评估过。看起来fibonacci(x-1)可以重用或提供fibonacci(x-2)的值,但根据C++标准的版本,它是:

A.)执行顺序未定义
B.)fibonacci(x-1)是上下文不同于CCD_ 24的上下文。

在第二种情况下,除了用编译器计算的值初始化x之外,在对chrono的调用之间不执行任何代码。如果您使用static constexpr long long int x作为局部变量,那么即使初始化也会消失,它将是数据或代码中某个地方的硬编码常数值。static形式上延长了对象的寿命,直到进程结束,而每次输入函数时都会初始化一个自动constexpr变量(与main()应该只输入一次无关)。

我不相信其他答案的解释。

以下是GCC团队成员Jakub Jelink在一个bug票证中谈论他们的constexpr缓存。他描述说,他们根据参数(以及其他内容)缓存constexpr结果。

源镜像中的相关代码。这有点复杂。根据代码中哈希表的处理方式,GCC根据常量参数值、模板参数和对象实例(如果它实际上是一个可以在constexpr对象上求值的方法)全局缓存constexpr调用的结果(每个编译单位)。尽管处理此问题的代码相当冗长。

此外,这里还有一个支持HolyBlackCat:记忆声明的测试

#include <stdio.h>

static constexpr unsigned long long test(int arg)
{
unsigned long long dummy = 0;
for (unsigned long long i = 0; i < 1000000ULL + arg; ++i)
{
dummy += 1;
}
return dummy;
}

int main(int argc, char **argv)
{   
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(5); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(6 /*!*/); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(7 /*!*/); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(8 /*!*/); printf("Result: %llun", result); }
{ constexpr unsigned long long result = test(9 /*!*/); printf("Result: %llun", result); }
return 0;
}

这个想法是运行一些constexpr函数,该函数在一些已知的实时量中运行。在我的机器上,这个功能花了大约2秒钟。确切的时间并不重要,重要的是执行时间实际上与循环迭代次数成正比。

以下观察结果暗示了记忆化:

  • 如果main中只有一行result存在,则编译时间大约为某个值x
  • 如果test(5)中所有带参数5result行都存在(但没有带其他参数的行),则编译时间为相同值x
  • 如果带有参数6, 7, 8, 9result行也存在,则编译时间大约是x的5倍

还要记住,虽然编译器在使用-O2编译时完美地将循环优化为生成代码中的一个简单添加,但在constexpr求值期间,它完全无法进行任何类型的优化,这一点从长得离谱的编译时间中可以明显看出。如果在那个阶段进行任何重要的优化,它肯定能够使循环崩溃。猜测:之前我曾向编译器贡献过代码,我认为管道体系结构实际上太复杂了,无法对编译时评估的constexpr代码进行全面优化,而且它实际上可能正在评估一个结构非常接近原始AST的表达式树。

g++ -std=c++14测试

在linux mint上运行的gcc 5.4.0.20160609版本。

最新更新