La chose la plus intéressante en PHP 8

PHP 7.4 vient d'être déclaré stable, et nous avons déjà soumis encore plus d'améliorations. Et le meilleur de tout, ce que PHP attend peut dire à Dmitry Stogov - l'un des principaux développeurs de PHP Open Source et, probablement, le plus ancien contributeur actif.

Tous les rapports de Dmitry portent uniquement sur les technologies et les solutions sur lesquelles il travaille personnellement. Dans les meilleures traditions d'Ontiko, sous la coupe, une version texte de l' histoire des innovations PHP 8 les plus intéressantes du point de vue de Dmitry, qui peut ouvrir de nouveaux cas d'utilisation. Tout d'abord, JIT et FFI - pas dans la clé de «perspectives incroyables», mais avec des détails de mise en œuvre et des pièges.


Pour référence: Dmitry Stogov s'est familiarisé avec la programmation en 1984, lorsque tous les lecteurs ne sont pas nés, et a réussi à apporter une contribution significative au développement d'outils de développement, et de PHP en particulier (bien que Dmitry améliore les performances PHP non spécifiquement pour les développeurs russes, ont-ils exprimé mes remerciements sous la forme du prix HighLoad ++). Dmitry est l'auteur de Turck MMCache for PHP (eAccelerator), Zend OPcache mainteneur, leader du projet PHPNG, qui a formé la base de PHP 7, et leader dans le développement de JIT pour PHP.

Développement des performances PHP


J'ai commencé à travailler sur les performances PHP il y a 15 ans lorsque j'ai rejoint Zend. Ensuite, nous avons publié la version 5.0 - la première dans laquelle le langage est devenu vraiment orienté objet. Depuis lors, nous avons pu améliorer les performances sur les tests synthétiques de 40 fois et sur les applications réelles de 6 fois.



Pendant ce temps, il y a eu deux moments décisifs:

  • Version 5.1, dans laquelle nous avons pu augmenter considérablement la vitesse d'interprétation. Nous avons implémenté un interprète spécialisé, ce qui a principalement affecté les tests de synthèse.
  • Version 7.0, dans laquelle toutes les structures de données clés ont été traitées et ont ainsi optimisé le travail avec la mémoire et le cache du processeur (en savoir plus sur ces optimisations ici ). Cela a conduit à une accélération plus que double à la fois dans les tests synthétiques et dans les applications réelles.

Toutes les autres versions ont progressivement augmenté la productivité en mettant en œuvre de nombreuses idées moins efficaces. Dans la version 7.1, par exemple, une grande attention a été accordée à l'optimisation du bytecode ( un article sur ces solutions).

Le diagramme montre qu'à la fin du développement de la 5e version et à la fin du cycle de développement de la 7e version, nous atteignons un plateau et ralentissons. Ainsi, au cours de la dernière année de travail sur la version 7.4, seule une augmentation de 2% de la productivité a été atteinte. Et ce n'est pas mal, car de nouvelles fonctionnalités telles que les propriétés typées et les types covariants sont apparues qui ralentissent PHP (Nikita Popov a parlé de ces nouveaux produits en PHP Russie).

Et maintenant tout le monde se demande à quoi s'attendre de la 8e version, peut-elle répéter le succès de la v7?

Pour JIT ou pas pour JIT


Les idées pour améliorer l'interprète ne sont pas encore épuisées, mais toutes nécessitent une étude très approfondie. Beaucoup d'entre eux doivent être rejetés au stade de la preuve de concept, car le gain qui peut être obtenu s'avère incommensurable avec la complication ou les limitations techniques imposées.

Mais il reste de l'espoir pour une nouvelle technologie révolutionnaire - bien sûr, je me souviens du JIT et de la réussite des moteurs JavaScript.

