

JVM Hardcore – Part 5 – Bytecode – Mathématiques et Conversions
Au cours de cet article nous allons nous intéresser aux instructions bytecode dédiées aux opérations sur les nombres. Ces opérations sont divisées en deux catégories :
- Les opérations arithmétiques (addition, soustraction, multiplication, division)
- Les opérations bit à bit (or, xor, not, and, etc.)
En mémoire, les entiers et les nombres à virgules sont représentés différemment. Pour cette raison, comme l’avons déjà vu, il y a plusieurs instructions pour une même opération. Les int
et les long
sont représentés en utilisant le complément à deux. Les float
et les double
sont quant à eux représentés en utilisant le standard IEEE 754.
Le code est disponible sur Github (tag et branche)
Tous les articles déjà publiés de la série portent le tag jvmhardcore.
Représentation de la pile
La JVM étant basée sur le modèle de la pile, il est essentiel de connaître quel est l’impact des instructions. Pour représenter l’état avant/après l’exécution d’une instruction, nous allons reprendre le format utiliser par la JVMS et qui est le suivant :
..., valeur1, valeur2 → ..., résultat
, où les valeurs les plus à droite sont au sommet de la pile. valeur1
et valeur2
étant les deux valeurs utilisées pour le calcul et résultat
le résultat.
Il est important de noter que dans cette représentation les long
et le double
sont considérés comme une seule valeur. Par conséquent, lorsque nécessaire nous présenterons les différents cas d’utilisation d’une instruction en utilisant plusieurs formes.
Opérations arithmétiques
Addition
Les quatre instructions suivantes permettent d’additionner deux nombres.
Les deux nombres sont dépilés et le résultat est empilé.
Pour une opération du type x + y
, où le signe +
peut être remplacé par -
, *
, /
ou %
(modulo), y
est au sommet de la pile et x
au dessous. L’addition et la multiplication étant commutatives cet ordre n’a pas importance. En revanche, pour les autres opérations il faut en tenir compte.
État de la pile avant → après exécution : ..., v1, v2 → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x60 |
iadd |
v1 + v2 , où v1 et v2 doivent être de type int |
0x61 |
ladd |
v1 + v2 , où v1 et v2 doivent être de type long |
0x62 |
fadd |
v1 + v2 , où v1 et v2 doivent être de type float |
0x63 |
dadd |
v1 + v2 , où v1 et v2 doivent être de type double |
Note : En Java l’opérateur +
permet aussi de concaténer des chaînes de caractères. La JVM n’a pas une telle instruction. En réalité le compilateur Java remplace toutes les concaténations par des StringBuilder pour les versions de Java supérieures ou égales à Java 5 et des StringBuffer pour les versions inférieures ou égales à Java 1.4. La différence entre les deux est que toutes les opérations d’un StringBuffer
sont synchronisées, contrairement à celles d’un StringBuilder
Pour changer un peu, nous allons voir une addition avec trois termes :
.class org/isk/jvmhardcore/bytecode/partfive/Addition .method add(III)I; iload_0 # [empty] -> idx0 iload_1 # idx0 -> idx0, idx1 iadd # idx0, idx1 -> résultatPartiel iload_2 # résultatPartiel -> résultatPartiel, idx2 iadd # résultatPartiel, idx2 -> résultat ireturn .methodend .classend
Test unitaire :
public void add() { final int result = Addition.add(1, 2, 3); Assert.assertEquals(6, result); }
Soustraction
Les quatre instructions suivantes permettent de soustraire deux nombres.
État de la pile avant → après exécution : ..., v1, v2 → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x64 |
isub |
v1 - v2 , où v1 et v2 doivent être de type int |
0x65 |
lsub |
v1 - v2 , où v1 et v2 doivent être de type long |
0x66 |
fsub |
v1 - v2 , où v1 et v2 doivent être de type float |
0x67 |
dsub |
v1 - v2 , où v1 et v2 doivent être de type double |
Exemple :
.class org/isk/jvmhardcore/bytecode/partfive/Subtraction .method subtract(DDD)D; dload_0 # [empty] -> idx0 dload_2 # idx0 -> idx0, idx2 dsub # idx0, idx2 -> résultatPartiel dload 4 # résultatPartiel -> résultatPartiel, idx4 dsub # résultat dreturn .methodend .classend
Test unitaire :
public void subtract() { final double result = Subtraction.subtract(1, 2, 3); Assert.assertEquals(-4d, result, 0.0001); }
Multiplication
Les quatre instructions suivantes permettent de multiplier deux nombres.
État de la pile avant → après exécution : ..., v1, v2 → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x68 |
imul |
v1 * v2 , où v1 et v2 doivent être de type int |
0x69 |
lmul |
v1 * v2 , où v1 et v2 doivent être de type long |
0x6a |
fmul |
v1 * v2 , où v1 et v2 doivent être de type float |
0x6b |
dmul |
v1 * v2 , où v1 et v2 doivent être de type double |
Exemple :
.class org/isk/jvmhardcore/bytecode/partfive/Multiplication .method multiply(FF)F; fload_0 # [empty] -> idx0 fload_1 # idx0 -> idx0, idx1 fmul # idx0, idx1 -> résultat freturn .methodend .classend
Test unitaire :
public void multiply() { final float result = Multiplication.multiply(3, 2); Assert.assertEquals(6, result, 0.0001); }
Division
Les quatre instructions suivantes permettent de diviser deux nombres.
État de la pile avant → après exécution : ..., v1, v2 → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x6c |
idiv |
v1 / v2 , où v1 et v2 doivent être de type int |
0x6d |
ldiv |
v1 / v2 , où v1 et v2 doivent être de type long |
0x6e |
fdiv |
v1 / v2 , où v1 et v2 doivent être de type float |
0x6f |
ddiv |
v1 / v2 , où v1 et v2 doivent être de type double |
Exemple :
.class org/isk/jvmhardcore/bytecode/partfive/Division .method divide(JJ)J lload_0 # [empty] -> idx0 lload_2 # idx0 -> idx0, idx2 ldiv # idx0, idx2 -> résultat lreturn .methodend .classend
Test unitaire :
public void divide() { final long result = Division.divide(3, 2); Assert.assertEquals(1, result); }
Reste
Les quatre instructions suivantes permettent d’obtenir le reste de la division de deux nombres.
État de la pile avant → après exécution : ..., v1, v2 → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x70 |
irem |
v1 % v2 , où v1 et v2 doivent être de type int |
0x71 |
lrem |
v1 % v2 , où v1 et v2 doivent être de type long |
0x72 |
frem |
v1 % v2 , où v1 et v2 doivent être de type float |
0x73 |
drem |
v1 % v2 , où v1 et v2 doivent être de type double |
Exemple :
.class org/isk/jvmhardcore/bytecode/partfive/Remainder .method getRemainder(II)I iload_0 # [empty] -> idx0 iload_1 # idx0 -> idx0, idx1 irem # idx0, idx1 -> result ireturn .methodend .classend
Test unitaire :
public void getRemainder() { final int result = Remainder.getRemainder(40, 7); Assert.assertEquals(5, result); }
Négation
Les quatre instructions suivantes permettent de changer le signe du nombre au sommet de la pile.
État de la pile avant → après exécution : ..., valeur → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x74 |
ineg |
Inverse le signe d’un int |
0x75 |
lneg |
Inverse le signe d’un long |
0x76 |
fneg |
Inverse le signe d’un float |
0x77 |
dneg |
Inverse le signe d’un double |
Exemple
.class org/isk/jvmhardcore/bytecode/partfive/Negation .method negate(I)I iload_0 # [empty] -> idx0 ineg # idx0 -> résultat ireturn .methodend .classend
Test unitaire
public void negate() { final int result = Negation.negate(-10); Assert.assertEquals(10, result); }
Opérations bit à bit
Opérations de décalage de bits
Les instructions suivantes permettent d’effectuer des opérations de décalage de bits.
Etat de la pile avant → après exécution : ..., v1, v2 → ..., r
.
Hex | Mnémonique | Description | |
---|---|---|---|
0x78 |
ishl |
v1 << v2 , où v1 et v2 doivent être de type int . Seulement les 5 bits les plus faibles de v1 sont pris en compte. |
|
0x79 |
lshl |
v1 << v2 , où v1 doit être de type long et v2 de type int . Seulement les 5 bits les plus faibles de v2 sont pris en compte. |
|
0x7a |
ishr |
v1 >> v2 , où v1 et v2 doivent être de type int . Seulement les 5 bits les plus faibles de v1 sont pris en compte. |
|
0x7b |
lshr |
v1 >> v2 , où v1 doit être de type long et v2 de type long . Seulement les 6 bits les plus faibles de v1 sont pris en compte. |
|
0x7c |
iushr |
v1 >>> v2 , où v1 et v2 doivent être de type int . Seulement les 5 bits les plus faibles de v1 sont pris en compte. |
|
0x7b |
lushr |
v1 >>> v2 , v1 doit être de type long et v2 de type long . Seulement les 6 bits les plus faibles de v1 sont pris en compte. |
Le fait que seulement 5 ou 6 bits de v2
soient utilisés n’a rien de surprenant, puisque la valeur de v2
correspond au nombre de bits déplacés. Or 5 bits permettent de représenter des nombres de 0 à 31 et 6 bits des nombres de 0 à 63. Au-delà, le résultat serait toujours 0 ou -1 :
// Si x est un int x << 32 = |x| >> 32 = x >>> 32 = 0 -|x| >> 32 = -1
Si x
était un long
et le décalage de 64 bits, nous aurions les mêmes résultats.
Il est aussi important de noter que les opérations de décalage de bits, et les opérations bit à bit d’une manière générale, ne s’effectuent que sur des int
et des long
. La représentation des float
et des double
en mémoire fait qu’une opération bit à bit n’aurait pas de sens.
Décalage de bit vers la gauche :
.class org/isk/jvmhardcore/bytecode/partfive/BitShifting_Left .method shift()I bipush 9 # [empty] -> 9 (1001) iconst_1 # 9 -> 9, 1 ishl # 9, 1 -> 18 ireturn .methodend .classend
Test unitaire :
public void shiftLeft() { final int result = BitShifting_Left.shift(); Assert.assertEquals(18, result); }
Décalage de bit vers la droite :
.class org/isk/jvmhardcore/bytecode/partfive/BitShifting_Right .method shift()I bipush 9 # [empty] -> 9 (1001) iconst_1 # 9 -> 9, 1 ishr # 9, 1 -> 4 ireturn .methodend .classend
Test unitaire :
public void shiftRight() { final int result = BitShifting_Right.shift(); Assert.assertEquals(4, result); }
Opérations logiques
Les instructions suivantes permettent d’effectuer des opérations logiques.
Etat de la pile avant → après exécution ..., v1, v2 → ..., r
.
Hex | Mnémonique | Description |
---|---|---|
0x7e |
iand |
v1 & v2 , où v1 et v2 doivent être de type int |
0x7f |
land |
v1 & v2 , où v1 et v2 doivent être de type long |
0x80 |
ior |
v1 | v2 , où v1 et v2 doivent être de type int |
0x81 |
lor |
v1 | v2 , où v1 et v2 doivent être de type long |
0x82 |
ixor |
v1 ^ v2 , où v1 et v2 doivent être de type int |
0x83 |
lxor |
v1 ^ v2 , où v1 et v2 doivent être de type long |
Nous pouvons noter que le signe tilde (~) permettant d’inverser les bits d’un nombre n’a pas d’instruction dédiée. L’opération ~x
est convertit par le compilateur de la manière suivante :
@ Considérons que la variable x est à l'index 0 des variables locales @ et qu'elle est de type int iload_0 iconst_m1 ixor @ x | -1 = ~x
A présent voyons quelques instructions, en commençant par iand
:
.class org/isk/jvmhardcore/bytecode/partfive/Iand .method iand()I bipush 10 # [empty] -> 10 (1010) bipush 7 # 10 -> 10, 7 (0111) iand # 10, 5 -> 2 (0010) ireturn .methodend .classend
Test unitaire :
public void iand() { final int result = Iand.iand(); Assert.assertEquals(2, result); }
ior
:
.class org/isk/jvmhardcore/bytecode/partfive/Ior .method ior()I bipush 10 # [empty] -> 10 (1010) bipush 7 # 10 -> 10, 5 (0111) ior # 10, 5 -> 15 (1111) ireturn .methodend .classend
Test unitaire :
public void ior() { final int result = Ior.ior(); Assert.assertEquals(15, result); }
ixor
:
.class org/isk/jvmhardcore/bytecode/partfive/Ixor .method ixor()I bipush 10 # [empty] -> 10 (1010) bipush 7 # 10 -> 10, 5 (0111) ixor # 10, 5 -> 13 (1101) ireturn .methodend .classend
Test unitaire :
public void ixor() { final int result = Ixor.ixor(); Assert.assertEquals(13, result); }
Conversions
Étant donné que la plupart des opérations que nous venons de voir nécessitent deux opérandes du même type, il faut parfois effectuer des conversions d’un type à un autre.
Les opérations suivantes permettent de changer le type d’un nombre.
État de la pile avant → après exécution : ..., valeur → ..., résultat
.
Hex | Mnémonique | Description |
---|---|---|
0x85 |
i2l |
Convertit un int en long |
0x86 |
i2f |
Convertit un int en float |
0x87 |
i2d |
Convertit un int en double |
0x88 |
l2i |
Convertit un long en int |
0x89 |
l2f |
Convertit un long en float |
0x8a |
l2d |
Convertit un long en double |
0x8b |
f2i |
Convertit un float en int |
0x8c |
f2l |
Convertit un float en long |
0x8d |
f2d |
Convertit un float en double |
0x8e |
d2i |
Convertit un double en int |
0x8f |
d2l |
Convertit un double en long |
0x90 |
d2f |
Convertit un double en float |
0x91 |
i2b |
Convertit un int en byte |
0x92 |
i2c |
Convertit un int en byte |
0x93 |
i2s |
Convertit un int en byte |
Avant de voir un exemple, il semble nécessaire d’effectuer quelques rappels :
- La JVM interprète les types
byte
,short
etchar
comme desint
. Par conséquent les variables identifiées par les typesB
,S
etC
dans le descripteur d’une méthode doivent être associés à des instructions ayant pour préfixei
. - En Java, les littérales numériques sont de type
int
pour les entiers etdouble
pour les nombres à virgule :int i = 138_009; double d = 354.89;
- En Java, pour pouvoir utiliser des littérales de types
long
oufloat
il est nécessaire de les faire suivre respectivement des lettresl
etf
:long l = 786_987_789_273_456l; float f = 1.32f;
- En Java lorsque l’on souhaite utiliser des littérales de type
byte
,short
etchar
il est nécessaire d’effectuer des conversions :addBytes((byte)10, (byte)5)
L’utilisation d’instructions manipulant des valeurs numériques est régie par les règles suivantes :
- Une assignation utilisant
- des littérales – où les littérales sont dans l’intervalle des valeurs possibles pour le type de la variable à laquelle est assignée la valeur
- ou des variables de même type,
n’implique aucune conversion :
byte a = -128; byte b = a;
- Si les deux opérandes sont de même type
- et que le type (ou le type du résultat) est directement utilisé par la JVM (
int
,long
,float
oudouble
) aucune conversion (explicite) est nécessaire - sinon une conversion est nécessaire
int i = 25, j = 42; int z = i + j; byte a = -128; byte b = 15; int c = a + b; byte d = (byte)(a + b)
- et que le type (ou le type du résultat) est directement utilisé par la JVM (
- Si les opérandes sont de types différents mais sans perte de précision – ou tout du moins dans les limites de la JVM – les conversions sont implicites (l’opération de conversion est bien présente au niveau bytecode, mais c’est à la charge du compilateur de la rajouter) :
long a = 13_342_099l; float b = 3e-17f; float c = a * b;
- Toutes les autres conversions doivent être explicitement effectuées.
Un exemple simple de conversion :
.class org/isk/jvmhardcore/bytecode/partfive/D2i .method d2i(D)I dload_0 d2i ireturn .methodend .classend
Test unitaire :
public void d2i() { final int result = D2i.d2i(14.98); Assert.assertEquals(14, result); }
Avant de conclure, notons que les trois dernières instructions (i2b
, i2s
et i2c
) sont utilisées lorsque des conversions explicites sont effectuées entre un int
et un byte
ou un short
ou un char
.
What’s next ?
Dans l’article suivant nous verrons les instructions permettant de manipuler la pile.
Nombre de vue : 74
Merci pour cette série, j’attends impatiemment la suite !
[…] Nous souhaitons connaître l’état de la pile – les valeurs présentes et leur ordre – après l’exécution des trois premières instructions (celles se trouvant avant le commentaire Start packing) pour vérifier le fonctionnement de l’instruction dup2. Or avec les instructions que nous avons étudiées jusqu’à présent, la seule solution est d’empaqueter les quatres valeurs présentes dans la pile (2, 1, 2, 1). En prenant le type int, l’empaquetage consistera à découper la valeur retournée en quatre blocs de 8 bits (4 * 8 = 32 bits). Le bloc le plus à gauche contiendra la valeur au sommet de la pile, le suivant la valeur juste au dessous, et ainsi de suite jusqu’au bas la pile, pour obtenir la valeur 0×1020102. Pour ce faire nous utilisons les instructions d’opération bit à bit que nous avons vu dans l’article précédent. […]
[…] vérifions le type des opérandes (comme nous l’avons vu dans la partie Mathématiques et Conversions, en Java les opérateurs arithmétiques prennent deux opérandes du même type, en raison du […]