Olá Habr! Apresento a você a tradução do artigo " Entendendo como o Graal funciona - um compilador Java JIT escrito em Java ".
1. Introdução
Uma das razões pelas quais me tornei um pesquisador em linguagens de programação é que, em uma grande comunidade de pessoas envolvidas em tecnologia da computação, quase todo mundo usa linguagens de programação, e muitas estão interessadas em como elas funcionam. Quando me deparei com programação quando criança e me familiarizei com uma linguagem de programação, a primeira coisa que eu queria saber era como ela funciona, e a primeira coisa que eu queria fazer era criar minha própria linguagem.
Nesta apresentação, mostrarei alguns dos mecanismos de trabalho da linguagem que todos vocês usam - Java. A peculiaridade é que usarei um projeto chamado Graal , que implementa o conceito de Java em Java .
Graal é apenas um dos componentes no trabalho de Java - é um compilador just-in-time . Essa é a parte da JVM que converte o bytecode Java em código de máquina durante a execução do programa e é um dos fatores que garantem o alto desempenho da plataforma. Parece-me também que o que a maioria das pessoas considera uma das partes mais complexas e vagas da JVM, que está além de sua compreensão. Alterar esta opinião é o objetivo deste discurso.
Se você souber o que é uma JVM; geralmente entender o que significam os termos bytecode e código de máquina ; e capaz de ler código escrito em Java, espero que seja suficiente para entender o material apresentado.
Começarei discutindo por que podemos querer um novo compilador JIT para a JVM escrito em Java e, depois disso, mostrarei que não há nada mais especial nisso, como você pode pensar, dividindo a tarefa em montagem, uso e demonstração do compilador que seu código é o mesmo que em qualquer outro aplicativo.
Vou abordar um pouco a teoria e depois mostrarei como ela é aplicada durante todo o processo de compilação, do bytecode ao código da máquina. Também mostrarei alguns detalhes e, no final, falaremos sobre os benefícios desse recurso, além de implementar o Java em Java por si só.
Usarei as capturas de tela do código no Eclipse, em vez de iniciá-las durante a apresentação, para evitar os problemas inevitáveis da codificação ao vivo.
O que é um compilador JIT?
Tenho certeza de que muitos de vocês sabem o que é um compilador JIT, mas ainda vou tocar no básico para que ninguém fique sentado aqui com medo de fazer essa pergunta principal.
Quando você executa o comando javac
ou compile-on-save no IDE, seu programa Java compila do código Java para o bytecode da JVM, que é a representação binária do programa. É mais compacto e simples que o código-fonte Java. No entanto, o processador comum do seu laptop ou servidor não pode apenas executar o bytecode da JVM.
Para a operação do seu programa, a JVM interpreta esse bytecode. Os intérpretes geralmente são muito mais lentos que o código da máquina em execução no processador. Por esse motivo, a JVM, enquanto o programa está em execução, pode iniciar outro compilador que converte seu bytecode em código de máquina, que o processador já pode executar.
Esse compilador, geralmente muito mais sofisticado que o javac
, realiza otimizações complexas para produzir código de máquina de alta qualidade como resultado.
Por que escrever um compilador JIT em Java?
Até o momento, uma implementação da JVM chamada OpenJDK inclui dois compiladores JIT principais. O compilador do cliente, conhecido como C1 , foi projetado para operação mais rápida, mas, ao mesmo tempo, produz código menos otimizado. O compilador do servidor, conhecido como opto ou C2 , requer um pouco mais de tempo para trabalhar, mas produz um código mais otimizado.
A idéia era que o compilador cliente fosse mais adequado para aplicativos de desktop, onde longas pausas no compilador JIT eram indesejáveis e o compilador de servidor para aplicativos de servidor de reprodução prolongada, o que poderia levar mais tempo compilando.
Hoje eles podem ser combinados para que o código seja compilado primeiro por C1 e, em seguida, se ele continuar sendo intensamente executado e fizer sentido gastar mais tempo, - C2. Isso é chamado de compilação em camadas .
Vamos falar do C2, o compilador de servidores que realiza mais otimizações.
Podemos clonar o OpenJDK a partir do espelho no GitHub ou simplesmente abrir a árvore do projeto no site.
$ git clone https://github.com/dmlloyd/openjdk.git
O código C2 está em openjdk / hotspot / src / share / vm / opto .

