PHP : Comment écrire un script High-Performance

Le PHP est au beau être langage de type script et critiqué pour être lent, il n’empêche qu’il reste l’un des langages pour serveur web des plus utilisés. Avec le temps, les sociétés majeurs du web qui utilisent le PHP, on cherché à optimiser le moteur. L’introduction de la notion de Garbage Collector dans la version 5.3 fut une avancée notable. Mais il existe de nombreux moyens d‘optimiser les performances d’un script.

Optimiser vos scripts PHP

Principalement utilisé dans le cadre de la simple génération de page, ces optimisations, bien que pouvant apporté un certain gain, ne sont pas assez visible pour l’utilisateur. L’optimisation des bases de données, des serveurs web avec l’utilisation de Nginx, l’optimisation des moteurs avec l’arrivée de HHVM ou encore de HippyVM vous permettrons de simplement accélérer le rendu de vos pages et d’optimiser le temps de réponses de vos requêtes de manière plus simple. Malgré cela, nous développons parfois des scripts PHP dans le but de faire des traitement lourd, ou les serveurs web ne peuvent rien.

Dans ce post, je vais vous détailler trois axes d’optimisations que j’ai appliqué récemment sur un script devant traiter des fichiers CSV ou XLS contenant une forte quantité d’informations. La quantité de mémoire utilisé atteignait sans soucis 1Go et pouvait durer plus d’1/2 heure.

Avant que je vous présente les trois notions PHP qui peuvent vous permettent d’optimiser le temps d’exécution ainsi que la quantité de mémoire pris, sachez qu’avant de vous plaindre d’un langage X, l’algorithmique de votre script est responsable d’une bonne partie de vos traitements. Le C++ a beau être infiniment plus rapide que le PHP, si votre algorithmes C++ est mauvais, cela ne résoudra pas immédiatement vos problèmes.

Désallouer ses variables, limiter la consommation de mémoire

Avant que PHP 5.3 ne sorte, le problème de PHP était le consommation de mémoire de manière abusive. J’ai d’ailleurs souvent entendu que la consommation mémoire d’un script PHP ne pouvait jamais diminué… Si cela fut vrai un temps, cela n’est heureusement plus vrai. Cette gestion fait appel à la notion de Garbage Collector que nous allons voir un peu plus bas.

Une bonne habitude perdu…

Dans certains langages, cela était obligatoire. En PHP, cette notion a carrément été oublié ! Cette petite fonction native unset(), est d’une utilité redoutable. Elle est équivalente à la fonction free() en C++ et permet de désallouer et donc de libérer immédiatement la mémoire utilisée par la variable. La variable est complètement détruite. Cela permet dans un premier temps de libérer PHP des variables inutilisées. Cela possède un autre intérêt, nous aider à mieux structurer nos algorithmes. Bien évidemment, lorsqu’une méthode se termine les variables sont désallouées automatiquement mais vous verrez plus bas que cela permet d’optimiser le temps que passe le « ramasse-miette » à travailler ou carrément de faire son travail dans le cas où on le désactive. Il y a donc également un gain en terme de rapidité. Et croyez-moi, le GC consomme beaucoup de ressources pour libérer de la mémoire.

Comme je l’ai dit dans le paragraphe précédent, à la fin d’une fonction ou d’une méthode, PHP supprime les variables inutilisées. Mais dans le cas de script traitant des données en masse il est possible qu’une fonction administre les autres. Comme dans certains langages, si il s’agit d’une fonction main, vous aurez probablement tendance à stocker des variables en retour de fonction avant de les passer à d’autres fonctions. On peut rapidement se retrouver avec une forte quantité de données. Dès lors qu’on ne s’en sert plus, aucun intérêt de se les « trimbaler », on la désalloue.

Récemment, lors de l’écriture d’un script ou je faisais de l’extraction de tout un fichier de plus de 15 Mo, une fois que je m’en étais servie, je la supprime et permet de gagner de la mémoire !

Comprendre le Garbage Collector

C’est en faisant des recherches sur la gestion de la mémoire que je suis tombé sur l’article d’un confrère. Dans cet article qui date maintenant un peu, Pascal Martin explique la nouveauté qui n’en désormais plus une, le Garbage Collector.

Qu’est-ce que le Garbage Collector ?

