Nombre maximal de valeurs dans la partie II de l'énumération

PremiĂšre partie, thĂ©orique  | DeuxiĂšme partie, pratique



Nous continuons à rechercher le nombre maximum de valeurs possible dans l'énumération.
Cette fois, nous nous concentrerons sur le cÎté pratique du problÚme et verrons comment l'IDE, le compilateur et la JVM réagiront à nos réalisations.

Contenu


  Outils
  Javac
  MĂ©thode d'extraction
  Constantes dynamiques de fichiers de classe
    DifficultĂ©s soudaines
    Futur brillant Test
  dangereux
  pour
    Javac et les performances des commutateurs
  Conclusion
  Ressources supplĂ©mentaires


Outils


Javac prend soin de nous: il supprime les caractÚres qu'il n'aime pas dans les identifiants et interdit d'en hériter java.lang.Enum, donc pour les expériences, nous avons besoin d'autres outils.

Nous allons tester des hypothÚses en utilisant asmtools , l'assembleur et le désassembleur pour la JVM, et générer des fichiers de classe à l'échelle industrielle en utilisant la bibliothÚque ASM .

Pour la simplicité de la compréhension, l'essence de ce qui se passe sera dupliquée dans un pseudocode de type java.


Javac


Comme point de départ, il est logique de prendre le meilleur résultat, réalisable sans astuces, avec l'aide d'un seul javac. Tout ici est simple - nous créons le fichier source avec l'énumération et ajouter des éléments jusqu'à ce que javac refuse de compiler ce avec la malédiction « code trop grand ».

Assez longtemps, depuis Java 1.7, ce nombre a été maintenu au niveau de 2_746 éléments. Mais quelque part aprÚs Java 11, il y a eu des changements dans l'algorithme de stockage des valeurs dans le pool constant et le nombre maximum a diminué à 2_743. Oui, oui, juste à cause du changement de l'ordre des éléments dans le pool de constantes!

Nous nous concentrerons sur la meilleure des valeurs.


Extraire la méthode


Comme l'un des facteurs limitants est lié à la taille du bytecode dans le bloc d'initialisation statique, nous allons essayer de rendre ce dernier aussi simple que possible.

Rappelez-vous à quoi cela ressemble sur l'exemple de l'énumération FizzBuzzde la premiÚre partie. Les commentaires fournissent des instructions de montage appropriées.

statique {}
static  {
    Fizz = new FizzBuzz("Fizz", 0);
    //  0: new           #2                  // class FizzBuzz
    //  3: dup
    //  4: ldc           #22                 // String Fizz
    //  6: iconst_0
    //  7: invokespecial #24                 // Method "<init>":(Ljava/lang/String;I)V
    // 10: putstatic     #25                 // Field Fizz:LFizzBuzz;
    Buzz = new FizzBuzz("Buzz", 1);
    // 13: new           #2                  // class FizzBuzz
    // 16: dup
    // 17: ldc           #28                 // String Buzz
    // 19: iconst_1
    // 20: invokespecial #24                 // Method "<init>":(Ljava/lang/String;I)V
    // 23: putstatic     #30                 // Field Buzz:LFizzBuzz;
    FizzBuzz = new FizzBuzz("FizzBuzz", 2);
    // 26: new           #2                  // class FizzBuzz
    // 29: dup
    // 30: ldc           #32                 // String FizzBuzz
    // 32: iconst_2
    // 33: invokespecial #24                 // Method "<init>":(Ljava/lang/String;I)V
    // 36: putstatic     #33                 // Field FizzBuzz:LFizzBuzz;

    $VALUES = new FizzBuzz[] {
    // 39: iconst_3
    // 40: anewarray     #2                  // class FizzBuzz
        Fizz, 
    // 43: dup
    // 44: iconst_0
    // 45: getstatic     #25                 // Field Fizz:LFizzBuzz;
    // 48: aastore
        Buzz, 
    // 49: dup
    // 50: iconst_1
    // 51: getstatic     #30                 // Field Buzz:LFizzBuzz;
    // 54: aastore
        FizzBuzz
    // 55: dup
    // 56: iconst_2
    // 57: getstatic     #33                 // Field FizzBuzz:LFizzBuzz;
    // 60: aastore
    };
    // 61: putstatic     #1                  // Field $VALUES:[LFizzBuzz;
    // 64: return
}


La premiÚre chose qui me vient à l'esprit est de placer la création et le remplissage du tableau $VALUESdans une méthode distincte.

$VALUES = createValues();

En dĂ©veloppant cette idĂ©e, la crĂ©ation d'instances d'Ă©lĂ©ments d'Ă©numĂ©ration peut ĂȘtre transfĂ©rĂ©e Ă  la mĂȘme mĂ©thode:

static  {
    FizzBuzz[] localValues = createValues();

    int index = 0;
    Fizz = localValues[index++];
    Buzz = localValues[index++];
    FizzBuzz = localValues[index++];

    $VALUES = localValues;
}

private static FizzBuzz[] createValues() {
    return new FizzBuzz[] {
        new FizzBuzz("Fizz", 0), 
        new FizzBuzz("Buzz", 1), 
        new FizzBuzz("FizzBuzz", 2)
    };
}

Déjà mieux, mais chaque capture d'un élément de tableau et l'incrément d'index qui en résulte coûtent 6 octets, ce qui est trop cher pour nous. Mettez-les dans une méthode distincte.


private static int valueIndex;

static  {
    $VALUES = createValues();

    valueIndex = 0;
    Fizz = nextValue();
    Buzz = nextValue();
    FizzBuzz = nextValue();
}

