Método embutido na JVM

Método embutido na JVM

1. Introdução

Neste tutorial, vamos dar uma olhada em qual método inlining está na Java Virtual Machine e como ele funciona.

Também veremos como obter e ler as informações relacionadas ao inlining do JVM e o que podemos fazer com essas informações para otimizar nosso código.

2. Que método é Inlining?

Basicamente,inlining is a way to optimize compiled source code at runtime by replacing the invocations of the most often executed methods with its bodies.

Embora haja compilação envolvida, ela não é realizada pelo compiladorjavac tradicional, mas pela própria JVM. Para ser mais preciso,it’s the responsibility of the Just-In-Time (JIT) compiler, que faz parte da JVM; javac apenas produz um bytecode e permite que o JIT faça a mágica e otimize o código-fonte.

Uma das consequências mais importantes dessa abordagem é que, se compilarmos o código usando Java antigo, o mesmo arquivo.class será mais rápido em JVMs mais recentes. Dessa forma, não precisamos recompilar o código-fonte, mas apenas atualizar o Java.

3. Como o JIT faz isso?

Essencialmente, oJIT compiler tries to inline the methods that we often call so that we can avoid the overhead of a method invocation. Isso leva duas coisas em consideração ao decidir se um método é incorporado ou não.

Primeiro, ele usa contadores para acompanhar quantas vezes invocamos o método. Quando o método é chamado mais de um número específico de vezes, fica "quente". Esse limite é definido como 10.000 por padrão, mas podemos configurá-lo por meio do sinalizador JVM durante a inicialização do Java. Definitivamente, não queremos embutir tudo, uma vez que seria demorado e produziria um bytecode enorme.

Devemos ter em mente que o alinhamento ocorrerá apenas quando chegarmos a um estado estável. Isso significa que precisamos repetir a execução várias vezes para fornecer informações de perfil suficientes para o compilador JIT.

Além disso, ser "quente" não garante que o método seja incorporado. Se for muito grande, o JIT não o embutirá. O tamanho aceitável é limitado pelo sinalizador-XX:FreqInlineSize=, que especifica o número máximo de instruções de bytecode para embutir para um método.

No entanto, é altamente recomendável não alterar o valor padrão desse sinalizador, a menos que estejamos absolutamente certos de saber o impacto que ele pode causar. O valor padrão depende da plataforma - para Linux de 64 bits, é 325.

The JIT inlines static, private,or final methods in general. E enquantopublic methods are also candidates for inlining, not every public method will necessarily be inlined. The JVM needs to determine that there’s only a single implementation of such a method. Qualquer subclasse adicional impediria a inclusão e o desempenho diminuirá inevitavelmente.

4. Encontrando Métodos Quentes

Certamente não queremos adivinhar o que o JIT está fazendo. Portanto, precisamos de alguma maneira de ver quais métodos estão embutidos ou não. Podemos facilmente conseguir isso e registrar todas essas informações na saída padrão configurando alguns sinalizadores da JVM adicionais durante a inicialização:

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

O primeiro sinalizador será registrado quando a compilação JIT acontecer. O segundo sinalizador ativa sinalizadores adicionais, incluindo-XX:+PrintInlining, que imprimirá quais métodos estão sendo embutidos e onde.

Isso nos mostrará os métodos incorporados na forma de uma árvore. As folhas são anotadas e marcadas com uma das seguintes opções:

  • inline (hot) - este método é marcado como ativo e inline

  • too big - o método não é quente, mas também seu bytecode gerado é muito grande, então não é embutido

  • hot method too big - este é um método quente, mas não está alinhado porque o bytecode é muito grande

Devemos prestar atenção ao terceiro valor e tentar otimizar os métodos com o rótulo “método quente muito grande”.

Geralmente, se encontrarmos um método quente com uma instrução condicional muito complexa, devemos tentar separar o conteúdo da instruçãoif- e aumentar a granularidade para que o JIT possa otimizar o código. O mesmo vale para as instruções de saveiroswitchefor-.

Podemos concluir que um método manual embutido é algo que não precisamos fazer para otimizar nosso código. A JVM faz isso com mais eficiência, e possivelmente tornaríamos o código longo e difícil de seguir.

4.1. Exemplo

Vamos agora ver como podemos verificar isso na prática. Criaremos primeiro uma classe simples que calcula a soma dos primeirosN inteiros positivos consecutivos:

public class ConsecutiveNumbersSum {

    private long totalSum;
    private int totalNumbers;

    public ConsecutiveNumbersSum(int totalNumbers) {
        this.totalNumbers = totalNumbers;
    }

    public long getTotalSum() {
        totalSum = 0;
        for (int i = 0; i < totalNumbers; i++) {
            totalSum += i;
        }
        return totalSum;
    }
}

A seguir, um método simples fará uso da classe para realizar o cálculo:

private static long calculateSum(int n) {
    return new ConsecutiveNumbersSum(n).getTotalSum();
}

Por fim, chamaremos o método várias vezes e veremos o que acontece:

for (int i = 1; i < NUMBERS_OF_ITERATIONS; i++) {
    calculateSum(i);
}

Na primeira execução, vamos executá-lo 1.000 vezes (menos do que o valor limite de 10.000 mencionado acima). Se pesquisarmos o resultado do métodocalculateSum(), não o encontraremos. Isso é esperado, pois não ligamos o suficiente.

Se agora alterarmos o número de iterações para 15.000 e pesquisarmos a saída novamente, veremos:

664 262 % com.example.inlining.InliningExample::main @ 2 (21 bytes)
  @ 10   com.example.inlining.InliningExample::calculateSum (12 bytes)   inline (hot)

Podemos ver que desta vez o método preenche as condições de inclusão e a JVM o incorporou.

É digno de nota mencionar novamente que se o método for muito grande, o JIT não o embutirá, independentemente do número de iterações. Podemos verificar isso adicionando outro sinalizador ao executar o aplicativo:

-XX:FreqInlineSize=10

Como podemos ver na saída anterior, o tamanho do nosso método é 12 bytes. O sinalizador-XX:FreqInlineSize limitará o tamanho do método elegível para inlining a 10 bytes. Consequentemente, o inlining não deve ocorrer desta vez. E, de fato, podemos confirmar isso dando uma outra olhada na saída:

330 266 % com.example.inlining.InliningExample::main @ 2 (21 bytes)
  @ 10   com.example.inlining.InliningExample::calculateSum (12 bytes)   hot method too big

Embora tenhamos alterado o valor do sinalizador aqui para fins de ilustração, devemos enfatizar a recomendação de não alterar o valor padrão do sinalizador-XX:FreqInlineSize, a menos que seja absolutamente necessário.

5. Conclusão

Neste artigo, vimos qual método está incluído na JVM e como o JIT faz isso. Descrevemos como podemos verificar se nossos métodos são elegíveis para inclusão ou não e sugerimos como usar essas informações, tentando reduzir o tamanho dos métodos longos freqüentemente chamados, grandes demais para serem incorporados.

Por fim, ilustramos como podemos identificar um método quente na prática.

Todos os trechos de código mencionados no artigo podem ser encontrados emour GitHub repository.