Pour ceux qui ne connaissent pas ou qui en ont déjà entendu parler mais n’ont jamais eu l’occasion de s’en servir, le Garbage Collector que l’on traduit par « ramasse-miette » en français, une fonctionnalité de bas niveau qui à intervalle X, stop le processus en cours d’exécution, et effectue un balayage de la mémoire alloué par le script pour détecter qu’elles sont les variables qui ne sont plus accessible pour les supprimer.

Je comprends pas, tout à l’heure on m’a dit qu’à la fin d’une méthode PHP détruit les variables.
Cela n’est pas tout à fait vrai. De la même manière que le C ou le C++ (parent du PHP), à la fin d’une méthode, PHP quitte et perd la référence qu’il avait sur l’objet. En C, seule la notion de pointeur existe, en C++ les notions de pointeur et de référence se côtoient.

Une fois que la référence sur l’objet a été perdu, et une fois que le Garbage Collector se lance, ce dernier déterminera que la variable n’est plus accessible et la détruira.

Cette notion est très présente en JAVA ainsi que dans d’autres langages plus récent et par conséquent de plus haut niveau.

Qu’est-ce que le Garbage Collector a apporté ?

Le GC a apporté une souplesse de développement considérable. Il est évident que développer en C est plus technique qu’en Java, PHP ou autres. Malgré cela, certaines facettes se font désormais oublié comme la gestion de la mémoire et c’est nôtre code qui en empathie parfois. Si cela a apporté de la souplesse dans nos développement, cela a également ralenti l’exécution de nos programmes.

Avec ces outils, ce qu’il ne faut pas oublier c’est qu’on peut toujours contrôler d’une certaine manière la mémoire. Question de volonté, ou de nécessité…

Pour finir, je pense que ce type d’outils sont très pratiques mais ils ne peuvent pas remplacer les instructions données par le développeurs. Pour ma part, je trouve que la notion C++ de shared_ptr est bien plus intéressante et pourrait offrir des gains de performances hors-norme à l’avenir si Zend décidait d’utiliser cette méthode dans certains cas.

Et en PHP ?

Depuis PHP 5.3, quatre fonctions ont été ajoutées.

  • gc_enable
  • gc_enabled
  • gc_disable
  • gc_collect_cycles
    Je vous laisse le loisir d’aller lire la documentation, mais pour faire rapide, elles permettent d’activer le GC, savoir si il est actif, le désactivé, et lancer manuellement la collecte.

Pascal Martin a publié dans le billet que je vous ai linké plus haut, un script où il effectue une batterie de test. On voit bien dans le rapport de ses tests que la version sans GC, atteint des consommations de mémoire énorme, jusqu’à même atteindre la limite et crasher.

Benchmark

Voici le code exécuté. Celui-ci a été repris du blog de Pascal Martin et plus particulièrement de son article.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Node {
public $parentNode;
public $childNodes = array();
function Node() {
$this->nodeValue = str_repeat('0123456789', 128);
}
}
function createRelationship() {
$parent = new Node();
$child = new Node();
$parent->childNodes[] = $child;
$child->parentNode = $parent;
}
for ($i = 0; $i < 500000; $i++) {
createRelationship();
// Cette partie est exécuté lors du deuxième test
if ($options["gc_manual"]) {
gc_collect_cycles();
}
}

J’effectue trois tests séparément :

  1. J’effectue ce test avec les options de base. Le Garbage Collector est activé et PHP l’exécute de manière régulière. Le script a durée 7.55 secondes et la mémoire utilisée a atteint 20.98 Mo.
  2. J’effectue le même test en désactivant le Garbage Collector et l’appelant à chaque tour de boucle. Le script dure 3.79 secondes et la mémoire utilisée a atteint un pic à 244.77 Ko.
  3. J’effectue un troisième test en désactivant le Garbage Collector et en ne collectant jamais manuellement. La mémoire doit donc se remplir fortement. Le script dure 4.46 secondes et la mémoire a atteint 1.98 Go.

Benchmark : PHP Script High-Performance
Benchmark : PHP Script High-Performance
Benchmark : PHP Script High-Performance

Avec ce test, on voit très bien l’importance de la gestion de la mémoire. Dans notre exemple qui est poussé à l’extrême, on voit très bien l’importance. D’un côté, le Garbage Collector effectue un travail énorme sur la consommation mémoire, mais qui va avoir tendance à ralentir l’exécution du script. On le voit très bien en comparant le test 1er (avec GC) et le 3ème test sans gestion.

