- YJIT e ZJIT são estruturas de compiladores JIT no Ruby 3.x que convertem código Ruby em linguagem de máquina para aumentar a velocidade de execução
- O YJIT conta quantas vezes cada função ou bloco é chamado e, ao atingir um certo limite, converte esse código em linguagem de máquina
- O código convertido é armazenado em blocos YJIT, e cada bloco converte vários comandos YARV em instruções de máquina ARM64 correspondentes
- Usando Branch Stub, observa os tipos de dados reais em tempo de execução e gera seletivamente as instruções de máquina apropriadas
- Essa estrutura é um mecanismo central para alcançar ao mesmo tempo melhor desempenho de execução do Ruby e eficiência no tratamento de tipos dinâmicos
Chapter 4: Compilando Ruby para linguagem de máquina
Interpreting vs. Compiling Ruby Code
- O texto original não traz detalhes
Counting Method and Block Calls
- O YJIT rastreia o número de chamadas de funções e blocos do programa para identificar código hotspot
- Ao lado da sequência de instruções YARV de cada função ou bloco, armazena os valores jit_entry e jit_entry_calls
jit_entry começa como null e depois armazena o ponteiro para o código de máquina gerado pelo YJIT
jit_entry_calls aumenta em 1 a cada chamada
- Quando o número de chamadas atinge o limite, o YJIT compila aquele código em linguagem de máquina
- No Ruby 3.5, o limite padrão é de 30 chamadas para programas pequenos e 120 chamadas para aplicações de grande porte
- Pode ser alterado em tempo de execução com a opção
--yjit-call-threshold
- Com isso, o YJIT converte em linguagem de máquina apenas o código executado com frequência, garantindo um caminho de execução mais eficiente
YJIT Blocks
- O YJIT armazena as instruções de máquina geradas em blocos YJIT
- Um bloco YJIT é diferente de um bloco Ruby e corresponde a uma parte da sequência de instruções YARV
- Cada função ou bloco Ruby é composto por vários blocos YJIT
- No programa de exemplo, o YJIT começa a compilar quando o bloco é executado pela 30ª vez
- Converte a primeira instrução YARV,
getlocal_WC_1, em linguagem de máquina e cria um novo bloco YJIT
- Depois compila também a instrução
getlocal_WC_0 e a inclui no mesmo bloco
- Segundo a Figura 4-8, o YJIT gera instruções ARM64 e carrega valores nos registradores x1 e x9 do processador M1
getlocal_WC_1 armazena na pilha a variável local do frame de pilha anterior, e getlocal_WC_0 armazena a variável da pilha atual
- As instruções de máquina geradas executam o mesmo comportamento
YJIT Branch Stubs
- Ao compilar a instrução
opt_plus, o YJIT encontra o problema de não saber o tipo dos operandos
- Dependendo do tipo, como inteiro, string ou ponto flutuante, as instruções de máquina necessárias são diferentes
- Ex.: soma de inteiros usa a instrução
adds, enquanto soma de ponto flutuante exige outra instrução
- Para resolver isso, o YJIT usa observação em tempo de execução em vez de análise prévia
- Durante a execução do programa, verifica os tipos reais dos valores passados e gera a linguagem de máquina correspondente
- Para esse comportamento, usa Branch Stub
- Quando um novo branch ainda não tem um bloco conectado, ele é ligado temporariamente a um stub
- Depois que o tipo real é confirmado, esse stub é substituído pelo bloco apropriado
ZJIT (apenas mencionado)
- O sumário inclui uma seção sobre ZJIT, mas o texto não traz explicações concretas
Resumo
- O YJIT é um compilador JIT para o Ruby 3.5 voltado a melhorar a eficiência de execução de uma linguagem de tipos dinâmicos
- Os pontos centrais são gatilho de compilação baseado em número de chamadas, estrutura de blocos YJIT e verificação de tipos em tempo de execução por meio de Branch Stub
- Na arquitetura ARM64, ele converte para instruções reais de máquina para aumentar a velocidade de execução do código Ruby
- O ZJIT é mencionado como um JIT de próxima geração, mas sem detalhes no texto
1 comentários
Comentários do Hacker News
Houve uma época em que o MacRuby era compilado para código nativo no macOS usando LLVM e integrado aos frameworks Objective‑C
Era uma ideia bem interessante, mas no fim parece que a Apple mudou de direção para o Swift
Quando sair a nova edição, pretendo comprar e ler Ruby Under a Microscope. Ainda gosto muito de Ruby, mas não tive muitas oportunidades de usá-la na prática
Hoje outras pessoas tocam o projeto, mas a impressão é que o foco atual está mais no DragonRuby (uma implementação de Ruby voltada para jogos)
Aliás, também existe um artigo na Wikipedia
Só que as APIs antigas talvez não sejam mais suportadas
O VB6 era realmente muito rápido para desenvolver, e dava para mexer com Direct3D e até ASP Classic
A elegância e a facilidade de desenvolvimento do Ruby me fazem lembrar daquela época
Se Ruby tivesse tido ferramentas de GUI no nível do VB6, acho que sua popularidade poderia ter sido bem diferente
Fico muito feliz em ver o Pat continuando a tocar o projeto
O primeiro livro Ruby Under a Microscope e os posts de blog dele foram uma grande inspiração para mim
Já cheguei a conhecê-lo pessoalmente numa conferência Euruko, e ele era realmente uma pessoa excelente
Quando li Ruby Under a Microscope pela primeira vez, achei muito divertido
Isso inclusive me ajudou antigamente a resolver desafios de CTF
Hoje em dia não acompanho mais de perto a implementação interna do Ruby, mas pretendo comprar a nova edição quando sair
Este texto me deu vontade de ler de novo a nova edição do livro
Já que estamos falando de compilação de Ruby, fiquei curioso se alguém já experimentou o Sorbet compiler, criado por desenvolvedores da Stripe
Post de anúncio open source do Sorbet Compiler
Compilação AOT é realmente muito difícil em Ruby
O interessante na abordagem do Sorbet é que ela consegue criar caminhos rápidos com base na verificação de tipos do Ruby
Eu também estou fazendo um compilador de Ruby como projeto pessoal, e estou usando como referência hokstad.com/compiler e
writing-a-compiler-in-ruby
No momento estou focado em passar no RubySpec, e depois pretendo tentar otimizações baseadas em tipos
Não é diretamente sobre compilação de Ruby, mas o livro Enterprise Integration with Ruby me trouxe muitos insights sobre o uso de Ruby fora do contexto web
Desde que conheci o MRuby, fiquei viciado na diversão de transformar meus projetos e scripts em executáveis independentes
Fico feliz que Ruby Under a Microscope continue sendo atualizado
Acho que é leitura obrigatória para quem quer entender o funcionamento interno do Ruby
Eu tinha curiosidade sobre como o YJIT rastreia a compilação por tipo de entrada quando um bloco é executado várias vezes
Queria entender como o Ruby lida com diferentes tipos, como int e float
Ele usa uma abordagem de "esperar para ver" e adia a compilação até que os tipos reais sejam fornecidos
Mantém versões separadas do bloco para cada tipo e chama a apropriada conforme a situação
Esse algoritmo é chamado de Basic Block Versioning
Maxime Chevalier‑Boisvert, da Shopify, explica isso muito bem na palestra da RubyConf 2021
O novo motor JIT, o ZJIT, parece usar uma abordagem diferente
Tornar linguagens dinamicamente tipadas rápidas com JIT normalmente cobra o preço de aumentar o uso de memória
Fora de empresas grandes como a Shopify, isso pode ser um problema ainda maior
Hoje em dia, instâncias de nuvem costumam oferecer algo em torno de 4 GiB de memória por núcleo, então algumas centenas de MB de código JIT são perfeitamente administráveis
Achei simples o jeito como o YJIT encontra hotspots apenas contando o número de chamadas de função
Fiquei pensando se não existe algo como nos JITs de JavaScript, que detectam operações pesadas dentro de loops
A estrutura de blocos do Ruby talvez até ajude nesse tipo de otimização
o JIT pode tratar esse bloco como se fosse uma função separada e otimizar iterações de forma natural
Esse ponto deve ser tratado mais a fundo no próximo capítulo