Primeiro de tudo, vale a pena notar que C2 é escrito em C ++ . Obviamente, isso não é algo ruim, mas há certas desvantagens. C ++ é uma linguagem insegura. Isso significa que erros no C ++ podem travar a VM. A razão para isso é provavelmente a idade do código, mas o código C2 C ++ se tornou muito difícil de manter e desenvolver.
Uma das principais figuras por trás do compilador C2, Cliff Click, disse que nunca mais escreveria VM novamente em C ++, e ouvimos a equipe da JVM do Twitter dizer que C2 está estagnado e precisa ser substituído por uma razão dificuldades de maior desenvolvimento.


https://www.youtube.com/watch?v=Hqw57GJSrac
Então, voltando à pergunta, o que é isso em Java que pode ajudar a resolver esses problemas? A mesma coisa que dá para escrever um programa em Java em vez de C ++. Provavelmente, isso é segurança (exceções em vez de falhas, nenhum vazamento de memória real ou ponteiros oscilantes ), bons ajudantes (depuradores, criadores de perfil e ferramentas como VisualVM ), bom suporte a IDE etc.
Você pode pensar: Como escrever algo como um compilador Java JIT? e que isso só é possível em uma linguagem de programação de sistema de baixo nível, como C ++. Nesta apresentação, espero convencê-lo de que isso não é de todo! Essencialmente, o compilador JIT deve apenas aceitar o bytecode da JVM e fornecer o código da máquina - você fornece byte[]
na entrada e também deseja recuperar o byte[]
. É preciso muito trabalho complexo para fazer isso, mas não afeta o nível do sistema e, portanto, não requer uma linguagem de sistema como C ou C ++.
Configuração Graal
A primeira coisa que precisamos é o Java 9. A interface Graal usada, chamada JVMCI, foi adicionada ao Java como parte da Interface do Compilador JVM JEP 243 no nível Java e a primeira versão que o inclui é Java 9. Eu uso 9 + 181 . No caso de quaisquer requisitos especiais, existem portas (backports) para Java 8.
$ export JAVA_HOME=`pwd`/jdk9 $ export PATH=$JAVA_HOME/bin:$PATH $ java -version java version "9" Java(TM) SE Runtime Environment (build 9+181) Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
A próxima coisa que precisamos é de um sistema de construção chamado mx
. É um pouco como Maven ou Gradle , mas provavelmente você não o escolheria para o seu aplicativo. Ele implementa certas funcionalidades para dar suporte a alguns casos de uso complexos, mas a usaremos apenas para montagens simples.
Você pode clonar mx
com o GitHub. Estou usando o commit #7353064
. Agora basta adicionar o executável ao caminho.
$ git clone https://github.com/graalvm/mx.git $ cd mx; git checkout 7353064 $ export PATH=`pwd`/mx:$PATH
Agora precisamos clonar o próprio Graal. Estou usando uma distribuição chamada GraalVM versão 0.28.2 .
$ git clone https://github.com/graalvm/graal.git --branch vm-enterprise-0.28.2
Este repositório contém outros projetos nos quais não estamos interessados, então apenas acessamos o subprojeto do compilador , que é o compilador Graal JIT, e o compilamos usando mx
.
$ cd graal/compiler $ mx build
Para trabalhar com o código Graal, usarei o Eclipse IDE . Estou usando o Eclipse 4.7.1. mx
pode gerar arquivos de projeto Eclipse para nós.
$ mx eclipseinit
Para abrir o diretório graal como uma área de trabalho, é necessário executar os arquivos, Importar ..., Geral, Projetos existentes e selecionar o diretório graal novamente . Se você não executou o Eclipse no Java 9, também pode ser necessário anexar as fontes JDK.

