Primera parte, teórica | Segunda parte, práctica
Continuamos buscando el número máximo posible de valores en la enumeración.Esta vez nos centraremos en el lado práctico del problema y veremos cómo el IDE, el compilador y la JVM responderán a nuestros logros.Contenido
Herramientas de Javac Método de extracción Constantes dinámicas de archivos de clase Dificultades repentinas Futuro brillante Prueba insegura Javac y rendimiento del conmutador Conclusión Recursos adicionalesHerramientas
Javac nos cuida: elimina los caracteres que no le gustan de los identificadores y prohíbe heredar de ellos java.lang.Enum
, por lo que para los experimentos necesitamos otras herramientas.Pondremos a prueba hipótesis utilizando asmtools (ensamblador y desensamblador para JVM) y generaremos archivos de clase a escala industrial, utilizando la biblioteca ASM .Para simplificar la comprensión, la esencia de lo que está sucediendo se duplicará en un pseudocódigo similar a Java.Javac
Como punto de partida, es lógico obtener el mejor resultado, alcanzable sin trucos, con la ayuda de uno solo javac
. Todo es simple aquí - creamos el archivo fuente con la enumeración y añadir elementos a ella hasta javac para compilar niega que la maldición “código demasiado grande”.Mucho tiempo, desde Java 1.7, este número se ha mantenido en el nivel de 2_746 elementos. Pero en algún lugar después de Java 11, hubo cambios en el algoritmo para almacenar valores en el grupo constante y el número máximo disminuyó a 2_743. ¡Sí, sí, solo por cambiar el orden de los elementos en el grupo de constantes!Nos centraremos en el mejor de los valores.Método de extracción
Dado que uno de los factores limitantes está relacionado con el tamaño del código de bytes en el bloque de inicialización estática, intentaremos hacer que este último sea lo más fácil posible.Recordemos cómo se ve en el ejemplo de la enumeración FizzBuzz
de la primera parte. Los comentarios proporcionan instrucciones de montaje apropiadas.estática {}static {
Fizz = new FizzBuzz("Fizz", 0);
Buzz = new FizzBuzz("Buzz", 1);
FizzBuzz = new FizzBuzz("FizzBuzz", 2);
$VALUES = new FizzBuzz[] {
Fizz,
Buzz,
FizzBuzz
};
}
Lo primero que viene a la mente es poner la creación y el llenado de la matriz $VALUES
en un método separado.$VALUES = createValues();
Al desarrollar esta idea, la creación de instancias de elementos de enumeración se puede transferir al mismo método: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)
};
}
Ya es mejor, pero cada captura de un elemento de matriz y el incremento del índice posterior cuestan 6 bytes, lo cual es demasiado costoso para nosotros. Ponlos en un método separado.
private static int valueIndex;
static {
$VALUES = createValues();
valueIndex = 0;
Fizz = nextValue();
Buzz = nextValue();
FizzBuzz = nextValue();
}
private static FizzBuzz nextValue() {
return $VALUES[valueIndex++];
}
Se requieren 11 bytes para inicializar y regresar del bloque de inicialización estático $VALUES
, valueIndex
y quedan 65_524 bytes más para inicializar los campos. La inicialización de cada campo requiere 6 bytes, lo que nos permite crear una enumeración de 10_920 elementos.¡Casi cuatro veces el crecimiento en comparación con javac definitivamente debe celebrarse mediante la generación de código!Código fuente del generador: ExtractMethodHugeEnumGenerator.javaEjemplo de clase generada : ExtractMethodHugeEnum.classConstantes dinámicas de archivo de clase
Es hora de recordar sobre JEP 309 y sus misteriosas constantes dinámicas .La esencia de la innovación en pocas palabras:a los tipos ya existentes respaldados por un grupo de constantes se agregó otro CONSTANT_Dynamic
. Al cargar una clase, se conoce el tipo de tal constante, pero se desconoce su valor. La primera carga de una constante conduce a una llamada al método bootstrap especificado en su declaración.El resultado de este método se convierte en un valor constante. No hay formas de cambiar el valor asociado con una constante ya inicializada. Lo cual es bastante lógico para una constante.Si también pensaste en Singleton, olvídalo de inmediato. La especificación enfatiza por separado que no hay garantías de seguridad de subprocesos en este caso y que el método de inicialización en código multiproceso puede llamarse más de una vez. Solo se garantiza que en el caso de varias llamadas al método bootstrap para la misma constante, la JVM arrojará una moneda y seleccionará uno de los valores calculados para el papel del valor constante, y los otros serán sacrificados al recolector de basura.Comportalmente, una constante CONSTANT_Dynamics se resuelve ejecutando su método bootstrap en los siguientes parámetros:
- un objeto de búsqueda local,
- la cadena que representa el componente de nombre de la constante,
- la clase que representa el tipo constante esperado, y
- cualquier argumento de arranque restante.
As with invokedynamic, multiple threads can race to resolve, but a unique winner will be chosen and any other contending answers discarded.
Para cargar los valores de la piscina de constantes en el código de bytes, se proporcionan comandos ldc
, ldc_w
y ldc2_w
. De interés para nosotros es el primero de ellos ldc
.A diferencia de los demás, solo puede cargar valores de los primeros 255 espacios del grupo constante, pero se necesita 1 byte menos en bytecode. Todo esto nos da ahorros de hasta 255 bytes y un 255 + ((65_524 - (255 * 5)) / 6) = 10_963
elemento en la enumeración. Esta vez el crecimiento no es tan impresionante, pero todavía está allí.Armado con este conocimiento, comencemos.En el bloque de inicialización estática, en lugar de llamadas a métodos, nextValue()
ahora cargaremos el valor de la constante dinámica. El valor del ordinal
índice ordinal del elemento de enumeración se pasará explícitamente, eliminando así el campo valueIndex
, el método de fábricanextValue()
y dudas sobre la seguridad del hilo de nuestra implementación.Como método de arranque, utilizaremos un subtipo especial de MethodHandle que imita el comportamiento de un operador new
en Java. La biblioteca estándar para obtener dicho identificador de método proporciona el método MethodHandles.Lookup :: findConstructor () , pero en nuestro caso, la JVM se encargará de la construcción del identificador de método deseado.Para usar el constructor de nuestra enumeración como método bootstrap, tendrá que modificarse ligeramente cambiando la firma. Los parámetros necesarios para el método bootstrap se agregarán al constructor tradicional del elemento de enumeración de nombres y el número de serie:private FizzBuzz(MethodHandles.Lookup lookup, String name, Class<?> enumClass, int ordinal) {
super(name, ordinal);
}
En forma de pseudocódigo, la inicialización se verá así:static {
Fizz = JVM_ldc(FizzBuzz::new, "Fizz", 0);
Buzz = JVM_ldc(FizzBuzz::new, "Buzz", 1);
FizzBuzz = JVM_ldc(FizzBuzz::new, "FizzBuzz", 2);
$VALUES = createValues();
}
En el ejemplo anterior, las instrucciones se ldc
designan como llamadas de método JVM_ldc()
, en el bytecode en su lugar serán las instrucciones JVM correspondientes.Como ahora tenemos una constante separada para cada elemento de la enumeración, la creación y el llenado de la matriz $VALUES
también se pueden implementar a través de una constante dinámica. El método bootstrap es muy simple:private static FizzBuzz[] createValues(MethodHandles.Lookup lookup, String name, Class<?> clazz, FizzBuzz... elements) {
return elements;
}
Todo el truco en la lista de parámetros estáticos para esta constante dinámica, allí enumeraremos todos los elementos que queremos poner $VALUES
:Métodos de arranque:
...
1: # 54 REF_invokeStatic FizzBuzz.createValues: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / Class; [LFizzBuzz;) [LFizzBuzz;
Argumentos del método:
# 1 # 0: Fizz: LFizzBuzz;
# 2 # 0: Zumbido: LFizzBuzz;
# 3 # 0: FizzBuzz: LFizzBuzz;
La JVM estropea la matriz de estos parámetros estáticos y la pasa a nuestro método bootstrap como un parámetro vararg elements
. El número máximo de parámetros estáticos es el tradicional 65_535, por lo que se garantiza que será suficiente para todos los elementos de la enumeración, sin importar cuántos haya.Para las transferencias con una gran cantidad de elementos, este cambio reducirá el tamaño del archivo de clase resultante y, en el caso de que, debido a la gran cantidad de elementos, el método haya createValues()
tenido que dividirse en varias partes, también guarda ranuras en el grupo constante.Y al final, es simplemente hermoso.Dificultades repentinas
Que superamos heroicamente generando clases manualmente.Las bibliotecas de alto nivel proporcionan una interfaz conveniente a cambio de cierta restricción de la libertad de acción. La biblioteca ASM que utilizamos para generar archivos de clase no es una excepción. No proporciona mecanismos para controlar directamente el contenido del grupo de constantes. Esto generalmente no es muy importante, pero no en nuestro caso.Como recordará, necesitamos los primeros 255 elementos del grupo constante para guardar bytes preciosos en el bloque de inicialización estática. Cuando se agregan constantes dinámicas de manera estándar, se ubicarán en índices aleatorios y se mezclarán con otros elementos que no son tan críticos para nosotros. Esto nos impedirá alcanzar el máximo.Fragmento de un conjunto de constantes formadas de la manera tradicional.Piscina constante:
# 1 = Utf8 FizzBuzz
# 2 = Clase # 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;
Afortunadamente, existe una solución alternativa: al crear una clase, puede especificar una clase de muestra de la que se copiará un grupo de constantes y un atributo con una descripción de los métodos de arranque. Solo que ahora tenemos que generarlo manualmente.De hecho, no es tan difícil como podría parecer a primera vista. El formato del archivo de clase es bastante simple y su generación manual es un proceso algo tedioso, pero nada complicado.Lo más importante aquí es un plan claro. Para enumerar a partir de los COUNT
elementos que necesitamos:COUNT
registros de tipo CONSTANT_Dynamic
: nuestras constantes dinámicasCOUNT
registros de tipo CONSTANT_NameAndType
: pares de enlaces al nombre del elemento de enumeración y su tipo. El tipo será el mismo para todos, este es el tipo de clase de nuestra enumeración.COUNT
escribir registros CONSTANT_Utf8
: directamente los nombres de los elementos de enumeraciónCOUNT
registros de tipo CONSTANT_Integer
: números de serie de elementos de enumeración pasados al constructor como un valor de parámetroordinal
- nombres de las clases actuales y básicas, atributos, firmas de métodos y otros detalles de implementación aburridos. Los interesados pueden consultar el código fuente del generador.
Hay muchos elementos constituyentes en el grupo de constantes que se refieren a otros elementos del grupo por índice, por lo que todos los índices que necesitamos deben calcularse por adelantado, elementNames
es una lista de los nombres de los elementos de nuestra enumeración: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;
Después de eso, comenzamos a escribir.Al principio: la firma del archivo de clase, los cuatro bytes conocidos por todos 0xCA 0xFE 0xBA 0xBE
y la versión del formato de archivo:
u4(CLASS_FILE_SIGNATURE);
u4(version);
Luego, un grupo 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);
Después de la constante de la piscina hablando de modificadores de acceso y banderas ( public
, final
, enun
, etc.), el nombre de la clase y su ancestro:u2(access);
u2(indexThisClass);
u2(indexSuperClass);
La clase ficticia que generamos no tendrá interfaces, ni campos, ni métodos, pero habrá un atributo con una descripción de los métodos de arranque:
u2(0);
u2(0);
u2(0);
u2(1);
Y aquí está el cuerpo del atributo mismo:
u2(indexBootstrapMethodsUtf8);
u4(2 + 6 * elementCount);
u2(elementCount);
for (int i = 0; i < elementCount; i++) {
u2(indexBootstrapMethodHandle);
u2(1);
u2(baseInteger + i);
}
Eso es todo, la clase está formada. Tomamos estos bytes y creamos a partir de ellos 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);
}
}
No fue tan difícil.Código fuente del generador: ConDyBootstrapClassGenerator.javaFuturo brillante
Nos desviamos brevemente de nuestros listados:
public class DiscoverConstantValueAttribute {
public static final String STRING = "Habrahabr, world!";
public static final Object OBJECT = new Object();
}
En el bloque de inicialización estática de esta clase, de repente solo habrá una operación de escritura, en el campo OBJECT
:
static {
OBJECT = new Object();
}
¿Pero que pasa STRING
?El equipo ayudará a arrojar luz sobre este acertijo javap -c -s -p -v DiscoverConstantValueAttribute.class
, aquí está el fragmento que nos interesa:
public static final java.lang.String STRING;
descriptor: Ljava/lang/String;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String Habrahabr, world!
El valor del campo final estático se ha movido del bloque de inicialización a un atributo separado ConstantValue
. Esto es lo que escriben sobre este atributo en JVMS11 §4.7.2 :Un atributo ConstantValue representa el valor de una expresión constante (JLS §15.28) y se usa de la siguiente manera:
- Si se establece el indicador ACC_STATIC en el elemento access_flags de la estructura field_info, al campo representado por la estructura field_info se le asigna el valor representado por su atributo ConstantValue como parte de la inicialización de la clase o interfaz que declara el campo (§5.5). Esto ocurre antes de la invocación del método de inicialización de clase o interfaz de esa clase o interfaz (§2.9.2).
- De lo contrario, la máquina virtual Java debe ignorar silenciosamente el atributo.
Si tal atributo ocurre al mismo tiempo static
y final
(aunque este último no se explica explícitamente aquí) en un campo, dicho campo se inicializa con el valor de este atributo. Y esto sucede incluso antes de que se llame al método de inicialización estática.Sería tentador utilizar este atributo para inicializar los elementos de la enumeración, en nuestro capítulo anterior solo había constantes, aunque dinámicas.Y no somos los primeros en pensar en esta dirección, hay una mención en JEP 309 ConstantValue
. Desafortunadamente, esta mención se encuentra en el capítulo Trabajo futuro:Trabajo
futuro Las posibles extensiones futuras incluyen:
...
- Adjuntar constantes dinámicas al atributo ConstantValue de campos estáticos
Mientras tanto, solo podemos soñar con los momentos en que esta función pasará del estado "bueno" a "listo". Luego, las restricciones sobre el tamaño del código en el bloque de inicialización perderán su influencia y el número máximo de elementos en la enumeración determinará las limitaciones del grupo constante.Según estimaciones aproximadas, en este caso podemos esperar un 65 489 / 4 = 16_372
elemento. Aquí 65_489
está el número de ranuras desocupadas del grupo constante, 46 de los 65_535 teóricamente posibles fueron a gastos generales. 4
- el número de ranuras requeridas para la declaración de un campo y la constante dinámica correspondiente.El número exacto, por supuesto, se puede encontrar solo después del lanzamiento de la versión JDK con soporte para esta función.Inseguro
Nuestro enemigo es el crecimiento lineal del bloque de inicialización con un aumento en el número de elementos de enumeración. Si hubiéramos encontrado una manera de reducir la inicialización en un bucle, eliminando así la relación entre el número de elementos en la enumeración y el tamaño del bloque de inicialización, habríamos hecho otro gran avance.Desafortunadamente, ninguna de las API públicas estándar permite escribir en static final
campos incluso dentro de un bloque de inicialización estático. Ni Reflection ni VarHandles ayudarán aquí. Nuestra única esperanza es grande y terrible sun.misc.Unsafe
.Una ejecución insegura de FizzBuzz podría verse así:FizzBuzz inseguroimport 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();
}
}
Este enfoque nos permite crear una enumeración con aproximadamente 21 mil elementos; para más, la capacidad del grupo de constantes no es suficiente.La documentación en Enum :: ordinal () requiere que su valor coincida con el número de secuencia del elemento correspondiente en la declaración de enumeración, por lo que debe almacenar explícitamente la lista de nombres de campo en el orden correcto, casi duplicando el tamaño del archivo de clase.public final int ordinal ()
Devuelve el ordinal de esta constante de enumeración (su posición en su declaración de enumeración, donde a la constante inicial se le asigna un ordinal de cero).
Aquí la API pública de los contenidos del grupo de constantes podría ayudar, ya sabemos cómo llenarla en el orden que necesitamos, pero no existe tal API y es poco probable que lo sea. El método Class :: getConstantPool () disponible en OpenJDK se declara como paquete privado y sería imprudente confiar en él en el código de usuario.El bloque de inicialización ahora es bastante compacto y casi independiente del número de elementos en la enumeración, por lo que createValues()
puede rechazarlo incrustando su cuerpo en el bucle: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;
}
Aquí ocurre un proceso similar a una avalancha: junto con el método createValues()
, las instrucciones para leer los campos de elementos de enumeración desaparecen, los registros de tipo Fieldref
para estos campos se vuelven innecesarios y, por lo tanto, los NameAndType
registros de tipo para los registros de tipo Fieldref
. En el grupo constante, 2 * < >
se liberan ranuras que se pueden usar para declarar elementos de enumeración adicionales.Pero no todo es tan optimista, las pruebas muestran una reducción significativa del rendimiento: inicializar una clase de enumeración con 65 mil elementos lleva un minuto y medio impensable. Como resultó bastante rápido, "el reflejo se ralentiza".La implementación de Class :: getDeclaredField () en OpenJDK tiene un comportamiento asintótico lineal del número de campos en la clase, y nuestro bloque de inicialización es cuadrático debido a esto.Agregar almacenamiento en caché mejora un poco la situación, aunque no la resuelve por completo: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;
}
El enfoque inseguro descrito en este capítulo le permite crear transferencias con el número de elementos hasta 65_410, que es casi 24 veces más que el resultado alcanzable con javac y está bastante cerca del límite teórico de 65_505 elementos calculados por nosotros en la publicación anterior del ciclo.Verificar rendimiento
Para las pruebas, tomamos la enumeración más grande y la generamos usando el comando java -jar HugeEnumGen.jar -a Unsafe UnsafeHugeEnum
. Como resultado, obtenemos un archivo de clase con un tamaño de 2 megabytes y 65_410 elementos.Cree un nuevo proyecto Java en IDEA y agregue la clase generada como una biblioteca externa.Casi de inmediato, se hace evidente que IDEA no está lista para tal prueba de esfuerzo:
la finalización automática de un elemento de enumeración lleva decenas de segundos tanto en el antiguo i5 móvil como en el i7 8700K más moderno. Y si intenta utilizar una solución rápida para agregar los elementos que faltan al conmutador, IDEA incluso deja de volver a dibujar las ventanas. Sospecho que temporalmente, pero no pude esperar a que se completara. La capacidad de respuesta durante la depuración también deja mucho que desear.Comencemos con una pequeña cantidad de elementos en 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;
}
}
}
Aquí no hay sorpresas, la compilación y el lanzamiento son regulares:$ 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é pasa con más artículos en switch
? ¿Podemos, por ejemplo, procesar switch
todos nuestros 65 mil elementos en uno a la vez?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;
}
Por desgracia no. Cuando intentamos compilar, recibimos un montón de mensajes de error:$ 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 y switch
Para comprender lo que está sucediendo, tenemos que descubrir cómo se produce la traducción switch
de los elementos de la enumeración.La especificación JVM tiene un capítulo separado en JVMS11 §3.10 Compilación de conmutadores , cuyas recomendaciones se reducen al switch
uso de una de las dos instrucciones de código de bytes , tableswitch
o lookupswitch
. switch
No encontraremos ninguna referencia a cadenas o elementos de enumeración en este capítulo.La mejor documentación es el código, por lo que es hora de sumergirse en la fuente javac
.La elección entre tableswitch
y lookupswitch
ocurre en Gen :: visitSwitch () y depende del número de opciones en switch
. En la mayoría de los casos, gana 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;
El encabezado tableswitch
es de aproximadamente 16 bytes más 4 bytes por valor. Por lo tanto, switch
bajo ninguna circunstancia puede haber más ( 65_535 - 16 ) / 4 = 16_379
elementos.De hecho, después de reducir el número de ramas case
en el cuerpo switch
a 16 mil, solo queda un error de compilación, el más misterioso:TestAll.java:18: error: code too large for try statement
switch(value) {
^
En busca de la fuente del error, volveremos un poco antes, a la etapa de deshacernos del azúcar sintáctico. Los switch
métodos son responsables de la traducción visitEnumSwitch()
, mapForEnum()
y la clase EnumMapping
de Lower.java .Allí también encontramos un pequeño comentario documental:El misterioso try
resulta ser parte de una clase auxiliar generada automáticamente TestAll$0
. Inside: una declaración de una matriz estática y un código para inicializarla.La matriz corrige la correspondencia entre los nombres de los elementos de enumeración y los switch
valores numéricos asignados a ellos durante la compilación , protegiendo así el código compilado de los efectos nocivos de la refactorización.Al reordenar, agregar nuevos o eliminar elementos de enumeración existentes, algunos de ellos pueden cambiar el valor ordinal()
y esto es de lo que protege un nivel adicional de indirección.try {
$SwitchMap$UnsafeHugeEnum[UnsafeHugeEnum.VALUE_00001.ordinal()] = 1;
}
catch(NoSuchFieldError e) { }
El código de inicialización es simple y consume de 15 a 17 bytes por elemento. Como resultado, el bloque de inicialización estática acomoda la inicialización de no más de 3_862 elementos. Este número resulta ser el número máximo de elementos de enumeración que podemos usar en uno switch
con la implementación actual javac
.Conclusión
Vimos que el uso de incluso una técnica tan simple como asignar la creación de elementos de enumeración e inicializar una matriz $VALUES
en un método separado le permite aumentar el número máximo de elementos en una enumeración de 2_746 a 10_920.Los resultados dinámicos constantes en el contexto de logros anteriores no se ven muy impresionantes y le permiten obtener solo 43 elementos más, pero con este enfoque es mucho más elegante agregar nuevas propiedades a la enumeración: simplemente modifique el constructor y pase los valores necesarios a través de los parámetros estáticos de la constante dinámica.Si en algún momento en el futuro ConstantValue
se enseñara el atributo a comprender las constantes dinámicas, este número podría aumentar de 10 mil a 16.Usarsun.misc.Unsafe
le permite dar un salto gigante y aumentar el número máximo de elementos a 65_410. Pero no olvide que Unsafe
esta es una API patentada que puede desaparecer con el tiempo y su uso es un riesgo considerable, ya que javac advierte directamente:Test.java:3: warning: Unsafe is internal proprietary API and may be removed in a future release
import sun.misc.Unsafe;
^
Pero, como resultó, no es suficiente generar una enumeración gigante, también debe poder usarla.Actualmente, existen problemas con el soporte de tales enumeraciones tanto desde el IDE como a nivel del compilador Java.Un gran número de campos en la clase puede degradar la capacidad de respuesta del IDE tanto durante la edición como durante la depuración. A veces hasta una caída completa.Las restricciones impuestas por el formato de archivo de clase y los detalles de implementación de javac hacen que sea imposible usar switch
más de 3_862 elementos en el código al mismo tiempo. De los aspectos positivos, vale la pena mencionar que estos pueden ser elementos arbitrarios 3_862.La mejora adicional de los resultados solo es posible a través del refinamiento del compilador de Java, pero esta es una historia completamente diferente.Materiales adicionales
Código fuente de GitHub: https://github.com/Maccimo/HugeEnumGeneratorArticleArchivo JAR recopilado: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0Ayuda de inicio compatible
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