Comment la machine virtuelle Hip Hop (HHVM) améliore-t-elle théoriquement les performances d'exécution PHP?

9

D'un niveau élevé, comment Facebook, et. utiliser pour améliorer les performances PHP avec la machine virtuelle Hip Hop?

En quoi diffère-t-il de l'exécution de code à l'aide du moteur zend traditionnel? Est-ce parce que les types sont éventuellement définis avec hack qui permettent des techniques de pré-optimisation?

Ma curiosité est née après avoir lu cet article, l' adoption de HHVM .

chrisjlee
la source

Réponses:

7

Ils ont remplacé les tracelets de TranslatorX64 par le nouveau HipHop Intermediate Representation (hhir) et une nouvelle couche d'indirection dans laquelle réside la logique de génération de hhir, qui est en fait appelée du même nom, hhir.

D'un niveau élevé, il utilise 6 instructions pour effectuer les 9 instructions requises auparavant, comme indiqué ici: "Il commence par les mêmes vérifications de type mais le corps de la traduction est de 6 instructions, nettement mieux que les 9 de TranslatorX64"

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

C'est principalement un artefact de la façon dont le système est conçu et c'est quelque chose que nous prévoyons de nettoyer éventuellement. Tout le code laissé dans TranslatorX64 est une machine nécessaire pour émettre du code et lier les traductions ensemble; le code qui comprenait comment traduire des bytecodes individuels a disparu de TranslatorX64.

Quand il a remplacé TranslatorX64, il générait un code environ 5% plus rapide et avait l'air beaucoup mieux lors d'une inspection manuelle. Nous avons suivi ses débuts en production avec un autre mini-verrouillage et avons obtenu 10% supplémentaires de gains de performances en plus de cela. Pour voir certaines de ces améliorations en action, regardons une fonction addPositive et une partie de sa traduction.

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

Cette fonction ressemble à beaucoup de code PHP: elle boucle sur un tableau et fait quelque chose avec chaque élément. Concentrons-nous sur les lignes 5 et 6 pour l'instant, avec leur 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)

Ces deux lignes chargent un élément à partir d'un tableau, le stockent dans une variable locale, puis comparent la valeur de ce local à 0 et sautent conditionnellement quelque part en fonction du résultat. Si vous souhaitez en savoir plus sur ce qui se passe dans le bytecode, vous pouvez parcourir bytecode.specification. Le JIT, à la fois maintenant et dans les jours TranslatorX64, décompose ce code en deux tracelets: un avec juste le CGetM, puis un autre avec le reste des instructions (une explication complète de la raison pour laquelle cela se produit n'est pas pertinente ici, mais c'est principalement parce que nous ne savons pas au moment de la compilation quel sera le type de l'élément de tableau). La traduction du CGetM se résume à un appel à une fonction d'assistance C ++ et n'est pas très intéressante, nous allons donc examiner le deuxième bracelet. Cet engagement était la retraite officielle de 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

Les quatre premières lignes sont des vérifications de type vérifiant que la valeur dans $ elem et la valeur en haut de la pile sont les types que nous attendons. Si l'un d'eux échoue, nous passerons au code qui déclenche une retraduction du bracelet, en utilisant les nouveaux types pour générer un morceau de code machine différemment spécialisé. La chair de la traduction suit, et le code a beaucoup de place pour l'amélioration. Il y a une charge morte sur la ligne 8, un registre facilement évitable pour enregistrer le déplacement sur la ligne 12 et une possibilité de propagation constante entre les lignes 10 et 16. Ce sont toutes les conséquences de l'approche du bytecode-at-a-time utilisée par TranslatorX64. Aucun compilateur respectable n'émettrait jamais de code comme celui-ci, mais les optimisations simples nécessaires pour l'éviter ne correspondent tout simplement pas au modèle TranslatorX64.

Voyons maintenant le même bracelet traduit en utilisant hhir, à la même révision 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

Il commence par les mêmes vérifications de type mais le corps de la traduction est de 6 instructions, nettement mieux que les 9 de TranslatorX64. Notez qu'il n'y a pas de charges mortes ni de registre pour enregistrer les mouvements, et le 0 immédiat du bytecode Int 0 a été propagé vers le cmp sur la ligne 12. Voici le hhir qui a été généré entre le bracelet et cette traduction:

  (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

Les instructions de bytecode ont été divisées en opérations plus petites et plus simples. De nombreuses opérations cachées dans le comportement de certains bytecodes sont explicitement représentées dans hhir, comme le LdStack sur la ligne 6 qui fait partie de SetL. En utilisant des temporaires non nommés (t1, t2, etc…) au lieu de registres physiques pour représenter le flux de valeurs, nous pouvons facilement suivre la définition et l'utilisation (s) de chaque valeur. Cela rend trivial de voir si la destination d'une charge est réellement utilisée, ou si l'une des entrées d'une instruction est vraiment une valeur constante d'il y a 3 bytecodes. Pour une explication beaucoup plus approfondie de ce qu'est son hhir et de son fonctionnement, consultez ir.specification.

Cet exemple ne montre que quelques-unes des améliorations apportées à TranslatorX64. Son déploiement en production et son retrait de TranslatorX64 en mai 2013 ont été une étape importante à franchir, mais ce n'était que le début. Depuis lors, nous avons implémenté de nombreuses autres optimisations qui seraient presque impossibles dans TranslatorX64, rendant hhvm presque deux fois plus efficace dans le processus. Il a également été crucial dans nos efforts pour faire fonctionner hhvm sur les processeurs ARM en isolant et en réduisant la quantité de code spécifique à l'architecture que nous devons réimplémenter. Surveillez un prochain article consacré à notre port ARM pour plus de détails! "

Paul W
la source
1

En bref: ils essaient de minimiser l'accès aléatoire à la mémoire et sautent entre les morceaux de code en mémoire pour bien jouer avec le cache du processeur.

Selon HHVM Performance Status, ils ont optimisé les types de données les plus fréquemment utilisés, qui sont des chaînes et des tableaux, pour minimiser l'accès aléatoire à la mémoire. L'idée est de garder les éléments de données utilisés ensemble (comme les éléments d'un tableau) aussi près les uns des autres que possible en mémoire, idéalement de manière linéaire. De cette façon, si les données s'intègrent dans le cache CPU L2 / L3, elles peuvent être traitées des ordres de grandeur plus rapidement que si elles étaient en RAM.

Une autre technique mentionnée consiste à compiler les chemins les plus fréquemment utilisés dans un code de telle manière que la version compilée soit aussi linéaire (ei a le moins de "sauts") que possible et charge les données en / hors de la mémoire aussi rarement que possible.

scriptin
la source