Bom Agora que tudo está pronto, vamos ver como funciona. Vamos usar esse código muito simples.
class Demo { public static void main(String[] args) { while (true) { workload(14, 2); } } private static int workload(int a, int b) { return a + b; } }
Primeiro, compilamos esse código javac
e, em seguida, executamos a JVM. Primeiro, mostrarei como o compilador C2 JIT padrão funciona. Para fazer isso, especificaremos vários sinalizadores: -XX:+PrintCompilation
, necessário para a JVM gravar um log ao compilar um método, e -XX:CompileOnly=Demo::workload
, para que apenas esse método seja compilado. Caso contrário, muita informação será exibida e a JVM será mais inteligente do que precisamos e otimizará o código que queremos ver.
$ javac Demo.java $ java \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 113 1 3 Demo::workload (4 bytes) ...
Não explicarei isso em detalhes, mas direi apenas que essa é uma saída de log que mostra que o método da workload
foi compilado.
Agora, como compilador JIT da nossa JVM Java 9, usamos o Graal recém compilado. Para fazer isso, adicione mais alguns sinalizadores.
--module-path=...
e --upgrade-module-path=...
adicionam Graal ao caminho do módulo . Deixe-me lembrá-lo de que o caminho do módulo apareceu no Java 9 como parte do sistema de módulos Jigsaw e, para nossos propósitos, podemos considerá-lo por analogia com o caminho de classe .
Precisamos do -XX:+UnlockExperimentalVMOptions
devido ao fato de a JVMCI (a interface usada pelo Graal) nesta versão ser um recurso experimental.
O sinalizador -XX:+EnableJVMCI
necessário para dizer que queremos usar a JVMCI e -XX:+UseJVMCICompiler
- para ativar e instalar um novo compilador JIT.
Para não complicar o exemplo e, em vez de usar C1 em conjunto com a JVMCI, tenha apenas o compilador JVMCI, especifique o sinalizador -XX:-TieredCompilation
, que desativará a compilação por etapas.
Como antes, especificamos os sinalizadores -XX:+PrintCompilation
e -XX:CompileOnly=Demo::workload
.
Como no exemplo anterior, vemos que um método foi compilado. Mas, desta vez, para compilação, usamos o Graal montado. Por enquanto, apenas aceite minha palavra.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 583 25 Demo::workload (4 bytes) ...
Interface do Compilador JVM
Você não acha que fizemos algo bastante incomum? Temos uma JVM instalada e substituímos o compilador JIT pelo novo compilado sem alterar nada na própria JVM. Esse recurso é fornecido por uma nova interface da JVM chamada JVMCI, a interface do compilador da JVM , que, como eu disse acima, era o JEP 243 e veio no Java 9.
A ideia é semelhante a algumas outras tecnologias JVM existentes.
Talvez você já tenha encontrado um processamento de código-fonte adicional em javac
usando a API de processamento de anotação Java . Esse mecanismo torna possível identificar anotações e o modelo de código-fonte no qual elas são usadas e criar novos arquivos com base nelas.
Além disso, você pode ter utilizado processamento de bytecode adicional na JVM usando agentes Java . Esse mecanismo permite modificar o bytecode Java interceptando-o no momento da inicialização.
A ideia da JVMCI é semelhante. Permite conectar seu próprio compilador Java JIT, escrito em Java.
Agora, quero dizer algumas palavras sobre como mostrarei o código durante esta apresentação. Primeiro, para entender a ideia, mostrarei alguns identificadores e lógicos simplificados na forma de texto nos slides, depois mudarei para as capturas de tela do Eclipse e mostrarei o código real, que pode ser um pouco mais complicado, mas a ideia principal permanecerá a mesma. A parte principal desta apresentação pretende mostrar que é realmente possível trabalhar com o código real do projeto e, portanto, não quero ocultá-lo, embora possa ser um pouco complicado.
A partir de agora, começo a dissipar a opinião de que o compilador JIT é muito complicado.
O que o compilador JIT aceita para entrada? Ele aceita o bytecode do método a ser compilado. E o bytecode, como o nome sugere, é apenas uma matriz de bytes.
O que o compilador JIT produz como resultado? Ele fornece o código da máquina do método. O código da máquina também é apenas uma matriz de bytes.
Como resultado, a interface que deve ser implementada ao gravar um novo compilador JIT para incorporá-lo na JVM será algo parecido com isto.
interface JVMCICompiler { byte[] compileMethod(byte[] bytecode); }
Portanto, se você não conseguia imaginar como o Java pode fazer algo tão baixo quanto a compilação JIT no código da máquina, agora você pode ver que esse não é um trabalho de baixo nível. Certo? Esta é apenas uma função de byte[]
para byte[]
.
Na realidade, tudo é um pouco mais complicado. Apenas o bytecode não é suficiente - também precisamos de mais informações, como o número de variáveis locais, o tamanho da pilha necessário e as informações coletadas pelo profiler intérprete para entender como o código é realmente executado. Portanto, imagine a entrada na forma de CompilationRequest
, que nos informará sobre o JavaMethod
que precisa ser compilado e fornecerá todas as informações necessárias.
interface JVMCICompiler { void compileMethod(CompilationRequest request); } interface CompilationRequest { JavaMethod getMethod(); } interface JavaMethod { byte[] getCode(); int getMaxLocals(); int getMaxStackSize(); ProfilingInfo getProfilingInfo(); ... }
Além disso, a interface não requer o retorno do código compilado. Em vez disso, outra API é usada para instalar o código da máquina na JVM.
HotSpot.installCode(targetCode);
Agora, para escrever um novo compilador JIT para a JVM, você só precisa implementar essa interface. installCode
informações sobre o método que precisa ser compilado e devemos compilá-lo no código da máquina e chamar installCode
.
class GraalCompiler implements JVMCICompiler { void compileMethod(CompilationRequest request) { HotSpot.installCode(...); } }
Vamos mudar para o IDE Eclipse com Graal e dar uma olhada em algumas interfaces e classes reais. Como mencionado anteriormente, eles serão um pouco mais complicados, mas não muito.