private static FizzBuzz nextValue() {
    return $VALUES[valueIndex++];
}

Il faut 11 octets pour s'initialiser et revenir du bloc d'initialisation statique $VALUES, valueIndexet il reste 65_524 octets pour initialiser les champs. L'initialisation de chaque champ nécessite 6 octets, ce qui nous permet de créer une énumération de 10_920 éléments.

La croissance de prĂšs de quatre fois par rapport Ă  javac doit certainement ĂȘtre cĂ©lĂ©brĂ©e par la gĂ©nĂ©ration de code!

Code source du générateur: ExtractMethodHugeEnumGenerator.java
Exemple de classe générée : ExtractMethodHugeEnum.class

Constantes dynamiques de fichier de classe


Il est temps de se souvenir du JEP 309 et de ses mystérieuses constantes dynamiques .

L'essence de l'innovation en un mot:

aux types déjà existants pris en charge par un pool de constantes, en a ajouté un autre CONSTANT_Dynamic. Lors du chargement d'une classe, le type d'une telle constante est connu, mais sa valeur est inconnue. Le premier chargement d'une constante conduit à un appel à la méthode bootstrap spécifiée dans sa déclaration.

Le résultat de cette méthode devient une valeur constante. Il n'existe aucun moyen de modifier la valeur associée à une constante déjà initialisée. Ce qui est assez logique pour une constante.

Si vous avez Ă©galement pensĂ© Ă  Singleton, oubliez-le immĂ©diatement. La spĂ©cification souligne sĂ©parĂ©ment qu'il n'y a aucune garantie de sĂ©curitĂ© des threads dans ce cas, et la mĂ©thode d'initialisation en code multi-thread peut ĂȘtre appelĂ©e plusieurs fois. Il est seulement garanti que dans le cas de plusieurs appels Ă  la mĂ©thode bootstrap pour la mĂȘme constante, la JVM lancera une piĂšce et sĂ©lectionnera l'une des valeurs calculĂ©es pour le rĂŽle de la valeur constante, et les autres seront sacrifiĂ©es au garbage collector.

Comportementalement, une constante CONSTANT_Dynamic est résolue en exécutant sa méthode d'amorçage sur les paramÚtres suivants:

  1. un objet Lookup local,
  2. la chaßne représentant le composant de nom de la constante,
  3. la classe représentant le type constant attendu, et
  4. tous les arguments d'amorçage restants.

As with invokedynamic, multiple threads can race to resolve, but a unique winner will be chosen and any other contending answers discarded.

Pour charger des valeurs à partir du pool de constantes dans le bytecode, des commandes sont fournies ldc, ldc_wet ldc2_w. Ce qui nous intéresse est le premier - ldc.

Contrairement aux autres, il ne peut charger des valeurs qu'à partir des 255 premiers emplacements du pool constant, mais il prend 1 octet de moins en bytecode. Tout cela nous permet d'économiser jusqu'à 255 octets et un 255 + ((65_524 - (255 * 5)) / 6) = 10_963élément dans l'énumération. Cette fois, la croissance n'est pas si impressionnante, mais elle est toujours là.

Armé de cette connaissance, commençons.

Dans le bloc d'initialisation statique, au lieu d'appels de méthode, nextValue()nous allons maintenant charger la valeur de la constante dynamique. La valeur de l' ordinalindex ordinal de l'élément d'énumération sera passée explicitement, éliminant ainsi le champ valueIndex, la méthode d'usinenextValue()et des doutes sur la sécurité des threads de notre implémentation.

Comme méthode d'amorçage, nous utiliserons un sous-type spécial de MethodHandle qui imite le comportement d'un opérateur newen Java. La bibliothÚque standard fournit une méthode MethodHandles.Lookup :: findConstructor () pour obtenir un tel descripteur de méthode , mais dans notre cas, la JVM se chargera de la construction du descripteur de méthode nécessaire.

Pour utiliser le constructeur de notre Ă©numĂ©ration comme mĂ©thode d'amorçage, il devra ĂȘtre lĂ©gĂšrement modifiĂ© en changeant la signature. Les paramĂštres requis pour la mĂ©thode d'amorçage seront ajoutĂ©s au constructeur traditionnel de l'Ă©lĂ©ment d'Ă©numĂ©ration de nom et du numĂ©ro de sĂ©rie:

private FizzBuzz(MethodHandles.Lookup lookup, String name, Class<?> enumClass, int ordinal) {
    super(name, ordinal);
}

Sous forme de pseudo-code, l'initialisation ressemblera Ă  ceci:

static  {
    Fizz = JVM_ldc(FizzBuzz::new, "Fizz", 0);
    Buzz = JVM_ldc(FizzBuzz::new, "Buzz", 1);
    FizzBuzz = JVM_ldc(FizzBuzz::new, "FizzBuzz", 2);

    $VALUES = createValues();
}

Dans l'exemple ci-dessus, les instructions sont ldcdésignées comme des appels de méthode JVM_ldc(), dans le bytecode à leur place seront les instructions JVM correspondantes.

Puisque nous avons maintenant une constante distincte pour chaque Ă©lĂ©ment de l'Ă©numĂ©ration, la crĂ©ation et le remplissage du tableau $VALUESpeuvent Ă©galement ĂȘtre implĂ©mentĂ©s via une constante dynamique. La mĂ©thode bootstrap est trĂšs simple:

private static FizzBuzz[] createValues(MethodHandles.Lookup lookup, String name, Class<?> clazz, FizzBuzz... elements) {
    return elements;
}

