使用尾递归进行简单加法的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属性相同。
答案 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)
@tailrec
。 List.sum
:非常慢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
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)
}
}