Número máximo de valores em enumeração, parte II

Primeira parte, teórica  | Parte dois, prática



Continuamos a procurar o número máximo possível de valores na enumeração.
Desta vez, focaremos o lado prático da questão e veremos como o IDE, o compilador e a JVM responderão às nossas conquistas.

Conteúdo


  Ferramentas
  Javac
  Método de extração
  Constantes dinâmicas do arquivo de classes
    Dificuldades repentinas
    Futuro brilhante Teste
  inseguro Javac e Switch Conclusão do
  desempenho Recursos adicionais
    
  
  


Ferramentas


Javac cuida de nós: corta caracteres que não gosta nos identificadores e proíbe herdar deles java.lang.Enum; portanto, para experimentos, precisamos de outras ferramentas. Testaremos

hipóteses usando asmtools - montador e desmontador para JVM e geraremos arquivos de classe em escala industrial - usando a biblioteca ASM .

Para simplificar o entendimento, a essência do que está acontecendo será duplicada em um pseudocódigo do tipo java.


Javac


Como ponto de partida, é lógico obter o melhor resultado possível de realizar sem truques, com a ajuda de apenas um javac. Tudo é simples aqui - criamos o arquivo de origem com a enumeração e adicionamos elementos até o javac se recusar a compilá- lo com a maldição “código muito grande”.

Muito tempo, desde o Java 1.7, esse número foi mantido no nível de 2_746 elementos. Mas em algum lugar após o Java 11, houve alterações no algoritmo para armazenar valores no pool constante e o número máximo diminuiu para 2_743. Sim, sim, apenas por alterar a ordem dos elementos no conjunto de constantes!

Vamos nos concentrar no melhor dos valores.


Método de extração


Como um dos fatores limitantes está relacionado ao tamanho do bytecode no bloco de inicialização estática, tentaremos torná-lo o mais fácil possível.

Lembre-se de como fica no exemplo da enumeração FizzBuzzda primeira parte. Os comentários fornecem instruções de montagem apropriadas.

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


A primeira coisa que vem à mente é colocar a criação e o preenchimento da matriz $VALUESem um método separado.

$VALUES = createValues();

Desenvolvendo essa idéia, a criação de instâncias de elementos de enumeração pode ser transferida para o mesmo 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)
    };
}

Já é melhor, mas cada captura de um elemento da matriz e o incremento subsequente do índice custam 6 bytes, o que é muito caro para nós. Coloque-os em um 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++];
}

São necessários 11 bytes para inicializar e retornar do bloco de inicialização estático $VALUES, valueIndexe 65_524 bytes restantes para inicializar os campos. A inicialização de cada campo requer 6 bytes, o que nos permite criar uma enumeração de 10_920 elementos.

Quase quatro vezes o crescimento em comparação com o javac deve ser definitivamente comemorado pela geração de código!

Código-fonte do gerador: ExtractMethodHugeEnumGenerator.java
Exemplo de classe gerada : ExtractMethodHugeEnum.class

Constantes dinâmicas de arquivo de classe


É hora de lembrar sobre o JEP 309 e suas constantes dinâmicas misteriosas .

A essência da inovação em poucas palavras:

Para tipos já existentes suportados por um conjunto de constantes, foi adicionado outro CONSTANT_Dynamic. Ao carregar uma classe, o tipo dessa constante é conhecido, mas seu valor é desconhecido. O primeiro carregamento de uma constante leva a uma chamada para o método de auto-inicialização especificado em sua declaração.

O resultado desse método se torna um valor constante. Não há maneiras de alterar o valor associado a uma constante já inicializada. O que é bastante lógico para uma constante.

Se você também pensou em Singleton, esqueça-o imediatamente. A especificação enfatiza separadamente que não há garantia de segurança do encadeamento nesse caso, e o método de inicialização no código multithread pode ser chamado mais de uma vez. É garantido apenas que, no caso de várias chamadas para o método de autoinicialização para a mesma constante, a JVM jogue uma moeda e selecione um dos valores calculados para a função do valor constante, e os outros serão sacrificados para o coletor de lixo.

Comportamental, uma constante CONSTANT_Dynamic é resolvida executando seu método de autoinicialização nos seguintes parâmetros:

  1. um objeto de pesquisa local,
  2. a String representando o componente de nome da constante,
  3. a classe que representa o tipo constante esperado e
  4. quaisquer argumentos de auto-inicialização restantes.

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