Toute l'astuce dans la liste des paramÚtres statiques pour cette constante dynamique, là nous allons lister tous les éléments que nous voulons mettre $VALUES:

BootstrapMethods:
  ...
  1: # 54 REF_invokeStatic FizzBuzz.createValues: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / Class; [LFizzBuzz;) [LFizzBuzz;
    Arguments de la méthode:
      # 1 # 0: Fizz: LFizzBuzz;
      # 2 # 0: Buzz: LFizzBuzz;
      # 3 # 0: FizzBuzz: LFizzBuzz;

La JVM gùche le tableau à partir de ces paramÚtres statiques et le transmet à notre méthode d'amorçage en tant que paramÚtre vararg elements. Le nombre maximum de paramÚtres statiques est traditionnel 65_535, il est donc garanti qu'il sera suffisant pour tous les éléments de l'énumération, quel que soit leur nombre.

Pour les transferts avec un grand nombre d'Ă©lĂ©ments, cette modification rĂ©duira la taille du fichier de classe rĂ©sultant, et dans le cas oĂč, en raison du grand nombre d'Ă©lĂ©ments, la mĂ©thode createValues()devait ĂȘtre divisĂ©e en plusieurs parties, elle enregistre Ă©galement des emplacements dans le pool constant.
Et au final, c'est tout simplement magnifique.


Difficultés soudaines


Ce que nous avons héroïquement surmonté en générant des classes manuellement.

Les bibliothÚques de haut niveau offrent une interface pratique en échange d'une certaine restriction de la liberté d'action. La bibliothÚque ASM que nous utilisons pour générer des fichiers de classe ne fait pas exception. Il ne fournit pas de mécanismes pour contrÎler directement le contenu du pool de constantes. Ce n'est généralement pas trÚs important, mais pas dans notre cas.

Comme vous vous en souvenez, nous avons besoin des 255 premiers Ă©lĂ©ments du pool constant pour enregistrer de prĂ©cieux octets dans le bloc d'initialisation statique. Lorsque des constantes dynamiques sont ajoutĂ©es de maniĂšre standard, elles seront situĂ©es Ă  des indices alĂ©atoires et mĂ©langĂ©es Ă  d'autres Ă©lĂ©ments qui ne sont pas si critiques pour nous. Cela nous empĂȘchera d'atteindre le maximum.

Fragment d'un ensemble de constantes formées de façon traditionnelle
Piscine constante:
   # 1 = Utf8 FizzBuzz
   #2 = Class              #1             // FizzBuzz
   #3 = Utf8               java/lang/Enum
   #4 = Class              #3             // java/lang/Enum
   #5 = Utf8               $VALUES
   #6 = Utf8               [LFizzBuzz;
   #7 = Utf8               valueIndex
   #8 = Utf8               I
   #9 = Utf8               Fizz
  #10 = Utf8               LFizzBuzz;
  #11 = Utf8               Buzz
  #12 = Utf8               FizzBuzz
  #13 = Utf8               values
  #14 = Utf8               ()[LFizzBuzz;
  #15 = NameAndType        #5:#6          // $VALUES:[LFizzBuzz;
  #16 = Fieldref           #2.#15         // FizzBuzz.$VALUES:[LFizzBuzz;
  #17 = Class              #6             // "[LFizzBuzz;"
  #18 = Utf8               clone
  #19 = Utf8               ()Ljava/lang/Object;
  #20 = NameAndType        #18:#19        // clone:()Ljava/lang/Object;
  #21 = Methodref          #17.#20        // "[LFizzBuzz;".clone:()Ljava/lang/Object;
  ...
  #40 = NameAndType        #9:#10         // Fizz:LFizzBuzz;
  #41 = Dynamic            #0:#40         // #0:Fizz:LFizzBuzz;
  #42 = Fieldref           #2.#40         // FizzBuzz.Fizz:LFizzBuzz;
  #43 = NameAndType        #11:#10        // Buzz:LFizzBuzz;
  #44 = Dynamic            #0:#43         // #0:Buzz:LFizzBuzz;
  #45 = Fieldref           #2.#43         // FizzBuzz.Buzz:LFizzBuzz;
  #46 = NameAndType        #12:#10        // FizzBuzz:LFizzBuzz;
  #47 = Dynamic            #0:#46         // #0:FizzBuzz:LFizzBuzz;
  #48 = Fieldref           #2.#46         // FizzBuzz.FizzBuzz:LFizzBuzz;



Heureusement, il existe une solution de contournement: lors de la création d'une classe, vous pouvez spécifier un exemple de classe à partir duquel un pool de constantes et un attribut avec une description des méthodes d'amorçage seront copiés. Seulement maintenant, nous devons le générer manuellement.

En fait, ce n'est pas aussi difficile que cela puisse paraßtre à premiÚre vue. Le format du fichier de classe est assez simple et sa génération manuelle est un processus quelque peu fastidieux, mais pas du tout compliqué.

