Mergulhe fundo no novo compilador Java JIT - Graal

Mergulhe fundo no novo compilador Java JIT - Graal

1. Visão geral

Neste tutorial, daremos uma olhada mais detalhada no novo compilador Java Just-In-Time (JIT), chamado Graal.

Veremos o que é o projetoGraal e descreveremos uma de suas partes, um compilador JIT dinâmico de alto desempenho.

2. O que é um compilador JIT

Vamos primeiro explicar o que o compilador JIT faz.

When we compile our Java program (e.g., using the javac command), we’ll end up with our source code compiled into the binary representation of our code – a JVM bytecode. Esse bytecode é mais simples e mais compacto que o nosso código-fonte, mas os processadores convencionais em nossos computadores não podem executá-lo.

To be able to run a Java program, the JVM interprets the bytecode. Como os intérpretes são geralmente muito mais lentos do que o código nativo em execução em um processador real, oJVM can run another compiler which will now compile our bytecode into the machine code that can be run by the processor. O chamado compilador just-in-time é muito mais sofisticado do que o compiladorjavac e executa otimizações complexas para gerar código de máquina de alta qualidade.

3. Análise mais detalhada do compilador JIT

A implementação do JDK pela Oracle é baseada no projeto OpenJDK de código aberto. Isso inclui oHotSpot virtual machine, disponível desde a versão 1.3 do Java. Écontains two conventional JIT-compilers: the client compiler, also called C1 and the server compiler, called opto or C2.

O C1 foi projetado para executar mais rapidamente e produzir código menos otimizado, enquanto o C2, por outro lado, leva um pouco mais de tempo para ser executado, mas produz um código melhor otimizado. O compilador cliente é mais adequado para aplicativos de desktop, uma vez que não queremos longas pausas para a compilação JIT. O compilador de servidor é melhor para aplicativos de servidor de execução longa que podem gastar mais tempo na compilação.

3.1. Compilação em camadas

Hoje, a instalação Java usa os dois compiladores JIT durante a execução normal do programa.

Como mencionamos na seção anterior, nosso programa Java, compilado porjavac, inicia sua execução em um modo interpretado. A JVM rastreia cada método chamado com freqüência e os compila. Para fazer isso, ele usa C1 para a compilação. Mas, o HotSpot ainda mantém um olho nas futuras chamadas desses métodos. Se o número de chamadas aumentar, a JVM recompilará esses métodos mais uma vez, mas desta vez usando C2.

Esta é a estratégia padrão usada pelo HotSpot, chamadatiered compilation.

3.2. O compilador de servidor

Agora vamos nos concentrar um pouco no C2, já que é o mais complexo dos dois. C2 foi extremamente otimizado e produz código que pode competir com C ou ser ainda mais rápido. O compilador do servidor em si é escrito em um dialeto específico de C.

No entanto, ele vem com alguns problemas. Devido a possíveis falhas de segmentação no C ++, isso pode causar o travamento da VM. Além disso, nenhuma grande melhoria foi implementada no compilador nos últimos anos. O código em C2 tornou-se difícil de manter, então não poderíamos esperar novos aprimoramentos importantes com o design atual. Com isso em mente, o novo compilador JIT está sendo criado no projeto chamado GraalVM.

4. Projeto GraalVM

O projetoGraalVM é um projeto de pesquisa criado pela Oracle com o objetivo de substituir totalmente o HotSpot. Podemos considerar Graal como vários projetos conectados: um novo compilador JIT para o HotSpot e uma nova máquina virtual poliglota. Ele oferece um ecossistema abrangente que suporta um grande conjunto de linguagens (Java e outras linguagens baseadas em JVM; JavaScript, Ruby, Python, R, C / C ++ e outras linguagens baseadas em LLVM).

Claro, vamos nos concentrar em Java.

4.1. Graal - um compilador JIT escrito em Java

Graal is a high-performance JIT compiler. Aceita o bytecode JVM e produz o código de máquina.

Existem várias vantagens principais de escrever um compilador em Java. Primeiro de tudo, segurança, o que significa que não há falhas, mas exceções e nenhum vazamento real de memória. Além disso, teremos um bom suporte de IDE e seremos capazes de usar depuradores ou profilers ou outras ferramentas convenientes. Além disso, o compilador pode ser independente do HotSpot e seria capaz de produzir uma versão mais rápida compilada por JIT.