Para carregar os valores do conjunto de constantes na bytecode, os comandos são fornecidos ldc, ldc_we ldc2_w. De interesse para nós é o primeiro deles - ldc.

Diferentemente dos outros, ele é capaz de carregar valores apenas dos primeiros 255 slots do pool constante, mas leva 1 byte a menos no bytecode. Tudo isso nos permite economizar até 255 bytes e um 255 + ((65_524 - (255 * 5)) / 6) = 10_963elemento na enumeração. Desta vez, o crescimento não é tão impressionante, mas ainda está lá.

Armado com esse conhecimento, vamos começar.

No bloco de inicialização estática, em vez de chamadas de método, nextValue()agora carregaremos o valor da constante dinâmica. O valor do ordinalíndice ordinal do elemento de enumeração será passado explicitamente, eliminando o campo valueIndex, o método de fábricanextValue()e dúvidas sobre a segurança do encadeamento de nossa implementação.

Como método de inicialização, usaremos um subtipo especial de MethodHandle que imita o comportamento de um operador newem Java. A biblioteca padrão fornece um método MethodHandles.Lookup :: findConstructor () para obter esse identificador de método , mas, no nosso caso, a JVM cuidará da construção do identificador de método necessário.

Para usar o construtor de nossa enumeração como um método de inicialização, ele precisará ser ligeiramente modificado alterando a assinatura. Os parâmetros necessários para o método de auto-inicialização serão adicionados ao construtor tradicional do elemento de enumeração de nome e número de série:

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

Sob a forma de pseudo-código, a inicialização terá a seguinte aparência:

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

    $VALUES = createValues();
}

No exemplo acima, as instruções são ldcdesignadas como chamadas de método JVM_ldc(); no bytecode, em seu lugar, estarão as instruções da JVM correspondentes.

Como agora temos uma constante separada para cada elemento da enumeração, a criação e o preenchimento da matriz $VALUEStambém podem ser implementados por meio de uma constante dinâmica. O método de inicialização é muito simples:

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

Todo o truque na lista de parâmetros estáticos para essa constante dinâmica, lá vamos listar todos os elementos que queremos colocar $VALUES:

Métodos de inicialização:
  ...
  1: # 54 REF_invokeStatic FizzBuzz.createValues: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / Class; [LFizzBuzz;) [LFizzBuzz;
    Argumentos do método:
      # 1 # 0: Fizz: LFizzBuzz;
      # 2 # 0: Buzz: LFizzBuzz;
      # 3 # 0: FizzBuzz: LFizzBuzz;

A JVM estraga a matriz desses parâmetros estáticos e a transmite ao nosso método de autoinicialização como um parâmetro vararg elements. O número máximo de parâmetros estáticos é tradicional 65_535, portanto, é garantido que seja suficiente para todos os elementos da enumeração, não importa quantos existam.

Para transferências com um grande número de elementos, essa alteração reduzirá o tamanho do arquivo de classe resultante e, no caso em que, devido ao grande número de elementos, o método createValues()precisou ser dividido em várias partes, ele também salva os slots no pool constante.
E no final, é simplesmente lindo.


Dificuldades repentinas


Que superamos heroicamente gerando classes manualmente.

Bibliotecas de alto nível fornecem uma interface conveniente em troca de alguma restrição à liberdade de ação. A biblioteca ASM que usamos para gerar arquivos de classe não é exceção. Ele não fornece mecanismos para controlar diretamente o conteúdo do pool de constantes. Isso geralmente não é muito importante, mas não no nosso caso.

Como você se lembra, precisamos dos primeiros 255 elementos do pool constante para salvar bytes preciosos no bloco de inicialização estática. Quando as constantes dinâmicas são adicionadas de maneira padrão, elas serão localizadas em índices aleatórios e misturadas com outros elementos que não são tão críticos para nós. Isso nos impedirá de alcançar o máximo.

Fragmento de um conjunto de constantes formado da maneira tradicional
Piscina 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;



Felizmente, existe uma solução alternativa - ao criar uma classe, você pode especificar uma classe de amostra da qual um conjunto de constantes e um atributo com uma descrição dos métodos de autoinicialização serão copiados. Só agora temos que gerá-lo manualmente.

