Wie verbessert die Hip Hop Virtual Machine (HHVM) theoretisch die PHP-Laufzeitleistung?

9

Wie funktioniert Facebook et al. Alle verwenden, um die PHP-Leistung mit der Hip Hop Virtual Machine zu verbessern?

Wie unterscheidet es sich von der Ausführung von Code mit der herkömmlichen Zend-Engine? Liegt es daran, dass Typen optional mit Hack definiert werden, die Voroptimierungstechniken ermöglichen?

Meine Neugier entstand nach dem Lesen dieses Artikels, HHVM Adoption .

Chrisjlee
quelle

Antworten:

7

Sie ersetzten die Tracelets von TranslatorX64 durch die neue HipHop Intermediate Representation (hhir) und eine neue Indirektionsebene, in der sich die Logik zur Erzeugung von hhir befindet, auf die tatsächlich derselbe Name hhir Bezug nimmt.

Auf hoher Ebene werden 6 Anweisungen verwendet, um die zuvor erforderlichen 9 Anweisungen auszuführen, wie hier angegeben: "Es beginnt mit denselben Typprüfungen, aber der Hauptteil der Übersetzung besteht aus 6 Anweisungen, die deutlich besser sind als die 9 von TranslatorX64."

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

Das ist meistens ein Artefakt des Systemdesigns und wir planen, es irgendwann aufzuräumen. Der gesamte in TranslatorX64 verbleibende Code ist eine Maschine, die erforderlich ist, um Code auszugeben und Übersetzungen miteinander zu verknüpfen. Der Code, der verstanden hat, wie einzelne Bytecodes übersetzt werden, ist aus TranslatorX64 verschwunden.

Als hhir TranslatorX64 ersetzte, wurde Code generiert, der ungefähr 5% schneller war und bei manueller Überprüfung deutlich besser aussah. Wir haben sein Produktionsdebüt mit einem weiteren Mini-Lockdown fortgesetzt und zusätzlich 10% an Leistungssteigerungen erzielt. Um einige dieser Verbesserungen in Aktion zu sehen, schauen wir uns eine Funktion addPositive und einen Teil ihrer Übersetzung an.

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

Diese Funktion sieht aus wie viel PHP-Code: Sie durchläuft ein Array und macht mit jedem Element etwas. Konzentrieren wir uns zunächst auf die Zeilen 5 und 6 sowie deren 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)

Diese beiden Zeilen laden ein Element aus einem Array, speichern es in einer lokalen Variablen, vergleichen dann den Wert dieses lokalen Elements mit 0 und springen basierend auf dem Ergebnis bedingt irgendwo hin. Wenn Sie mehr über die Vorgänge im Bytecode erfahren möchten, können Sie die bytecode.specification durchblättern. Die JIT teilt diesen Code sowohl jetzt als auch in den Tagen von TranslatorX64 in zwei Tracelets auf: eines nur mit dem CGetM, das andere mit den restlichen Anweisungen (eine vollständige Erklärung, warum dies geschieht, ist hier nicht relevant, aber es ist hauptsächlich, weil wir zur Kompilierungszeit nicht wissen, wie der Typ des Array-Elements aussehen wird). Die Übersetzung des CGetM läuft auf einen Aufruf einer C ++ - Hilfsfunktion hinaus und ist nicht sehr interessant, daher werden wir uns das zweite Tracelet ansehen. Dieses Commit war der offizielle Rücktritt von 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

