Mesure de performances et optimisation en PHP
dimanche 18 juin 2006 à 23:03 (Julien Tartarin)
Développer une application, ou plutôt des pages (scripts) à fort trafic (même si tout est relatif) n'est pas la chose la plus évidente à faire. Ces derniers mois m'ont appris quelques éléments non négligeables que je me permets de partager ici.
Pourquoi mesurer les performances ?
Je vois là trois raisons :
- savoir !
- estimer les limites du code (taille critique)
- choisir la meilleure solution en cas de doute sur un algorithme ou une fonction
Quelles données mesurer ? C'est une bonne question, et la réponse dépend beaucoup des points à optimiser dans un code !
La durée d'exécution du script, le nombre de requêtes SQL effectuées, la durée de chaque requête, la mémoire (RAM) utilisée par le script, le temps d'une boucle ou d'une opération d'écriture, d'une requête réseau, etc. sont autant d'informations qui peuvent être complètement futiles ou indispensables, selon le contexte.
Quels outils mettre en place ?
À chaque type de mesure son outil. Trois type d'informations peuvent être mesurées : le temps, la mémoire et le "nombre".
Pour les compteurs, l'outil à utiliser est... une variable, qu'on incrémente à chaque action. Assez basique, en somme.
Pour mesurer le temps et la mémoire, je connais deux outils simples et deux moyens de les enregistrer, selon les chiffres souhaités : global ou par "index".
Pour le temps, ça revient à enregistrer le temps en début de script et celui en fin de script et retenir la différence, ou alors stocker dans un tableau le temps à plusieurs endroits du script (d'où "index").
Pour la mémoire, on peut mesurer l'espace alloué au script à la fin de celui-ci ou stocker l'usage courant en mémoire à des endroits stratégiques. Attention à cette deuxième alternative, il ne faut pas oublier que stocker des données revient à augmenter la mémoire allouée, il faut donc tenir compte de ça.
Il en va de même pour la mesure du temps. Bien que quasiment négligeable, la mesure du temps et son stockage prend quelques microsecondes. L'outil de mesure modifie donc le temps d'exécution du script et la mémoire qu'il utilise.
Fonctions PHP
microtime() est une fonction PHP disponible par défaut, elle permet d'obtenir l'heure courante avec une précision de l'ordre de la microseconde ! Pour avoir un temps de référence en millisecondes (unité qui a plus de sens à l'échelle de PHP), il suffit de diviser par 1000 (j'ai toujours été bon en maths).
memory_get_usage() est disponible si PHP a été compilé avec l'option --enable-memory-limit (gestion de la mémoire), ce qui est le cas sur la plupart des installations. Cette fonction indique la mémoire allouée au script en octets, donc pour se rendre compte de ce que ça donne, on divise par 1024 (pour avoir des kilo-octets, c'est plus facile à lire)
xdebug
xdebug est un outil assez génial et très facile à installer. Comme son nom l'indique il est pensé pour le debug, du coup il ajoute tout un tas de fonctions à PHP.
Il modifie aussi la gestion des erreurs par PHP et indique toutes les fonctions touchées par l'erreur. Par exemple si vous utilisez une fonction, PHP indiquera que l'erreur vient de cette fonction sans en dire plus, xdebug rajoutera l'endroit où a été appelée la fonction ! La fonction var_dump() est légèrement modifiée pour afficher un résultat humainement lisible (inutile de rajouter des <pre> ou de lire la source pour avoir les infos recherchées).
xdebug_time_index() s'occupe directement de donner le temps en microsecondes depuis le lancement du script. L'avantage ici est que contrairement à une récupération maison de l'heure au début du script puis à la fin, le temps de chargement du script en mémoire est compté !
On a constaté sur nos scripts que la différence entre le temps indiqué par xdebug et celui indiqué par notre calcul maison était d'environ 20 millisecondes, valeur qu'on a réussi à faire tomber à 8 millisecondes en répartissant les fonctions selon leur contexte (on ne charge pas une fonction si elle n'a aucune chance d'être utilisée).
xdebug_peak_memory_usage() indique la quantité maximum de mémoire allouée au script à un point donné, un peu comme la fonction memory_get_usage() de PHP, en somme.
Là encore, l'outil de PHP et xdebug diffèrent ! La différence dépend également des fonctions déclarées... contrairement à tout ce qui est indiqué dans la doc PHP. À ce niveau là d'ailleurs, la présence de commentaires n'a aucune influence (ouf !). Mais la mémoire ajoutée par la déclaration de certaines fonctions ne dépend pas de leur taille, plutôt de leur complexité et des fonctions exotiques utilisées.
Malgré le fait que ces fonctions ne soient à aucun moment utilisées, PHP les charge en mémoire, et charge peut-être certaines librairies (internes) à la volée selon la présence ou non de codes particuliers (boucles, fonctions réseaux, etc.). Expériences à pousser de ce côté là !
Des chiffres ?
Sur mon blog, propulsé par Dotclear, vous pouvez obtenir le temps de génération et la mémoire utilisée (selon xdebug) dans un commentaire HTML en fin de source.
Sur une page d'article, je viens d'obtenir un temps de génération de 420,52 millisecondes pour... 2986 ko de mémoire allouée. Wow ! La question est : que ce passerait-il si 177 requêtes simultanées arrivaient sur cette page (ce serveur est doté de 512 Mo de RAM) ?
Cette énorme allocation de mémoire vient d'après moi du fait que Dotclear est développé comme une application, et non comme un ensemble de scripts. C'est à la fois sa force et son point faible, puisqu'une plus grande extensibilité et facilité de maintenance du code se paie par une baisse de performances.
Juste pour info, sur Lexode on tourne à environ 30 millisecondes et 300 ko de mémoire allouée (ça dépend évidemment des pages, mais en général c'est ça). Sur la Beta v8 de Lexode, 30 millisecondes et 700 ko (le doublement de mémoire allouée nous étonne un peu, il faut qu'on vérifie si ça dépend pas de la configuration du serveur puisque chez Oxeva PHP tourne en Fast CGI (contre un simple module Apache sur nos serveurs de développement).
Un peu d'optimisation
Quelques idées en vrac pour faire baisser les chiffres mesurés :
- mettre en place un système de cache
- utiliser la directive
SQL_CACHEsur certaines requêtes SQL - regrouper les fonctions par domaine d'utilisation : seules les fonctions potentiellement utilisées sont déclarées
- ajouter un output buffer, soit avec
ob_start()etob_end_flush(), soit en activant la directiveoutput_bufferingau sein dephp.ini

#1 Le dimanche 18 juin 2006 à 23:23, par Verdict
Interessant comme article (même si je ne suis qu'encore au xhtml/css)
"xdebug est un outil assez génial et très facile à installer" faut aimer l'anglais :/ (j'ai 14 ans)
#2 Le dimanche 8 avril 2007 à 14:34, par Palleas
Un spam est passé ! \o/
#3 Le dimanche 8 avril 2007 à 17:58, par Julien Tartarin
Argl =(