Ответ 1
Ну, бенчмаркинг 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::[email protected] (line 52)
; - org.sample.ScalaBench::[email protected] (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::[email protected] (line 63)
; - org.sample.JavaBench::[email protected] (line 63)
; - org.sample.JavaBench::[email protected] (line 54)
Обратите внимание, что в версии Java компилятор использовал трюк для перевода вычисления целочисленного остатка в умножение и смещение вправо (см. Hacker Delight, глава 10, раздел 19). Это возможно, когда компилятор обнаруживает, что мы вычисляем остаток по отношению к константе, что предполагает, что версия Java поражает эту слабую оптимизацию, но версия Scala не указала. Вы можете разобраться в разборке байт-кода, чтобы выяснить, какие причуды в scalac вмешались, но суть этого упражнения в том, что удивительные небольшие различия в генерации кода значительно увеличиваются с помощью тестов.
P.S. Так много для @tailrec
...
UPDATE: более подробное объяснение эффекта: http://shipilev.net/blog/2014/java-scala-divided-we-fail/