De fato, não é tão difícil quanto parece à primeira vista. O formato do arquivo de classe é bastante simples e sua geração manual é um processo um tanto tedioso, mas nem um pouco complicado.

A coisa mais importante aqui é um plano claro. Para enumerar os COUNTelementos que precisamos:

  • COUNTregistros de tipo CONSTANT_Dynamic- nossas constantes dinâmicas
  • COUNTregistros de tipo CONSTANT_NameAndType- pares de links para o nome do elemento de enumeração e seu tipo. O tipo será o mesmo para todos, este é o tipo de classe da nossa enumeração.
  • COUNTtype records CONSTANT_Utf8- diretamente os nomes dos elementos de enumeração
  • COUNTregistros do tipo CONSTANT_Integer- números de série dos elementos de enumeração passados ​​para o construtor como um valor de parâmetroordinal
  • nomes das classes atual e base, atributos, assinaturas de método e outros detalhes de implementação chatos. Os interessados ​​podem procurar o código fonte do gerador.

Existem muitos elementos constituintes no conjunto de constantes que se referem a outros elementos do conjunto por índice; portanto, todos os índices que precisamos devem ser calculados com antecedência, elementNamesé uma lista dos nomes dos elementos de nossa enumeração:

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;

Depois disso, começamos a escrever.

No início - a assinatura do arquivo de classe, os quatro bytes conhecidos por todos 0xCA 0xFE 0xBA 0xBEe a versão do formato do arquivo:

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

Então - um conjunto 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);


Após a constante piscina falando sobre modificadores de acesso e bandeiras ( public, final, enune assim por diante), o nome da classe e seu ancestral:

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

A classe fictícia que geramos não terá interfaces, campos ou métodos, mas haverá um atributo com uma descrição dos métodos de inicialização:

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

E aqui está o corpo do próprio atributo:

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

Isso é tudo, a classe é formada. Pegamos esses bytes e criamos a partir deles 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);
    }
}

Não foi tão difícil.

Código-fonte do gerador: ConDyBootstrapClassGenerator.java

Futuro brilhante


Discordamos brevemente de nossas listas:


public class DiscoverConstantValueAttribute {

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

    public static final Object OBJECT = new Object();

}


No bloco de inicialização estática dessa classe, de repente haverá apenas uma operação de gravação, no 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
}


Mas que tal STRING?
A equipe ajudará a esclarecer esse enigma javap -c -s -p -v DiscoverConstantValueAttribute.class, eis o fragmento que nos interessa:


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


O valor do campo final estático foi movido do bloco de inicialização para um atributo separado ConstantValue. Aqui está o que eles escrevem sobre esse atributo na JVMS11 §4.7.2 :

Um atributo ConstantValue representa o valor de uma expressão constante (JLS §15.28) e é usado da seguinte maneira:
  • Se o sinalizador ACC_STATIC no item access_flags da estrutura field_info estiver definido, o campo representado pela estrutura field_info receberá o valor representado por seu atributo ConstantValue como parte da inicialização da classe ou interface que declara o campo (§5.5). Isso ocorre antes da chamada do método de inicialização de classe ou interface dessa classe ou interface (§2.9.2).
  • Caso contrário, a Java Virtual Machine deve ignorar silenciosamente o atributo.


Se esse atributo ocorrer ao mesmo tempo statice final(embora o último não esteja explicitamente explicitado aqui) em um campo, esse campo será inicializado com o valor desse atributo. E isso acontece mesmo antes de o método de inicialização estática ser chamado.

Seria tentador usar esse atributo para inicializar os elementos da enumeração; em nosso capítulo anterior, havia apenas constantes, ainda que dinâmicas.

E não somos os primeiros a pensar nessa direção, há uma menção no JEP 309 ConstantValue. Infelizmente, essa menção está no capítulo de trabalho Futuro:

Trabalhos

futuros Possíveis extensões futuras incluem:

...
  • Anexando constantes dinâmicas ao atributo ConstantValue de campos estáticos


Enquanto isso, só podemos sonhar com os momentos em que esse recurso passará do estado “bom a fazer” para “pronto”. Em seguida, as restrições no tamanho do código no bloco de inicialização perderão sua influência e o número máximo de elementos na enumeração determinará as limitações do pool constante.