La chose la plus importante ici est un plan clair. Pour énumérer les COUNTéléments dont nous avons besoin:

  • COUNTenregistrements de type CONSTANT_Dynamic- nos constantes dynamiques
  • COUNTenregistrements de type CONSTANT_NameAndType- paires de liens vers le nom de l'Ă©lĂ©ment d'Ă©numĂ©ration et son type. Le type sera le mĂȘme pour tout le monde, c'est le type de classe de notre Ă©numĂ©ration.
  • COUNTenregistrements de type CONSTANT_Utf8- directement les noms des Ă©lĂ©ments d'Ă©numĂ©ration
  • COUNTenregistrements de type CONSTANT_Integer- numĂ©ros de sĂ©rie des Ă©lĂ©ments d'Ă©numĂ©ration transmis au constructeur comme valeur de paramĂštreordinal
  • noms des classes actuelles et de base, attributs, signatures de mĂ©thode et autres dĂ©tails d'implĂ©mentation ennuyeux. Les personnes intĂ©ressĂ©es peuvent consulter le code source du gĂ©nĂ©rateur.

Il y a beaucoup d'Ă©lĂ©ments constitutifs dans le pool de constantes qui se rĂ©fĂšrent Ă  d'autres Ă©lĂ©ments du pool par index, donc tous les indices dont nous avons besoin doivent ĂȘtre calculĂ©s Ă  l'avance, elementNamesest une liste des noms des Ă©lĂ©ments de notre Ă©numĂ©ration:

int elementCount = elementNames.size();

int baseConDy = 1;
int baseNameAndType = baseConDy + elementCount;
int baseUtf8 = baseNameAndType + elementCount;
int baseInteger = baseUtf8 + elementCount;
int indexThisClass = baseInteger + elementCount;
int indexThisClassUtf8 = indexThisClass + 1;
int indexSuperClass = indexThisClassUtf8 + 1;
int indexSuperClassUtf8 = indexSuperClass + 1;
int indexBootstrapMethodsUtf8 = indexSuperClassUtf8 + 1;
int indexConDyDescriptorUtf8 = indexBootstrapMethodsUtf8 + 1;
int indexBootstrapMethodHandle = indexConDyDescriptorUtf8 + 1;
int indexBootstrapMethodRef = indexBootstrapMethodHandle + 1;
int indexBootstrapMethodNameAndType = indexBootstrapMethodRef + 1;
int indexBootstrapMethodName = indexBootstrapMethodNameAndType + 1;
int indexBootstrapMethodDescriptor = indexBootstrapMethodName + 1;

int constantPoolSize = indexBootstrapMethodDescriptor + 1;

AprÚs cela, nous commençons à écrire.

Au début - la signature du fichier de classe, les quatre octets connus de tous 0xCA 0xFE 0xBA 0xBEet la version du format de fichier:

// Class file header
u4(CLASS_FILE_SIGNATURE);
u4(version);

Ensuite - un pool de constantes:

Pool de constantes
// Constant pool
u2(constantPoolSize);

// N * CONSTANT_Dynamic
for (int i = 0; i < elementCount; i++) {
    u1u2u2(CONSTANT_Dynamic, i, baseNameAndType + i);
}

// N * CONSTANT_NameAndType
for (int i = 0; i < elementCount; i++) {
    u1u2u2(CONSTANT_NameAndType, baseUtf8 + i, indexConDyDescriptorUtf8);
}

// N * CONSTANT_Utf8
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < elementCount; i++) {
    u1(CONSTANT_Utf8);
    utf8(elementNames.get(i));
}

// N * CONSTANT_Integer
for (int i = 0; i < elementCount; i++) {
    u1(CONSTANT_Integer);
    u4(i);
}

// ThisClass
u1(CONSTANT_Class);
u2(indexThisClassUtf8);

// ThisClassUtf8
u1(CONSTANT_Utf8);
utf8(enumClassName);

// SuperClass
u1(CONSTANT_Class);
u2(indexSuperClassUtf8);

// SuperClassUtf8
u1(CONSTANT_Utf8);
utf8(JAVA_LANG_ENUM);

// BootstrapMethodsUtf8
u1(CONSTANT_Utf8);
utf8(ATTRIBUTE_NAME_BOOTSTRAP_METHODS);

// ConDyDescriptorUtf8
u1(CONSTANT_Utf8);
utf8(binaryEnumClassName);

// BootstrapMethodHandle
u1(CONSTANT_MethodHandle);
u1(REF_newInvokeSpecial);
u2(indexBootstrapMethodRef);

// BootstrapMethodRef
u1u2u2(CONSTANT_Methodref, indexThisClass, indexBootstrapMethodNameAndType);

// BootstrapMethodNameAndType
u1u2u2(CONSTANT_NameAndType, indexBootstrapMethodName, indexBootstrapMethodDescriptor);

// BootstrapMethodName
u1(CONSTANT_Utf8);
utf8(BOOTSTRAP_METHOD_NAME);

// BootstrapMethodDescriptor
u1(CONSTANT_Utf8);
utf8(BOOTSTRAP_METHOD_DESCRIPTOR);


AprĂšs la constante de la piscine Ă  parler de modificateurs d'accĂšs et des drapeaux ( public, final, enun, etc.), le nom de classe et son ancĂȘtre:

u2(access);
u2(indexThisClass);
u2(indexSuperClass);

La classe factice que nous avons générée n'aura pas d'interfaces, pas de champs, pas de méthodes, mais il y aura un attribut avec une description des méthodes d'amorçage:

// Interfaces count
u2(0);
// Fields count
u2(0);
// Methods count
u2(0);
// Attributes count
u2(1);

Et voici le corps de l'attribut lui-mĂȘme:

// BootstrapMethods attribute
u2(indexBootstrapMethodsUtf8);
// BootstrapMethods attribute size
u4(2 /* num_bootstrap_methods */ + 6 * elementCount);
// Bootstrap method count
u2(elementCount);

