Número máximo de valores en enum Parte II

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 adicionales


Herramientas


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 FizzBuzzde la primera parte. Los comentarios proporcionan instrucciones de montaje apropiadas.

estática {}
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
}


Lo primero que viene a la mente es poner la creación y el llenado de la matriz $VALUESen 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, valueIndexy 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.java
Ejemplo de clase generada : ExtractMethodHugeEnum.class

Constantes 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:

  1. un objeto de búsqueda local,
  2. la cadena que representa el componente de nombre de la constante,
  3. la clase que representa el tipo constante esperado, y
  4. 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_wy 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_963elemento 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 newen 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 ldcdesignan 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 $VALUEStambié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 COUNTelementos que necesitamos:

  • COUNTregistros de tipo CONSTANT_Dynamic: nuestras constantes dinámicas
  • COUNTregistros 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.
  • COUNTescribir registros CONSTANT_Utf8: directamente los nombres de los elementos de enumeración
  • COUNTregistros 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, elementNameses 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 0xBEy la versión del formato de archivo:

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

Luego, un grupo 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);


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:

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

Y aquí está el cuerpo del atributo mismo:

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

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.java

Futuro 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();
    //  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
}


¿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 staticy 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_372elemento. Aquí 65_489está 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 finalcampos 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 inseguro
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();
    }

}


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 Fieldrefpara estos campos se vuelven innecesarios y, por lo tanto, los NameAndTyperegistros 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 switchtodos 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 switchde 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 switchuso de una de las dos instrucciones de código de bytes , tableswitcho lookupswitch. switchNo 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 tableswitchy lookupswitchocurre en Gen :: visitSwitch () y depende del número de opciones en switch. En la mayoría de los casos, gana 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;

El encabezado tableswitches de aproximadamente 16 bytes más 4 bytes por valor. Por lo tanto, switchbajo ninguna circunstancia puede haber más ( 65_535 - 16 ) / 4 = 16_379elementos.

De hecho, después de reducir el número de ramas caseen el cuerpo switcha 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 switchmétodos son responsables de la traducción visitEnumSwitch(), mapForEnum()y la clase EnumMappingde Lower.java .

Allí también encontramos un pequeño comentario documental:

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.
 */


El misterioso tryresulta 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 switchvalores 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;
    //  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

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 switchcon 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 $VALUESen 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 ConstantValuese enseñara el atributo a comprender las constantes dinámicas, este número podría aumentar de 10 mil a 16.

Usarsun.misc.Unsafele permite dar un salto gigante y aumentar el número máximo de elementos a 65_410. Pero no olvide que Unsafeesta 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 switchmá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/HugeEnumGeneratorArticle

Archivo JAR recopilado: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0

Ayuda 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



All Articles