私はこれに遭遇しました 古い質問 と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-Bit Server 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バージョンは2倍高速です。
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の奇妙な点が介入しましたが、この演習のポイントは、コード生成における驚くべきわずかな違いがベンチマークによって大幅に拡大されることです。
追伸@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バージョンのパフォーマンスが向上した場合、テール再帰の最適化はここで効果があると思います(Javaバージョンが再帰の代わりにループ、それは再び同じように実行されます)。
私は この質問 を見て、Scalaバージョンを編集して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 one:
> fsc ScalaMain.scala
> scala ScalaMain
....
time: 6373
> fsc -optimize ScalaMain.scala
....
time: 4703
Java末尾呼び出しがないためです。最適化されたJava再帰の代わりにループを使用すると、同じように速く実行されます:
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
結論として、元のScalaのバージョンは、t
をfinal
と宣言しなかったため、低速でした(直接的または間接的に Beryllium)として の answer が指摘しています)そして、元の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はメソッド呼び出しを最適化できないため、処理速度は遅くなります。