En fait, les travaux sur JIT pour PHP sont en cours depuis 2012. Il y avait 3 ou 4 implémentations, nous avons travaillé avec des collègues Intel, des pirates JavaScript, mais d'une manière ou d'une autre, il n'était pas possible d'inclure JIT dans la branche principale. Au final, en PHP 8, nous avons inclus JIT dans le compilateur et vu une double accélération, mais uniquement sur des tests synthétiques, mais sur des applications réelles, au contraire, un ralentissement.



Bien sûr, ce n'est pas ce que nous recherchons.

Quelle est la question? Peut-être que nous faisons quelque chose de mal, peut-être que WordPress est si mauvais, et aucun JIT ne l'aidera (oui, en fait, c'est le cas). Peut-être que nous avons déjà rendu l'interprète trop bon, mais en JavaScript c'est pire. Dans les tests de calcul, c'est vrai: l'interpréteur PHP est l'un des meilleurs .



Au test de Mandelbrot, il dépasse même des joyaux comme LuaJIT, un interprète écrit en langage assembleur. Dans ce test, nous ne sommes que 4 fois derrière le compilateur d'optimisation GCC-5.3. Avec JIT, nous avons pu obtenir de meilleurs résultats dans le test de Mandelbrot. En fait, nous le faisons déjà, c'est-à-dire que nous sommes capables de générer du code qui rivalise avec le compilateur C.

Pourquoi alors ne pouvons-nous pas accélérer les applications réelles? Pour comprendre, je vais vous dire comment nous faisons JIT. Commençons par les bases.

Comment PHP fonctionne



Le serveur accepte la demande, la compile en bytecode, qui à son tour est envoyé à la machine virtuelle pour exécution. En exécutant le bytecode, la machine virtuelle peut également appeler d'autres fichiers PHP, qui sont à nouveau recompilés en bytecode et exécutés à nouveau.

Une fois la requête terminée, toutes les informations qui s'y rapportent, y compris le code d'octet, sont supprimées de la mémoire. Autrement dit, chaque script PHP doit être compilé à nouveau sur chaque demande. Bien sûr, il est tout simplement impossible d'intégrer la compilation JIT dans un tel schéma, car le compilateur doit être très rapide.

Mais très probablement, personne n'utilise PHP dans sa forme nue, tout le monde l'utilise avec OPcache.

PHP + OPcache



L'objectif principal d'OPcache est de se débarrasser des scripts de recompilation à chaque demande. Il est intégré dans un point spécialement conçu pour lui, intercepte toutes les demandes de compilation et met en cache le bytecode compilé dans la mémoire partagée.

En même temps, non seulement le temps de compilation est enregistré, mais également la mémoire, car la mémoire de bytecode précédente était allouée dans l'espace d'adressage de chaque processus, et maintenant elle existe en une seule copie.

Vous pouvez déjà intégrer JIT dans ce circuit, ce que nous ferons. Mais d'abord, je vais vous montrer comment fonctionne l'interprète.



Un interprète est d'abord une boucle qui appelle son propre gestionnaire pour chaque instruction.

Nous utilisons deux registres:

  • execute_data - pointeur vers la trame d'activation actuelle;
  • opline - pointeur vers l'instruction virtuelle exécutable actuelle.

En utilisant l'extension gcc, ces deux types de registres sont mappés sur des registres matériels réels et, de ce fait, ils fonctionnent très rapidement.

Dans la boucle, nous appelons simplement le gestionnaire pour chaque instruction, après quoi, à la fin de chaque gestionnaire, nous déplaçons le pointeur sur l'instruction suivante.

Il est important de noter que l'adresse du gestionnaire est écrite directement dans le bytecode. Il peut y avoir plusieurs gestionnaires différents pour une même instruction. Cela a été à l'origine inventé pour la spécialisation afin que les gestionnaires puissent se spécialiser dans les types d'opérandes. La même technologie est utilisée pour JIT, car si vous écrivez l'adresse dans le nouveau code généré en tant que gestionnaire, les gestionnaires JIT seront lancés sans aucune modification dans l'interpréteur.

