Cordes compactes en Java 9

1. Vue d’ensemble

Les chaînes Strings en Java sont représentées de manière interne par un char[] ​​contenant les caractères de la chaîne String . Et chaque char est composé de 2 octets car Java utilise en interne UTF-16.

Par exemple, si un Chaine contient un mot en anglais, les 8 premiers bits seront tous égaux à 0 pour chaque char , puisqu’un caractère ASCII peut être représenté avec un seul octet.

De nombreux caractères requièrent 16 bits pour les représenter, mais statistiquement, il n’exige que 8 bits - représentation de type LATIN-1. Il est donc possible d’améliorer la consommation de mémoire et les performances.

Il est également important que _String s occupe généralement une grande partie de l’espace de la JVM. Et, en raison de la manière dont elles sont stockées par la JVM, une instance String_ peut généralement occuper le double espace dont elle a réellement besoin .

Dans cet article, nous aborderons l’option Chaîne comprimée, introduite dans JDK6, et la nouvelle Chaîne compacte, récemment introduite avec JDK9. Ces deux outils ont été conçus pour optimiser la consommation de mémoire des chaînes sur le JMV.

2. Chaîne comprimée - Java 6

La version de performance 21 de la mise à jour 21 de JDK 6 a introduit une nouvelle option de machine virtuelle:

-XX:+UseCompressedStrings

Lorsque cette option est activée, les chaînes Strings sont stockées sous le nom byte[] ​​au lieu de char[]– , ce qui permet d’économiser beaucoup de mémoire. Cependant, cette option a finalement été supprimée dans JDK 7, principalement parce qu’elle avait des conséquences inattendues sur les performances.

3. Corde compacte - Java 9

Java 9 a introduit le concept de compact Strings ba _ck . _

Cela signifie que chaque fois que nous créons une chaîne si tous les caractères de cette chaîne peuvent être représentés à l’aide d’un octet - représentation LATIN-1, un tableau d’octets sera utilisé en interne, de telle sorte qu’un octet soit donné pour un caractère.

Dans d’autres cas, si un caractère nécessite plus de 8 bits pour le représenter, tous les caractères sont stockés avec deux octets pour chaque représentation - UTF-16.

Donc, fondamentalement, chaque fois que cela est possible, il utilisera un seul octet pour chaque caractère.

Maintenant, la question est de savoir comment toutes les opérations de String vont fonctionner. Comment distinguera-t-il les représentations LATIN-1 et UTF-16?

  • Eh bien, pour résoudre ce problème, un autre changement est apporté à la mise en œuvre interne de la chaîne. Nous avons un dernier champ coder , qui conserve cette information. **

3.1. String Implémentation en Java 9

Jusqu’à présent, le String était stocké en tant que char[] :

private final char[]value;

A partir de maintenant, ce sera un byte[]:

private final byte[]value;

La variable coder :

private final byte coder;

Où le coder peut être:

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

La plupart des opérations String vérifient maintenant le codeur et l’envoient vers l’implémentation spécifique:

public int indexOf(int ch, int fromIndex) {
    return isLatin1()
      ? StringLatin1.indexOf(value, ch, fromIndex)
      : StringUTF16.indexOf(value, ch, fromIndex);
}

private boolean isLatin1() {
    return COMPACT__STRINGS && coder == LATIN1;
}

Avec toutes les informations dont la JVM a besoin prêtes et disponibles, l’option CompactString VM est activée par défaut. Pour le désactiver, nous pouvons utiliser:

+XX:-CompactStrings

3.2. Comment coder fonctionne

Dans l’implémentation de la classe String de Java 9, la longueur est calculée comme suit:

public int length() {
    return value.length >> coder;
}

Si String ne contient que LATIN-1, la valeur de coder sera 0 et la longueur de String sera identique à celle du tableau d’octets.

Dans d’autres cas, si la chaîne String est dans la représentation UTF-16, la valeur de coder sera égale à 1 et, par conséquent, sa longueur sera égale à la moitié de celle du tableau d’octets réel.

  • Notez que toutes les modifications apportées pour Compact String, sont dans l’implémentation interne de la classe String et sont totalement transparentes pour les développeurs utilisant String users.

4. Cordes compactes vs chaînes compressées

Dans le cas de JDK 6 Compressed Strings, un problème majeur rencontré était que le constructeur String n’acceptait que char[] ​​comme argument. De plus, de nombreuses opérations String dépendent de la représentation char[]__ ​​et non d’un tableau d’octets. De ce fait, il a fallu effectuer beaucoup de déballage, ce qui a affecté la performance.

Tandis que dans le cas de Compact String, le maintien du «codeur» de champ supplémentaire peut également augmenter les frais généraux. Pour atténuer le coût du coder et du décompactage de __byte s en char s (en cas de représentation UTF-16), certaines des méthodes sont https://en.wikipedia.org/wiki/Intrinsic function[intrinsified]et le code ASM généré par le compilateur JIT a également été améliorée.

Ce changement a eu des résultats contre-intuitifs. LATIN-1 indexOf (String) appelle une méthode intrinsèque, alors que indexOf (char) ne le fait pas. Dans le cas de UTF-16, ces deux méthodes appellent une méthode intrinsèque. Ce problème n’affecte que le LATIN-1 String et sera résolu dans les prochaines versions.

Ainsi, Compact Strings sont meilleurs que les Compressed Strings en termes de performances.

Pour savoir combien de mémoire est économisée à l’aide de Compact Strings, différentes vidages de segment d’application Java ont été analysés. Et, alors que les résultats dépendaient fortement des applications spécifiques, les améliorations globales étaient presque toujours considérables.

4.1. Différence de performance

Voyons un exemple très simple de la différence de performances entre l’activation et la désactivation de Compact Strings:

long startTime = System.currentTimeMillis();

List strings = IntStream.rangeClosed(1, 10__000__000)
  .mapToObj(Integer::toString)
  .collect(toList());

long totalTime = System.currentTimeMillis() - startTime;
System.out.println(
  "Generated " + strings.size() + " strings in " + totalTime + " ms.");

startTime = System.currentTimeMillis();

String appended = (String) strings.stream()
  .limit(100__000)
  .reduce("", (l, r) -> l.toString() + r.toString());

totalTime = System.currentTimeMillis() - startTime;
System.out.println("Created string of length " + appended.length()
  + " in " + totalTime + " ms.");

Nous créons ici 10 millions de __String __s et les ajoutons ensuite de manière naïve. Lorsque nous exécutons ce code (les chaînes compactes sont activées par défaut), nous obtenons le résultat:

Generated 10000000 strings in 854 ms.
Created string of length 488895 in 5130 ms.

De même, si nous l’exécutons en désactivant les chaînes compactes à l’aide de:

-XX: option -CompactStrings , le résultat est:

Generated 10000000 strings in 936 ms.
Created string of length 488895 in 9727 ms.

Clairement, il s’agit d’un test de surface, et il ne peut pas être très représentatif - c’est seulement un aperçu de ce que la nouvelle option peut faire pour améliorer les performances dans ce scénario particulier.

5. Conclusion

Dans ce didacticiel, nous avons vu les tentatives d’optimisation des performances et de la consommation de mémoire sur la machine virtuelle Java - en stockant __String __s de manière à optimiser l’efficacité de la mémoire.

Comme toujours, le code complet est disponible à l’adresse over sur Github .