for (int i = 0; i < elementCount; i++) {
    // bootstrap_method_ref
    u2(indexBootstrapMethodHandle);
    // num_bootstrap_arguments
    u2(1);
    // bootstrap_arguments[1]
    u2(baseInteger + i);
}

C'est tout, la classe est formée. Nous prenons ces octets et créons à partir d'eux ClassReader:

private ClassReader getBootstrapClassReader(int version, int access, String enumClassName, List<String> elementNames) {
    byte[] bootstrapClassBytes = new ConDyBootstrapClassGenerator(
        version,
        access,
        enumClassName,
        elementNames
    )
    .generate();

    if (bootstrapClassBytes == null) {
        return null;
    } else {
        return new ClassReader(bootstrapClassBytes);
    }
}

Ce n'Ă©tait pas si difficile.

Code source du générateur: ConDyBootstrapClassGenerator.java

Brillant avenir


Nous nous Ă©loignons briĂšvement de nos listes:


public class DiscoverConstantValueAttribute {

    public static final String STRING = "Habrahabr, world!";

    public static final Object OBJECT = new Object();

}


Dans le bloc d'initialisation statique de cette classe, il n'y aura soudainement qu'une seule opération d'écriture, dans le champ OBJECT:


static {
    OBJECT = new Object();
    //  0: new           #2                  // class java/lang/Object
    //  3: dup
    //  4: invokespecial #1                  // Method java/lang/Object."<init>":()V
    //  7: putstatic     #7                  // Field OBJECT:Ljava/lang/Object;
    // 10: return
}


Mais qu'en est-il STRING?
L'équipe va aider à faire la lumiÚre sur cette énigme javap -c -s -p -v DiscoverConstantValueAttribute.class, voici le fragment qui nous intéresse:


public static final java.lang.String STRING;
  descriptor: Ljava/lang/String;
  flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: String Habrahabr, world!


La valeur du champ final statique est passée du bloc d'initialisation à un attribut distinct ConstantValue. Voici ce qu'ils écrivent sur cet attribut dans JVMS11 §4.7.2 :

Un attribut ConstantValue représente la valeur d'une expression constante (JLS §15.28) et est utilisé comme suit:
  • Si l'indicateur ACC_STATIC dans l'Ă©lĂ©ment access_flags de la structure field_info est dĂ©fini, le champ reprĂ©sentĂ© par la structure field_info se voit attribuer la valeur reprĂ©sentĂ©e par son attribut ConstantValue dans le cadre de l'initialisation de la classe ou de l'interface dĂ©clarant le champ (§5.5). Cela se produit avant l'invocation de la mĂ©thode d'initialisation de classe ou d'interface de cette classe ou interface (§2.9.2).
  • Sinon, la machine virtuelle Java doit ignorer silencieusement l'attribut.


Si un tel attribut se produit en mĂȘme temps staticet final(bien que ce dernier ne soit pas explicitement indiquĂ© ici) dans un champ, ce champ est initialisĂ© avec la valeur de cet attribut. Et cela se produit avant mĂȘme que la mĂ©thode d'initialisation statique soit appelĂ©e.

Il serait tentant d'utiliser cet attribut pour initialiser les éléments de l'énumération, dans notre chapitre avant-dernier, il n'y avait que des constantes, quoique dynamiques.

Et nous ne sommes pas les premiers Ă  penser dans ce sens, il y a une mention dans JEP 309 ConstantValue. Malheureusement, cette mention est dans le chapitre Travaux futurs:

Travaux

futurs Les extensions futures possibles comprennent:

...
  • Attacher des constantes dynamiques Ă  l'attribut ConstantValue des champs statiques


En attendant, on ne peut que rĂȘver des moments oĂč cette fonctionnalitĂ© passera de l'Ă©tat «bon Ă  faire» Ă  «prĂȘt». Ensuite, les restrictions sur la taille du code dans le bloc d'initialisation perdront leur influence et le nombre maximal d'Ă©lĂ©ments dans l'Ă©numĂ©ration dĂ©terminera les limitations du pool constant.

Selon des estimations approximatives, dans ce cas, nous pouvons espĂ©rer un 65 489 / 4 = 16_372Ă©lĂ©ment. Voici 65_489le nombre d'emplacements inoccupĂ©s du pool constant, 46 des 65_535 thĂ©oriquement possibles sont allĂ©s au-dessus. 4- le nombre d'emplacements requis pour la dĂ©claration d'un champ et la constante dynamique correspondante.

Le nombre exact, bien sĂ»r, ne peut ĂȘtre trouvĂ© qu'aprĂšs la sortie de la version JDK avec prise en charge de cette fonctionnalitĂ©.


Peu sûr


Notre ennemi est la croissance linéaire du bloc d'initialisation avec une augmentation du nombre d'éléments d'énumération. Si nous avions trouvé un moyen de limiter l'initialisation dans une boucle, supprimant ainsi la relation entre le nombre d'éléments dans l'énumération et la taille du bloc d'initialisation, nous ferions une autre percée.

Malheureusement, aucune des API publiques standard ne permet d'Ă©crire dans des static finalchamps mĂȘme Ă  l'intĂ©rieur d'un bloc d'initialisation statique. Ni Reflection ni VarHandles n'aideront ici. Notre seul espoir est grand et terrible sun.misc.Unsafe.

Une exécution dangereuse de FizzBuzz pourrait ressembler à ceci:

FizzBuzz dangereux
import java.lang.reflect.Field;
import sun.misc.Unsafe;

public enum FizzBuzz {

    private static final FizzBuzz[] $VALUES;