Dans l'exemple ci-dessus, le gestionnaire écrit pour l'instruction d'ajout est présenté à droite. Il prend des opérandes (ici le premier et le second peuvent être une variable constante, temporaire ou locale), lit les opérandes, vérifie les types, produit une logique directe - addition - puis revient à la boucle, qui transfère le contrôle au gestionnaire suivant.

Des fonctions spécialisées sont générées à partir de cette description. Puisqu'il y avait trois premiers opérandes possibles, trois seconds possibles, nous obtenons 9 fonctions différentes.



Dans ces fonctions, au lieu de méthodes universelles d'obtention d'opérandes, on utilise des méthodes spécifiques qui n'effectuent aucune vérification.

Machine virtuelle hybride


Une autre complication que nous avons faite dans la version 7.2 est la machine virtuelle dite hybride.

Si auparavant, nous appelions toujours le gestionnaire à l'aide d'un appel indirect directement dans la boucle d'interpréteur, maintenant pour chaque gestionnaire, nous avons également entré une étiquette dans le corps de la boucle, que nous sautons pour utiliser le saut indirect et où nous appelons le gestionnaire lui-même, mais directement.



Il semblait qu'auparavant, ils avaient passé un appel indirect, maintenant deux: une transition indirecte et un appel direct, et un tel système devrait fonctionner plus lentement. Mais en fait, cela fonctionne plus rapidement, car nous aidons le processeur à prévoir les transitions. Auparavant, il y avait un point à partir duquel la transition vers différents endroits était effectuée. Le processeur se trompait souvent, car il ne pouvait tout simplement pas se rappeler qu'il fallait d'abord sauter sur une instruction, puis sur une autre. Maintenant, après chaque appel direct, il y a une transition indirecte vers l'étiquette suivante. Par conséquent, lorsque la boucle PHP est exécutée, les instructions PHP virtuelles sont organisées en séquences stables, qui sont ensuite exécutées presque linéairement.

La machine virtuelle hybride a permis d'augmenter la productivité de 5 à 10% supplémentaires.

PHP + OPcache + JIT


JIT est implémenté dans le cadre d'OPcache.



Une fois le bytecode compilé et optimisé, un compilateur JIT est lancé pour lui, qui ne fonctionne plus avec le code source. A partir du bytecode PHP, le compilateur JIT génère du code natif, après quoi l'adresse de la première instruction (en fait la fonction) est modifiée dans le bytecode.

Après cela, le code natif déjà généré commence à être appelé à partir de l'interpréteur existant sans aucune modification. Je vais vous montrer un exemple simple.



A gauche, une certaine fonction est écrite en PHP qui compte la somme des nombres de 0 à 100. A droite, le bytecode généré. La première instruction assigne 0 à la somme, la seconde fait de même pour i, puis un saut inconditionnel vers l'étiquette. Sur l'étiquette L1, la condition de sortie du cycle est vérifiée: si elle est remplie, alors sortir, sinon, passer au cycle. Ensuite, ajoutez à la somme i, écrivez le résultat dans le montant, augmentez i de 1.

Directement à partir d'ici, nous générons du code assembleur, ce qui s'avère plutôt bien.



La première instruction QM_ASSIGN compilée en seulement deux instructions machine (2-3 lignes). Le registre %esi contient un pointeur sur la trame d'activation actuelle. Au décalage 30 se trouve un montant variable. La première instruction écrit la valeur 0, la seconde écrit 4 - il s'agit d'un identifiant de type entier ( IS_LONG ). Pour la variable i compilateur s'est rendu compte qu'elle est toujours longue, et pour cela il n'est pas nécessaire de stocker le type. De plus, il peut être stocké dans un registre machine. Par conséquent, ici simplement XOR du registre avec lui-même est l'instruction de réinitialisation la plus simple et la moins chère.

Ensuite, de la même manière, une transition inconditionnelle, on vérifie si un événement externe s'est produit, on vérifie l'état du cycle, on entre dans le cycle. La boucle vérifie si la somme est entière: si oui, alors lisez la valeur entière, ajoutez-y la valeur i, vérifiez si un dépassement s'est produit, réécrivez le résultat dans la somme et ajoutez 1 à %edx .

