Class Chronometer, ou comment benchmarker vos scripts PHP ?

Suite à quelques demandes d'astuces d'optimisation de la part de mes followers sur Twitter, voici un premier article sur une série assez longue concernant l'optimisation de vos scripts PHP. Pour ce premier billet, nous n'allons pas réellement examiner les manières d'améliorer les performances de ses scripts, mais plus un outil nous permettant de nous aider à cette tâche.

Nous allons ici créer une classe Chronometer, qui nous permettra facilement de détecter la rapidité d'exécution d'une fonction, et de repérer les différences de performances entre deux fonctions.

Définissons donc tout d'abord notre classe :

<?php
     class Chronometer
     {
     }
?>

Nous allons, pour commencer, définir une méthode Start et Stop qui nous permettront de démarrer ou d'arrêter nos tests. Et, pour bien faire, nous développerons aussi une méthode nous permettant de déterminer la différence entre ces deux temps.

public function Start()
{
     $this->beginTime = microtime(true);
}

public function Stop()
{
     $this->stopTime = microtime(true);
}

public function GetLength()
{
     return $this->stopTime - $this->beginTime;
}

Que fait donc cette mystérieuse fonction, microtime ? Elle nous retourne simplement le timestamp (nombre de secondes écoulées depuis le 1er janvier 1970) UNIX. Depuis PHP 5, cette fonction prend en argument un booléen, qui nous permet de récupérer ce temps sous forme d'un flottant. En effet, sous PHP 4, le format était un peu étrange : millisecondes secondes. Plutôt moyen pour traiter tout cela correctement, non ?

A présent, nous pouvons détecter facilement la durée de génération de notre page PHP. Mais, nous allons plutôt l'améliorer afin de regarder plus précisément le temps d'exécution d'une fonction. Pour ce faire, la fonction call_user_func_array va nous servir. Cette fonction au si joli nom nous permet d'exécuter une fonction (premier paramètre) prenant un ou plusieurs arguments (deuxième paramètre sous forme d'un tableau). Sachant cela, on peut créer une méthode pour déterminer la durée d'exécution d'une fonction :

public function GetExecutionLength($function, $arguments)
{
	if( !is_array($arguments) )	throw new Exception("<strong>Invalid argument:</strong> expected <em>array</em> rather than '.gettype($arguments).' for argument 2 of <em>".__METHOD__.'</em>.');

	ob_start();

	$this->Start();
	call_user_func_array($function, $arguments);
	$this->Stop();

	ob_end_clean();

	return $this->GetLength();
}

On vérifie tout d'abord que le second argument est bien un tableau (condition à suivre obligatoirement pour la fonction call_user_func_array), en levant une exception si tel n'est pas le cas (nous sommes en PHP 5, ce qui implique une gestion plus orientée objet des erreurs). Puis nous démarrons notre chronomètre, exécutons notre fonction, et retournons sa durée d'exécution.

N'en déplaîse à mes étudiants à qui je rabâche régulièrement que les fonctions de buffer peuvent généralement être oubliées (sauf cas bien particuliers) : nous les utilisons ici. ob_start et ob_end_clean sont en effet employées ici afin de ne rien afficher. En effet, si un echo a lieu dans la fonction testée, inutile de nous polluer l'affichage. ;)

A présent, nous avons une durée instantanée d'exécution de fonction. Cependant, celle-ci peut-être complètement erronée. Imaginez que votre Windows Vista décide d'indexer vos fichiers durant votre benchmark. Moins de ressources seront disponibles pour Wamp, et donc le résultat serait faussé. Pour pallier à cela, nous allons procéder à plusieurs fois le même test. C'est simplement de la statistique : plus la population est grande, plus représentatif sera le résultat.

public function GetAverageExecutionLength($function, $arguments, $iteration)
{
	$total = 0;

	for( $i = 0 ; $i < $iteration ; $i++ )	$total += $this->GetExecutionLength($function, $arguments);

	// Average time in milliseconds.
	return $total / $iteration;
}

Enfin, développons une fonction nous permettant de comparer facilement deux fonctions entre elles.

public function GetOptimizationRate($function1, $function2, $arguments, $iteration)
{
	$average1 = $this->GetAverageExecutionLength($function1, $arguments, $iteration);
	$average2 = $this->GetAverageExecutionLength($function2, $arguments, $iteration);

	return ( ($average1 - $average2) / min($average1, $average2) ) * 100;
}

public function EchoOptimizationRate($function1, $function2, $arguments, $iteration)
{
	echo $function1.' is '.$this->GetOptimizationRate($function1,$function2, $arguments, $iteration).' % faster than '.$function2.'.';
}

La formule du return de la première méthode indique simplement la performance relative (positive ou négative) des deux fonctions, sous forme de pourcentage. Quant à la seconde, cela nous évite d'inverser le nom des deux fonctions, et donc de commettre des contre-sens parfois problématiques.

Essayons donc notre toute nouvelle fonction, en reprenant un résultat déjà connu : les performances relatives de floatval et du casting direct. Créeons donc le code requis, et effectuons le test.

function A($i){ floatval($i); }
function B($i){ (float)$i; }

$c = new Chronometer();
$c->EchoOptimizationRate('A', 'B', array(3.14), 10000000);

Plus vous augmenterez le paramètre d'itération, plus le résultat sera proche de la réalité, et plus vous devrez attendre avant de voir le très précieux taux s'afficher. A ce propos, vous pouvez toujours enlever la limite des 30 secondes d'exécution par défaut en écrivant la ligne :

set_time_limit(0);

Avec l'exemple ci-dessus, j'obtiens :

A is -11.892284371309 % faster than B.

Ce qui confirme (à 2 % près) le résultat déterminé dans le billet de comparaison de ces fonctions.

Pour télécharger cette classe, ainsi que le petit test ci-dessus : Classe Chronometre.

Mots-clefs : , , ,

8 réactions sur cet article.

  1. Korri says:

    Bonjour Jonathan,

    Je voulais faire quelques tests de performance et je me souvenais que tu disait avoir fait une classe pour, me voilà donc.

    Cette classe me parait bien, si ce n'est :

    • microtime(true); Retourne une valeur en secondes, mais tu a mis millisecondes partout dans les commentaires.
    • Pour comparer deux fonction, il me semble qu'il serais plus "juste" de les exécuter l'une après l'autre dans une boucle et pas X fois l'une puis X fois l'autre (Un ralentissement de ta machine pendant une seconde alors que tu en est a la première pourrais considérablement fausser la donne).

    Voila, sinon elle est est déja adoptée dans mon dossier de tests. ;)

    Bonne journée !

  2. CANYON says:

    Salut Jonathan ! Nexen donne un lien en 404, juste pour te le dire et bien te le signaler.

    J'ai pas pas testé mais vais essayer ta classe en te remerciant.

  3. Komrod says:

    Il y a une grosse erreur dans le script dans la fonction GetLength(), il faut mettre stopTime au lieu de endTime, comme dans le code suivant :

    public function GetLength()
    {
         return $this->stopTime - $this->beginTime;
    }

    Sinon ça ne marche pas du tout. Merci. ^^

  4. Merci de ta vigilance, Komrod ! Tout est rentré dans l'ordre. :)

Ils ont fait un lien vers cet article :

  1. Nexen.net
  2. Accélérer WordPress - Plugins pour optimiser sa bande passante | Jonathan Petitcolas
  3. Benchmark d’un php simple | Point2Zero
  4. Calculer le temps d’exécution d’une fonction en C++ | Jonathan Petitcolas

Réagissez sur cet article !