Eles substituíram os tracelets do TranslatorX64 pela nova Representação Intermediária do HipHop (hhir) e uma nova camada de indireção na qual reside a lógica para gerar o hhir, que é realmente chamado pelo mesmo nome, hhir.
De alto nível, está usando 6 instruções para executar as 9 instruções necessárias antes, conforme observado aqui: "Começa com as mesmas verificação de tipo, mas o corpo da tradução é de 6 instruções, significativamente melhor que as 9 do TranslatorX64"
Isso é principalmente um artefato de como o sistema é projetado e é algo que planejamos eventualmente limpar. Todo o código deixado no TranslatorX64 é um mecanismo necessário para emitir código e vincular traduções; o código que entendeu como traduzir bytecodes individuais foi retirado do TranslatorX64.
Quando o hhir substituiu o TranslatorX64, ele estava gerando um código aproximadamente 5% mais rápido e com uma aparência significativamente melhor na inspeção manual. Seguimos sua estréia na produção com outro mini bloqueio e obtivemos 10% a mais de ganhos de desempenho. Para ver algumas dessas melhorias em ação, vejamos uma função addPositive e parte de sua tradução.
function addPositive($arr) {
$n = count($arr);
$sum = 0;
for ($i = 0; $i < $n; $i++) {
$elem = $arr[$i];
if ($elem > 0) {
$sum = $sum + $elem;
}
}
return $sum;
}
Essa função parece muito código PHP: faz um loop em uma matriz e faz algo com cada elemento. Vamos nos concentrar nas linhas 5 e 6 por enquanto, junto com o código de bytes:
$elem = $arr[$i];
if ($elem > 0) {
// line 5
85: CGetM <L:0 EL:3>
98: SetL 4
100: PopC
// line 6
101: Int 0
110: CGetL2 4
112: Gt
113: JmpZ 13 (126)
Essas duas linhas carregam um elemento de uma matriz, armazenam-no em uma variável local, comparam o valor desse local com 0 e pulam condicionalmente em algum lugar com base no resultado. Se você estiver interessado em mais detalhes sobre o que está acontecendo no bytecode, poderá percorrer o bytecode.specification. O JIT, agora e de volta aos dias do TranslatorX64, divide esse código em dois tracelets: um com apenas o CGetM e outro com o restante das instruções (uma explicação completa de por que isso acontece não é relevante aqui, mas é principalmente porque não sabemos em tempo de compilação qual será o tipo do elemento da matriz). A tradução do CGetM se resume a uma chamada para uma função auxiliar de C ++ e não é muito interessante; portanto, veremos o segundo tracelet. Esse comprometimento foi a aposentadoria oficial do TranslatorX64,
cmpl $0xa, 0xc(%rbx)
jnz 0x276004b2
cmpl $0xc, -0x44(%rbp)
jnle 0x276004b2
101: SetL 4
103: PopC
movq (%rbx), %rax
movq -0x50(%rbp), %r13
104: Int 0
xor %ecx, %ecx
113: CGetL2 4
mov %rax, %rdx
movl $0xa, -0x44(%rbp)
movq %rax, -0x50(%rbp)
add $0x10, %rbx
cmp %rcx, %rdx
115: Gt
116: JmpZ 13 (129)
jle 0x7608200
As quatro primeiras linhas são checagens verificando se o valor em $ elem e o valor no topo da pilha são os tipos que esperamos. Se um deles falhar, pularemos para o código que aciona uma retranslação do tracelet, usando os novos tipos para gerar um pedaço de código de máquina diferentemente especializado. A descrição da tradução segue e o código tem muito espaço para melhorias. Há uma carga inoperante na linha 8, um registro facilmente evitável para registrar o movimento na linha 12 e uma oportunidade de propagação constante entre as linhas 10 e 16. Todas essas são conseqüências da abordagem de bytecode de cada vez usada pelo TranslatorX64. Nenhum compilador respeitável jamais emitirá código como esse, mas as otimizações simples necessárias para evitá-lo simplesmente não se encaixam no modelo TranslatorX64.
Agora vamos ver o mesmo tracelet traduzido usando hhir, na mesma revisão hhvm:
cmpl $0xa, 0xc(%rbx)
jnz 0x276004bf
cmpl $0xc, -0x44(%rbp)
jnle 0x276004bf
101: SetL 4
movq (%rbx), %rcx
movl $0xa, -0x44(%rbp)
movq %rcx, -0x50(%rbp)
115: Gt
116: JmpZ 13 (129)
add $0x10, %rbx
cmp $0x0, %rcx
jle 0x76081c0
Começa com o mesmo tipo de verificação, mas o corpo da tradução é de 6 instruções, significativamente melhor que o 9 do TranslatorX64. Observe que não há cargas mortas ou registre-se para registrar movimentos, e o 0 imediato do bytecode Int 0 foi propagado até o cmp na linha 12. Aqui está o hhir que foi gerado entre o tracelet e essa tradução:
(00) DefLabel
(02) t1:FramePtr = DefFP
(03) t2:StkPtr = DefSP<6> t1:FramePtr
(05) t3:StkPtr = GuardStk<Int,0> t2:StkPtr
(06) GuardLoc<Uncounted,4> t1:FramePtr
(11) t4:Int = LdStack<Int,0> t3:StkPtr
(13) StLoc<4> t1:FramePtr, t4:Int
(27) t10:StkPtr = SpillStack t3:StkPtr, 1
(35) SyncABIRegs t1:FramePtr, t10:StkPtr
(36) ReqBindJmpLte<129,121> t4:Int, 0
As instruções do bytecode foram divididas em operações menores e mais simples. Muitas operações ocultas no comportamento de certos bytecodes são explicitamente representadas no hhir, como o LdStack na linha 6, que faz parte do SetL. Usando temporários não nomeados (t1, t2, etc ...) em vez de registros físicos para representar o fluxo de valores, podemos rastrear facilmente a definição e o uso de cada valor. Isso torna trivial verificar se o destino de uma carga é realmente usado ou se uma das entradas de uma instrução é realmente um valor constante de 3 bytecodes atrás. Para uma explicação muito mais completa do que é hhir e como ele funciona, dê uma olhada na especificação ir.
Este exemplo mostrou apenas algumas das melhorias que ele fez no TranslatorX64. A implantação do hhir na produção e a desativação do TranslatorX64 em maio de 2013 foi um grande marco a ser alcançado, mas foi apenas o começo. Desde então, implementamos muito mais otimizações que seriam quase impossíveis no TranslatorX64, tornando o hhvm quase duas vezes mais eficiente no processo. Também foi crucial em nossos esforços para executar o hhvm nos processadores ARM, isolando e reduzindo a quantidade de código específico da arquitetura que precisamos reimplementar. Fique atento a uma publicação futura dedicada à nossa porta ARM para obter mais detalhes! "