On peut voir que le code est presque optimal. Il serait possible de l'optimiser encore plus, en se débarrassant de la vérification de la somme du type à chaque itération de la boucle. Mais c'est déjà une optimisation assez compliquée, nous ne le faisons pas encore. Nous développons JIT comme une technologie assez simple , nous n'essayons pas de faire ce que Java HotSpot essaie de faire, V8 - nous avons moins de puissance.

Quel est le problème avec jit


Pourquoi, avec un si bon code assembleur, ne pouvons-nous pas accélérer de vraies applications?

En fait, devraient-ils?

  • Si le goulot d'étranglement n'est pas dans le CPU, alors JIT n'aidera pas.
  • Trop de code est généré (gonflement du code).
  • L'inférence de type statique ne fonctionne pas toujours.
  • Code honnête (pour les cas qui ne sont jamais exécutés).
  • Prise en charge de l'état cohérent de la machine virtuelle (et soudain une exception).
  • Les cours ne vivent que pour une seule demande.

Si l'application attend 80% du temps pour une réponse de la base de données, JIT n'aidera pas. Si nous appelons des fonctions externes gourmandes en ressources, par exemple, en les associant à une expression régulière, JIT appellera également les mêmes fonctions de la même manière. De plus, si une application construit de grandes structures de données - arbres, graphiques, puis les lit, puis en utilisant JIT nous générons du code qui lira en moins d'instructions, mais pour charger les données elles-mêmes, cela prendra le même temps, mais vous devrez également charger le code.

Comme vous l'avez déjà vu, JIT peut même ralentir une application réelle, car elle génère beaucoup de code et sa lecture devient un problème - lors de la lecture de grandes quantités de code, d'autres données sont forcées de sortir du cache, ce qui conduit à un ralentissement.

Plans modestes pour PHP 8


L'une des améliorations que nous voulons réaliser en PHP 8 est de générer moins de code . Maintenant, comme je l'ai dit, nous générons du code natif pour l'ensemble du script, que nous chargeons à l'étape de chargement. Mais la moitié des fonctions ne seront certainement pas appelées. Nous sommes donc allés un peu plus loin et avons introduit un déclencheur qui nous permet de configurer quand nous voulons exécuter JIT. Il peut être exécuté:

  • pour toutes les fonctions;
  • uniquement pour les fonctions lors de leur premier appel;
  • vous pouvez accrocher un compteur sur chaque fonction et compiler uniquement les fonctions qui sont vraiment chaudes.

Un tel schéma peut fonctionner un peu mieux, mais toujours pas optimal, car à nouveau dans chaque fonction il y a des chemins qui sont exécutés et des chemins qui ne sont jamais exécutés. Étant donné que PHP est un langage de programmation dynamique, c'est-à-dire que chaque variable peut avoir différents types, il s'avère que vous devez prendre en charge tous les types prédits par l'analyseur statique. Et il le fait souvent avec prudence lorsqu'il n'a pas été en mesure de prouver que l'autre type ne pouvait pas le faire.

Dans ces conditions, nous allons nous éloigner de la compilation honnête et commencer à le faire de manière spéculative.



À l'avenir, nous prévoyons d'abord d'analyser les fonctions les plus «chaudes» pendant un certain temps au cours du travail de l'application, d'examiner les chemins du programme, de quels types de variables il s'agit, peut-être même de se souvenir des conditions aux limites, et ensuite de générer le code de fonction optimal pour le courant manière d'exécution - uniquement pour les sections qui sont réellement exécutées.

Pour tout le reste, nous mettrons des talons. Il y aura tout de même des vérifications et des sorties possibles où le processus de désoptimisation commencera, c'est-à-dire que nous restaurerons l'état de la machine virtuelle nécessaire à l'interprétation et le remettrons à l'interpréteur pour exécution.

