Aller plus loin avec un dump des opcodes d'un script PHP

21 février 2013php, opcode
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

J’ai publié il y a quelques jours un article expliquant comment obtenir un dump des opcodes d’un script PHP, en utilisant l’extension VLD - Vulcan Logic Dumper.

Pour le compléter et aller un peu plus loin, je vais maintenant montrer comment obtenir un graphe des chemins correspondant à un dump d’opcodes – et donc, au code PHP initialement écrit.

Et en seconde partie de cet article, je me baserai sur quelques dumps d’opcodes pour montrer que, des fois, mettre à jour PHP est une bonne idée ;-)


Quelques options plus avancées de VLD : dump de chemins / branches

En plus de la simple fonctionnalité de dump d’opcodes, l’extension VLD1 propose quelques options supplémentaires :

  • L’option vld.dump_paths permet d’activer un dump des chemins et branches d’exécution du code PHP,
  • L’option vld.save_paths permet d’enregistrer ce dump supplémentaire vers le fichier /tmp/paths.dot (format qui peut être utilisé avec graphviz),
  • Et enfin, l’option vld.save_dir permet de spécifier un autre répertoire vers lequel enregistrer ce fichier.

A titre d’exemple, considérons la portion de code PHP suivante :

<?php
$tmp = mt_rand(0, 2);
if ($tmp === 0) {
    echo "zero";
}
else if ($tmp === 1) {
    echo "un";
}
else {
    echo "deux";
}
echo "\n";

Rien de bien particulier : un bloc if / else if / else, avec quelques sorties écran.

Maintenant, utilisons VLD pour générer un dump des opcodes correspondant à ce script, ainsi que le dump des chemins correspondant – dump qui sera enregistré vers l’emplacement par défaut, /tmp/paths.dot :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 -dvld.dump_paths=1 -dvld.save_paths=1 exemples/005-dump-paths-1.php
filename:       /.../exemples/005-dump-paths-1.php
function name:  (null)
number of ops:  15
compiled vars:  !0 = $tmp
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   SEND_VAL                                                 0
         1      SEND_VAL                                                 2
         2      DO_FCALL                                      2  $0      'mt_rand'
         3      ASSIGN                                                   !0, $0
   3     4      IS_IDENTICAL                                     ~2      !0, 0
         5    > JMPZ                                                     ~2, ->8
   4     6  >   ECHO                                                     'zero'
   5     7    > JMP                                                      ->13
   6     8  >   IS_IDENTICAL                                     ~3      !0, 1
         9    > JMPZ                                                     ~3, ->12
   7    10  >   ECHO                                                     'un'
   8    11    > JMP                                                      ->13
  10    12  >   ECHO                                                     'deux'
  12    13  >   ECHO                                                     '%0A'
  13    14    > RETURN                                                   1

branch: #  0; line:     2-    3; sop:     0; eop:     5; out1:   6; out2:   8
branch: #  6; line:     4-    5; sop:     6; eop:     7; out1:  13
branch: #  8; line:     6-    6; sop:     8; eop:     9; out1:  10; out2:  12
branch: # 10; line:     7-    8; sop:    10; eop:    11; out1:  13
branch: # 12; line:    10-   12; sop:    12; eop:    12; out1:  13
branch: # 13; line:    12-   13; sop:    13; eop:    14
path #1: 0, 6, 13,
path #2: 0, 8, 10, 13,
path #3: 0, 8, 12, 13,

le fichier /tmp/paths.dot est bien généré en sortie :

$ ll /tmp/paths.dot
-rw-r--r-- 1 squale squale 711 janv.  3 15:25 /tmp/paths.dot

Et, pour les curieux, voici à quoi il ressemble :

$ cat /tmp/paths.dot
digraph {
subgraph cluster_file_01471438 { label="file /.../exemples/005-dump-paths-1.php";
subgraph cluster_01471438 {
        label="__main";
        graph [rankdir="LR"];
        node [shape = record];
        "__main_0" [ label = "{ op #0 | line 2-3 }" ];
        __main_0 -> __main_6;
        __main_0 -> __main_8;
        "__main_6" [ label = "{ op #6 | line 4-5 }" ];
        __main_6 -> __main_13;
        "__main_8" [ label = "{ op #8 | line 6-6 }" ];
        __main_8 -> __main_10;
        __main_8 -> __main_12;
        "__main_10" [ label = "{ op #10 | line 7-8 }" ];
        __main_10 -> __main_13;
        "__main_12" [ label = "{ op #12 | line 10-12 }" ];
        __main_12 -> __main_13;
        "__main_13" [ label = "{ op #13 | line 12-13 }" ];
}
}
}

A partir du fichier .dot généré par VLD, il est possible de générer un .png, en utilisant l’outil dot de graphviz :

dot -Tpng /tmp/paths.dot > /tmp/paths.png

Ce qui nous donne en sortie une image de ce type (j’ai supprimé le nom du fichier qui figurait normalement en haut de l’image, pour réduire un peu ses dimensions) :

