Je vais résumer ce que j'ai appris sur le mécanisme du compilateur JIT de la JVM.
~/workspace/$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)
Compilateur Just In Time, implémenté sur JVM. Just In Time, un compilateur qui compile "ce dont vous avez besoin, quand vous en avez besoin".
En gros, le flux suivant
Code source->compiler->Code intermédiaire->Exécuter (compiler)
Le langage JVM ne compile pas tout le code source d'intérêt lorsque vous exécutez la compilation (javac
pour java, sbt compile
pour scala, etc.).
Tout d'abord, créez un code intermédiaire. (code octet java). En prenant en sandwich le processus de génération de ce code intermédiaire, le même code peut être exécuté sur n'importe quel système d'exploitation tant qu'il existe un environnement JVM. (JVM doit être adapté à chaque système d'exploitation)
Après cela, le code intermédiaire créé n'est pas compilé (converti en code natif) à la fois. L'interpréteur compile le code source à chaque fois qu'il est exécuté. (Interpréteur Java dans la figure ci-dessus)
Il y a deux raisons à cela.
Comme la compilation prend du temps, pour le code qui n'est appelé qu'une seule fois, son exécution avec l'interpréteur réduira le temps total d'exécution. D'un autre côté, pour le code fréquemment appelé, le code compilé peut être exécuté plus rapidement et doit être compilé. La discussion du seuil sur la JVM pour savoir s'il faut exécuter avec l'interpréteur ou compiler et exécuter sera décrite plus loin.
Vous pouvez obtenir les informations dont vous avez besoin pour compiler lorsque vous l'exécutez dans l'interpréteur. Diverses optimisations peuvent être effectuées au moment de la compilation en utilisant les informations acquises. Cette optimisation fait une différence dans le temps d'exécution même dans ce code compilé.
Par exemple, considérons la méthode ʻequals ()`
J'ai le code suivant.
test.scala
val b = obj1.equals(obj2)
Lorsque l'interpréteur atteint la méthode ʻequals () , il devient nécessaire de rechercher si la méthode ʻequals ()
est la méthode définie dans obj1 ou la méthode de l'objet String. Avec seulement l'interpréteur, ce serait une perte de temps de chercher à chaque fois que la méthode ʻequals () `est atteinte.
Si l'interpréteur détermine que obj1 est une méthode d'un objet String, il compile la méthode ʻequals () `comme une méthode d'un objet String. Il compile et élimine le temps qu'il passe à explorer lors de l'interprétation, ce qui accélère le code.
Comme vous pouvez le voir, le compilateur JIT ne compile pas le code tout de suite car il ne peut pas être optimisé sans exécuter et examiner le code.
Il existe trois types de compilateurs JIT. A partir de java8, le troisième compilateur hiérarchique est défini par défaut.
Compiler à un stade précoce
Rassemblez des informations sur le comportement de votre code avant de le compiler. Comme mentionné ci-dessus, il est optimisé et compilé, il est donc plus rapide que le compilateur client.
Une collection de compilateur client et de compilateur serveur. Au début, il est compilé en C1 et lorsque les informations d'optimisation sont collectées (lorsque le code chauffe), il est compilé en C2.
Vous pouvez vérifier le compilateur configuré avec java -version Dans mon cas
~/workspace/$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)
Chaque compilateur doit être utilisé correctement en fonction de l'application à créer. Par exemple, si vous exécutez une application GUI sur une machine virtuelle Java, vous devez utiliser un compilateur client car il est préférable pour UX d'avoir un temps d'accès initial plus rapide que d'augmenter la vitesse de traitement à mesure que vous l'utilisez.
Sélectionnez le compilateur en fonction de l'application et de ce que vous souhaitez réaliser.
Comme mentionné ci-dessus, le code d'octet est d'abord exécuté par l'interpréteur. Ensuite, à quel moment pouvez-vous passer de l'interpréteur au compilateur JIT? Il y a deux seuils
Nombre d'appels de la méthode cible
Lorsque ce nombre dépasse le seuil, la méthode cible est mise en file d'attente pour la compilation et compilée.
Le nombre de fois que le traitement retourne du code dans la boucle au sein de la méthode
Lorsque ce nombre dépasse le seuil, la boucle elle-même devient la cible de la compilation et est compilée. La compilation effectuée à ce moment-là est appelée compilation OSR. Lorsque la compilation est terminée, il est échangé contre le code compilé sur la pile, et le code compilé est exécuté à partir du processus suivant.
Les seuils de compteur ci-dessus sont différents entre le compilateur client et le compilateur serveur. Vous devez régler ce seuil correctement.
Si vous abaissez le seuil de compilation standard avec le compilateur serveur, les informations requises pour la compilation seront réduites, ce qui rendra son optimisation difficile et entraînera un code de compilation lent.
Pourtant, il y a un mérite d'abaisser le seuil
C'est vrai.
À cet égard, il est probable que le seuil du compteur d'appels / du compteur de bord de sac sera finalement atteint au fur et à mesure que le code continue à s'exécuter. Cependant, la valeur du compteur est soustraite au fil du temps.
Comme mentionné ci-dessus, vous devez régler correctement.
Observez et ajustez le comportement du compilateur JIT avec le code suivant
Vous pouvez spécifier l'option jvm dans .jvmopts
comme indiqué ci-dessous.
.jvmopts
-XX:+PrintCompilation
-XX:CompileThreshold=1
-XX:+PrintCompilation
Il crache le journal de compilation comme indiqué ci-dessous
Le format est
Désoptimisation de la taille du nom de la méthode d'attribut ID de compilation d'horodatage
$ sbt run
41 1 3 java.lang.Object::<init> (1 bytes)
42 2 3 java.lang.String::hashCode (55 bytes)
44 3 3 java.lang.String::charAt (29 bytes)
45 4 3 java.lang.String::equals (81 bytes)
45 5 n 0 java.lang.System::arraycopy (native) (static)
45 6 3 java.lang.Math::min (11 bytes)
45 7 3 java.lang.String::length (6 bytes)
52 8 1 java.lang.Object::<init> (1 bytes)
52 1 3 java.lang.Object::<init> (1 bytes) made not entrant
53 9 3 java.util.jar.Attributes$Name::isValid (32 bytes)
53 10 3 java.util.jar.Attributes$Name::isAlpha (30 bytes)
・ ・ ・ ・
XX:CompileThreshold=1000
Vous pouvez spécifier le nombre d'exécutions de la boucle de méthode avant sa compilation.
Essayez-le avec le code suivant
Test.scala
object Test extends App{
def compileTest() = {
for (i <- 0 to 1000) {
sampleLoop(i)
}
}
def sampleLoop(num: Int) = {
println(s"loopppp${num}")
}
println(compileTest())
}
.jvmopts
-XX:+PrintCompilation
-XX:CompileThreshold=1
résultat
Depuis que j'ai défini -XX: CompileThreshold = 1
, je peux confirmer que la méthode compileTest
a été compilée en exécutant ce code une fois.
De plus, la méthode sampleLoop
est également une boucle, elle est donc compilée.
9983 9336 3 Test$$$Lambda$3666/873055587::apply$mcVI$sp (5 bytes)
9983 9338 3 Test$::sampleLoop (1 bytes)
9983 9337 3 Test$::$anonfun$compileTest$1 (8 bytes)
9984 9334 4 java.lang.invoke.MethodType::makeImpl (66 bytes)
9986 9339 ! 3 scala.Enumeration$Val::toString (55 bytes)
・ ・ ・
La méthode compileTest
est compilée 9 secondes après le démarrage de la JVM.
Par exemple, qu'en est-il des paramètres suivants?
object Test extends App{
def compileTests() = {
for (i <- 0 to 10) { //Passer à 10 boucles
sampleLoop(i)
}
}
def sampleLoop(num: Int) = {
println(s"loopppp${num}")
}
println(compileTests())
}
.jvmopts
-XX:+PrintCompilation
-XX:CompileThreshold=100 #Changer le seuil à 100 fois
Si vous définissez -XX: CompileThreshold = 100
etc., la méthode compileTest
ne sera pas compilée simplement en exécutant le code ci-dessus une fois.
De plus, la méthode sampleLoop
n'est pas exécutée car elle n'est pas exécutée 100 fois.
Il est facile de comprendre si vous regardez réellement le processus de compilation JIT.
Recommended Posts