Je suis tombé sur cette ancienne question et j'ai fait l'expérience suivante avec scala 2.10.3.
J'ai réécrit la version Scala pour utiliser la récursivité explicite de la queue:
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))
}
}
et je l'ai comparé à la version Java version. J'ai consciemment rendu les fonctions non statiques pour une comparaison équitable avec 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));
}
}
Voici les résultats sur mon ordinateur:
> Java JavaMain
....
time: 9651
> scala ScalaMain
....
time: 20592
Il s'agit de scala 2.10.3 sur (Java HotSpot (TM) 64-Bit Server VM, Java 1.7.0_51).
Ma question est quel est le coût caché avec la version scala?
Merci beaucoup.
Eh bien, l'analyse comparative d'OP n'est pas l'idéal. Des tonnes d'effets doivent être atténués, y compris l'échauffement, l'élimination du code mort, la fourche, etc. Heureusement, JMH s'occupe déjà de beaucoup de choses et a des liaisons pour les deux Java et Scala. Veuillez suivre les procédures sur la page JMH pour obtenir le projet de référence, puis vous pouvez transplanter les références ci-dessous.
Voici l'exemple Java benchmark:
@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();
}
}
... et voici l'exemple Scala benchmark:
@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()
}
}
Si vous les exécutez sur JDK 8 GA, Linux x86_64, vous obtiendrez:
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
Remarquez que nous jonglons avec t
pour voir si l'effet est local pour la valeur particulière de t
. Ce n'est pas le cas, l'effet est systématique, et Java étant deux fois plus rapide.
PrintAssembly nous éclairera à ce sujet. Celui-ci est le bloc le plus chaud de Scala benchmark:
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
... et c'est un bloc similaire en 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)
Remarquez comment dans la version Java, le compilateur a utilisé l'astuce pour traduire le calcul du reste entier en multiplication et en se déplaçant vers la droite (voir Hacker's Delight, Ch. 10, Sect. 19). Ceci est possible lorsque le compilateur détecte nous calculons le reste par rapport à la constante, ce qui suggère Java a atteint cette douce optimisation, mais Scala ne l'a pas fait. Vous pouvez creuser dans le démontage du bytecode pour comprendre ce qui est intervenu dans scalac est intervenu, mais le but de cet exercice est que les différences minuscules surprenantes dans la génération de code sont amplifiées par des repères beaucoup.
P.S. Tant pour @tailrec
...
MISE À JOUR: Une explication plus approfondie de l'effet: http://shipilev.net/blog/2014/Java-scala-divided-we-fail/
J'ai changé le val
private val t = 20
à une définition constante
private final val t = 20
et a obtenu une augmentation significative des performances, il semble maintenant que les deux versions fonctionnent presque également [sur mon système, voir la mise à jour et les commentaires].
Je n'ai pas examiné le bytecode, mais si vous utilisez val t = 20
vous pouvez voir en utilisant javap
qu'il existe une méthode (et que la version est aussi lente que celle avec le private val
).
Je suppose donc que même un private val
implique l'appel d'une méthode, et ce n'est pas directement comparable à un final
en Java.
Mise à jour
Sur mon système, j'ai obtenu ces résultats
Version Java: temps: 14725
Version Scala: temps: 13228
Utilisation d'OpenJDK 1.7 sur un Linux 32 bits.
D'après mon expérience, le JDK d'Oracle sur un système 64 bits fonctionne réellement mieux, donc cela explique probablement que d'autres mesures donnent des résultats encore meilleurs en faveur de la version Scala.
Quant à la version Scala fonctionnant mieux, je suppose que l'optimisation de la récursivité de queue a un effet ici (voir la réponse de Phil, si la version Java Java est réécrite pour utiliser un boucle au lieu de récursivité, il fonctionne à nouveau également).
J'ai regardé cette question et édité la version Scala pour avoir t
à l'intérieur 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))
}
}
La nouvelle version Scala s'exécute désormais deux fois plus vite que la version originale Java:
> fsc ScalaMain.scala
> scala ScalaMain
....
time: 6373
> fsc -optimize ScalaMain.scala
....
time: 4703
J'ai compris que c'était parce que Java n'avait pas d'appels de queue. Le Java optimisé avec boucle au lieu de récursivité s'exécute tout aussi rapidement:
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));
}
}
Maintenant, ma confusion est complètement résolue:
> Java JavaMain
....
time: 4795
En conclusion, la version originale Scala était lente car je n'ai pas déclaré t
comme final
(directement ou indirectement, comme Beryllium 's - réponse souligne). Et la version originale Java était lente en raison du manque d'appels de queue.
Pour rendre la version Java complètement équivalente à votre code Scala, vous devez la changer comme ceci.).
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);
}
Il est plus lent car la JVM ne peut pas optimiser les appels de méthode.