Agora, quero mostrar que podemos fazer alterações no Graal e usá-las imediatamente no Java 9. Adicionarei uma nova mensagem de log que será exibida ao compilar o método usando o Graal. Inclua-o no método de interface implementado, chamado pela JVMCI.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println("Going to compile " + request.getMethod().getName()); ... } }

Por enquanto, desative o log de compilação no HotSpot. Agora podemos ver nossa mensagem da versão modificada do compilador.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:CompileOnly=Demo::workload \ Demo Going to compile workload
Se você tentar repetir isso sozinho, notará que nem precisa executar nosso sistema de compilação - mx build
. Chega, normal para o Eclipse, compile ao salvar . E certamente não precisamos reconstruir a própria JVM. Simplesmente incorporamos o compilador modificado na JVM existente.
Count Graal
Bem, sabemos que Graal converte um byte[]
em outro byte[]
. Agora vamos falar sobre a teoria e estruturas de dados que ele usa, porque eles são um pouco incomuns, mesmo para o compilador.
Essencialmente, o compilador lida com seu programa. Para isso, o programa deve ser apresentado na forma de algum tipo de estrutura de dados. Uma opção é bytecode e listas de instruções semelhantes, mas elas não são muito expressivas.
Em vez disso, Graal usa um gráfico para representar seu programa. Se usarmos um operador de adição simples que resume duas variáveis locais, o gráfico incluirá um nó para carregar cada variável, um nó para a soma e duas arestas que mostram que o resultado do carregamento das variáveis locais é inserido no operador de adição.
Às vezes, isso é chamado de gráfico de dependência do programa .
Tendo uma expressão da forma x + y
obtemos nós para as variáveis locais y
, e um nó de sua soma.

As bordas azuis neste gráfico mostram a direção do fluxo de dados da leitura de variáveis locais para a soma.
Além disso, podemos usar arestas para refletir a ordem de execução do programa. Se, em vez de ler variáveis locais, chamarmos métodos, precisamos lembrar a ordem da chamada e não poderemos reorganizá-los (sem conhecer o código interno). Para fazer isso, existem arestas adicionais que definem essa ordem. Eles são mostrados em vermelho.

