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
而(i(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)))