Benchmarking PHP – Partie 1

Pour ceux qui ne connaîtrait pas encore PHP, je vous laisse découvrir sa définition sur le site officiel.

Comme tous les langages de scripting, PHP est un outil très permissif et configurable à volonté qui facilite grandement le travail des développeurs.
Cependant, la permissivité des outils est parfois source de mauvaises habitudes pour les développeurs et ce qui peut apparaître comme étant une fonctionnalité intéressante pour gagner du temps, s’avère en fait être un problème en terme de performance au final.

Le premier benchmark de cette série porte sur les différentes façons d’accéder à une entrée de tableau associatif sans vérifier son existence au préalable ou, au contraire, en vérifiant son éligibilité selon plusieurs méthodes.

Protocole de test

Pour tester les performances de chacun des cas suivants, nous tentons d’accéder une entrée inexistante ‘id’ du tableau associatif $_GET et vérifions que sa valeur est différente de 0. Pour faire apparaître d’éventuelles différences dans nos tests, ils seront effectués sur un million d’itérations.

Voici, les différents tests qui seront effectués dans ce benchmark :

1. Aucune protection

Dans le premier test, nous n’effectuons aucun test d’existence de l’entrée dans le tableau. Le code se résume donc à :

if ($_GET['id'] != 0)

2. Protection avec array_key_exists() :

Dans le deuxième test, nous vérifions que la clé ‘id’ existe au sein du tableau $_GET grâce à la fonction array_key_exists(). Le code se résume cette fois-ci par :

if (array_key_exists('id', $_GET) && $_GET['id'] != 0)

3. Protection avec isset() :

Dans le troisième test, nous vérifions simplement si $_GET[‘id’] a été défini avant de tester sa valeur grâce à la fonction isset(). Le code est cette fois-ci :

if (isset($_GET['id']) && $_GET['id'] != 0)

4. Test avec empty() :

Dans le dernier test, nous vérifions uniquement si la variable $_GET[‘id’] est vide ou non grâce à la fonction empty(). Le code du test est donc :

if (!empty($_GET['id']))

Résultats

Voilà les résultats de l’exécution des tests :

Nombre d'itérations : 1000000

Aucune protection : 0.652613162994 ms
Protection avec array_key_exists() : 0.448069095612 ms
Protection avec isset() : 0.246340990067 ms
Protection avec empty() : 0.248647928238 ms

Nos grands gagnants sont donc isset() et empty().

Comme pouvait le laisser supposer l’introduction de cet article, ne pas protéger ses tests de variable est permis par PHP mais entraîne une réduction de performances. Le surcoût de vouloir accéder à une variable qui n’existe pas est que PHP déclenche une erreur de type E_NOTICE. Le réglage par défaut de PHP induit que cette erreur ne sera pas reportée à l’écran pour l’utilisateur via un message d’erreur, mais le déclenchement de cette erreur est bien réel.

Le cheminement de PHP dans ce cas-là est le suivant (qu’il s’agisse du gestionnaire d’erreur par défaut ou non) :

  1. Utilisation d’une entrée de tableau associatif qui n’existe pas
  2. Déclenchement d’une erreur
  3. Récupération de l’erreur par le gestionnaire d’erreur
  4. Test du niveau de reporting actuel via error_reporting()
  5. Affichage ou non d’un message d’erreur

On constate donc que ce chemin est certes le plus court pour le développeur, mais est relativement long pour PHP.

Certes, l’exécution d’un million d’itérations dure moins d’une seconde, il est donc tout à fait légitime de négliger cet aspect, mais gardons à l’esprit qu’un simple !empty($_GET[‘id’]) est trois fois plus rapide pour PHP, pour une longueur de code similaire et pour le même résultat.

En changeant cette habitude permissive, nous devrions pouvoir améliorer les performances générales de nos codes sources. Même si cela ne constitue qu’un infime gain, sur un environnement concurrentiel à très forte demande, cela peut contribuer à une meilleure qualité de service.

Note : comme le montre le code ci-dessous, les tests originaux ont été effectués sur un tableau vide. Les mêmes tests ont été effectués sur un tableau de 100000 entrées (ne contenant toujours pas l’entrée recherchée) et les valeurs sont similaires.

Code du test

<?php
    header('Content-type: text/plain; charset=UTF-8');
    set_time_limit(0);
    error_reporting(E_ALL ^ E_NOTICE);

    define('ITERATION_COUNT', 1000000);
    printf("Nombre d'itérations : %d\n\n", ITERATION_COUNT);

    $start = microtime(true);
    for ($i = 0; $i < ITERATION_COUNT; $i++) {
        if ($_GET['id'] != 0)
            echo 'ok';
    }
    $elapsed = microtime(true) - $start;
    echo "Aucune protection : {$elapsed} ms\n";

    $start = microtime(true);
    for ($i = 0; $i < ITERATION_COUNT; $i++) {
        if (array_key_exists('id', $_GET) && $_GET['id'] != 0)
            echo 'ok';
    }
    $elapsed = microtime(true) - $start;
    echo "Protection avec array_key_exists() : {$elapsed} ms\n";

    $start = microtime(true);
    for ($i = 0; $i < ITERATION_COUNT; $i++) {
        if (isset($_GET['id']) && $_GET['id'] != 0)
            echo 'ok';
    }
    $elapsed = microtime(true) - $start;
    echo "Protection avec isset() : {$elapsed} ms\n";

    $start = microtime(true);
    for ($i = 0; $i < ITERATION_COUNT; $i++) {
        if (!empty($_GET['id']))
            echo 'ok';
    }
    $elapsed = microtime(true) - $start;
    echo "Protection avec empty() : {$elapsed} ms\n";
?>
Publicités