    public static final FizzBuzz Fizz;
    public static final FizzBuzz Buzz;
    public static final FizzBuzz FizzBuzz;

    public static FizzBuzz[] values() {
        return (FizzBuzz[]) $VALUES.clone();
    }

    public static FizzBuzz valueOf(String name) {
        return (FizzBuzz) Enum.valueOf(FizzBuzz.class, name);
    }

    private FizzBuzz(String name, int ordinal) {
        super(name, ordinal);
    }

    private static FizzBuzz[] createValues() {
        return new FizzBuzz[] {
            Fizz,
            Buzz,
            FizzBuzz
        }
    }

    static  {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);

        String[] fieldNames = "Fizz,Buzz,FizzBuzz".split(",");
        for(int i = 0; i < fieldNames.length; i++) {
            String fieldName = fieldNames[i];
            Field field = FizzBuzz.class.getDeclaredField(fieldName);
            long fieldOffset = unsafe.staticFieldOffset(field);
            unsafe.putObject(FizzBuzz.class, fieldOffset, new FizzBuzz(fieldName, i));
        }

        $VALUES = createValues();
    }

}


Cette approche nous permet de créer un dénombrement avec environ 21 mille éléments; de plus, la capacité du pool de constantes n'est pas suffisante.

La documentation sur Enum :: ordinal () requiert que sa valeur corresponde au numéro de séquence de l'élément correspondant dans la déclaration d'énumération, vous devez donc stocker explicitement la liste des noms de champs dans le bon ordre, doublant ainsi presque la taille du fichier de classe.

public final int ordinal ()

Retourne l'ordinal de cette constante d'Ă©numĂ©ration (sa position dans sa dĂ©claration d'Ă©numĂ©ration, oĂč la constante initiale se voit attribuer un ordinal de zĂ©ro).

Ici, l'API publique au contenu du pool de constantes pourrait aider, nous savons déjà comment la remplir dans l'ordre dont nous avons besoin, mais il n'y a pas une telle API et il est peu probable qu'elle le soit. La méthode Class :: getConstantPool () disponible dans OpenJDK est déclarée comme package-private et il serait téméraire de s'y fier dans le code utilisateur.

Le bloc d'initialisation est maintenant assez compact et presque indépendant du nombre d'éléments dans l'énumération, vous createValues()pouvez donc le refuser en incorporant son corps dans la boucle:

static  {
    Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);

    String[] fieldNames = "Fizz,Buzz,FizzBuzz".split(",");
    FizzBuzz[] localValues = new FizzBuzz[fieldNames.length];
    for(int i = 0; i < fieldNames.length; i++) {
        String fieldName = fieldNames[i];
        Field field = FizzBuzz.class.getDeclaredField(fieldName);
        long fieldOffset = unsafe.staticFieldOffset(field);
        unsafe.putObject(
            FizzBuzz.class,
            fieldOffset,
            (localValues[i] = new FizzBuzz(fieldName, i))
        );
    }

    $VALUES = localValues;
}

Ici, un processus semblable Ă  une avalanche se produit: avec la mĂ©thode createValues(), les instructions de lecture des champs des Ă©lĂ©ments d'Ă©numĂ©ration disparaissent, les enregistrements de type Fieldrefpour ces champs deviennent inutiles , et donc les NameAndTypeenregistrements de type pour les enregistrements de type Fieldref. Dans le pool constant, des 2 * < >emplacements sont libĂ©rĂ©s qui peuvent ĂȘtre utilisĂ©s pour dĂ©clarer des Ă©lĂ©ments d'Ă©numĂ©ration supplĂ©mentaires.

Mais tout n'est pas si rose, les tests montrent une baisse significative des performances: l'initialisation d'une classe d'énumération avec 65 000 éléments prend impensable une minute et demie. Comme il s'est avéré assez rapidement, "le réflexe ralentit".

L'implémentation de Class :: getDeclaredField () dans OpenJDK a un comportement asymptotique linéaire du nombre de champs dans la classe, et notre bloc d'initialisation est quadratique à cause de cela.

L'ajout de la mise en cache amĂ©liore quelque peu la situation, mĂȘme si elle ne la rĂ©sout pas complĂštement:

static  {
    Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);

    String[] fieldNames = "Fizz,Buzz,FizzBuzz".split(",");
    Field[] fields = FizzBuzz.class.getDeclaredFields();
    HashMap<String, Field> cache = new HashMap<>(fields.length);

    for(Field field : fields) {
        cache.put(field.getName(), field);
    }

    for (int i = 0; i < fieldNames.length; i++) {
        String fieldName = fieldNames[i];
        Field field = cache.get(fieldName);
        long fieldOffset = unsafe.staticFieldOffset(field);
        unsafe.putObject(
            FizzBuzz.class,
            fieldOffset,
            (localValues[i] = new FizzBuzz(fieldName, i))
        );
    }    

    $VALUES = localValues;
}

L'approche dangereuse décrite dans ce chapitre vous permet de créer des transferts avec le nombre d'éléments jusqu'à 65_410, ce qui est presque 24 fois plus que le résultat atteignable avec javac et est assez proche de la limite théorique de 65_505 éléments que nous avons calculée dans la publication précédente du cycle.


VĂ©rifier les performances


Pour les tests, nous prenons la plus grande énumération, en la générant à l'aide de la commande java -jar HugeEnumGen.jar -a Unsafe UnsafeHugeEnum. En conséquence, nous obtenons un fichier de classe de 2 mégaoctets et 65_410 éléments de taille.

Créez un nouveau projet Java dans IDEA et ajoutez la classe générée en tant que bibliothÚque externe.