O compilador Graal foi criado com essas vantagens em mente. It uses the new JVM Compiler Interface – JVMCI to communicate with the VM. Para ativar o uso do novo compilador JIT, precisamos definir as seguintes opções ao executar Java a partir da linha de comando:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

O que isso significa é quewe can run a simple program in three different ways: with the regular tiered compilers, with the JVMCI version of Graal on Java 10 or with the GraalVM itself.

4.2. Interface do Compilador JVM

A JVMCI faz parte do OpenJDK desde o JDK 9, portanto, podemos usar qualquer OpenJDK ou Oracle JDK padrão para executar o Graal.

O que o JVMCI realmente nos permite fazer é excluir a compilação em camadas padrão e conectar nosso novo compilador (ou seja, Graal) sem a necessidade de alterar nada na JVM.

A interface é bastante simples. Quando Graal está compilando um método, ele passa o bytecode desse método como a entrada para o JVMCI '. Como saída, obteremos o código de máquina compilado. Tanto a entrada como a saída são apenas matrizes de bytes:

interface JVMCICompiler {
    byte[] compileMethod(byte[] bytecode);
}

Em cenários da vida real, geralmente precisaremos de mais algumas informações, como o número de variáveis ​​locais, o tamanho da pilha e as informações coletadas da criação de perfil no interpretador para que possamos saber como o código está sendo executado na prática.

Essencialmente, ao chamarcompileMethod () da interfaceJVMCICompiler, precisaremos passar um objetoCompilationRequest. Em seguida, ele retornará o método Java que queremos compilar e, nesse método, encontraremos todas as informações de que precisamos.

4.3. Graal em Ação

O próprio Graal é executado pela VM, portanto, primeiro será interpretado e compilado por JIT quando ficar quente. Vejamos um exemplo, que também pode ser encontrado emGraalVM’s official site:

public class CountUppercase {
    static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);

    public static void main(String[] args) {
        String sentence = String.join(" ", args);
        for (int iter = 0; iter < ITERATIONS; iter++) {
            if (ITERATIONS != 1) {
                System.out.println("-- iteration " + (iter + 1) + " --");
            }
            long total = 0, start = System.currentTimeMillis(), last = start;
            for (int i = 1; i < 10_000_000; i++) {
                total += sentence
                  .chars()
                  .filter(Character::isUpperCase)
                  .count();
                if (i % 1_000_000 == 0) {
                    long now = System.currentTimeMillis();
                    System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
                    last = now;
                }
            }
            System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
        }
    }
}

Agora, vamos compilá-lo e executá-lo:

javac CountUppercase.java
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

Isso resultará em uma saída semelhante à seguinte:

1 (1581 ms)
2 (480 ms)
3 (364 ms)
4 (231 ms)
5 (196 ms)
6 (121 ms)
7 (116 ms)
8 (116 ms)
9 (116 ms)
total: 59999994 (3436 ms)

Podemos ver queit takes more time in the beginning. Esse tempo de aquecimento depende de vários fatores, como a quantidade de código multithread no aplicativo ou o número de threads que a VM usa. Se houver menos núcleos, o tempo de aquecimento pode ser maior.

Se quisermos ver as estatísticas das compilações do Graal, precisamos adicionar o seguinte sinalizador ao executar nosso programa:

-Dgraal.PrintCompilation=true

Isso mostrará os dados relacionados ao método compilado, o tempo gasto, os bytecodes processados ​​(que também incluem métodos incorporados), o tamanho do código da máquina produzido e a quantidade de memória alocada durante a compilação. A saída da execução ocupa muito espaço, então não vamos mostrá-la aqui.

4.4. Comparando com o compilador de camada superior

Vamos agora comparar os resultados acima com a execução do mesmo programa compilado com o compilador de camada superior. Para fazer isso, precisamos dizer à VM para não usar o compilador JVMCI:

java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:-UseJVMCICompiler
1 (510 ms)
2 (375 ms)
3 (365 ms)
4 (368 ms)
5 (348 ms)
6 (370 ms)
7 (353 ms)
8 (348 ms)
9 (369 ms)
total: 59999994 (4004 ms)

Podemos ver que há uma diferença menor entre os tempos individuais. Isso também resulta em um tempo inicial mais curto.

4.5. A estrutura de dados por trás do Graal

Como dissemos anteriormente, Graal basicamente transforma uma matriz de bytes em outra matriz de bytes. Nesta seção, vamos nos concentrar no que está por trás desse processo. Os exemplos a seguir dependem deChris Seaton’s talk at JokerConf 2017.

