Tutoriel sur la compréhension de la machine virtuelle Java

Image non disponible


précédentsommairesuivant

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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void add() {
  final int result = Addition.add(1, 2, 3);
 
  Assert.assertEquals(6, result);
}

Source

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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void subtract() {
  final double result = Subtraction.subtract(1, 2, 3);
 
  Assert.assertEquals(-4d, result, 0.0001);
}

Source

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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void multiply() {
  final float result = Multiplication.multiply(3, 2);
 
  Assert.assertEquals(6, result, 0.0001);
}

Source

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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void divide() {
  final long result = Division.divide(3, 2);
 
  Assert.assertEquals(1, result);
}

Source

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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void getRemainder() {
  final int result = Remainder.getRemainder(40, 7);
 
  Assert.assertEquals(5, result);
}

Source

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 :

 
Sélectionnez
1.
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

Source

Test unitaire

 
Sélectionnez
1.
2.
3.
4.
5.
public void negate() {
  final int result = Negation.negate(-10);
 
  Assert.assertEquals(10, result);
}

Source

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 :

 
Sélectionnez
// 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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
public void shiftLeft() {
  final int result = BitShifting_Left.shift();
   Assert.assertEquals(18, result);
}

Source

Décalage de bits vers la droite :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void shiftRight() {
  final int result = BitShifting_Right.shift();
 
  Assert.assertEquals(4, result);
}

Source

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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void iand() {
  final int result = Iand.iand();
 
  Assert.assertEquals(2, result);
}

Source

ior :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void ior() {
  final int result = Ior.ior();
 
  Assert.assertEquals(15, result);
}

Source

ixor :

 
Sélectionnez
1.
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

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void ixor() {
  final int result = Ixor.ixor();
 
  Assert.assertEquals(13, result);
}

Source

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 :
 
Sélectionnez
1.
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 :
 
Sélectionnez
1.
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 :
 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.
 
Sélectionnez
1.
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) :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
.class org/isk/jvmhardcore/bytecode/partfive/D2i
  .method d2i(D)I
    dload_0
    d2i
    ireturn
  .methodend
.classend

Source

Test unitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
public void d2i() {
  final int result = D2i.d2i(14.98);
 
  Assert.assertEquals(14, result);
}

Source

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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2016 SOAT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.