为什么Scala的尾递归慢于Java?

时间:2015-03-07 14:42:46

标签: java scala jvm tail-recursion

使用尾递归进行简单加法的Scala代码

def add(list : List[Int],sum:Int):Int = {
    //Thread.dumpStack()
    if (list.isEmpty) {
        sum
    } else {
        val headVal  = list.head
        add(list.tail, sum + headVal)
    }
}

以下是递归模式下添加的java代码。

public static int add(List<Integer> list, Integer sum) {
    // Thread.dumpStack();
    if (list.isEmpty()) {
        return sum;
    } else {
        int headVal = list.remove(0);
        return add(list, sum + headVal);
    }
}

Java版本运行速度至少快10倍。这是1000条目。在{之前和之后使用System.nanoTime() API测量的时间。

Scala版本2.10,java版本Java 7.两次运行的JVM属性相同。

4 个答案:

答案 0 :(得分:6)

首先,您展示的Scala方法add不在(类)上下文中。如果在类中使用此方法,则不能应用尾递归优化,因为该方法既不是final也不是private。如果添加@tailrec,编译将失败。如果我用10000运行它,会导致堆栈溢出。

至于Java版本:Java版本使用 mutable List:头/尾分解改变基础列表。 所以在总结之后你不能再使用那个列表,因为它是空的。

此外,Scala中的List与Java List具有完全不同的含义; Scala列表用于头/尾分解。据我所知java.util.List没有tail方法,Java编译器也没有应用tailrec优化,因此比较是“不公平的”。

无论如何,我已经在不同的场景下运行了一些基于JMH的测试。

您可以真正比较的唯一两个场景是“Scala while”和“Java for”。它们既不使用OOP也不使用函数式编程,这只是必要的。

五种不同Scala方案的结果

(请滚动到右边,最后一栏有一个小描述)

Benchmark                   Mode   Samples         Mean   Mean error    Units
a.b.s.b.Benchmark5a.run    thrpt        10      238,515        7,769   ops/ms Like in the question, but with @tailrec
a.b.s.b.Benchmark5b.run    thrpt        10      130,202        2,232   ops/ms Using List.sum
a.b.s.b.Benchmark5c.run    thrpt        10     2756,132       29,920   ops/ms while (no List, vars, imperative)
a.b.s.b.Benchmark5d.run    thrpt        10      237,286        2,203   ops/ms tailrec version with pattern matching
a.b.s.b.Benchmark5e.run    thrpt        10      214,719        2,483   ops/ms Like in the question (= no tailrec opt)
  • 5a和5e就像问题一样; 5a与@tailrec
  • 5b:List.sum:非常慢
  • 5c:不是一个大惊喜,命令式版本是最快的版本。
  • 5d使用模式匹配而不是if(这本来就是“我的风格”),我正在添加这个的来源:
package app.benchmark.scala.benchmark5

import scala.annotation._
import org.openjdk.jmh.annotations.GenerateMicroBenchmark
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.runner.Runner
import org.openjdk.jmh.runner.RunnerException
import org.openjdk.jmh.runner.options.Options
import org.openjdk.jmh.runner.options.OptionsBuilder

@State(Scope.Benchmark)
object BenchmarkState5d {
  val list = List.range(1, 1000)
}

class Benchmark5d {
  private def add(list : List[Int]): Int = {
    @tailrec
    def add(list : List[Int], sum: Int): Int = {
      list match {
        case Nil =>
          sum
        case h :: t =>
          add(t, h + sum)
      }
    }
    add(list, 0)
  }

  @GenerateMicroBenchmark
  def run() = {
    add(BenchmarkState5d.list)
  }
}

三种Java方案

Benchmark                   Mode   Samples         Mean   Mean error    Units
a.b.j.b.Benchmark5a.run    thrpt        10       40,437        0,532   ops/ms mutable (rebuilds the list in each iteration)
a.b.j.b.Benchmark5b.run    thrpt        10        0,450        0,008   ops/ms subList
a.b.j.b.Benchmark5c.run    thrpt        10     2735,951       29,177   ops/ms for

如果你真的想在函数式编程风格(= immutable,tail recursion,head / tail分解)的意义上进行比较,那么Java版本要慢五倍。

正如Marko Topolnik的评论所指出的那样:

  

subList不会复制尾部,但是当应用于LinkedList时,它会做一些相对较差的事情:它包装原始列表并使用偏移量来容纳语义。结果是O(n)递归算法变为O(n2)---就像重复复制尾部一样。另外,包装器会增加,因此您最终会将列表包裹一千次。绝对不能与头/尾名单相媲美

public class Benchmark5a {
    public static int add(List<Integer> list, Integer sum) {
        if (list.isEmpty()) {
            return sum;
        } else {
            int headVal = list.remove(0);
            return add(list, sum + headVal);
        }
    }

    @GenerateMicroBenchmark
    public long run() {
        final List<Integer> list = new LinkedList<Integer>();
        for(int i = 0; i < 1000; i++) {
            list.add(i);
        }
        return add(list, 0);
    }

    public static void main(String[] args) {
        System.out.println(new Benchmark5a().run());
    }
}

@State(Scope.Benchmark)
class BenchmarkState5b {
    public final static List<Integer> list = new LinkedList<Integer>();

    static {
        for(int i = 0; i < 1000; i++) {
            list.add(i);
        }
    }
}

public class Benchmark5b {
    public static int add(List<Integer> list, int sum) {
        if (list.isEmpty()) {
            return sum;
        } else {
            int headVal = list.get(0);
            return add(list.subList(1, list.size()), sum + headVal);
        }
    }

    @GenerateMicroBenchmark
    public long run() {
        return add(BenchmarkState5b.list, 0);
    }

    public static void main(String[] args) {
        System.out.println(new Benchmark5b().run());
    }
}

Scala详细信息

(所有结果仅显示最后一个场景,以及整体结果)

[...]

# VM invoker: /home/oracle-jdk-1.8-8u40/data/oracle-jdk-1.8.0_40/jre/bin/java
# VM options: <none>
# Fork: 1 of 1
# Warmup: 3 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: app.benchmark.scala.benchmark5.Benchmark5e.run
# Warmup Iteration   1: 166,153 ops/ms
# Warmup Iteration   2: 215,242 ops/ms
# Warmup Iteration   3: 216,632 ops/ms
Iteration   1: 215,526 ops/ms
Iteration   2: 213,720 ops/ms
Iteration   3: 213,967 ops/ms
Iteration   4: 215,468 ops/ms
Iteration   5: 216,247 ops/ms
Iteration   6: 217,514 ops/ms
Iteration   7: 215,503 ops/ms
Iteration   8: 211,969 ops/ms
Iteration   9: 212,989 ops/ms
Iteration  10: 214,291 ops/ms

Result : 214,719 ±(99.9%) 2,483 ops/ms
  Statistics: (min, avg, max) = (211,969, 214,719, 217,514), stdev = 1,642
  Confidence interval (99.9%): [212,236, 217,202]


Benchmark                   Mode   Samples         Mean   Mean error    Units
a.b.s.b.Benchmark5a.run    thrpt        10      238,515        7,769   ops/ms
a.b.s.b.Benchmark5b.run    thrpt        10      130,202        2,232   ops/ms
a.b.s.b.Benchmark5c.run    thrpt        10     2756,132       29,920   ops/ms
a.b.s.b.Benchmark5d.run    thrpt        10      237,286        2,203   ops/ms
a.b.s.b.Benchmark5e.run    thrpt        10      214,719        2,483   ops/ms

Java详细结果

# VM invoker: /home/oracle-jdk-1.8-8u40/data/oracle-jdk-1.8.0_40/jre/bin/java
# VM options: <none>
# Fork: 1 of 1
# Warmup: 3 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: app.benchmark.java.benchmark5.Benchmark5c.run
# Warmup Iteration   1: 2777,495 ops/ms
# Warmup Iteration   2: 2888,040 ops/ms
# Warmup Iteration   3: 2692,851 ops/ms
Iteration   1: 2737,169 ops/ms
Iteration   2: 2745,368 ops/ms
Iteration   3: 2754,105 ops/ms
Iteration   4: 2706,131 ops/ms
Iteration   5: 2721,593 ops/ms
Iteration   6: 2769,261 ops/ms
Iteration   7: 2734,461 ops/ms
Iteration   8: 2741,494 ops/ms
Iteration   9: 2740,012 ops/ms
Iteration  10: 2709,915 ops/ms

Result : 2735,951 ±(99.9%) 29,177 ops/ms
  Statistics: (min, avg, max) = (2706,131, 2735,951, 2769,261), stdev = 19,299
  Confidence interval (99.9%): [2706,774, 2765,128]


Benchmark                   Mode   Samples         Mean   Mean error    Units
a.b.j.b.Benchmark5a.run    thrpt        10       40,437        0,532   ops/ms
a.b.j.b.Benchmark5b.run    thrpt        10        0,450        0,008   ops/ms
a.b.j.b.Benchmark5c.run    thrpt        10     2735,951       29,177   ops/ms

更新:使用ArrayList

添加了另一个Java方案5d
Benchmark                   Mode   Samples         Mean   Mean error    Units
a.b.j.b.Benchmark5a.run    thrpt        10       34,931        0,504   ops/ms
a.b.j.b.Benchmark5b.run    thrpt        10        0,430        0,005   ops/ms
a.b.j.b.Benchmark5c.run    thrpt        10     2610,085        9,664   ops/ms
a.b.j.b.Benchmark5d.run    thrpt        10       56,693        1,218   ops/ms

答案 1 :(得分:3)

你不能从这么短的实验中得出任何有关表现的有意义的结论。您可能遇到了垃圾收集,可能还有一些其他进程占用了CPU,所有类可能都没有加载,JVM可能会在您的测试运行时优化代码。任何一项都会对测试结果产生不利影响。

处理1000个元素将非常快。您应该让实验足够长,以使定时测量或外部影响的不准确性产生较小的影响。尝试使用一百万个元素,如果仍然需要几秒钟,请尝试1000万个。

在JVM启动之前,请考虑运行测试几次,然后再将测量结果用于预热&#34; JVM。您希望确保已加载任何延迟加载的类,并且在开始测试之前JVM执行的任何实时优化都已完成。

使用较长的元素列表重复几次实验。然后丢掉最快的结果和最慢的结果并平均其余的结果。

答案 2 :(得分:0)

可能会改进给定的scala示例:使用Scala中的尾递归函数,通常使用@tailrec注释提供编译器提示。有关详细信息,请参阅here。 [注:由于拼写错误而更新]

答案 3 :(得分:0)

我尝试了您的代码,Scala版本的速度提高了约5倍。它需要不到4秒,而Java版本需要将近20秒。

爪哇:

import java.util.List;
import java.util.LinkedList;

public class ListTest {
  public static int add(List<Integer> list, Integer sum) {
    if (list.isEmpty()) {
        return sum;
    } else {
        int headVal = list.remove(0);
        return add(list, sum + headVal);
    }
}

  public static void main(String[] args) {
    List<Integer> list = new LinkedList<>();
    int sum = 0;

    long start = System.nanoTime();
    for(int j = 0; j < 1000000; j++) {
      list.clear();
      for(int i = 1; i <= 1000; i++) list.add(i);
      sum = add(list, 0);
    }
    long end = System.nanoTime();
    System.out.println("time = " + ((end - start)/1e6) + "ms");
    System.out.println("sum = " + sum);
  }
}

Scala的:

object ListTest {
  def add(list : List[Int],sum:Int): Int = {
    if (list.isEmpty) {
        sum
    } else {
        val headVal  = list.head
        add(list.tail, sum + headVal)
    }
  }

  def 

main(args: Array[String]) {
    val list = List.range(1, 1001)
    var sum = 0

    val start = System.nanoTime
    for(i <- 1 to 1000000) sum = add(list, 0);
    val end = System.nanoTime

    println("time = " + ((end - start)/1e6) + "ms")
    println("sum = " + sum)
  }
}