Un schéma similaire est utilisé à la fois dans HotSpot Java VM et V8. Mais l'adaptation de la technologie à PHP présente un certain nombre de difficultés. Tout d'abord, c'est que nous avons partagé le bytecode et le code natif partagé utilisé à partir de différents processus. Nous ne pouvons pas les changer directement dans la mémoire partagée, nous devons d'abord copier quelque part, changer, puis valider de nouveau dans la mémoire partagée.

Préchargement. Le problème de la liaison de classe


En fait, de nombreuses idées d'améliorations PHP qui ont longtemps été incluses dans PHP 7 et même PHP 5 proviennent de travaux liés à JIT. Aujourd'hui, je vais parler d'une autre de ces technologies - c'est le préchargement. Cette technologie est déjà incluse dans PHP 7.4 et permet de spécifier un ensemble de fichiers, de les charger au démarrage du serveur et de rendre permanentes toutes les fonctions de ces fichiers.

L'un des problèmes que la technologie de préchargement résout est le problème de la liaison de classe. Le fait est que lorsque nous compilons simplement des fichiers en PHP, chaque fichier est compilé séparément des autres. Cela se fait car chacun d'eux peut être modifié séparément. Vous ne pouvez pas associer une classe d'un script à une classe d'un autre script, car à la demande suivante, l'une d'entre elles peut changer et quelque chose ne va pas. De plus, dans plusieurs fichiers, il peut y avoir une classe du même nom, et avec une demande, l'un d'eux est utilisé comme parent, et avec l'autre, une autre classe d'un autre fichier est utilisée (avec le même nom, mais complètement différent). Il s'avère que lors de la génération de code qui sera exécuté sur plusieurs requêtes, vous ne pouvez pas faire référence à des classes ou des méthodes, car elles sont recréées à chaque fois (la durée de vie du code dépasse la durée de vie de la classe).

Le préchargement vous permet de lier les classes initialement et, en conséquence, de générer le code de manière plus optimale. Au minimum, pour les frameworks qui seront chargés à l'aide du préchargement.

Cette technologie aide non seulement pour la liaison de classe. Quelque chose de similaire est implémenté en Java en tant que partage de données de classe. Là, cette technologie est principalement destinée à accélérer le lancement d'applications et à réduire la quantité totale de mémoire consommée. Les mêmes avantages sont obtenus en PHP, car maintenant la liaison de classe ne se fait pas en runtime, mais se fait une seule fois. De plus, les classes associées sont désormais stockées non pas dans l'espace d'adressage de chaque processus, mais dans la mémoire partagée, et donc la consommation totale de mémoire diminue.

L'utilisation du préchargement aide également à l'optimisation globale de tous les scripts PHP, supprime complètement la surcharge OPcache et vous permet de générer du code JIT plus efficace.

Mais il y a aussi des inconvénients. Les scripts chargés au démarrage ne peuvent pas être remplacés sans redémarrer PHP. Si nous avons téléchargé quelque chose et l'avons rendu permanent, nous ne pouvons plus le décharger. Par conséquent, la technologie peut être utilisée avec des cadres stables, mais si vous déployez l'application plusieurs fois par jour, elle ne fonctionnera probablement pas pour vous.

La technologie a été conçue comme transparente, c'est-à-dire qu'elle a permis de charger des applications existantes (ou des parties de celles-ci) sans aucun changement. Mais après l'implémentation, il s'est avéré que ce n'est pas tout à fait vrai. Toutes les applications ne fonctionnent pas comme prévu si elles ont été chargées à l'aide de la précharge . Par exemple, si un code est appelé dans l'application sur la base des résultats de la vérification de function_exists ou class_exists , et que la fonction devient constante, respectivement, function_exists renvoie toujours true , et le code qui était précédemment appelé était censé être appelé.

