Como a máquina virtual de hip hop (HHVM) aprimora teoricamente o desempenho do tempo de execução do PHP?

9

De alto nível, como o Facebook, et. para melhorar o desempenho do PHP com a máquina virtual Hip Hop?

Como ele difere da execução de código usando o mecanismo zend tradicional? É porque os tipos são definidos opcionalmente com o hack, que permitem técnicas de pré-otimização?

Minha curiosidade surgiu depois de ler este artigo, adoção do HHVM .

chrisjlee
fonte

Respostas:

7

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"

http://hhvm.com/blog/2027/faster-and-cheaper-the-evolution-of-the-hhvm-jit

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! "

Paul W
fonte
1

Resumindo: eles tentam minimizar o acesso aleatório à memória e saltar entre partes do código na memória para reproduzir bem o cache da CPU.

De acordo com o HHVM Performance Status, eles otimizaram os tipos de dados usados ​​com mais freqüência, que são cadeias de caracteres e matrizes, para minimizar o acesso aleatório à memória. A idéia é manter os dados usados ​​juntos (como itens em uma matriz) o mais próximo possível do outro na memória, idealmente de maneira linear. Dessa forma, se os dados se ajustarem ao cache da CPU L2 / L3, eles poderão ser processados ​​em ordem de magnitude mais rapidamente do que se estivessem na RAM.

Outra técnica mencionada é a compilação de caminhos usados ​​com mais freqüência em um código, de forma que a versão compilada seja o mais linear possível (ei tenha a menor quantidade de "saltos") possível e carregue os dados dentro / fora da memória o mais raramente possível.

scriptin
fonte