Chemins correspondant à l’exemple if/else if/else

On retrouve, à partir des opcodes, la structure de notre script – et ce graphe de chemins peut faciliter la compréhension du dump d’opcodes brutes, en particulier dans des cas de scripts / fonctions plus longs que l’exemple que j’ai pris ici.


D'ailleurs, en générant le graphe de chemins pour l'exemple utlisé à la [section "Un peu de structures conditionnelles / boucles" de mon précédent article](/post/php-obtenir-dump-opcodes#structures-conditionnelles-boucles), voici ce que l'on obtient :

Chemins correspondant à l’exemple for/if/else

Utiliser ceci en parallèle des explications textuelles données un peu plus haut devrait vous aider à y voir plus clair ;-)

Opcodes : des différences entre les versions de PHP ?

Chaque nouvelle version de PHP vient avec son lot de modifications, d’améliorations, et d’optimisations.

Certaines d’entre elles peuvent parfois porter sur le processus de compilation de code PHP en opcodes – l’idée générale, un peu simplifiée certes, étant que moins il y a d’opcodes à exécuter, plus cette exécution devrait être rapide.

Bien entendu, il ne faut pas s’attendre à ce que les opcodes générées pour une portion de script PHP changent du tout au tout entre deux versions de PHP… Mais, voici quelques exemples courts, sur lesquels on constate de petites différences.

Premier exemple

Tout d’abord, prenons la portion de code suivante :

<?php
$who = 'World';
$str = "Hello, $who!";
echo $str;

En quelques mots : on utilise l’interpolation de variables pour combiner deux chaines de caractères, et on affiche la chaîne résultante – oui, encore un Hello, World.

Avec PHP 5.3 et PHP 5.4, la sortie obtenue en utilisant VLD est la suivante :

$ ~/bin/php-5.4/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/010-diff-versions-php-1.php
filename:       /.../exemples/010-diff-versions-php-1.php
function name:  (null)
number of ops:  7
compiled vars:  !0 = $who, !1 = $str
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 'World'
   3     1      ADD_STRING                                       ~1      'Hello%2C+'
         2      ADD_VAR                                          ~1      ~1, !0
         3      ADD_CHAR                                         ~1      ~1, 33
         4      ASSIGN                                                   !1, ~1
   4     5      ECHO                                                     !1
   5     6    > RETURN                                                   1

branch: #  0; line:     2-    5; sop:     0; eop:     6
path #1: 0,

Par contre, avec PHP 5.2.17, la sortie est la suivante :

$ ~/bin/php-5.2/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/010-diff-versions-php-1.php
filename:       /.../exemples/010-diff-versions-php-1.php
function name:  (null)
number of ops:  9
compiled vars:  !0 = $who, !1 = $str
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 'World'
   3     1      INIT_STRING                                      ~1      
         2      ADD_STRING                                       ~1      ~1, 'Hello%2C+'
         3      ADD_VAR                                          ~1      ~1, !0
         4      ADD_CHAR                                         ~1      ~1, 33
         5      ASSIGN                                                   !1, ~1
   4     6      ECHO                                                     !1
   5     7    > RETURN                                                   1
         8*   > ZEND_HANDLE_EXCEPTION                                    

branch: #  0; line:     2-    5; sop:     0; eop:     8
path #1: 0,

La compilation de nos trois lignes de code PHP mène à 2 opcodes de plus avec PHP 5.2 qu’avec PHP 5.3 :

  • Un opcode INIT_STRING, en seconde instruction,
  • et un opcode ZEND_HANDLE_EXCEPTION en toute dernière ligne – opcode qui ne sera jamais exécuté, d’ailleurs (VLD l’a marqué d’une *) : il est positionné après un RETURN.

En remontant un peu plus loin dans l’histoire, avec PHP 5.1.6, nous aurions obtenu encore plus d’opcodes :

$ ~/bin/php-5.1/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/010-diff-versions-php-1.php
filename:       /.../exemples/010-diff-versions-php-1.php
function name:  (null)
number of ops:  10
compiled vars:  !0 = $who, !1 = $str
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 'World'
   3     1      INIT_STRING                                      ~1      
         2      ADD_STRING                                       ~1      ~1, 'Hello'
         3      ADD_STRING                                       ~1      ~1, '%2C+'
         4      ADD_VAR                                          ~1      ~1, !0
         5      ADD_STRING                                       ~1      ~1, '%21'
         6      ASSIGN                                                   !1, ~1
   4     7      ECHO                                                     !1
   5     8    > RETURN                                                   1
         9*   > ZEND_HANDLE_EXCEPTION                                    

branch: #  0; line:     2-    5; sop:     0; eop:     9
path #1: 0,

Ce n’est pas vraiment mis en évidence ici, puisque la chaine de caractères utilisant de l’interpolation de variables n’est pas très longue, mais PHP 5.1 générait un opcode ADD_STRING pour chaque ensemble de caractères séparés par un espace !
Vous pourriez faire le test avec une chaine comme "Voici une $var plus longue.", qui nécessite 11 opcodes en PHP 5.1 – et seulement 3 en PHP 5.4.


