Aller plus loin avec un dump des opcodes d'un script PHP
21 février 2013 —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) :
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 :
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 unRETURN
.
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 faire – et qui n’est même pas de la “micro”-optimisation vu les benchs – est 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.
-
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. ↩︎
-
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… ↩︎