J'ai quelques difficultés à comprendre LookUpSwitch et TableSwitch dans le bytecode Java.
Si je comprends bien, LookUpSwitch et TableSwitch correspondent-ils à l'instruction switch
du code source Java? Pourquoi une instruction Java génère-t-elle 2 octets différents?
Jasmin documentation de chacun:
La différence est qu'un lookupswitch utilise une table avec des clés et des étiquettes alors qu'un tableswitch utilise une table avec des étiquettes uniquement .
Lors de l'exécution de tableswitch , la valeur int au sommet de la pile est directement utilisée comme index dans la table pour saisir la destination du saut et effectuer le saut immédiatement. L'ensemble du processus lookup + jump est une opération O(1) , ce qui signifie que le processus est extrêmement rapide.
Lors de l'exécution de lookupswitch , la valeur int située en haut de la pile est comparée aux clés de la table jusqu'à ce qu'une correspondance soit trouvée, puis la destination du saut située à côté de cette clé est utilisée pour effectuer le saut. Puisqu'une table lookupswitch doit toujours doit être triée pour que keyX <keyY pour chaque X <Y, l'ensemble du processus lookup + jump soit une opération O (log n) , car la clé sera recherchée à l'aide d'un fichier binaire. algorithme de recherche (il n'est pas nécessaire de comparer la valeur int à toutes les clés possibles pour trouver une correspondance ou pour déterminer qu'aucune des clés ne correspond). O (log n) est un peu plus lent que O (1), mais il n’en reste pas moins bon puisque de nombreux algorithmes bien connus sont O (log n) et qu’ils sont généralement considérés comme rapides; même O(n) ou O (n * log n) est toujours considéré comme un très bon algorithme (les algorithmes lents/mauvais ont O (n ^ 2), O (n ^ 3) ou même pire) .
La décision de l’instruction à utiliser est prise par le compilateur sur la base du fait que compact l’instruction switch est, par ex.
switch (inputValue) {
case 1: // ...
case 2: // ...
case 3: // ...
default: // ...
}
L'interrupteur ci-dessus est parfaitement compact, il n'a pas de "trous" numériques. Le compilateur créera un tableswitch comme ceci:
tableswitch 1 3
OneLabel
TwoLabel
ThreeLabel
default: DefaultLabel
Le pseudo-code de la page Jasmin explique assez bien ceci:
int val = pop(); // pop an int from the stack
if (val < low || val > high) { // if its less than <low> or greater than <high>,
pc += default; // branch to default
} else { // otherwise
pc += table[val - low]; // branch to entry in table
}
Ce code est assez clair sur le fonctionnement d'un tel tableswitch. val
est inputValue
, low
serait 1 (la valeur de casse la plus basse du commutateur) et high
serait de 3 (la valeur de casse la plus élevée du commutateur).
Même avec certains trous, un commutateur peut être compact, par exemple.
switch (inputValue) {
case 1: // ...
case 3: // ...
case 4: // ...
case 5: // ...
default: // ...
}
L'interrupteur ci-dessus est "presque compact", il n'a qu'un seul trou. Un compilateur pourrait générer l'instruction suivante:
tableswitch 1 6
OneLabel
FakeTwoLabel
ThreeLabel
FourLabel
FiveLabel
default: DefaultLabel
; <...code left out...>
FakeTwoLabel:
DefaultLabel:
; default code
Comme vous pouvez le constater, le compilateur doit ajouter un cas fake pour 2, FakeTwoLabel
. Puisque 2 n'est pas une valeur réelle du commutateur, FakeTwoLabel est en fait une étiquette qui modifie le flux de code exactement là où se trouve le cas par défaut, puisqu'un 2 vaut en fait d'exécuter le cas par défaut.
Il n’est donc pas nécessaire que le commutateur soit parfaitement compact pour que le compilateur crée un commutateur de table, mais il devrait au moins être assez proche de la compacité. Considérons maintenant le commutateur suivant:
switch (inputValue) {
case 1: // ...
case 10: // ...
case 100: // ...
case 1000: // ...
default: // ...
}
Cet interrupteur est loin d'être compact, il a plus de cent fois plus de trous que les valeurs. On appellerait cela un commutateur de rechange. Le compilateur devrait générer presque mille faux cas pour exprimer ce commutateur sous la forme d'un commutateur de table. Le résultat serait une table énorme, augmentant considérablement la taille du fichier de classe. Ce n'est pas pratique Au lieu de cela, il générera un commutateur de recherche:
lookupswitch
1 : Label1
10 : Label10
100 : Label100
1000 : Label1000
default : DefaultLabel
Cette table a seulement 5 entrées, au lieu de plus de mille. La table a 4 valeurs réelles, O (log 4) est 2 (log est ici log à la base de 2 BTW, pas à la base de 10, car l’ordinateur fonctionne sur des nombres binaires). Cela signifie qu'il faut au plus deux comparaisons VM pour rechercher le libellé de la valeur inputValue ou pour conclure que la valeur ne figure pas dans la table et que la valeur par défaut doit donc être exécutée. Même si la table comportait 100 entrées, il faudrait au plus 7 comparaisons VM pour trouver l'étiquette correcte ou décider de passer à l'étiquette par défaut (et 7 comparaisons sont beaucoup moins que 100 comparaisons, don ' vous ne pensez pas?).
Il est donc insensé que ces deux instructions soient interchangeables ou que le motif de deux instructions ait des raisons historiques. Il existe deux instructions pour deux types de situations différentes, une pour les commutateurs avec des valeurs compactes (pour une vitesse maximale) et une pour les commutateurs avec des valeurs de réserve (pas une vitesse maximale, mais une vitesse toujours bonne et une représentation de table très compacte, quels que soient les trous numériques).
À quel moment exactement javac 1.8.0_45 compile est-il compilé?
Pour décider quand utiliser lequel, vous pouvez utiliser l'algorithme de choix javac
comme base.
Nous savons que la source de javac
se trouve dans le repo langtools
.
Alors nous grep:
hg grep -i tableswitch
et le premier résultat est langtools/src/share/classes/com/Sun/tools/javac/jvm/Gen.Java :
// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;
Où:
hi
: valeur maximale de casselo
: valeur de cas minimumNous concluons donc qu’il prend en compte à la fois la complexité temporelle et spatiale, avec un poids de 3 pour la complexité temporelle.
TODO Je ne comprends pas pourquoi lookup_time_cost = nlabels
et non log(nlabels)
, puisqu’une tableswitch
pourrait être faite dans O(log(n)) avec recherche binaire.
Bonus: les implémentations C++ font également un choix analogue entre un O(1) table de saut et O(long(n)) recherche binaire: Avantage de passer à l'instruction if-else
Spécification de la machine virtuelle Java décrivez la différence. "L'instruction tableswitch est utilisée lorsque les cas du commutateur peuvent être efficacement représentés sous forme d'indices dans une table de décalages cibles." La spécification décrit les plus de détails.
Je soupçonne qu’il s’agit essentiellement d’historique, en raison de la liaison spécifique du bytecode Java avec le code machine souligné (par exemple, le propre processeur de Sun).
Le tableswitch est essentiellement un saut calculé, où la destination provient d'une table de recherche. Au contraire, lookupswitch nécessite la comparaison de chaque valeur, essentiellement une itération d'éléments de table jusqu'à ce que la valeur correspondante soit trouvée.
Évidemment, ces codes d'opération sont interchangeables, mais en fonction de valeurs, l'un ou l'autre pourrait être plus rapide ou plus compact (par exemple, comparer un jeu de clés avec de grands espaces et un jeu séquentiel de clés).