Comprendre JIT en PHP 8

La traduction de l'article a été préparée à la veille du début du cours "Backend-developer in PHP"








TL; DR



Le compilateur Just In Time en PHP 8 est implémenté dans le cadre de l'extension Opcache et est conçu pour compiler le code d'exploitation dans les instructions du processeur au moment de l'exécution.



Cela signifie qu'avec JIT, certains codes de fonctionnement n'ont pas à être interprétés par Zend VM, ces instructions seront exécutées directement comme des instructions au niveau du processeur.



JIT en PHP 8



L'une des fonctionnalités les plus commentées de PHP 8 est le compilateur Just In Time (JIT). Il est entendu dans de nombreux blogs et communautés - il y a beaucoup de buzz autour de lui, mais jusqu'à présent, je n'ai pas trouvé beaucoup de détails sur le fonctionnement de JIT en détail.



Après de nombreuses tentatives et frustrations pour trouver des informations utiles, j'ai décidé d'étudier le code source PHP. En combinant ma petite connaissance de C avec toutes les informations éparses que j'ai pu rassembler jusqu'à présent, j'ai réussi à préparer cet article et j'espère qu'il vous aidera à mieux comprendre PHP JIT.



Pour simplifier les choses: lorsque JIT fonctionne comme prévu, votre code ne sera pas exécuté via la machine virtuelle Zend, mais il s'exécutera directement comme un ensemble d'instructions au niveau du processeur.



C'est toute l'idée.



Mais pour mieux comprendre cela, nous devons réfléchir à la façon dont php fonctionne en interne. Ce n'est pas très difficile, mais il faut une introduction.



J'ai déjà écrit un article avec un aperçu rapide du fonctionnement de php . Si vous pensez que cet article devient trop compliqué, lisez simplement son prédécesseur et revenez. Cela devrait rendre les choses un peu plus faciles.



Comment le code PHP est-il exécuté?



Nous savons tous que php est un langage interprété. Mais qu'est-ce que cela signifie réellement?



Chaque fois que vous voulez exécuter du code PHP, que ce soit un extrait de code ou une application Web entière, vous devez passer par l'interpréteur php. Les plus couramment utilisés sont PHP FPM et l'interpréteur CLI. Leur travail est très simple: obtenir le code php, l'interpréter et renvoyer le résultat.



Ceci est une image commune pour chaque langue interprétée. Certaines étapes peuvent varier, mais l'idée générale est la même. En PHP, cela fonctionne comme ceci:



  1. Le code PHP est lu et converti en un ensemble de mots-clés appelés jetons. Ce processus permet à l'interpréteur de comprendre dans quelle partie du programme chaque morceau de code est écrit. Cette première étape s'appelle Lexing ou Tokenizing .
  2. , PHP . (Abstract Syntax Tree — AST) , (parsing). AST , , . , «echo 1 + 1» « 1 + 1» , , « , — 1 + 1».
  3. AST, , . -, , (Intermediate Representation IR), PHP (Opcode). AST .
  4. Maintenant que nous avons les opcodes, vient le plus intéressant: l'implémentation du code! PHP a un moteur appelé Zend VM qui est capable d'obtenir une liste d'opcodes et de les exécuter. Une fois que tous les opcodes ont été exécutés, le programme se termine.




Pour rendre les choses un peu plus claires, j'ai fait un diagramme:





Un diagramme simplifié du processus d'interprétation PHP.



Assez simple comme vous pouvez le voir. Mais il y a aussi un goulot d'étranglement ici: à quoi sert le lexing et l'analyse de votre code chaque fois que vous l'exécutez si votre code php ne change même pas souvent?



Après tout, nous ne sommes intéressés que par les opcodes, non? Droite! C'est pourquoi l' extension Opcache existe .



Extension Opcache



L'extension Opcache est fournie avec PHP et il n'y a généralement aucune raison particulière de la désactiver. Si vous utilisez PHP, vous devriez probablement activer Opcache.



Ce qu'il fait, c'est ajouter une couche de cache d'opcode partagée en ligne. Son travail est de récupérer les opcodes récemment générés à partir de notre AST et de les mettre en cache afin que les exécutions ultérieures puissent facilement ignorer les phases de lexing et d'analyse.



Voici un schéma du même processus avec l'extension Opcache en tête:





Flux d'interprétation PHP avec Opcache. Si le fichier a déjà été analysé, php extrait l'opcode mis en cache pour lui, plutôt que de le réanalyser.



C'est tout simplement fascinant de voir à quel point les étapes de lexing, d'analyse et de compilation sont ignorées.

Remarque : c'est là que la fonction de préchargement de PHP 7.4 est utile ! Cela vous permet de dire à PHP FPM d'analyser votre base de code, de la convertir en opcodes et de les mettre en cache avant même de faire quoi que ce soit.


Vous pouvez commencer à vous demander où vous pouvez coller JIT ici, non?! Du moins je l'espère, c'est pourquoi j'écris cet article ...



Que fait le compilateur Just In Time?



Après avoir écouté l'explication de Ziv dans un épisode de podcasts PHP et JIT de PHP Internals News , j'ai pu avoir une idée de ce que le JIT est réellement censé faire ...



Si Opcache permet une récupération plus rapide de l'opcode afin qu'il puisse accéder directement à la VM Zend, JIT destiné à le faire fonctionner sans Zend VM du tout.