Presque immĂ©diatement, il devient Ă©vident que IDEA n'est pas prĂȘt pour un tel test de rĂ©sistance: la



saisie automatique d'un Ă©lĂ©ment de dĂ©nombrement prend des dizaines de secondes Ă  la fois sur l'ancien mobile i5 et sur le plus moderne i7 8700K. Et si vous essayez d'utiliser la correction rapide pour ajouter les Ă©lĂ©ments manquants au commutateur, IDEA arrĂȘte mĂȘme de redessiner les fenĂȘtres. Je soupçonne cela temporairement, mais je n'ai pas attendu la fin. La rĂ©activitĂ© lors du dĂ©bogage laisse Ă©galement beaucoup Ă  dĂ©sirer.

Commençons par un petit nombre d'éléments dans switch:

public class TestFew {

    public static void main(String... args) {
        for(String arg : args) {
            System.out.print(arg + " : ");

            try {
                UnsafeHugeEnum value = UnsafeHugeEnum.valueOf(arg);

                doSwitch(value);
            } catch(Throwable e) {
                e.printStackTrace(System.out);
            }
        }
    }

    private static void doSwitch(UnsafeHugeEnum value) {
        switch(value) {
            case VALUE_00001:
                System.out.println("First");
                break;
            case VALUE_31415:
                System.out.println("(int) (10_000 * Math.PI)");
                break;
            case VALUE_65410:
                System.out.println("Last");
                break;
            default:
                System.out.println("Unexpected value: " + value);
                break;
        }
    }

}

Il n'y a pas de surprise ici, la compilation et le lancement sont réguliers:

$ java TestFew VALUE_00001 VALUE_00400 VALUE_31415 VALUE_65410
VALUE_00001 : First
VALUE_00400 : Unexpected value: VALUE_00400
VALUE_31415 : (int) (10_000 * Math.PI)
VALUE_65410 : Last

Qu'en est-il de plus d'articles dans switch? Pouvons-nous, par exemple, traiter switchtous nos 65 000 éléments en une seule fois?

switch(value) {
    case VALUE_00001:
    case VALUE_00002:
        ...
    case VALUE_65410:
        System.out.println("One of known values: " + value);
        break;
    default:
        System.out.println("Unexpected value: " + value);
        break;
}

HĂ©las non. Lorsque nous essayons de compiler, nous obtenons tout un tas de messages d'erreur:

$ javac -fullversion
javac full version "14.0.1+7"

