使用泛型在编译时生成斐波那契序列 算法复杂度但是在C ++中却可以!在Rust中做!

问题描述

在具有模板元编程的C ++中,您可以通过这种方式轻松地在编译时计算斐波那契数列。

template<int  N>
constexpr int fibonacci() {return fibonacci<N-1>() + fibonacci<N-2>(); }
template<>
constexpr int fibonacci<1>() { return 1; }
template<>
constexpr int fibonacci<0>() { return 0; }

但是据我所知,在锈中,您不能只通过一个泛型传递常量,我也知道,有时锈将某些函数优化为汇编代码中的常量。示例:https://rosettacode.org/wiki/Compile-time_calculation#Rust

但是问题的常规递归方法并未将其优化为常数。

fn fibo(n: i32) -> i32 {
    match n {
        0 => 0,1 => 1,n => fibo(n - 1) + fibo(n - 2),}
}

// Call it with
fibo(45); // It takes around 5 secs,calculated at runtime

好吧,到目前为止,我无法理解,只是编译器不知道如何优化它,但是我找到了一种方法,可以在编译时使用Iterators进行计算!

struct Fibo(u32,u32);

impl Iterator for Fibo {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        *self = Fibo(self.1,self.1 + self.0);
        Some(self.0)
    }
}

fn fibo() -> Fibo {
    Fibo(0,1)
}

// Call it with
fibo().take(45).collect::<Vec<_>>()[44]; // This gets the 45th element calculated at compile-time,instantly

这时我只想知道为什么会发生这种情况。

解决方法

算法复杂度

计算斐波那契数列的幼稚方式具有指数复杂性

fn fibo(n: i32) -> i32 {
    match n {
        0 => 0,1 => 1,n => fibo(n - 1) + fibo(n - 2),}
}

您可以将其可视化为:

  • fibo(0):1个通话。
  • fibo(1):1个通话。
  • fibo(2):3个呼叫-fibo(2)fibo(1)fibo(0)
  • fibo(3):5个呼叫-fibo(3)fibo(2)(价值3),fibo(1)
  • fibo(4):9个呼叫-fibo(4)fibo(3)(价值5)和fibo(2)(价值3)。

但是,迭代器版本完全不同。重写为函数可以归结为:

fn fibo(n: i32) -> i32 {
    fn rec(i: i32,current: i32,next: i32) -> i32 {
        if i == 0 { current } else { rec(i - 1,next,current + next) }
    }

    rec(n,1)
}

确切执行n + 1步骤...提供n >= 0

但是在C ++中却可以!

C ++编译器倾向于将 memoization 用于模板实例化和constexpr评估。他们没有,严格来说这是实现细节,但是出于效率考虑,他们这样做。

在这种情况下,fibo的记忆版本将指数复杂度转换为线性复杂度,而 则更容易计算。

在Rust中做!

可以使用当前beta在Rust编译时在Rust中计算斐波那契,从而稳定const函数中的分支。

请参见the playground

const fn fibo(n: i32) -> i32 {
    const fn rec(i: i32,1)
}

fn main() {
    const RESULT: usize = fibo(9) as usize;

    let array: [i32; RESULT] = [
        0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,1
    ];
    
    println!("{}",array[0]);
}

有一种技巧可以在编译时不带分支的情况下表达计算,允许在稳定时在编译时计算fibo,但是我不确定rustc不会执行递归调用。

,

我查看了您的第二个代码示例的程序集输出,并且编译器似乎并未将其优化为常量。可能发生了非常不同的事情。

您称为“经典”递归算法的方法是计算斐波那契数的最差方法,因为函数调用的次数随n呈指数增长。迭代方法要好得多,因为它只需要使用n线性增长的多次迭代。对于n = 44,对于递归方法而言,约有10万亿个函数调用,而对于迭代方法而言,则有44个循环迭代。当然,后者在运行时看起来“即时”,但这并不意味着此处会发生任何特殊的编译器魔术。

(对于大型n,您需要任意精度的算术,最好的方法是二进制矩阵加电。)

现在是您的第二个问题,即如何在编译时让Rust对其进行评估。实际上,C ++中的模板元编程实际上是编译时计算的关键,而Rust具有一种更简单的方法:常量函数。 const fns的某些方面仍在不断发展,但是在当前的beta版本(将在大约两周内稳定发布)中,您可以通过一种非常简单的方式编写Fibonacci函数:

pub const fn fibo(mut n: u64) -> u64 {
    let mut a = 1;
    let mut b = 0;
    while n > 0 {
        let tmp = b;
        b += a;
        a = tmp;
        n -= 1;
    }
    b
}

pub const K: u64 = fibo(93);

Playground

Rust中也有const泛型,但是它们很不稳定(并且仍然很容易出错)。可能您可以执行类似于C ++模板元编程版本的操作,但是我没有对此进行研究。

,
const fn fibo(n: i32) -> i32 {
    match n {
        0 => 0,}
}
const A: i32 = fibo(45);

此代码将在编译时计算。
但是编译它需要很长时间,并且无法在操场上进行编译。 因此生锈可能无法对其进行优化。
您还可以看到the mir和ir