De acordo com estimativas aproximadas, neste caso, podemos esperar por um 65 489 / 4 = 16_372elemento. Aqui 65_489está o número de slots desocupados do pool constante, 46 dos 65_535 teoricamente possíveis foram para cima. 4- o número de faixas horárias necessárias para a declaração de um campo e a constante dinâmica correspondente.

O número exato, é claro, só pode ser descoberto após o lançamento da versão do JDK com suporte para esse recurso.


Inseguro


Nosso inimigo é o crescimento linear do bloco de inicialização com um aumento no número de elementos de enumeração. Se tivéssemos encontrado uma maneira de reduzir a inicialização em um loop, removendo a relação entre o número de elementos na enumeração e o tamanho do bloco de inicialização, faríamos outra descoberta.

Infelizmente, nenhuma das APIs públicas padrão permite gravar static finalcampos, mesmo dentro de um bloco de inicialização estático. Nem o Reflection nem o VarHandles ajudarão aqui. Nossa única esperança é grande e terrível sun.misc.Unsafe.

Uma execução insegura do FizzBuzz pode ser algo como isto:

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

}


Essa abordagem nos permite criar uma enumeração com aproximadamente 21 mil elementos; para mais, a capacidade do pool de constantes não é suficiente.

A documentação em Enum :: ordinal () exige que seu valor corresponda ao número de sequência do elemento correspondente na declaração de enumeração, portanto, você deve armazenar explicitamente a lista de nomes de campos na ordem correta, quase duplicando o tamanho do arquivo de classe.

public final int ordinal ()

Retorna o ordinal desta constante de enumeração (sua posição na declaração de enum, onde a constante inicial é atribuída a um ordinal de zero).

Aqui a API pública para o conteúdo do pool de constantes pode ajudar, já sabemos como preenchê-la na ordem em que precisamos, mas não existe essa API e é improvável que isso ocorra. O método Class :: getConstantPool () disponível no OpenJDK é declarado como pacote-privado e seria imprudente confiar nele no código do usuário.

O bloco de inicialização agora é bastante compacto e quase independente do número de elementos na enumeração, para que você createValues()possa recusá-lo incorporando seu corpo no loop:

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

Aqui ocorre um processo semelhante a uma avalanche: juntamente com o método createValues(), as instruções para ler os campos dos elementos de enumeração desaparecem, os registros de tipo Fieldrefpara esses campos se tornam desnecessários e, portanto, os NameAndTyperegistros de tipo para os registros de tipo Fieldref. No pool constante, 2 * < >são liberados slots que podem ser usados ​​para declarar elementos adicionais de enumeração.

Mas nem tudo é tão otimista, os testes mostram uma diminuição significativa do desempenho: inicializar uma classe de enumeração com 65 mil elementos leva um minuto e meio impensável. Como se viu rapidamente, "o reflexo diminui".

A implementação de Class :: getDeclaredField () no OpenJDK possui um comportamento assintótico linear do número de campos na classe e nosso bloco de inicialização é quadrático por causa disso.

A adição de cache melhora um pouco a situação, embora não a resolva completamente:

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

A abordagem insegura descrita neste capítulo permite criar transferências com o número de elementos de até 65_410, quase 24 vezes mais que o resultado alcançável com javac e bastante próximo do limite teórico de 65_505 elementos calculados por nós na publicação anterior do ciclo.


Verifique o desempenho


Para os testes, tomamos o maior enumeração, gerando-lo usando o comando java -jar HugeEnumGen.jar -a Unsafe UnsafeHugeEnum. Como resultado, obtemos um arquivo de classe com um tamanho de 2 megabytes e 65_410 elementos.

Crie um novo projeto Java no IDEA e inclua a classe gerada como uma biblioteca externa.

Quase imediatamente, torna-se evidente que o IDEA não está pronto para esse teste de estresse: o



preenchimento automático de um elemento de enumeração leva dezenas de segundos, tanto no antigo i5 móvel quanto no i7 8700K mais moderno. E se você tentar usar a correção rápida para adicionar os elementos ausentes ao comutador, o IDEA até pára de redesenhar as janelas. Suspeito que temporariamente, mas não esperei pela conclusão. A capacidade de resposta durante a depuração também deixa muito a desejar.

Vamos começar com um pequeno número de elementos em 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;
        }
    }

}

Não há surpresas aqui, a compilação e o lançamento são 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