Zend VM est un programme C qui agit comme une couche entre le code d'exploitation et le processeur lui-même. Le JIT génère le code compilé au moment de l'exécution, afin que php puisse ignorer la VM Zend et passer directement au processeur . En théorie, nous devrions en bénéficier en termes de performances.



Cela semblait étrange au début, car pour compiler du code machine, vous devez écrire une implémentation très spécifique pour chaque type d'architecture. Mais en fait, c'est bien réel.



L'implémentation JIT en PHP utilise la bibliothèque DynASM (Dynamic Assembler) , qui mappe un ensemble d'instructions CPU dans un format spécifique au code d'assemblage pour de nombreux types différents de CPU. Ainsi, le compilateur Just In Time convertit le code d'exploitation en code machine spécifique à l'architecture à l'aide de DynASM.



Bien qu'une pensée me hante encore ...



Si le préchargement est capable d'analyser le code php pour le rendre opérationnel avant l'exécution, et que DynASM peut compiler le code opérationnel en code machine (compilation Just In Time), pourquoi diable ne compilons-nous pas PHP correctement en place en utilisant la compilation Ahead of Time?!



L'une des pensées que j'ai tirées de l'épisode de podcast était que PHP est faiblement typé, ce qui signifie que PHP ne sait souvent pas de quel type est une variable jusqu'à ce que Zend VM essaie d'exécuter un opcode spécifique.



Vous pouvez comprendre cela en regardant le type d'union zend_value , qui a de nombreux pointeurs vers différentes représentations de type pour une variable. Chaque fois que Zend VM tente d'extraire une valeur de zend_value, il utilise des macros telles que ZSTR_VALqui essaient d'accéder au pointeur de chaîne à partir de la concaténation de valeurs.



Par exemple, ce gestionnaire Zend VM doit gérer l'expression inférieure ou égale à (<=). Voyez comment il se branche dans de nombreux chemins de code différents pour deviner les types d'opérandes.



La duplication de cette logique d'inférence de type avec du code machine n'est pas faisable et pourrait potentiellement rendre les choses encore plus lentes.



La compilation finale une fois que les types ont été évalués n'est pas non plus une bonne option car la compilation en code machine est une tâche gourmande en ressources processeur. Donc, compiler TOUT au moment de l'exécution est une mauvaise idée.



Comment se comporte le compilateur Just In Time?



Nous savons maintenant que nous ne pouvons pas déduire de types pour générer une pré-compilation suffisamment bonne. Nous savons également que la compilation au moment de l'exécution est coûteuse. Comment JIT peut-il être utile pour PHP?



Pour équilibrer cette équation, PHP JIT essaie de ne compiler que quelques opcodes qui, à son avis, en valent la peine. Pour ce faire, il profile les opcodes exécutés par la machine virtuelle Zend et vérifie lesquels ont du sens à compiler. (selon votre configuration) .



Lorsqu'un opcode particulier est compilé, il délègue alors l'exécution à ce code compilé au lieu de déléguer à la VM Zend. Il ressemble au diagramme ci-dessous:





Flux d'interprétation PHP avec JIT. S'ils sont déjà compilés, les opcodes ne sont pas exécutés via la VM Zend.



Ainsi, il y a quelques instructions dans l'extension Opcache qui déterminent si certains codes d'exploitation doivent être compilés ou non. Si tel est le cas, le compilateur le convertit en code machine à l'aide de DynASM et exécute ce code machine nouvellement généré.



Fait intéressant, puisque l'implémentation actuelle a une limite de mégaoctets pour le code compilé (également configurable), l'exécution de code devrait pouvoir basculer de manière transparente entre JIT et le code interprété.



Au fait, cette conférence de Benoit Jacquemont sur JIT de php m'a TRÈS aidé à comprendre cela.



Je ne sais toujours pas dans quels cas spécifiques la compilation a lieu, mais je pense que je ne veux pas vraiment le savoir encore.



Votre gain de productivité ne sera donc probablement pas colossal



J'espère qu'il est beaucoup plus clair maintenant POURQUOI tout le monde dit que la plupart des applications PHP n'obtiendront pas beaucoup d'avantages en termes de performances en utilisant le compilateur Just In Time. Et pourquoi la recommandation de Ziv pour le profilage et l'expérimentation de différentes configurations JIT pour votre application est la meilleure solution.



Les opcodes compilés seront généralement répartis sur plusieurs requêtes si vous utilisez PHP FPM, mais cela ne change toujours pas la donne.



Cela est dû au fait que JIT optimise les opérations du processeur et que de nos jours, la plupart des applications php sont plus liées aux E / S qu'autre chose. Peu importe si les opérations de traitement sont compilées si vous devez quand même accéder au disque ou au réseau. Les horaires seront très similaires.



Si seulement...



Vous faites quelque chose de non-E / S, comme le traitement d'image ou l'apprentissage automatique. Tout autre chose que les E / S bénéficiera du compilateur Just In Time. C'est aussi la raison pour laquelle les gens disent maintenant qu'ils penchent davantage vers l'écriture de fonctions PHP natives écrites en PHP plutôt que C. La surcharge ne sera pas radicalement différente si de telles fonctions sont compilées de toute façon.



Un moment intéressant en tant que programmeur PHP ...



J'espère que cet article vous a été utile et que vous avez mieux compris ce qu'est JIT en PHP 8. N'hésitez pas à me tweeter si vous voulez ajouter quelque chose que j'aurais oublié ici, et n'oubliez pas de partager cela avec vos collègues développeurs, cela ajoutera sûrement un peu de valeur à vos conversations!-- @nawarian






PHP:







All Articles