Die ersten vier Zeilen sind Typprüfungen, die bestätigen, dass der Wert in $ elem und der Wert oben auf dem Stapel die erwarteten Typen sind. Wenn einer von beiden fehlschlägt, springen wir zu Code, der eine erneute Übersetzung des Tracelets auslöst, und verwenden die neuen Typen, um einen anders spezialisierten Teil des Maschinencodes zu generieren. Das Fleisch der Übersetzung folgt, und der Code bietet viel Raum für Verbesserungen. Es gibt eine Eigenlast in Zeile 8, ein leicht vermeidbares Register zum Registrieren der Bewegung in Zeile 12 und eine Möglichkeit für eine konstante Ausbreitung zwischen den Zeilen 10 und 16. Dies sind alles Konsequenzen des von TranslatorX64 verwendeten Bytecode-zu-Zeit-Ansatzes. Kein seriöser Compiler würde jemals solchen Code ausgeben, aber die einfachen Optimierungen, die erforderlich sind, um ihn zu vermeiden, passen einfach nicht in das TranslatorX64-Modell.

Nun sehen wir dasselbe Tracelet, das mit hhir übersetzt wurde, bei derselben hhvm-Revision:

  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

Es beginnt mit den gleichen Typprüfungen, aber der Hauptteil der Übersetzung besteht aus 6 Anweisungen, deutlich besser als die 9 von TranslatorX64. Beachten Sie, dass es keine Eigenlasten oder Register zum Registrieren von Bewegungen gibt und die unmittelbare 0 aus dem Int 0 -Bytecode in Zeile 12 an den cmp weitergegeben wurde. Hier ist das hhir, das zwischen dem Tracelet und dieser Übersetzung generiert wurde:

  (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

Die Bytecode-Anweisungen wurden in kleinere, einfachere Operationen unterteilt. Viele Operationen, die im Verhalten bestimmter Bytecodes verborgen sind, werden explizit in hhir dargestellt, z. B. der LdStack in Zeile 6, der Teil der SetL ist. Durch die Verwendung unbenannter Provisorien (t1, t2 usw.) anstelle von physischen Registern zur Darstellung des Werteflusses können wir die Definition und Verwendung (en) jedes Werts leicht verfolgen. Dies macht es trivial zu sehen, ob das Ziel einer Last tatsächlich verwendet wird oder ob eine der Eingaben in einen Befehl wirklich ein konstanter Wert von vor 3 Bytecodes ist. Eine ausführlichere Erklärung dessen, was hhir ist und wie es funktioniert, finden Sie unter ir.specification.

Dieses Beispiel zeigt nur einige der Verbesserungen, die hhir gegenüber TranslatorX64 vorgenommen hat. Die Bereitstellung von hhir für die Produktion und die Einstellung von TranslatorX64 im Mai 2013 war ein großer Meilenstein, aber erst der Anfang. Seitdem haben wir viele weitere Optimierungen implementiert, die in TranslatorX64 nahezu unmöglich wären, wodurch hhvm fast doppelt so effizient ist. Es war auch entscheidend für unsere Bemühungen, hhvm auf ARM-Prozessoren zum Laufen zu bringen, indem die Menge an architekturspezifischem Code, die wir neu implementieren müssen, isoliert und reduziert wird. Weitere Informationen finden Sie in einem bevorstehenden Beitrag zu unserem ARM-Port! "

Paul W.
quelle
1

Kurz gesagt: Sie versuchen, den zufälligen Speicherzugriff zu minimieren, und springen zwischen Codeteilen im Speicher, um mit dem CPU-Cache gut zu spielen.

Gemäß dem HHVM-Leistungsstatus haben sie die am häufigsten verwendeten Datentypen (Zeichenfolgen und Arrays) optimiert, um den zufälligen Speicherzugriff zu minimieren. Die Idee ist, Datenstücke, die zusammen verwendet werden (wie Elemente in einem Array), im Speicher so nahe wie möglich beieinander zu halten, idealerweise linear. Auf diese Weise können Daten, die in den CPU L2 / L3-Cache passen, um Größenordnungen schneller verarbeitet werden als im RAM.

Eine andere erwähnte Technik besteht darin, die am häufigsten verwendeten Pfade in einem Code so zu kompilieren, dass die kompilierte Version so linear wie möglich ist (ei hat die geringste Anzahl von "Sprüngen") und Daten so selten wie möglich in den Speicher / aus dem Speicher geladen werden.

scriptin
quelle