Performance Scala函数式编程比传统编码慢吗?

Performance Scala函数式编程比传统编码慢吗?,performance,scala,functional-programming,Performance,Scala,Functional Programming,在我第一次尝试创建功能代码时,我遇到了一个性能问题 我从一个常见的任务开始—将两个数组的元素相乘,并总结结果: var first:Array[Float] ... var second:Array[Float] ... var sum=0f; for (ix<-0 until first.length) sum += first(ix) * second(ix); 当我对这两种方法进行基准测试时,第二种方法需要40倍的时间才能完成 为什么第二种方法要花这么长时间?如

在我第一次尝试创建功能代码时,我遇到了一个性能问题

我从一个常见的任务开始—将两个数组的元素相乘,并总结结果:

var first:Array[Float] ...
var second:Array[Float] ...    
var sum=0f; 
for (ix<-0 until first.length) 
    sum += first(ix) * second(ix);
当我对这两种方法进行基准测试时,第二种方法需要40倍的时间才能完成


为什么第二种方法要花这么长时间?如何改进工作,使之既能提高速度又能使用函数式编程风格?

我不是Scala程序员,因此可能有一种更有效的方法,但类似的方法呢。这可以对尾部调用进行优化,所以性能应该可以

def multiply_and_sum(l1:List[Int], l2:List[Int], sum:Int):Int = {
    if (l1 != Nil && l2 != Nil) {
        multiply_and_sum(l1.tail, l2.tail, sum + (l1.head * l2.head))
    }
    else {
        sum
    }
}

val first = Array(1,2,3,4,5)
val second = Array(6,7,8,9,10)
multiply_and_sum(first.toList, second.toList, 0)  //Returns: 130

这是一个微基准,它取决于编译器如何优化代码。这里有3个循环

拉链。地图。折叠

现在,我相当确定Scala编译器不能将这三个循环融合到一个循环中,并且底层数据类型是严格的,因此每个(.)对应于正在创建的中间数组。强制/可变解决方案每次都会重用缓冲区,避免复制

现在,理解组成这三个函数意味着什么是理解函数式编程语言性能的关键——事实上,在Haskell中,这三个循环将优化为一个循环,以重用底层缓冲区——但Scala无法做到这一点

然而,坚持使用combinator方法也有好处——通过区分这三个函数,代码的并行化将更容易(用parMap等替换map)。事实上,给定正确的数组类型,(例如a),一个足够智能的编译器将能够自动并行化您的代码,从而获得更高的性能

因此,总而言之:

  • 幼稚的翻译可能会产生意想不到的副本和低效
  • 聪明的FP编译器消除了这种开销(但Scala还不能)
  • 如果您想重新确定代码的目标,例如并行化代码,那么坚持高级方法是有好处的

您的功能解决方案速度较慢,因为它正在生成不必要的临时数据结构。删除这些函数称为去叶,通过将匿名函数滚动到单个匿名函数中并使用单个聚合器,可以在严格的函数语言中轻松完成。例如,使用
zip
map
reduce
以F#编写的解决方案:

let dot xs ys = Array.zip xs ys |> Array.map (fun (x, y) -> x * y) -> Array.reduce ( * )
可以使用
fold2
重写,以避免所有临时数据结构:

let dot xs ys = Array.fold2 (fun t x y -> t + x * y) 0.0 xs ys

这要快得多,同样的转换可以在Scala和其他严格的函数式语言中完成。在F#中,您还可以将
fold2
定义为
inline
,以便将高阶函数与其函数参数内联,从而恢复命令式循环的最佳性能。

Scala集合库是完全通用的,所提供的操作是为获得最大性能而选择的,不是最高速度。因此,是的,如果您使用带有Scala的函数范式而不注意(特别是如果您使用的是原始数据类型),那么您的代码运行(在大多数情况下)所需的时间将比使用命令式/迭代范式而不注意的时间要长

这就是说,您可以轻松创建非通用功能操作,这些操作可以快速执行所需的任务。在使用成对浮点数的情况下,我们可以执行以下操作:

