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émentairesOutils
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 FizzBuzz
de la premiÚre partie. Les commentaires fournissent des instructions de montage appropriées.statique {}static {
Fizz = new FizzBuzz("Fizz", 0);
Buzz = new FizzBuzz("Buzz", 1);
FizzBuzz = new FizzBuzz("FizzBuzz", 2);
$VALUES = new FizzBuzz[] {
Fizz,
Buzz,
FizzBuzz
};
}
La premiÚre chose qui me vient à l'esprit est de placer la création et le remplissage du tableau $VALUES
dans 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
, valueIndex
et 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.javaExemple de classe gĂ©nĂ©rĂ©e : ExtractMethodHugeEnum.classConstantes 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:
- un objet Lookup local,
- la chaßne représentant le composant de nom de la constante,
- la classe représentant le type constant attendu, et
- 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_w
et 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' ordinal
index 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 new
en 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 ldc
dé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 $VALUES
peuvent Ă©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 traditionnellePiscine 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:COUNT
enregistrements de type CONSTANT_Dynamic
- nos constantes dynamiquesCOUNT
enregistrements 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.COUNT
enregistrements de type CONSTANT_Utf8
- directement les noms des éléments d'énumérationCOUNT
enregistrements 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, elementNames
est 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 0xBE
et la version du format de fichier:
u4(CLASS_FILE_SIGNATURE);
u4(version);
Ensuite - un pool de constantes:Pool de constantes
u2(constantPoolSize);
for (int i = 0; i < elementCount; i++) {
u1u2u2(CONSTANT_Dynamic, i, baseNameAndType + i);
}
for (int i = 0; i < elementCount; i++) {
u1u2u2(CONSTANT_NameAndType, baseUtf8 + i, indexConDyDescriptorUtf8);
}
for (int i = 0; i < elementCount; i++) {
u1(CONSTANT_Utf8);
utf8(elementNames.get(i));
}
for (int i = 0; i < elementCount; i++) {
u1(CONSTANT_Integer);
u4(i);
}
u1(CONSTANT_Class);
u2(indexThisClassUtf8);
u1(CONSTANT_Utf8);
utf8(enumClassName);
u1(CONSTANT_Class);
u2(indexSuperClassUtf8);
u1(CONSTANT_Utf8);
utf8(JAVA_LANG_ENUM);
u1(CONSTANT_Utf8);
utf8(ATTRIBUTE_NAME_BOOTSTRAP_METHODS);
u1(CONSTANT_Utf8);
utf8(binaryEnumClassName);
u1(CONSTANT_MethodHandle);
u1(REF_newInvokeSpecial);
u2(indexBootstrapMethodRef);
u1u2u2(CONSTANT_Methodref, indexThisClass, indexBootstrapMethodNameAndType);
u1u2u2(CONSTANT_NameAndType, indexBootstrapMethodName, indexBootstrapMethodDescriptor);
u1(CONSTANT_Utf8);
utf8(BOOTSTRAP_METHOD_NAME);
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:
u2(0);
u2(0);
u2(0);
u2(1);
Et voici le corps de l'attribut lui-mĂȘme:
u2(indexBootstrapMethodsUtf8);
u4(2 + 6 * elementCount);
u2(elementCount);
for (int i = 0; i < elementCount; i++) {
u2(indexBootstrapMethodHandle);
u2(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.javaBrillant 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();
}
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 static
et 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_489
le 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 final
champs 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 dangereuximport 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 Fieldref
pour ces champs deviennent inutiles , et donc les NameAndType
enregistrements 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 switch
tous 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 switch
des é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' switch
utilisation de l'une des deux instructions de bytecode, tableswitch
ou lookupswitch
. switch
Nous 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 tableswitch
et lookupswitch
se produit dans Gen :: visitSwitch () et dépend du nombre d'options dans switch
. Dans la plupart des cas, gagne tableswitch
:
long table_space_cost = 4 + ((long) hi - lo + 1);
long table_time_cost = 3;
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 tableswitch
fait environ 16 octets plus 4 octets par valeur. Ainsi, switch
en 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 case
dans 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 switch
méthodes sont responsables de la traduction visitEnumSwitch()
, mapForEnum()
et la classe EnumMapping
dans Lower.java .On y trouve également un petit commentaire documentaire:Le mystérieux try
fait 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 switch
valeurs 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;
}
catch(NoSuchFieldError e) { }
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 switch
avec 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 $VALUES
dans 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 ConstantValue
sera enseigné à comprendre les constantes dynamiques, ce nombre pourrait atteindre 10 mille à 16.Utilisationsun.misc.Unsafe
vous 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 Unsafe
s'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 switch
plus 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/HugeEnumGeneratorArticleFichier JAR collecté: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0Aide 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