Portanto, o gráfico Graal, de fato, é dois gráficos combinados em um. Os nós são os mesmos, mas algumas arestas indicam a direção do fluxo de dados, enquanto outras mostram a transferência de controle entre eles.
Para ver o gráfico Graal, você pode usar uma ferramenta chamada IdealGraphVisualiser ou IGV . A inicialização é realizada usando o mx igv
.

Depois disso, execute a JVM com o sinalizador -Dgraal.Dump
.
Um fluxo de dados simples pode ser visto escrevendo uma expressão simples.
int average(int a, int b) { return (a + b) / 2; }

Você pode ver como os parâmetros 0
( P(0
) e 1
( P(1)
) vão para a entrada da operação de adição, que, juntamente com a constante 2
( C(2)
), vai para a entrada da operação de divisão, após a qual esse valor é retornado.
Para analisar um fluxo mais complexo de dados e controle, introduzimos um ciclo.
int average(int[] values) { int sum = 0; for (int n = 0; n < values.length; n++) { sum += values[n]; } return sum / values.length; }


Nesse caso, temos nós do início e do fim do loop, lendo os elementos da matriz e lendo o comprimento da matriz. Como antes, as linhas azuis indicam a direção do fluxo de dados e as linhas vermelhas indicam o fluxo de controle.
Agora você pode ver por que essa estrutura de dados às vezes é chamada de mar de nós ou sopa de nós .
Quero dizer que o C2 usa uma estrutura de dados muito semelhante e, de fato, foi o C2 que popularizou a idéia de um compilador de um mar de nós , portanto isso não é uma inovação do Graal.
Não mostrarei o processo de construção deste gráfico até a próxima parte da apresentação, mas quando o Graal recebe o programa nesse formato, a otimização e a compilação são realizadas modificando essa estrutura de dados. E essa é uma das razões pelas quais escrever um compilador JIT em Java faz sentido. Java é uma linguagem orientada a objetos e um gráfico é uma coleção de objetos conectados por arestas na forma de links.
Do bytecode ao código da máquina
Vamos ver como essas idéias se parecem na prática e seguir algumas etapas do processo de compilação.
Obtendo o Bytecode
A compilação começa com o bytecode. Vamos voltar ao nosso pequeno exemplo de soma.
int workload(int a, int b) { return a + b; }
Nós produziremos o bytecode recebido na entrada imediatamente antes do início da compilação.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println(request.getMethod().getName() + " bytecode: " + Arrays.toString(request.getMethod().getCode())); ... } }
workload bytecode: [26, 27, 96, -84]
Como você pode ver, a entrada para o compilador é bytecode.
Analisador de Bytecode e Construtor de Gráficos
O construtor , percebendo essa matriz de bytes como o bytecode da JVM, a converte em um gráfico Graal. Essa é uma espécie de interpretação abstrata - o construtor interpreta o bytecode Java, mas, em vez de passar valores, manipula as extremidades livres das arestas e as conecta gradualmente.
Vamos aproveitar o fato de o Graal ser escrito em Java e ver como ele funciona usando as ferramentas de navegação do Eclipse. Sabemos que em nosso exemplo há um nó de adição, então vamos descobrir onde ele é criado.



Pode-se ver que eles são criados pelo analisador de bytecode, e isso nos levou ao IADD
processamento da IADD
( 96
, que vimos na matriz de entrada impressa).
private void genArithmeticOp(JavaKind kind, int opcode) { ValueNode y = frameState.pop(kind); ValueNode x = frameState.pop(kind); ValueNode v; switch (opcode) { ... case LADD: v = genIntegerAdd(x, y); break; ... } frameState.push(kind, append(v)); }
Eu disse acima que esta é uma interpretação abstrata, porque tudo isso é muito semelhante a um intérprete de bytecode. Se fosse um intérprete de JVM real, ele retiraria dois valores da pilha, executaria a adição e retornaria o resultado. Nesse caso, removemos dois nós da pilha que, quando o programa é iniciado, serão cálculos, adicionamos, que é o resultado da soma, um novo nó para adição e o colocamos na pilha.
Assim, o gráfico é construído Graal.
Obtendo o código da máquina
Para converter um gráfico Graal em código de máquina, você precisa gerar bytes para todos os seus nós. Isso é feito separadamente para cada nó, chamando seu método generate
.
void generate(Generator gen) { gen.emitAdd(a, b); }
Repito, aqui trabalhamos com um nível muito alto de abstração. Temos uma classe com a qual emitimos instruções de código de máquina sem entrar em detalhes de como isso funciona.
Os detalhes são emitAdd
um pouco complexos e abstratos, porque os operadores aritméticos exigem codificação para muitas combinações diferentes de operandos, mas, ao mesmo tempo, podem compartilhar a maior parte de seu código. Portanto, vou simplificar um pouco mais o programa.
int workload(int a) { return a + 1; }
Nesse caso, a instrução de incremento será usada e mostrarei como fica no assembler.
void incl(Register dst) { int encode = prefixAndEncode(dst.encoding); emitByte(0xFF); emitByte(0xC0 | encode); } void emitByte(int b) { data.put((byte) (b & 0xFF)); }