O trabalho do compilador básico, em geral, é agir em nosso programa. Isso significa que deve simbolizá-lo com uma estrutura de dados apropriada. Graal uses a graph for such a purpose, the so-called program-dependence-graph.

Em um cenário simples, onde queremos adicionar duas variáveis ​​locais, ou seja,x + y,we would have one node for loading each variable and another node for adding them. Ao lado dele,we’d also have two edges representing the data flow:

image

The data flow edges are displayed in blue. Eles estão apontando que, quando as variáveis ​​locais são carregadas, o resultado vai para a operação de adição.

Vamos agora apresentaranother type of edges, the ones that describe the control flow. Para fazer isso, vamos estender nosso exemplo chamando métodos para recuperar nossas variáveis ​​em vez de lê-las diretamente. Quando fazemos isso, precisamos acompanhar a ordem de chamada dos métodos. Representaremos este pedido com as setas vermelhas:

image

Aqui, podemos ver que os nós não mudaram na verdade, mas adicionamos as bordas do fluxo de controle.

4.6. Gráficos reais

Podemos examinar os gráficos Graal reais comIdealGraphVisualiser. Para executá-lo, usamos o scommandmx igv . Também precisamos configurar a JVM definindo o sinalizador-Dgraal.Dump.

Vejamos um exemplo simples:

int average(int a, int b) {
    return (a + b) / 2;
}

Isso tem um fluxo de dados muito simples:

image

No gráfico acima, podemos ver uma representação clara do nosso método. Os parâmetros P (0) e P (1) fluem para a operação de adição, que entra na operação de divisão com a constante C (2). Finalmente, o resultado é retornado.

Agora vamos mudar o exemplo anterior para ser aplicável a uma série de números:

int average(int[] values) {
    int sum = 0;
    for (int n = 0; n < values.length; n++) {
        sum += values[n];
    }
    return sum / values.length;
}

Podemos ver que a adição de um loop nos levou ao gráfico muito mais complexo:

image

O que podemos notarhere are:

  • os nós de loop inicial e final

  • os nós que representam a leitura da matriz e a leitura do comprimento da matriz

  • bordas de fluxo de dados e controle, como antes.

This data structure is sometimes called a sea-of-nodes, or a soup-of-nodes. Precisamos mencionar que o compilador C2 usa uma estrutura de dados semelhante, então não é algo novo, inovou exclusivamente para Graal.

Vale lembrar que Graal otimiza e compila nosso programa modificando a estrutura de dados acima mencionada. Podemos ver porque foi uma boa escolha escrever o compilador Graal JIT em Java:a graph is nothing more than a set of objects with references connecting them as the edges. That structure is perfectly compatible with the object-oriented language, which in this case is Java.

4.7. Modo Compilador Ahead-of-Time

Também é importante mencionar quewe can also use the Graal compiler in the Ahead-of-Time compiler mode in Java 10. Como já dissemos, o compilador Graal foi escrito do zero. Está em conformidade com uma nova interface limpa, a JVMCI, que nos permite integrá-la ao HotSpot. Isso não significa que o compilador está vinculado a ele.

Uma maneira de usar o compilador é usar uma abordagem orientada a perfis para compilar apenas os métodos quentes, maswe can also make use of Graal to do a total compilation of all methods in an offline mode without executing the code. Esta é a chamada "Compilação Ahead-of-Time",JEP 295,, mas não vamos nos aprofundar na tecnologia de compilação AOT aqui.

A principal razão pela qual usaríamos o Graal dessa maneira é acelerar o tempo de inicialização até que a abordagem regular de Compilação em camadas no HotSpot possa assumir o controle.

5. Conclusão

Neste artigo, exploramos as funcionalidades do novo compilador Java JIT como parte do projeto Graal.

Primeiro descrevemos os compiladores JIT tradicionais e discutimos novos recursos do Graal, especialmente a nova interface do JVM Compiler. Em seguida, ilustramos como os dois compiladores funcionam e comparamos suas performances.

Depois disso, falamos sobre a estrutura de dados que Graal usa para manipular nosso programa e, por fim, sobre o modo de compilador AOT como outra forma de usar Graal.

Como sempre, o código-fonte pode ser encontradoover on GitHub. Lembre-se de que a JVM precisa ser configurada com os sinalizadores específicos - que foram descritos aqui.