【发布时间】:2011-05-05 08:30:20
【问题描述】:
问题分为两部分。第一个是概念性的。接下来在 Scala 中更具体地研究同一个问题。
- 在编程语言中仅使用不可变数据结构是否会导致在实践中实现某些算法/逻辑本质上在计算上更加昂贵?这说明了不变性是纯函数式语言的核心原则。还有其他因素会影响这一点吗?
- 让我们举一个更具体的例子。 Quicksort 通常是使用内存数据结构上的可变操作来教授和实现的。如何以与可变版本相当的计算和存储开销以纯功能方式实现这样的事情。特别是在 Scala 中。我在下面列出了一些粗略的基准。
更多详情:
我来自命令式编程背景(C++、Java)。我一直在探索函数式编程,特别是 Scala。
纯函数式编程的一些主要原则:
- 函数是一等公民。
- 函数没有副作用,因此对象/数据结构为immutable。
尽管现代JVMs 在创建对象方面非常高效,而garbage collection 对于短期对象来说非常便宜,但最好还是尽量减少对象创建吧?至少在并发和锁定不是问题的单线程应用程序中。由于 Scala 是一种混合范式,因此可以在必要时选择使用可变对象编写命令式代码。但是,作为一个花了很多年时间尝试重用对象并最小化分配的人。我想很好地了解甚至不允许这样做的思想流派。
作为一个具体案例,this tutorial6 中的这段代码 sn-p 让我有些惊讶。它有一个 Java 版本的 Quicksort,然后是一个整洁的 Scala 实现。
这是我对实现进行基准测试的尝试。我没有做过详细的分析。但是,我的猜测是 Scala 版本更慢,因为分配的对象数量是线性的(每个递归调用一个)。尾调用优化有没有可能发挥作用?如果我是对的,Scala 支持自递归调用的尾调用优化。所以,它应该只是帮助它。我正在使用 Scala 2.8。
Java 版本
public class QuickSortJ {
public static void sort(int[] xs) {
sort(xs, 0, xs.length -1 );
}
static void sort(int[] xs, int l, int r) {
if (r >= l) return;
int pivot = xs[l];
int a = l; int b = r;
while (a <= b){
while (xs[a] <= pivot) a++;
while (xs[b] > pivot) b--;
if (a < b) swap(xs, a, b);
}
sort(xs, l, b);
sort(xs, a, r);
}
static void swap(int[] arr, int i, int j) {
int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
}
Scala 版本
object QuickSortS {
def sort(xs: Array[Int]): Array[Int] =
if (xs.length <= 1) xs
else {
val pivot = xs(xs.length / 2)
Array.concat(
sort(xs filter (pivot >)),
xs filter (pivot ==),
sort(xs filter (pivot <)))
}
}
比较实现的 Scala 代码
import java.util.Date
import scala.testing.Benchmark
class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {
val ints = new Array[Int](100000);
override def prefix = name
override def setUp = {
val ran = new java.util.Random(5);
for (i <- 0 to ints.length - 1)
ints(i) = ran.nextInt();
}
override def run = sortfn(ints)
}
val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java " )
benchImmut.main( Array("5"))
benchMut.main( Array("5"))
结果
连续五次运行的时间(以毫秒为单位)
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
【问题讨论】:
-
天真地实现或使用为命令式语言开发的方法会很昂贵。智能编译器(例如 GHC、Haskell 编译器 - 而 Haskell 只有 不可变值)可以利用不可变性并实现可以与使用可变性的代码相媲美的性能。不用说,快速排序的幼稚实现仍然很慢,因为它使用大量递归和
O(n)list concat 等代价高昂的东西。虽然它比伪代码版本短 ;) -
一篇很棒的相关博客文章在这里:blogs.sun.com/jrose/entry/larval_objects_in_the_vm 鼓励他,因为这将使 Java 以及函数式 VM 语言受益匪浅
-
这个 SO 线程有很多关于函数式编程效率的详细讨论。 stackoverflow.com/questions/1990464/… 。解答了很多我想知道的问题。
-
这里最天真的事情是你的基准。你不能用这样的代码对任何东西进行基准测试!在得出任何结论之前,您应该认真阅读一些关于在 JVM 上进行基准测试的文章……您是否知道 JVM 在您运行代码之前可能还没有 JITted?您是否适当地设置了堆初始和最大大小(这样您就不会考虑 JVM 进程请求更多内存的时间?)?您是否知道正在编译或重新编译哪些方法?你知道GC吗?你从这段代码中得到的结果毫无意义!
-
@userunknown 不,这是声明性的。命令式编程“通过命令改变状态”,而函数式编程“是一种声明式编程范例”,“避免改变状态”(Wikipedia)。所以,是的,函数式和命令式是两个完全不同的东西,你写的代码不是命令式的。
标签: java scala functional-programming