Você pode ver que o resultado são os bytes adicionados ao padrão ByteBuffer
- apenas criando uma matriz de bytes.
Saída do código da máquina
Vejamos o código da máquina de saída da mesma maneira que fizemos com o bytecode de entrada anterior - adicione uma lista dos bytes no local da instalação.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationResult compileHelper(...) { ... System.err.println(method.getName() + " machine code: " + Arrays.toString(result.getTargetCode())); ... } }

. HotSpot. . OpenJDK, , -, JVM, .
$ cd openjdk/hotspot/src/share/tools/hsdis $ curl -O http://ftp.heanet.ie/mirrors/gnu/binutils/binutils-2.24.tar.gz $ tar -xzf binutils-2.24.tar.gz $ make BINUTILS=binutils-2.24 ARCH=amd64 CFLAGS=-Wno-error $ cp build/macosx-amd64/hsdis-amd64.dylib ../../../../../..
: -XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly \ -XX:CompileOnly=Demo::workload \ Demo
.
workload machine code: [15, 31, 68, 0, 0, 3, -14, -117, -58, -123, 5, ...] ... 0x000000010f71cda0: nopl 0x0(%rax,%rax,1) 0x000000010f71cda5: add %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x000000010f71cda7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x000000010f71cda9: test %eax,-0xcba8da9(%rip)
. , . generate
, .
class AddNode { void generate(...) { ... gen.emitSub(op1, op2, false) ...

, , , .
workload mechine code: [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...] 0x0000000107f451a0: nopl 0x0(%rax,%rax,1) 0x0000000107f451a5: sub %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x0000000107f451a7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x0000000107f451a9: test %eax,-0x1db81a9(%rip)
, ? Graal ; ; ; . , Graal.
[26, 27, 96, -84] → [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...]
, , . Graal , .
— . .
interface Phase { void run(Graph graph); }
(canonicalisation)
. , , ( constant folding ) .
— canonical
.
interface Node { Node canonical(); }
, , , . — . -(-x)
x
.
class NegateNode implements Node { Node canonical() { if (value instanceof NegateNode) { return ((NegateNode) value).getValue(); } else { return this; } } }

Graal . , .
Java, canonical
.
Global value numbering
Global value numbering (GVN) — . a + b
, — .
int workload(int a, int b) { return (a + b) * (a + b); }
Graal . — . GVN . hash map , , .


, — , , - . , , , , — .
int workload() { return (getA() + getB()) * (getA() + getB()); }

(lock coarsening)
. . , , , ( inlining ).
void workload() { synchronized (monitor) { counter++; } synchronized (monitor) { counter++; } }
, , , , .
void workload() { monitor.enter(); counter++; monitor.exit(); monitor.enter(); counter++; monitor.exit(); }
. .
void workload() { monitor.enter(); counter++; counter++; monitor.exit(); }
Graal LockEliminationPhase
. run
, . , , .
void run(StructuredGraph graph) { for (monitorExitNode monitorExitNode : graph.getNodes(MonitorExitNode.class)) { FixedNode next = monitorExitNode.next(); if (next instanceof monitorEnterNode) { AccessmonitorNode monitorEnterNode = (AccessmonitorNode) next; if (monitorEnterNode.object() ## monitorExitNode.object()) { monitorExitNode.remove(); monitorEnterNode.remove(); } } } }

, , , , 2
.
void workload() { monitor.enter(); counter += 2; monitor.exit(); }
IGV . , , \ , , , 2
.


Graal , , , . , , , .
Graal , , , , , , .
Graal , , . ? , ?
, , . . , , , , . , , , , , .
( register allocation ). Graal , JIT-, — ( linear scan algorithm ).
, , , - , .
, , , , (.. ), . , , .
( graph scheduling ). . , . , , , .
, .
Graal?
, , , Graal — , Oracle . , Graal?
(final-tier compiler)
C JVMCI Graal HotSpot — , . ( HotSpot) Graal , .
Twitter Graal , Java 9 . : -XX:+UseJVMCICompiler
.
JVMCI , Graal JVM. (deploy) - JVM, Graal. Java-, Graal, JVM.
OpenJDK Metropolis JVM Java. Graal .

http://cr.openjdk.java.net/\~jrose/metropolis/Metropolis-Proposal.html
Graal . Graal JVM, Graal . , Graal . , - , , JNI.
Charles Nutter JRuby Graal Ruby. , - .
AOT (ahead-of-time)
Graal — Java. JVMCI , Graal, , , Graal . , Graal , JIT-.
JIT- AOT- , Graal . AOT Graal.
Java 9 JIT-, . JVM, .
AOT Java 9 Graal, Linux. , , .
. SubstrateVM — AOT-, Java- JVM . , - (statically linked) . JVM , . SubstrateVM Graal. ( just-in-time ) SubstrateVM, , Graal . Graal AOT- .
$ javac Hello.java $ graalvm-0.28.2/bin/native-image Hello classlist: 966.44 ms (cap): 804.46 ms setup: 1,514.31 ms (typeflow): 2,580.70 ms (objects): 719.04 ms (features): 16.27 ms analysis: 3,422.58 ms universe: 262.09 ms (parse): 528.44 ms (inline): 1,259.94 ms (compile): 6,716.20 ms compile: 8,817.97 ms image: 1,070.29 ms debuginfo: 672.64 ms write: 1,797.45 ms [total]: 17,907.56 ms $ ls -lh hello -rwxr-xr-x 1 chrisseaton staff 6.6M 4 Oct 18:35 hello $ file ./hello ./hellojava: Mach-O 64-bit executable x86_64 $ time ./hello Hello! real 0m0.010s user 0m0.003s sys 0m0.003s
Truffle
Graal Truffle . Truffle — JVM.
, JVM, , JIT- (, , , JIT- JVM , ). Truffle — , , Truffle, , ( partial evaluation ).
, ( inlining ) ( constant folding ) . Graal , Truffle .
Graal — Truffle. Ruby, TruffleRuby Truffle , , Graal. TruffleRuby — Ruby, 10 , , , .
https://github.com/graalvm/truffleruby
Conclusões
, , , JIT- Java . JIT- , , , - . , , . JIT- , byte[]
JVM byte[]
.
, Java. , C++.
Java- Graal - . , , .
. , Eclipse . (definitions), .. .
JIT JIT- JVM, JITWatch , , Graal , . , , - , Graal JVM. IDE, hello-world .
SubstrateVM Truffle, Graal, , Java . , Graal Java. , , - LLVM , , , , .
, , Graal JVM. Porque JVMCI Java 9, Graal , , Java-.
Graal — . , Graal. , !
More information about TruffleRuby
Low Overhead Polling For Ruby
Top 10 Things To Do With GraalVM
Ruby Objects as C Structs and Vice Versa
Understanding How Graal Works — a Java JIT Compiler Written in Java
Flip-Flops — the 1-in-10-million operator
Deoptimizing Ruby
Very High Performance C Extensions For JRuby+Truffle
Optimising Small Data Structures in JRuby+Truffle
Pushing Pixels with JRuby+Truffle
Tracing With Zero Overhead in JRuby+Truffle
How Method Dispatch Works in JRuby+Truffle
A Truffle/Graal High Performance Backend for JRuby