大家好,我写了两个y组合子
:y1
和 y2
其中,y2
相比 y1
是把 Copy trait
变为了 引用
。
按理来说,引用避免了内存复制,应该更加高效;但现实却恰恰相反,引用慢了接近 4
倍,且生成了数倍复杂的 ASM
下面是测速代码:
use std::{fmt::Display, time::{Duration, Instant}};
fn main() {
measure_time(|| fib1(50)).as_secs().echo(); // 耗时:37 秒
measure_time(|| fib2(50)).as_secs().echo(); // 耗时:139 秒,慢了接近 4 倍
}
fn fib1(n: usize) -> usize {
n.y1(|f, n| match n {
0 => 0,
1 => 1,
n => f(n - 1) + f(n - 2),
})
}
fn fib2(n: usize) -> usize {
n.y2(|f, n| match n {
0 => 0,
1 => 1,
n => f(n - 1) + f(n - 2),
})
}
trait Ext: Sized {
fn y1<R>(self, f: impl Copy + Fn(&dyn Fn(Self) -> R, Self) -> R) -> R {
// The Y Combinator
fn y<T, R>(f: impl Copy + Fn(&dyn Fn(T) -> R, T) -> R) -> impl Fn(T) -> R {
move |a| f(&y(f), a)
}
// Chainable
y(f)(self)
}
fn y2<R>(self, f: impl Fn(&dyn Fn(Self) -> R, Self) -> R) -> R {
// The Y Combinator
fn y<T, R>(f: &dyn Fn(&dyn Fn(T) -> R, T) -> R) -> impl Fn(T) -> R + '_ {
move |a| f(&y(f), a)
}
// Chainable
y(&f)(self)
}
fn tap<R>(self, f: impl FnOnce(&Self) -> R) -> Self {
f(&self);
self
}
fn echo(self) -> Self where Self: Display {
self.tap(|s| println!("{}", s))
}
}
impl<T> Ext for T {}
fn measure_time<R>(f: impl FnOnce() -> R) -> Duration {
Instant::now().tap(|_| f()).elapsed()
}
为什么不需要 Copy
的 y2
耗时更长,且 ASM
更复杂?
1
共 3 条评论, 1 页
评论区
写评论我不认同此观点。 “尾递归优化”是把“递归”优化成“循环”,而斐波那契数列循环到
50
根本要不了这么长时间。 甚至,循环到一万,都溢出了,仍不到1
秒。--
👇
Easonzero: 个人觉得, 首先你的lambda是一个纯函数, 因此当使用模板的时候, 相当于移动一个栈上的usize, 所以理论上并不会慢于引用的方式.
至于y2比y1慢, 我有个猜测是, 因为y2这个递归传递的是引用, 所以f drop的时机是y2递归树的根节点退出的时候, 那么编译器就无法做尾递归优化, 导致当递归较深时, y2性能大幅度下降.
个人觉得, 首先你的lambda是一个纯函数, 因此当使用模板的时候, 相当于移动一个栈上的usize, 所以理论上并不会慢于引用的方式.
至于y2比y1慢, 我有个猜测是, 因为y2这个递归传递的是引用, 所以f drop的时机是y2递归树的根节点退出的时候, 那么编译器就无法做尾递归优化, 导致当递归较深时, y2性能大幅度下降.
同问