In che modo la macchina virtuale hip hop (HHVM) migliora teoricamente le prestazioni di runtime di PHP?


9

Da un livello elevato, come funziona Facebook, et. Come utilizzare per migliorare le prestazioni di PHP con la macchina virtuale Hip Hop?

In che cosa differisce dall'esecuzione del codice usando il motore zend tradizionale? È perché i tipi sono facoltativamente definiti con l'hack che consentono tecniche di pre-ottimizzazione?

La mia curiosità è sorta dopo aver letto questo articolo, l'adozione di HHVM .

Risposte:


7

Hanno sostituito i tracelets di TranslatorX64 con il nuovo HipHop Intermediate Representation (hhir) e un nuovo livello indiretto in cui risiede la logica per generare hhir, che in realtà viene chiamato con lo stesso nome, hhir.

Da un livello elevato, sta usando 6 istruzioni per eseguire le istruzioni 9 richieste in precedenza, come indicato qui: "Inizia con gli stessi controlli di battitura ma il corpo della traduzione è di 6 istruzioni, significativamente migliore delle 9 di TranslatorX64"

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

Questo è principalmente un artefatto di come è progettato il sistema ed è qualcosa che abbiamo intenzione di ripulire alla fine. Tutto il codice lasciato in TranslatorX64 è un meccanismo necessario per emettere codice e collegare le traduzioni insieme; il codice che ha capito come tradurre i singoli bytecode è passato da TranslatorX64.

Quando hhir ha sostituito TranslatorX64, stava generando un codice che era circa il 5% più veloce e sembrava notevolmente migliore durante l'ispezione manuale. Abbiamo seguito il suo debutto in produzione con un altro mini-blocco e abbiamo ottenuto un ulteriore 10% di guadagni in termini di prestazioni. Per vedere alcuni di questi miglioramenti in azione, diamo un'occhiata a una funzione addPositive e parte della sua traduzione.

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;
    }

Questa funzione assomiglia a un sacco di codice PHP: scorre su un array e fa qualcosa con ogni elemento. Concentriamoci sulle linee 5 e 6 per ora, insieme al loro bytecode:

    $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)

Queste due righe caricano un elemento da un array, lo memorizzano in una variabile locale, quindi confrontano il valore di quel locale con 0 e saltano condizionatamente da qualche parte in base al risultato. Se sei interessato a maggiori dettagli su cosa sta succedendo nel bytecode, puoi sfogliare bytecode.specification. Il JIT, sia adesso che nei giorni di TranslatorX64, suddivide questo codice in due tracelet: uno con solo il CGetM, poi un altro con il resto delle istruzioni (una spiegazione completa del perché questo non è rilevante qui, ma è principalmente perché non sappiamo al momento della compilazione quale sarà il tipo di elemento array). La traduzione di CGetM si riduce a una chiamata a una funzione di supporto C ++ e non è molto interessante, quindi vedremo il secondo braccialetto. Questo impegno era la pensione ufficiale di 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

Le prime quattro righe sono i typechecks che verificano che il valore in $ elem e il valore in cima allo stack siano i tipi che ci aspettiamo. Se uno dei due fallisce, passeremo al codice che innesca una ritraduzione del braccialetto, usando i nuovi tipi per generare un pezzo di codice macchina diversamente specializzato. Segue la carne della traduzione e il codice ha ampi margini di miglioramento. C'è un carico morto sulla linea 8, un registro facilmente evitabile per registrare lo spostamento sulla linea 12 e un'opportunità per la propagazione costante tra le linee 10 e 16. Queste sono tutte conseguenze dell'approccio bytecode-at-a-time usato da TranslatorX64. Nessun compilatore rispettabile emetterebbe mai codice come questo, ma le semplici ottimizzazioni necessarie per evitarlo semplicemente non si adattano al modello TranslatorX64.

Ora vediamo lo stesso braccialetto tradotto usando hhir, alla stessa revisione 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

Comincia con gli stessi controlli di battitura ma il corpo della traduzione è composto da 6 istruzioni, significativamente migliori rispetto alle 9 di TranslatorX64. Si noti che non ci sono carichi morti o registri per registrare le mosse e lo 0 immediato dal bytecode Int 0 è stato propagato fino al cmp sulla linea 12. Ecco l'hhir che è stato generato tra il braccialetto e quella traduzione:

  (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

Le istruzioni per bytecode sono state suddivise in operazioni più piccole e più semplici. Molte operazioni nascoste nel comportamento di alcuni bytecode sono esplicitamente rappresentate in hhir, come il LdStack sulla linea 6 che fa parte del SetL. Usando provvisori senza nome (t1, t2, ecc ...) anziché registri fisici per rappresentare il flusso di valori, possiamo facilmente tracciare la definizione e gli usi di ciascun valore. Ciò rende banale vedere se la destinazione di un carico viene effettivamente utilizzata o se uno degli input di un'istruzione è realmente un valore costante di 3 bytecode fa. Per una spiegazione molto più approfondita di cos'è hhir e di come funziona, dai un'occhiata a ir.specification.

Questo esempio ha mostrato solo alcuni dei miglioramenti apportati su TranslatorX64. La distribuzione di hhir nella produzione e il ritiro di TranslatorX64 a maggio 2013 è stata una pietra miliare da raggiungere, ma era solo l'inizio. Da allora, abbiamo implementato molte altre ottimizzazioni che sarebbero quasi impossibili in TranslatorX64, rendendo hhvm quasi due volte più efficiente nel processo. È stato anche cruciale nei nostri sforzi per far funzionare hhvm su processori ARM isolando e riducendo la quantità di codice specifico dell'architettura che dobbiamo reimplementare. Guarda un prossimo post dedicato alla nostra porta ARM per maggiori dettagli! "


1

In breve: cercano di ridurre al minimo l'accesso casuale alla memoria e i salti tra pezzi di codice in memoria per giocare bene con la cache della CPU.

Secondo HHVM Performance Status hanno ottimizzato i tipi di dati utilizzati più frequentemente, che sono stringhe e array, per ridurre al minimo l'accesso casuale alla memoria. L'idea è di tenere i pezzi di dati usati insieme (come gli elementi in un array) il più vicino possibile in memoria, idealmente in modo lineare. In questo modo, se i dati si inseriscono nella cache L2 / L3 della CPU, possono essere elaborati ordini di grandezza più velocemente rispetto a quelli che si trovavano nella RAM.

Un'altra tecnica menzionata è la compilazione di percorsi utilizzati più frequentemente in un codice in modo tale che la versione compilata sia il più lineare possibile (e abbia il minor numero di "salti") e carichi i dati nella memoria interna / esterna il più raramente possibile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.