VI. Mathématiques et Conversions▲
Au cours de cette partie, 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 nous 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).
VI-A. 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 utilisé 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, le 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.
VI-B. Opérations arithmétiques▲
VI-B-1. 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, où le signe + peut être remplacé par -, *, / ou % (modulo), y est au sommet de la pile et x en dessous. L'addition et la multiplication étant commutatives, cet ordre n'a pas d'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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
.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 :
2.
3.
4.
5.
public
void
add
(
) {
final
int
result =
Addition.add
(
1
, 2
, 3
);
Assert.assertEquals
(
6
, result);
}
VI-B-2. 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
.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 :
2.
3.
4.
5.
public
void
subtract
(
) {
final
double
result =
Subtraction.subtract
(
1
, 2
, 3
);
Assert.assertEquals
(
-
4d
, result, 0
.0001
);
}
VI-B-3. 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 :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
multiply
(
) {
final
float
result =
Multiplication.multiply
(
3
, 2
);
Assert.assertEquals
(
6
, result, 0
.0001
);
}
VI-B-4. 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 :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
divide
(
) {
final
long
result =
Division.divide
(
3
, 2
);
Assert.assertEquals
(
1
, result);
}
VI-B-5. 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 :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
getRemainder
(
) {
final
int
result =
Remainder.getRemainder
(
40
, 7
);
Assert.assertEquals
(
5
, result);
}
VI-B-6. 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 :
2.
3.
4.
5.
6.
7.
.class org/isk/jvmhardcore/bytecode/partfive/Negation
.method negate(I)I
iload_0 # [empty] -> idx0
ineg # idx0 -> résultat
ireturn
.methodend
.classend
Test unitaire
2.
3.
4.
5.
public
void
negate
(
) {
final
int
result =
Negation.negate
(
-
10
);
Assert.assertEquals
(
10
, result);
}
VI-C. Opérations bit à bit▲
VI-C-1. Opérations de décalage de bits▲
Les instructions suivantes permettent d'effectuer des opérations de décalage de bits.
État 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 bits vers la gauche :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
public
void
shiftLeft
(
) {
final
int
result =
BitShifting_Left.shift
(
);
Assert.assertEquals
(
18
, result);
}
Décalage de bits vers la droite :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
shiftRight
(
) {
final
int
result =
BitShifting_Right.shift
(
);
Assert.assertEquals
(
4
, result);
}
VI-D. Opérations logiques▲
Les instructions suivantes permettent d'effectuer des opérations logiques.
État 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 convertie par le compilateur de la manière suivante :
2.
3.
4.
5.
@
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
À présent, voyons quelques instructions, en commençant par iand :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
iand
(
) {
final
int
result =
Iand.iand
(
);
Assert.assertEquals
(
2
, result);
}
ior :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
ior
(
) {
final
int
result =
Ior.ior
(
);
Assert.assertEquals
(
15
, result);
}
ixor :
2.
3.
4.
5.
6.
7.
8.
.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 :
2.
3.
4.
5.
public
void
ixor
(
) {
final
int
result =
Ixor.ixor
(
);
Assert.assertEquals
(
13
, result);
}
VI-E. 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 et char comme des int. Par conséquent, les variables identifiées par les types B, S et C dans le descripteur d'une méthode doivent être associées à des instructions ayant pour préfixe i ;
- en Java, les littérales numériques sont de type int pour les entiers et double pour les nombres à virgule :
2.
int
i =
138_009;
double
d =
354
.89
;
- en Java, pour pouvoir utiliser des littérales de types long ou float il est nécessaire de les faire suivre respectivement des lettres l et f :
2.
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 et char 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 :
1 - 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 :
2.
byte
a =
-
128
;
byte
b =
a;
2 - Si les deux opérandes sont de même type :
- 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.
2.
3.
4.
5.
6.
7.
int
i =
25
, j =
42
;
int
z =
i +
j;
byte
a =
-
128
;
byte
b =
15
;
int
c =
a +
b;
byte
d =
(
byte
)(
a +
b)
3 - 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) :
2.
3.
long
a =
13_342_099l;
float
b =
3e
-
17f
;
float
c =
a *
b;
4 - Toutes les autres conversions doivent être explicitement effectuées.
Un exemple simple de conversion :
2.
3.
4.
5.
6.
7.
.class org/isk/jvmhardcore/bytecode/partfive/D2i
.method d2i(D)I
dload_0
d2i
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
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.
VI-F. What's next ?▲
Dans la partie suivante, nous verrons les instructions permettant de manipuler la pile.