class FastFloatOps(a: Array[Float]) {
  def fastMapOnto(f: Float => Float) = {
    var i = 0
    while (i < a.length) { a(i) = f(a(i)); i += 1 }
    this
  }
  def fastMapWith(b: Array[Float])(f: (Float,Float) => Float) = {
    val len = a.length min b.length
    val c = new Array[Float](len)
    var i = 0
    while (i < len) { c(i) = f(a(i),b(i)); i += 1 }
    c
  }
  def fastReduce(f: (Float,Float) => Float) = {
    if (a.length==0) Float.NaN
    else {
      var r = a(0)
      var i = 1
      while (i < a.length) { r = f(r,a(i)); i += 1 }
      r
    }
  }
}
implicit def farray2fastfarray(a: Array[Float]) = new FastFloatOps(a)
类FastFloatOps(a:Array[Float]){ def FastMapOn(f:Float=>Float)={ 变量i=0 而(iFloat)={ val len=a.长度最小b.长度 val c=新数组[Float](len) 变量i=0 而(i浮动)={ 如果(a.length==0)Float.NaN 否则{ var r=a(0) 变量i=1 而(i 然后这些操作会快得多。(如果您使用Double和2.8.RC1,速度会更快,因为这样函数
(Double,Double)=>Double
将是专门的,而不是泛型的;如果您使用的是更早的东西,您可以创建自己的
抽象类F{def F F(a:Float):Float}
,然后使用
新的F{def(a:Float)=a*a}
而不是
(a:Float)=>a*a


不管怎么说,关键是Scala中函数式编码的速度慢不是因为函数式风格,而是因为库的设计考虑到了最大的能力/灵活性,而不是最大的速度。这是合理的,因为每个人的速度要求都有细微的差异,所以很难很好地涵盖所有人。但是如果是som如果你做的不仅仅是一点点,那么你可以编写自己的东西,其中功能风格的性能损失非常小。

Don Stewart有一个很好的答案,但是从一个循环到三个循环是如何造成40倍的减速的,这可能并不明显。我要补充他的答案,Scala通过它编译到JVMecodes,Scala编译器不仅没有将三个循环融合为一个循环,而且Scala编译器几乎肯定会分配所有的中间数组。众所周知,JVM的实现并不是为了处理函数式语言所需的分配率而设计的。分配在功能上是一项巨大的成本l程序,这就是Don Stewart和他的同事为Haskell实现的循环融合转换非常强大:它们消除了大量的分配。当你没有这些转换,再加上你使用的是一个昂贵的分配器,比如在典型的JVM上,这就是大减速的原因

Scala是一个很好的实验e的工具
class FastFloatOps(a: Array[Float]) {
  def fastMapOnto(f: Float => Float) = {
    var i = 0
    while (i < a.length) { a(i) = f(a(i)); i += 1 }
    this
  }
  def fastMapWith(b: Array[Float])(f: (Float,Float) => Float) = {
    val len = a.length min b.length
    val c = new Array[Float](len)
    var i = 0
    while (i < len) { c(i) = f(a(i),b(i)); i += 1 }
    c
  }
  def fastReduce(f: (Float,Float) => Float) = {
    if (a.length==0) Float.NaN
    else {
      var r = a(0)
      var i = 1
      while (i < a.length) { r = f(r,a(i)); i += 1 }
      r
    }
  }
}
implicit def farray2fastfarray(a: Array[Float]) = new FastFloatOps(a)
first.zip(second)
map{ case (a,b) => a*b }
reduceLeft(_+_)
sum = first.zip(second).foldLeft(0f) { case (a, (b, c)) => a + b * c }
sum = first.view.zip(second).map{ case (a,b) => a*b }.reduceLeft(_+_)
sum = (first,second).zipped.map{ case (a,b) => a*b }.reduceLeft(_+_)
(xs, ys).zipped map (_ * _) reduceLeft(_ + _)
loopArray 461 437 436 437 435 reduceArray 6573 6544 6718 6828 6554 loopVector 5877 5773 5775 5791 5657 reduceVector 5064 4880 4844 4828 4926 loopArrayBoxed 2627 2551 2569 2537 2546 reduceArrayBoxed 4809 4434 4496 4434 4365 loopVectorBoxed 7577 7450 7456 7463 7432 reduceVectorBoxed 5116 4903 5006 4957 5122
def multiplyAndSum (l1: Array[Int], l2: Array[Int]) : Int = 
{
    def productSum (idx: Int, sum: Int) : Int = 
        if (idx < l1.length)
            productSum (idx + 1, sum + (l1(idx) * l2(idx))) else 
                sum
    if (l2.length == l1.length) 
        productSum (0, 0) else 
    error ("lengths don't fit " + l1.length + " != " + l2.length) 
}


val first = (1 to 500).map (_ * 1.1) toArray                                                
val second = (11 to 510).map (_ * 1.2) toArray     
def loopi (n: Int) = (1 to n).foreach (dummy => multiplyAndSum (first, second))
println (timed (loopi (100*1000)))