3

这是在以下答案之一中提出的声明:https ://softwareengineering.stackexchange.com/a/136146/18748

尝试一两种函数式语言。尝试使用递归在 Erlang 中实现阶乘,并在 20000 时看到你的下巴撞到地板上!5 秒后返回(站点没有堆栈溢出)

为什么它比在 Java/C/C++/Python(任何)中使用递归/迭代更快/更有效?使这种情况发生的基本数学/理论概念是什么?不幸的是,我在本科时从未接触过函数式编程(以“C”开头),所以我可能只是不知道“为什么”。

4

4 回答 4

2

递归解决方案在 Java 或 C++ 中可能会更慢且更笨重,但我们不会在 Java 和 C++ 中那样做,所以。:)

至于为什么,函数式语言总是大量使用递归(因为默认情况下,它们要么必须跳过障碍,要么根本不允许修改变量,这本身就使得大多数形式的迭代无效或完全不可能) . 因此,他们必须有效地优化它才能具有竞争力。

几乎它们都实现了一种称为“尾调用消除”的优化,它使用当前调用的堆栈空间进行下一次调用,并将“调用”变成“跳转”。那个小小的改变基本上把递归变成了迭代,但不要提醒FPers。当涉及过程语言中的迭代和函数语言中的递归时,两者将证明在性能方面大致相同。(但是,如果其中任何一个仍然更快,那就是迭代。)

其余的是库和数字类型等。体面的数学代码 ==> 体面的数学表现。除了大多数人不太关心之外,没有什么可以阻止过程或 OO 语言进行类似的优化。另一方面,函数式程序员喜欢盯着他们计算斐波那契数列和 20000 的难易程度!以及我们大多数人无论如何都不会使用的其他数字。

于 2013-03-11T22:43:22.617 回答
1

本质上,函数式语言使递归变得便宜。

特别是通过尾调用优化。

于 2013-03-11T22:41:34.147 回答
1

尾调用优化

惰性求值:“使用延迟求值时,表达式不会在绑定到变量后立即求值,而是在求值器被迫产生表达式的值时进行求值。”

这个问题中说明了这两个概念,关于在 Clojure 中实现阶乘的各种方法。您还可以在 Stuart Halloway 和 Aaron Bedra 的书Programming Clojure中找到关于惰性序列和 TCO 的详细讨论。

从 Programming Clojure 中采用的以下函数创建了一个惰性序列,其中包含斐波那契数列的前 1M 个成员,并实现了第 10 万个成员:

user=> (time (nth (take 1000000 (map first (iterate (fn [[a b]] [b (+ a b)]) [0N 1N]))) 100000))
"Elapsed time: 252.045 msecs"
25974069347221724166155034.... 
(20900 total digits)

(512MB 堆,英特尔酷睿 i7,2.5GHz)

于 2013-03-11T23:01:52.333 回答
1

并不快(比什么?),它与尾调用优化无关(在这里抛出一个流行词还不够,还应该解释为什么尾调用优化应该比循环更快?根本不是这样! )

请注意,相反,我不是函数式编程的仇恨者!但是传播神话并不适用于函数式编程。

顺便说一句,这里有没有人实际尝试过计算(和打印,这应该消耗至少 50% 所需的 CPU 周期)20000 需要多长时间!?

我做了:

main _ = println (product [2n..20000n])

这是一种编译为 Java 的 JVM 语言,它使用 Java 大整数(已知速度很慢)。它还受到 JVM 启动成本的影响。而且这不是最快的方法(例如,显式递归可以节省列表构造)。

结果是:

181920632023034513482764175686645876607160990147875264
...
... many many lines with digits
...          
000000000000000000000000000000000000000000000000000000000000000

real    0m3.330s
user    0m4.472s
sys     0m0.212s

(在 Intel® Core™ i3 CPU M 350 @ 2.27GHz × 4 上)

我们可以放心地假设,在 C 中与 GMP 相同的情况甚至不会使用 50% 的时间。

因此,功能性更快是一个神话,功能性更慢。这甚至不是一个神话,只要一个人不说它的速度更快/更慢,这简直就是胡说八道。

于 2013-03-12T00:48:40.290 回答