$ javac TestAll.java
TestAll.java:18: error: code too large for try statement
        switch(value) {
        ^
TestAll.java:65433: error: too many constants
                break;
                ^
TestAll.java:17: error: code too large
    private static void doSwitch(UnsafeHugeEnum value) {
                        ^
TestAll.java:1: error: too many constants
public class TestAll {
       ^
4 errors



Javac et interrupteur


Pour comprendre ce qui se passe, il faut comprendre comment se produit la traduction switchdes éléments de l'énumération.

La spécification JVM comporte un chapitre distinct dans JVMS11 §3.10 Compilation Switches , dont les recommandations se résument à l' switchutilisation de l'une des deux instructions de bytecode, tableswitchou lookupswitch. switchNous ne trouverons aucune référence à des chaßnes ou à des éléments d'énumération dans ce chapitre.

La meilleure documentation est le code, il est donc temps de plonger dans la source javac.

Le choix entre tableswitchet lookupswitchse produit dans Gen :: visitSwitch () et dépend du nombre d'options dans switch. Dans la plupart des cas, gagne tableswitch:

// 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;

L'en-tĂȘte tableswitchfait environ 16 octets plus 4 octets par valeur. Ainsi, switchen aucun cas il ne peut y avoir plus d' ( 65_535 - 16 ) / 4 = 16_379Ă©lĂ©ments.

En effet, aprÚs avoir réduit le nombre de branches casedans le corps switchà 16 mille, il ne reste qu'une seule erreur de compilation, la plus mystérieuse:

TestAll.java:18: error: code too large for try statement
        switch(value) {
        ^

A la recherche de la source de l'erreur, nous reviendrons un peu plus tÎt, au stade de l'élimination du sucre syntaxique. Les switchméthodes sont responsables de la traduction visitEnumSwitch(), mapForEnum()et la classe EnumMappingdans Lower.java .

On y trouve Ă©galement un petit commentaire documentaire:

EnumMapping JavaDoc
/** This map gives a translation table to be used for enum
 *  switches.
 *
 *  <p>For each enum that appears as the type of a switch
 *  expression, we maintain an EnumMapping to assist in the
 *  translation, as exemplified by the following example:
 *
 *  <p>we translate
 *  <pre>
 *          switch(colorExpression) {
 *          case red: stmt1;
 *          case green: stmt2;
 *          }
 *  </pre>
 *  into
 *  <pre>
 *          switch(Outer$0.$EnumMap$Color[colorExpression.ordinal()]) {
 *          case 1: stmt1;
 *          case 2: stmt2
 *          }
 *  </pre>
 *  with the auxiliary table initialized as follows:
 *  <pre>
 *          class Outer$0 {
 *              synthetic final int[] $EnumMap$Color = new int[Color.values().length];
 *              static {
 *                  try { $EnumMap$Color[red.ordinal()] = 1; } catch (NoSuchFieldError ex) {}
 *                  try { $EnumMap$Color[green.ordinal()] = 2; } catch (NoSuchFieldError ex) {}
 *              }
 *          }
 *  </pre>
 *  class EnumMapping provides mapping data and support methods for this translation.
 */


Le mystérieux tryfait partie d'une classe d'assistance générée automatiquement TestAll$0. Inside - une déclaration d'un tableau statique et du code pour l'initialiser.

Le tableau corrige la correspondance entre les noms des éléments d'énumération et les switchvaleurs numériques qui leur sont attribuées lors de la compilation , protégeant ainsi le code compilé des effets néfastes de la refactorisation.

Lors de la réorganisation, de l'ajout de nouveaux ou de la suppression d'éléments d'énumération existants, certains d'entre eux peuvent modifier la valeur ordinal()et c'est ce que protÚge un niveau d'indirection supplémentaire.

try {
    $SwitchMap$UnsafeHugeEnum[UnsafeHugeEnum.VALUE_00001.ordinal()] = 1;
    //  9: getstatic     #2                  // Field $SwitchMap$UnsafeHugeEnum:[I
    // 12: getstatic     #3                  // Field UnsafeHugeEnum.VALUE_00001:LUnsafeHugeEnum;
    // 15: invokevirtual #4                  // Method UnsafeHugeEnum.ordinal:()I
    // 18: iconst_1
    // 19: iastore
}
// 20: goto          24
catch(NoSuchFieldError e) { }
// 23: astore_0

Le code d'initialisation est simple et consomme de 15 Ă  17 octets par Ă©lĂ©ment. En consĂ©quence, le bloc d'initialisation statique prend en charge l'initialisation de pas plus de 3_862 Ă©lĂ©ments. Ce nombre s'avĂšre ĂȘtre le nombre maximal d'Ă©lĂ©ments d'Ă©numĂ©ration que nous pouvons utiliser en un switchavec l'implĂ©mentation actuelle javac.


Conclusion


Nous avons vu que l'utilisation d'une technique aussi simple que l'allocation de la création d'éléments d'énumération et l'initialisation d'un tableau $VALUESdans une méthode distincte vous permet d'augmenter le nombre maximal d'éléments dans une énumération de 2_746 à 10_920.

Les résultats dynamiques constants dans le contexte des réalisations précédentes ne semblent pas trÚs impressionnants et vous permettent d'obtenir seulement 43 éléments de plus, mais avec cette approche, il est beaucoup plus élégant d'ajouter de nouvelles propriétés à l'énumération - il suffit de modifier le constructeur et de lui transmettre les valeurs nécessaires via les paramÚtres statiques de la constante dynamique.

Si quelque temps dans l'attribut futur ConstantValuesera enseigné à comprendre les constantes dynamiques, ce nombre pourrait atteindre 10 mille à 16.

Utilisationsun.misc.Unsafevous permet de faire un bond de géant et d'augmenter le nombre maximum d'éléments à 65_410. Mais n'oubliez pas qu'il Unsafes'agit d'une API propriétaire qui peut disparaßtre avec le temps et que son utilisation représente un risque considérable, comme le prévient directement javac:

Test.java:3: warning: Unsafe is internal proprietary API and may be removed in a future release
import sun.misc.Unsafe;
               ^

Mais, il s'est avéré qu'il ne suffit pas de générer un dénombrement géant, vous devez également pouvoir l'utiliser.

Actuellement, il existe des problÚmes avec la prise en charge de telles énumérations à la fois à partir de l'EDI et au niveau du compilateur Java.

Un grand nombre de champs de la classe peuvent dégrader la réactivité de l'IDE à la fois lors de l'édition et du débogage. Parfois jusqu'à un blocage complet.

Les restrictions imposĂ©es par le format de fichier de classe et les dĂ©tails d'implĂ©mentation de javac rendent impossible l'utilisation de switchplus de 3_862 Ă©lĂ©ments dans le code en mĂȘme temps. Parmi les aspects positifs, il convient de mentionner que ceux-ci peuvent ĂȘtre des Ă©lĂ©ments arbitraires 3_862.

Une amélioration supplémentaire des résultats n'est possible que grùce au raffinement du compilateur Java, mais c'est une histoire complÚtement différente.


Matériaux additionnels


Code source GitHub: https://github.com/Maccimo/HugeEnumGeneratorArticle

Fichier JAR collecté: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0

Aide au démarrage prise en charge

Huge enumeration generator

    https://github.com/Maccimo/HugeEnumGeneratorArticle

Additional information (in Russian):

    https://habr.com/ru/post/483392/
    https://habr.com/ru/post/501870/

Usage:
    java -jar HugeEnumGen.jar [ <options> ] <enum name>

    <enum name>
        An enumeration class name.
        Should be a valid Java identifier. May contain package name.

Options:

    -d <directory>
        Output directory path.
        Current working directory by default.

    -e <item list file>
        Path to UTF8-encoded text file with list of enumeration item names.
        Item names will be autogenerated if absent.
        Mutually exclusive with the -c option.

    -c <count>
        Count of autogenerated enumeration item names.
        Mutually exclusive with the -e option.
        Default value: Algorithm-depended

    -a <algorithm>
        Enumeration generation algorithm.
        Supported algorithms:
          ConDy          - Employ Constant Dynamic (JEP 309) for enum elements initialization
          ExtractMethod  - Extract enum elements initialization code to separate method
          Unsafe         - Employ sun.misc.Unsafe for enum elements initialization

        Default algorithm: ExtractMethod

    -h / -?
        Show this help page.

Example:

    java -jar HugeEnumGen.jar -d ./bin -c 2020 com.habr.maccimo.HugeEnum2020



All Articles