Là où notre travail de performance s’effectue, c’est sur le test 2. Pas de gestion automatique de la mémoire, une gestion manuelle optimisée dans le cadre de ce script. On parvient à accélérer la vitesse de traitement par presque deux fois (7.55s / 3.79s = 1.99). De cette manière, on a également limité notre mémoire à 245 Ko contre 21 Mo pour la gestion automatique. Soit un coefficient de presque 88 ( (20.98 * 1024) / 244.77 = 87.77).

Conclusion

Bien que l’exemple ci au dessus pousse à l’extrême la gestion de la mémoire, ce test a pour but de nous montrer à quel point il peut être important d’étudier la gestion de la mémoire dans nos scripts. Les gains peuvent impressionnant dans certains cas. Du temps de traitement, de la mémoire et tout ce qui s’en suit peuvent être économisé.

Comprendre et utiliser les références

Comme je vous en parlais un peu plus haut, PHP implémente le passage de variable par référence, et comme je vous le disais plus haut ce type de passage est plus ou moins équivalent au passage par pointeur.

  • Comprendre le passage par référence
  • Comprendre le passage par pointeur
    C’est une partie importante, pas uniquement pour le PHP car cette notion existait bien avant mais pour l’informatique. De base, lorsque vous déclarez une fonction, le passage est réalisé par copie.
    // Passage par copie
    pulic function foo($arg) {…}

    Cela implique quelques avantages ou inconvénients en fonction de votre code. L’objet est copié, ce qui signifie que la mémoire alloué augmentera du poids de l’objet passé. Cela peut être ridicule, si vous passez un booléen, mais cela peut-être bien plus important si vous passez un tableau par exemple.
1
2
3
4
5
6
7
8
9
10
11
public function foo() {
$a = 1;
bar($a);
echo $a; //$a vaut 1
}
public function bar($arg) {
$arg = 2;
}

Dans ce cas, on voit que la valeur n’a pas été modifié, cela peut-être intéressant si on utilise la variable $a comme une base et qu’on souhaite ne jamais touché à sa valeur. On pourrait ici parler d’une notion qui n’existe pas en PHP, d’une variable constante (const).

1
2
3
4
5
6
7
8
9
10
11
12
public function foo() {
$a = 1;
$a = bar($a);
echo $a; //$a vaut 2
}
public function bar($arg) {
$arg = 2;
return $arg;
}

Je suis sûr que vous avez déjà écrit des méthodes de cette manière. Dans ce cas là, c’est clairement une erreur. Bien évidemment, la syntaxe est parfaitement vrai, mais une allocation a été réalisée pour la copie du paramètre, et la désallocation d’une variable a été faite. Des appels mémoires qui coutent chers et du temps de traitement inutile. La forme ci-dessous serait équivalente mais bien plus rapide.

1
2
3
4
5
6
7
8
9
10
11
public function foo() {
$a = 1;
bar($a);
echo $a; //$a vaut 2
}
public function bar(&$arg) {
$arg = 2;
}

Ici on a réalisé exactement le même traitement mais c’est tout simplement plus rapide et moins consommateur.

Cet optimisation est très simple à réaliser et peut même simplifier la lecture de votre code. Il existe plusieurs syntaxes à connaitre pour faire du passage par référence.

  1. Passage de paramètre par référence (que l’on vient de voir)
  2. Retour de fonction par référence (non détaillé ici, car le moteur PHP optimise cette partie et je n’ai pas sentie de gain probant)
  3. Copie d’une variable par référence, peu utile mais redoutable. Je vous laisse lire la documentation de PHP qui extrêmement bien détaillé. Vous pourriez sans problème apprendre des choses que j’ai omis :)

Autre optimisation

Grâce aux trois optimisations ci-dessus, vous devriez sans problème accélérer vos traitements. Cependant, il existe d’autres articles. Je vous liste les articles intéressants sur différentes optimisations que vous pourrez faire.

Conclusion

Les trois parties que je viens de vous détailler constitue une bonne alternative à mon gout pour optimiser ses scripts et éviter de relancer des développements dans un langage plus rapide comme un langage compilé.

J’ai réalisé ces trois passes sur l’un de mes scripts et j’ai entre autre réussi à diminuer la consommation mémoire par 3 approximativement et réaliser un gain de rapidité par 2.

Ce travail a été réalisé dans le cadre d’un projet dirigé par Wanadev que je souhaite remercier.

J’espère avoir pu vous éclairer et vous souhaite bon courage dans vos futurs développements. :)