java – 功能编程 – 是不变的昂贵吗?

问题分为两部分。第一是概念。接下来在Scala中更具体地讨论同一个问题。

>在编程语言中仅使用不可变的数据结构使得实现某些算法/逻辑在实践中本身在计算上更昂贵吗?这意味着不变性是纯功能语言的核心原则。是否有其他因素影响这一点?
>让我们举个更具体的例子。 Quicksort通常使用在存储器内数据结构上的可变操作来教导和实现。如何以可变的版本的可比计算和存储开销以PURE功能方式实现这样的事情。特别是在Scala。我在下面包括一些粗糙的基准。

更多细节:

我来自命令式编程背景(C,Java)。我一直在探索函数式编程,特别是Scala。

纯函数编程的一些主要原则:

>职能是一流的公民。
>函数没有副作用,因此对象/数据结构是immutable

即使现代JVMs是非常高效的对象创建和garbage collection是非常便宜的短暂的对象,它可能还是更好的最小化对象创建权?至少在单线程应用程序中,并发和锁定不是问题。由于Scala是一个混合范例,如果需要,可以选择用可变对象编写命令性代码。但是,作为一个人花了很多年来试图重用对象和最小化分配。我想要一个很好的理解的思想学校,甚至不会允许这样。

作为一个具体的例子,我对这个代码片段在this tutorial 6有点惊讶。它有一个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
由于这里有一些误解,我想澄清一些观点。

>“就地”快速排序不是真正就地(和快速排序不是通过定义就地)。它需要以递归步骤的堆栈空间的形式的附加存储,在最好的情况下是O(log n),在最坏的情况下是O(n)。
>实现操作数组的快速排序的功能变体违反了目的。数组从不是不可变的。
>快速排序的“正确”功能实现使用不可变列表。它当然不是就地,但它具有相同的最坏情况渐近运行时(O(n ^ 2))和空间复杂性(O(n))作为程序的就地版本。

平均而言,其运行时间仍然与现场变量(O(n log n))的运行时间一致。然而,其空间复杂性仍然是O(n)。

功能快速实现有两个明显的缺点。在下面,让我们考虑这个参考实现在Haskell(我不知道Scala …)从Haskell introduction

qsort []     = []
qsort (x:xs) = qsort lesser ++ [x] ++ qsort greater
    where lesser  = (filter (< x) xs)
          greater = (filter (>= x) xs)

>第一个缺点是枢轴元件的选择,这是非常不灵活的。现代快速处理实现的强度在很大程度上取决于对枢轴的智能选择(比较“Engineering a sort function” by Bentley et al.)。上述算法在这方面很差,这大大降低了平均性能。
>其次,该算法使用列表连接(而不是列表构造),其是O(n)操作。这不影响渐近的复杂性,但它是一个可衡量的因素。

第三个缺点是有些隐藏:不同于“就地”变体,这种实现不断地从堆中请求内存用于列表的cons单元,并且可能散布整个地方的内存。结果,该算法具有非常差的高速缓存局部性。我不知道现代功能编程语言中的智能分配器是否可以减轻这一点 – 但在现代机器上,缓存未命中已成为主要的性能杀手。

结论是什么?与其他人不同,我不会说quicksort本质上是必须的,这就是为什么它在FP环境中执行不好。恰恰相反,我认为快速排序是一个功能算法的一个完美的例子:它无缝地转换成一个不可变的环境,它的渐近运行时间和空间复杂性与程序实现一致,甚至其过程实现采用递归。

但是这种算法在被约束到不可变域时仍然执行得更糟。其原因是该算法具有受益于只能在阵列上有效执行的许多(有时是低级)微调的特有属性。快速排序的幼稚描述错过了所有这些复杂(在功能和程序变体中)。

阅读“工程排序功能”后,我不能再考虑快速排序优雅的算法。有效地实施,它是一个笨重的混乱,工程师的工作,而不是一个艺术家的(不是贬值工程!这有自己的审美)。

但我也想指出,这一点是特别是quicksort。不是每个算法都适合同样的低级调整。许多算法和数据结构真的可以在不可变环境中表达而没有性能损失。

而不变性甚至可以通过消除昂贵的副本或交叉线程同步的需要来降低性能成本。

所以,为了回答原来的问题,“是不可变性昂贵吗?” – 在快速排序的特定情况下,有一个成本,这是不可变性的结果。但一般来说,没有。

http://stackoverflow.com/questions/4101924/functional-programming-is-immutability-expensive

本站文章除注明转载外,均为本站原创或编译
转载请明显位置注明出处:java – 功能编程 – 是不变的昂贵吗?