Techniquement, le préchargement est activé à l'aide d'une seule directive de configuration opcache.preload, à l'entrée de laquelle vous donnez un fichier de script - un fichier PHP standard qui sera lancé au stade du démarrage de l'application (non seulement chargé, mais exécuté).

 <?php function _preload(string $preload, string $pattern = "/\.php$/") { if (is_file($path) && preg_match($pattern, $path)) { opcache_compile_file($path) or die("Preloading failed"); } else if (is_dir($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { if ($file !== "." && $file !== "..") { _preload($path . "/" . $file, $pattern); } } closedir($dh); } } } _preload("/usr/local/lib/ZendFramework"); 

C'est l'un des scénarios possibles qui lit récursivement tous les fichiers d'un certain répertoire (dans ce cas, ZendFramework). Vous pouvez implémenter absolument n'importe quel script en PHP: lisez avec une liste, ajoutez des exceptions, ou même croisez avec composer afin qu'il pousse les fichiers nécessaires au préchargement. Tout cela est une question de technologie, et le plus intéressant n'est pas de savoir comment expédier, mais quoi expédier.

Que charger en préchargement


J'ai essayé cette technologie sur WordPress. Si vous venez de télécharger tous les fichiers * .php, WordPress cessera de fonctionner en raison de la fonctionnalité mentionnée précédemment: il a un contrôle function_exists, qui devient toujours vrai. Par conséquent, j'ai dû légèrement modifier le script de l'exemple précédent (ajouter des exceptions), puis, sans aucune modification dans WordPress lui-même, cela a fonctionné.

Vitesse [req / seq]Mémoire [Mo]Nombre de scriptsNombre de fonctionsNombre de cours
Rien3780000
Tous (presque *)3957,52541770148
Seuls les scripts utilisés3964,584153251

En conséquence, en raison du préchargement, nous avons obtenu une accélération de ~ 5% , ce qui n'est déjà pas mal.

J'ai téléchargé presque tous les fichiers, mais la moitié d'entre eux n'ont pas été utilisés. Vous pouvez faire encore mieux - pilotez l'application, voyez quels fichiers ont été téléchargés. Vous pouvez le faire en utilisant la fonction opcache_get_status() , qui renverra tous les fichiers mis en cache par OPcache et en créera une liste pour le préchargement. Ainsi, vous pouvez économiser 3 Mo et obtenir un peu plus d'accélération. Le fait est que plus la mémoire est requise, plus le cache du processeur devient sale et moins il est efficace. Moins la mémoire est utilisée, plus la vitesse est élevée.

FFI - Interface de fonction étrangère


Une autre technologie liée à JIT qui a été développée pour PHP est FFI (Foreign Function Interface) ou, en russe, la possibilité d'appeler des fonctions écrites dans d'autres langages de programmation compilés sans compilation. Les implémentations d'une telle technologie en Python ont impressionné mon patron (Zeev Surazki), et j'ai été très impressionné lorsque j'ai commencé à l'adapter à PHP.

Il y a déjà eu plusieurs tentatives en PHP pour créer une extension pour FFI, mais ils ont tous utilisé leur propre langage ou API pour décrire les interfaces. J'ai espionné l'idée dans LuaJIT, où le langage C (un sous-ensemble) est utilisé pour décrire les interfaces, et le résultat est un jouet très cool. Maintenant, quand j'ai besoin de vérifier comment quelque chose fonctionne en C, je l'écris en PHP - cela se produit, directement sur la ligne de commande.

FFI vous permet de travailler avec des structures de données définies en C et peut être intégré à JIT pour générer un code plus efficace. Son implémentation basée sur libffi est déjà incluse dans PHP 7.4.