Que tirer de ceci ?
  • Avec PHP 5.1, utiliser des chaines de caractères entourées de double-quotes, avec de l’interpolation de variables, n’était vraiment pas fantastique au niveau du nombre d’opcodes générés – c’est probablement un peu de là que vient la (il fût un temps micro-optimisation / ) généralisation un peu sauvage en “utilisez des chaines de caractères entre simple-quotes”.
  • Mais PHP 5.2 a déjà grandement amélioré les choses, rendant quasiment obsolète cette affirmation – du moins sur des cas tels que celui présenté ici.
  • Et PHP 5.3 a encore amélioré ce cas.

Autrement dit, la meilleure “optimisation” que vous puissiez faireet qui n’est même pas de la “micro”-optimisation vu les benchsest d’utiliser une version à jour de PHP !

Second exemple

Comme second exemple, considérons la portion de code suivante, qui définit une fonction, et l’appelle en affichant sa valeur de retour :

<?php
function add($val1, $val2)
{
    return $val1 + $val2;
}

echo test(10, 5);

Avec PHP 5.4, la sortie obtenue avec VLD est la suivante :

$ ~/bin/php-5.4/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/010-diff-versions-php-2.php
filename:       /.../exemples/010-diff-versions-php-2.php
function name:  (null)
number of ops:  7
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   NOP                                                      
   7     1      INIT_FCALL_BY_NAME                                       'test'
         2      SEND_VAL                                                 10
         3      SEND_VAL                                                 5
         4      DO_FCALL_BY_NAME                              2  $0      
         5      ECHO                                                     $0
   8     6    > RETURN                                                   1

branch: #  0; line:     2-    8; sop:     0; eop:     6
path #1: 0,
Function add:
filename:       /.../exemples/010-diff-versions-php-2.php
function name:  add
number of ops:  5
compiled vars:  !0 = $val1, !1 = $val2
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   RECV                                             !0      
         1      RECV                                             !1      
   4     2      ADD                                              ~0      !0, !1
         3    > RETURN                                                   ~0
   5     4*   > RETURN                                                   null

branch: #  0; line:     2-    5; sop:     0; eop:     4
path #1: 0,
End of function add.

En premier, nous avons la définition de la fonction ; et ensuite, son appel.

Avec PHP 5.2, nous aurions la sortie suivante :

$ ~/bin/php-5.2/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/010-diff-versions-php-2.php
filename:       /.../exemples/010-diff-versions-php-2.php
function name:  (null)
number of ops:  8
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   NOP                                                      
   7     1      INIT_FCALL_BY_NAME                                       'test'
         2      SEND_VAL                                                 10
         3      SEND_VAL                                                 5
         4      DO_FCALL_BY_NAME                              2  $0      
         5      ECHO                                                     $0
   8     6    > RETURN                                                   1
         7*   > ZEND_HANDLE_EXCEPTION                                    

branch: #  0; line:     2-    8; sop:     0; eop:     7
path #1: 0,
Function add:
filename:       /.../exemples/010-diff-versions-php-2.php
function name:  add
number of ops:  6
compiled vars:  !0 = $val1, !1 = $val2
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   RECV                                                     1
         1      RECV                                                     2
   4     2      ADD                                              ~0      !0, !1
         3    > RETURN                                                   ~0
   5     4*     RETURN                                                   null
         5*   > ZEND_HANDLE_EXCEPTION                                    

branch: #  0; line:     2-    5; sop:     0; eop:     5
path #1: 0,
End of function add.

En somme, cette fois-ci, peu de différence : on notera juste la présence d’une opcode ZEND_HANDLE_EXCEPTION inutile à la fin de chacun des deux extraits, avec PHP 5.2.


Arrivés à la fin de ce second article sur les opcodes de PHP, j’espère avoir réussi à attirer votre curiosité : c’était le but2 ^^

Je n’ai pas du tout parlé de la phase d’exécution de ces opcodes, qui correspond réellement à l’exécution d’un script PHP (la génération des opcodes correspondant à la compilation du PHP) ; si vous êtes joueur et avez envie d’en apprendre plus, je vous encourage à ouvrir le fichier Zend/zend_vm_def.h, qui fait parti des sources de PHP : vous y trouverez la définition de chaque opcode géré par le moteur d’exécution de PHP.



  1. Je ne reviens par dans cet article sur l’installation de l’extension VLD, ni son fonctionnement de base ; en cas de besoin, je vous encourage à lire l’article Obtenir un dump des opcodes d’un script PHP que j’ai publié il y a quelques jours. ↩︎

  2. Autant le sujet des opcodes peut attirer notre curiosité, du point de vue d’un développeur purement “PHP”, il est somme toute plus que rare que nous ayons à nous en soucier… ↩︎