E quanto a mais itens switch? Podemos, por exemplo, processar switchtodos os nossos 65 mil elementos em um de uma só 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;
}

Infelizmente, não. Quando tentamos compilar, recebemos várias mensagens de erro:

$ 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 e switch


Para entender o que está acontecendo, precisamos descobrir como ocorre a tradução switchdos elementos da enumeração.

A especificação da JVM possui um capítulo separado em JVMS11 §3.10 Compiling Switches , cujas recomendações se resumem ao switchuso de uma das duas instruções de bytecode, tableswitchou lookupswitch. switchNão encontraremos referências a cadeias ou elementos de enumeração neste capítulo.

A melhor documentação é o código, então é hora de mergulhar na fonte javac.

A escolha entre tableswitche lookupswitchocorre em Gen :: visitSwitch () e depende do número de opções em switch. Na maioria dos casos, ganha 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;

O cabeçalho tableswitchtem aproximadamente 16 bytes mais 4 bytes por valor. Assim, switchsob nenhuma circunstância pode haver mais ( 65_535 - 16 ) / 4 = 16_379elementos.

De fato, depois de reduzir o número de ramificações caseno corpo switchpara 16 mil, resta apenas um erro de compilação, o mais misterioso:

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

Em busca da fonte do erro, retornaremos um pouco antes, ao estágio de eliminação do açúcar sintático. Os switchmétodos são responsáveis pela tradução visitEnumSwitch(), mapForEnum()e da classe EnumMappingem Lower.java .

Também encontramos um pequeno comentário 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.
 */


O misterioso tryacaba por fazer parte de uma classe auxiliar gerada automaticamente TestAll$0. Inside - uma declaração de uma matriz estática e código para inicializá-la.

A matriz corrige a correspondência entre os nomes dos elementos de enumeração e os switchvalores numéricos atribuídos a eles durante a compilação , protegendo assim o código compilado dos efeitos nocivos da refatoração.

Ao reordenar, adicionar novos ou excluir elementos de enumeração existentes, alguns deles podem alterar o valor ordinal()e é disso que um nível adicional de indireto protege.

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

O código de inicialização é simples e consome de 15 a 17 bytes por elemento. Como resultado, o bloco de inicialização estático acomoda a inicialização de não mais que 3_862 elementos. Esse número acaba sendo o número máximo de elementos de enumeração que podemos usar em um switchcom a implementação atual javac.


Conclusão


Vimos que o uso de uma técnica tão simples como alocar a criação de elementos de enumeração e inicializar uma matriz $VALUESem um método separado permite aumentar o número máximo de elementos em uma enumeração de 2_746 para 10_920.

Os resultados dinâmicos constantes no contexto das realizações anteriores não parecem muito impressionantes e permitem obter apenas 43 elementos a mais, mas com essa abordagem, é muito mais elegante adicionar novas propriedades à enumeração - basta modificar o construtor e transmitir os valores necessários pelos parâmetros estáticos da constante dinâmica.

Se, em algum momento no futuro, o atributo ConstantValuefor ensinado a entender as constantes dinâmicas, esse número poderá subir para 10 mil a 16.

Usesun.misc.Unsafepermite dar um salto gigante e aumentar o número máximo de elementos para 65_410. Mas não se esqueça que Unsafeesta é uma API proprietária, que com o tempo pode desaparecer e seu uso é um risco considerável, como javac avisa diretamente:

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

Mas, como se viu, não é suficiente gerar uma enumeração gigante, você também precisa poder usá-la.

Atualmente, existem problemas com o suporte de tais enumerações no IDE e no nível do compilador Java.

Um grande número de campos na classe pode degradar a capacidade de resposta do IDE durante a edição e durante a depuração. Às vezes, até um travamento completo.

As restrições impostas pelo formato do arquivo de classe e pelos detalhes de implementação do javac tornam impossível o uso de switchmais de 3_862 elementos no código ao mesmo tempo. Dos aspectos positivos, vale ressaltar que esses podem ser elementos arbitrários 3_862.

Melhorias adicionais nos resultados são possíveis apenas através do refinamento do compilador Java, mas essa é uma história completamente diferente.


Materiais adicionais


Código-fonte do GitHub: https://github.com/Maccimo/HugeEnumGeneratorArticle

Arquivo JAR coletado: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0

Ajuda de inicialização suportada

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