Mais:

  • Ce sont 1000 nouvelles façons de se tirer une balle dans le pied.
  • Nécessite des connaissances en C et parfois une gestion manuelle de la mémoire.
  • Ne prend pas en charge le préprocesseur C (#include, #define, ...) et C ++.
  • Les performances sans JIT sont assez faibles.

Bien que, pour certains, ce sera pratique, car le compilateur n'est pas nécessaire. Même sous Windows, cela fonctionnera sans Visual-C de PHP.

Je vais vous montrer comment utiliser FFI pour implémenter une véritable application GUI pour Linux.

Ne vous inquiétez pas du code C, j'ai moi-même écrit une interface graphique en C il y a environ 20 ans, mais j'ai trouvé cet exemple sur Internet.

 #include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Hello from C"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); gtk_widget_show_all(window); } int main() { int status; GtkApplication *app; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), 0, NULL); g_object_unref(app); return status; } 

Le programme crée l'application, se bloque sur l'événement de rappel d'activation, lance l'application. Dans le rappel, créez une fenêtre, affectez-lui la taille du titre et affichez-la.

Et maintenant, la même chose réécrite en PHP:

 <?php $ffi = FFI::cdef(" … // #include <gtk/gtk.h> ", "libgtk-3.so.0"); function activate($app, $user_data) { global $ffi; $window = $ffi->gtk_application_window_new($app); $ffi->gtk_window_set_title($window, "Hello from PHP"); $ffi->gtk_window_set_default_size($window, 200, 200); $ffi->gtk_widget_show_all($window); } $app = $ffi->gtk_application_new("org.gtk.example", 0); $ffi->g_signal_connect_data($app, "activate", "activate", NULL, NULL, 0); $ffi->g_application_run($app, 0, NULL); $ffi->g_object_unref($app); 

Ici, l'objet FFI est créé en premier. Une description de l'interface lui est envoyée en entrée - essentiellement un fichier h - et la bibliothèque que nous voulons télécharger. Après cela, toutes les fonctions décrites dans l'interface deviennent disponibles en tant que méthodes de l'objet ffi, et tous les paramètres transférés sont traduits automatiquement et de manière absolument transparente dans la représentation machine nécessaire.

On peut voir que tout est exactement le même que dans l'exemple précédent. La seule différence est qu'en C, nous avons envoyé un rappel en tant qu'adresse, et en PHP, la connexion se fait par le nom donné par la chaîne.

Voyons maintenant à quoi ressemble l'interface. Dans la première partie, nous déterminons les types et les fonctions en C, et dans la dernière ligne nous chargeons la bibliothèque partagée:

 <?php $ffi = FFI::cdef(" typedef struct _GtkApplication GtkApplication; typedef struct _GtkWidget GtkWidget; typedef void (*GCallback)(void*,void*); int g_application_run (GtkApplication *app, int argc, char **argv); unsigned long * g_signal_connect_data (void *ptr, const char *signal, GCallback handler, void *data, GCallback *destroy, int flags); void g_object_unref (void *ptr); GtkApplication * gtk_application_new (const char *app_id, int flags); GtkWidget * gtk_application_window_new (GtkApplication *app); void gtk_window_set_title (GtkWidget *win, const char *title); void gtk_window_set_default_size (GtkWidget *win, int width, int height); void gtk_widget_show_all (GtkWidget *win); ", "libgtk-3.so.0"); ... 

Dans ce cas, ces définitions C sont copiées à partir des fichiers h de la bibliothèque GTK, presque inchangées.

Afin de ne pas interférer avec C et PHP dans le même fichier, vous pouvez mettre l'intégralité du code C dans un fichier séparé, par exemple, avec le nom gtk-ffi.h et ajouter au début un couple de define'ov spéciaux qui spécifient le nom de l'interface et la bibliothèque à charger:

 #define FFI_SCOPE "GTK" #define FFI_LIB "libgtk-3.so.0" 

Ainsi, nous avons sélectionné la description complète de l'interface C dans un seul fichier. Ce gtk-ffi.h est presque réel, mais malheureusement, nous n'avons pas encore implémenté un préprocesseur C, ce qui signifie que les macros et les inclus ne fonctionneront pas.

Maintenant, chargeons cette interface en PHP:

 <?php final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::load(__DIR__ . "/gtk_ffi.h"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

Étant donné que le FFI est une technologie plutôt dangereuse, nous ne voulons pas le donner aux mains de personne. Cachons au moins l'objet FFI, c'est-à-dire le rendons privé dans la classe. Et nous allons créer un objet FFI non pas en utilisant FFI::cdef , mais en utilisant FFI::load , qui ne lit que notre fichier h de l'exemple précédent.

Le reste du code n'a pas beaucoup changé, seulement en tant que gestionnaire d'événements, nous avons commencé à utiliser une fonction sans nom et à passer le titre à l'aide d'une liaison lexicale. Autrement dit, nous utilisons à la fois C et les points forts de PHP, qui ne sont pas disponibles en C.

Une bibliothèque ainsi créée pourrait déjà être utilisée dans votre application. Mais c'est bien si cela ne fonctionne que sur la ligne de commande , et si vous le placez dans le serveur Web, le fichier gtk_ffi.h sera lu à chaque demande, une bibliothèque sera créée et chargée, la liaison sera faite ... Et tout ce travail répétitif chargera votre serveur.

Pour éviter cela et, en fait, permettre d'écrire des extensions PHP en PHP lui-même, nous avons décidé de croiser FFI avec le préchargement.

Préchargement FFI +


Le code n'a pas beaucoup changé, seulement maintenant nous donnons les fichiers h au préchargement, et nous exécutons FFI::load directement au moment du préchargement, et non lorsque nous créons l'objet. Autrement dit, le chargement de la bibliothèque, toutes les analyses et les liaisons sont effectuées une fois (au démarrage du serveur), et avec l'aide de FFI::scope("GTK") nous avons accès à une interface préchargée par nom dans notre script.

 <?php FFI::load(__DIR__ . "/gtk_ffi.h"); final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::scope("GTK"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

Dans ce mode de réalisation, FFI peut être utilisé à partir d'un serveur Web. Bien sûr, ce n'est pas pour l'interface graphique, mais de cette façon, vous pouvez écrire, par exemple, une liaison à la base de données.

Une extension ainsi créée peut être utilisée directement depuis la ligne de commande:
 $ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");' 

Un autre avantage du croisement et du préchargement FFI est la possibilité d'interdire l'utilisation de FFI pour tous les scripts de niveau utilisateur. Vous pouvez spécifier ffi.enable = preload, ce qui signifie que nous faisons confiance aux fichiers préchargés, mais appeler FFI à partir de scripts PHP normaux est interdit.

Travailler avec des structures de données C


Une autre caractéristique intéressante de FFI est qu'il peut fonctionner avec des structures de données natives. Vous pouvez à tout moment créer en mémoire toute structure de données décrite en C.

 <?php $points = FFI::new("struct {int x,y;} [100]"); for ($x = 0; $x < count($points); $x++) { $points[$x]->x = $x; $points[$x]->y = $x * $x; } var_dump($points[25]->y); // 625 var_dump(FFI::sizeof($points)); // 800  foreach ($points as &$p) { $p->x += 10; } var_dump($points[25]->x); // 35 

100 ( FFI::new != new FFI), integer. , C. PHP, . count, / foreach . 800 , PHP PHP' , 10 .

FFI:

  • PHP.
  • PHP.

Python/CFFI : (Cario, JpegTran), (ffmpeg), (LibreOfficeKit), (SDL) (TensorFlow).

, FFI .

- PHP. , , callback' , . FFI. , . FFI c JIT, , LuaJIT, . , , .

 for ($k=0; $k<1000; $k++) { for ($i=$n-1; $i>=0; $i--) { $Y[$i] += $X[$i]; } } 

FFI .
Native ArraysFFI Arrays
PyPy0,0100,081
Python0,2120,343
LuaJIt -joff0,0370,412
LuaJit -jon0,0030,002
Php0,0400,093
PHP + JIT0,0160,087

: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) Intel, Andy Wingo (JS hacker, Igalia), Mike Pall ( LuaJIT).

, , .

PHP Russia 2020 ! telegram- , 2019 youtube- , , — .

Source: https://habr.com/ru/post/fr481092/


All Articles