[英]Hidden performance cost in Scala?
我遇到了這個老問題 ,並使用scala 2.10.3進行了以下實驗。
我重寫了Scala版本以使用顯式尾遞歸:
import scala.annotation.tailrec
object ScalaMain {
private val t = 20
private def run() {
var i = 10
while(!isEvenlyDivisible(2, i, t))
i += 2
println(i)
}
@tailrec private def isEvenlyDivisible(i: Int, a: Int, b: Int): Boolean = {
if (i > b) true
else (a % i == 0) && isEvenlyDivisible(i+1, a, b)
}
def main(args: Array[String]) {
val t1 = System.currentTimeMillis()
var i = 0
while (i < 20) {
run()
i += 1
}
val t2 = System.currentTimeMillis()
println("time: " + (t2 - t1))
}
}
並將其與以下Java版本進行比較。 為了與Scala公平比較,我有意識地使函數非靜態:
public class JavaMain {
private final int t = 20;
private void run() {
int i = 10;
while (!isEvenlyDivisible(2, i, t))
i += 2;
System.out.println(i);
}
private boolean isEvenlyDivisible(int i, int a, int b) {
if (i > b) return true;
else return (a % i == 0) && isEvenlyDivisible(i+1, a, b);
}
public static void main(String[] args) {
JavaMain o = new JavaMain();
long t1 = System.currentTimeMillis();
for (int i = 0; i < 20; ++i)
o.run();
long t2 = System.currentTimeMillis();
System.out.println("time: " + (t2 - t1));
}
}
以下是我的計算機上的結果:
> java JavaMain
....
time: 9651
> scala ScalaMain
....
time: 20592
這是scala 2.10.3 on(Java HotSpot(TM)64位服務器VM,Java 1.7.0_51)。
我的問題是scala版本的隱藏成本是多少?
非常感謝。
那么,OP的基准測試並不理想。 需要減少大量的影響,包括預熱,死代碼消除,分叉等。幸運的是, JMH已經處理了很多事情,並且對Java和Scala都有綁定。 請按照JMH頁面上的程序獲取基准測試項目,然后您可以移植下面的基准測試。
這是示例Java基准測試:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(3)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
public class JavaBench {
@Param({"1", "5", "10", "15", "20"})
int t;
private int run() {
int i = 10;
while(!isEvenlyDivisible(2, i, t))
i += 2;
return i;
}
private boolean isEvenlyDivisible(int i, int a, int b) {
if (i > b)
return true;
else
return (a % i == 0) && isEvenlyDivisible(i + 1, a, b);
}
@GenerateMicroBenchmark
public int test() {
return run();
}
}
......這是示例Scala基准測試:
@BenchmarkMode(Array(Mode.AverageTime))
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(3)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
class ScalaBench {
@Param(Array("1", "5", "10", "15", "20"))
var t: Int = _
private def run(): Int = {
var i = 10
while(!isEvenlyDivisible(2, i, t))
i += 2
i
}
@tailrec private def isEvenlyDivisible(i: Int, a: Int, b: Int): Boolean = {
if (i > b) true
else (a % i == 0) && isEvenlyDivisible(i + 1, a, b)
}
@GenerateMicroBenchmark
def test(): Int = {
run()
}
}
如果你在JDK 8 GA,Linux x86_64上運行它們,那么你會得到:
Benchmark (t) Mode Samples Mean Mean error Units
o.s.ScalaBench.test 1 avgt 15 0.005 0.000 us/op
o.s.ScalaBench.test 5 avgt 15 0.489 0.001 us/op
o.s.ScalaBench.test 10 avgt 15 23.672 0.087 us/op
o.s.ScalaBench.test 15 avgt 15 3406.492 9.239 us/op
o.s.ScalaBench.test 20 avgt 15 2483221.694 5973.236 us/op
Benchmark (t) Mode Samples Mean Mean error Units
o.s.JavaBench.test 1 avgt 15 0.002 0.000 us/op
o.s.JavaBench.test 5 avgt 15 0.254 0.007 us/op
o.s.JavaBench.test 10 avgt 15 12.578 0.098 us/op
o.s.JavaBench.test 15 avgt 15 1628.694 11.282 us/op
o.s.JavaBench.test 20 avgt 15 1066113.157 11274.385 us/op
注意我們玩弄t
以查看效果是否是t
的特定值的局部效應。 它不是,效果是系統的,Java版本的速度是其兩倍。
PrintAssembly將對此有所了解。 這是Scala基准測試中最熱門的一個:
0x00007fe759199d42: test %r8d,%r8d
0x00007fe759199d45: je 0x00007fe759199d76 ;*irem
; - org.sample.ScalaBench::isEvenlyDivisible@11 (line 52)
; - org.sample.ScalaBench::run@10 (line 45)
0x00007fe759199d47: mov %ecx,%eax
0x00007fe759199d49: cmp $0x80000000,%eax
0x00007fe759199d4e: jne 0x00007fe759199d58
0x00007fe759199d50: xor %edx,%edx
0x00007fe759199d52: cmp $0xffffffffffffffff,%r8d
0x00007fe759199d56: je 0x00007fe759199d5c
0x00007fe759199d58: cltd
0x00007fe759199d59: idiv %r8d
...這是Java中類似的塊:
0x00007f4a811848cf: movslq %ebp,%r10
0x00007f4a811848d2: mov %ebp,%r9d
0x00007f4a811848d5: sar $0x1f,%r9d
0x00007f4a811848d9: imul $0x55555556,%r10,%r10
0x00007f4a811848e0: sar $0x20,%r10
0x00007f4a811848e4: mov %r10d,%r11d
0x00007f4a811848e7: sub %r9d,%r11d ;*irem
; - org.sample.JavaBench::isEvenlyDivisible@9 (line 63)
; - org.sample.JavaBench::isEvenlyDivisible@19 (line 63)
; - org.sample.JavaBench::run@10 (line 54)
請注意,在Java版本中,編譯器如何使用技巧將整數余數計算轉換為乘法和右移(參見Hacker's Delight,Ch.10,Sect.19)。 當編譯器檢測到我們根據常量計算余數時,這是可能的,這表明Java版本達到了甜蜜的優化,但Scala版本卻沒有。 您可以深入研究字節碼反匯編以找出scalac中干預的怪癖,但本練習的重點是代碼生成中令人驚訝的微小差異被基准放大了很多。
PS @tailrec
......
更新:對效果的更全面的解釋: http : //shipilev.net/blog/2014/java-scala-divided-we-fail/
我改變了val
private val t = 20
一個恆定的定義
private final val t = 20
並且得到了顯着的性能提升,現在似乎兩個版本的表現幾乎相同[在我的系統上,請參閱更新和評論]。
我沒有研究字節碼,但如果使用val t = 20
你可以看到使用javap
有一個方法(並且該版本與private val
版本一樣慢)。
所以我假設即使是private val
涉及調用一個方法,而這與Java中的final
無法直接比較。
更新
在我的系統上,我得到了這些結果
Java版本:時間:14725
Scala版本:時間:13228
在32位Linux上使用OpenJDK 1.7。
根據我的經驗,64位系統上的Oracle JDK實際上表現更好,因此這可能解釋了其他測量結果會產生更好的結果,有利於Scala版本。
至於Scala版本表現更好我假設尾遞歸優化確實在這里產生影響(參見Phil的答案,如果Java版本被重寫為使用循環而不是遞歸,它會再次執行)。
我看了看這個問題和編輯的斯卡拉版本有t
內run
:
object ScalaMain {
private def run() {
val t = 20
var i = 10
while(!isEvenlyDivisible(2, i, t))
i += 2
println(i)
}
@tailrec private def isEvenlyDivisible(i: Int, a: Int, b: Int): Boolean = {
if (i > b) true
else (a % i == 0) && isEvenlyDivisible(i+1, a, b)
}
def main(args: Array[String]) {
val t1 = System.currentTimeMillis()
var i = 0
while (i < 20) {
run()
i += 1
}
val t2 = System.currentTimeMillis()
println("time: " + (t2 - t1))
}
}
新的Scala版本現在運行速度是原始Java版本的兩倍:
> fsc ScalaMain.scala
> scala ScalaMain
....
time: 6373
> fsc -optimize ScalaMain.scala
....
time: 4703
我發現這是因為Java沒有尾調用。 優化的Java with loop而不是遞歸運行速度同樣快:
public class JavaMain {
private static final int t = 20;
private void run() {
int i = 10;
while (!isEvenlyDivisible(i, t))
i += 2;
System.out.println(i);
}
private boolean isEvenlyDivisible(int a, int b) {
for (int i = 2; i <= b; ++i) {
if (a % i != 0)
return false;
}
return true;
}
public static void main(String[] args) {
JavaMain o = new JavaMain();
long t1 = System.currentTimeMillis();
for (int i = 0; i < 20; ++i)
o.run();
long t2 = System.currentTimeMillis();
System.out.println("time: " + (t2 - t1));
}
}
現在我的困惑完全解決了:
> java JavaMain
....
time: 4795
總之,最初的版本斯卡拉是緩慢的,因為我沒有申報t
是final
(直接或間接地為鈹的答案指出)。 由於缺少尾調用,原始Java版本很慢。
要使Java版本完全等同於Scala代碼,您需要像這樣更改它。
private int t = 20;
private int t() {
return this.t;
}
private void run() {
int i = 10;
while (!isEvenlyDivisible(2, i, t()))
i += 2;
System.out.println(i);
}
它較慢,因為JVM無法優化方法調用。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.