iPerSec
internet Performance & Security

La face cachée de require_once

octobre 26th, 2006 by jfbus

… ou quel est l’impact sur les performances (pas si neutre) d’utiliser abondament require_once.

Optimiser un site internet écrit en PHP comporte un certain nombre de facettes. Certaines sont bien connues et documentées : optimisation des requêtes SQL, cache (de code PHP, de pages, …), optimisation des algorithmes, … L’étape d’optimisation suivante consiste à s’intéresser au fonctionnement de PHP.

Nous allons ici analyser le fonctionnement de require et require_once.

Cet article essaie d’être indépendant de la version PHP utilisée. Sachez toutefois que PHP 5.1 commence à inclure des optimisations pour régler le problème évoqué ici (en particulier le “realpath cache”).

require et include sont quasiment identiques (en termes de performances). Vous pouvez remplacer dans ce qui suit require_once par include_once et require par include.

La face cachée de require_once

Il est plutôt confortable de généraliser l’utilisation de require_once (ou include_once) pour gérer ses inclusions en PHP. Le cas extrême consiste à systématiquement inclure les dépendances (classes mères, librairies) de tous ses fichiers :

ClasseFilleA.class.php
<?php
require_once("ClasseMere.class.php");
class ClasseFilleA extends ClasseMere {
[...]

ClasseFilleB.class.php
<?php
require_once("ClasseMere.class.php");
class ClasseFilleB extends ClasseMere {
[...]

Facile à coder, facile à maintenir, me direz vous. Que des avantages !

Et bien non, pas que des avantages. La cible potentielle d’un site Internet étant plutôt vaste, la question des performances se pose, et il s’avère à l’expérience que le coût technique de tous ces appels à require_once est loin d’être négligeable. Ce coût peut sérieusement limiter la tenue à la charge de votre site Internet.

Autopsie d’un require_once

Analysons le fonctionnement de la ligne de code suivante :
require_once("ClasseMere.class.php");
Pour exécuter cette ligne, PHP doit :

  1. trouver le fichier dans le include_path
  2. normaliser le chemin (remplacer les liens symboliques par des chemins physiques)
  3. ouvrir le fichier PHP
  4. compiler le fichier ouvert

Si l’on utilise require, PHP effectue les opérations suivantes :

  1. trouver le fichier dans le include_path
  2. le compiler, en commençant par
  3. normaliser le chemin (remplacer les liens symboliques par des chemins physiques)
  4. ouvrir le fichier PHP

Chacune de ces étape fait des appels au disque.

Linux essaie de cacher en mémoire un maximum d’informations trouvées sur disque (informations sur les répertoires/fichiers, contenus des fichiers, …). Grâce à ce cache, les “appels disques” faits au système n’aboutissent généralement pas souvent au disque. Ces informations sont stockées dans la mémoire libre (non utilisée par les applicatifs). Ce qui explique qu’un système Linux actif a généralement 100% de sa mémoire occupée, sans que ce soit un problème (cette mémoire cache est libérée lorsqu’un applicatif a besoin de mémoire).
Il faut donc dans la plupart des cas prévoir sensiblement plus de mémoire sur un serveur Linux que ce que les applicatifs vont utiliser, surtout si l’on a des disques lents (ex : disque ATA ou SATA).

La normalisation du chemin teste tous les répertoires de l’arborescence en partant de la racine (4 niveaux de répertoire = 4 appels). Cette normalisation est répétée pour chaque recherche dans le include_path (qui rajoute un appel disque supplémentaire pour vérifier la présence du fichier).

Si votre fichier trouvé en 4è position, avec à chaque fois 4 niveaux de répertoire, cela veut dire une vingtaine d’appels disque.

L’ouverture du fichier PHP a l’impact standard d’une ouverture du fichier, et la compilation d’un fichier revient (vis à vis du disque) à sa lecture.

Quel est l’impact d’un accélérateur PHP (cache d’opcode) ?

Vous avez sans doute déjà installé un accélérateur PHP sur votre serveur, pour augmenter les performances de votre site web.

Il faut savoir que l’accélérateur PHP intercepte l’appel de compilation du fichier. Dans le cas d’un require_once, les étapes 1 à 3 sont gardées. Dans le cas d’un require, seul l’étape 1 est conservée.

L’accélérateur PHP fait généralement un appel au disque (stat) pour vérifier la date de dernière modification du fichier, appel qui peut être généralement débrayé pour ceux qui veulent des performances extrêmes.

Dans le cas d’un fichier en 4e position de l’include path, avec des chemins à 4 niveaux, un require_once fera donc toujours la vingtaine d’appels disque, ce qui semble perfectible. Ces 20 appels seront répétés à chaque require_once, même si le fichier est déjà inclus. 5 require_once de ce type = 100 appels disque.

Le require lui, est beaucoup plus rapide, puisque beaucoup d’appels disque sont court-circuités par l’accélérateur.

Comment optimiser les performances ?

De cette analyse, nous pouvons tirer 2 conclusions :

  • évitez require_once comme la peste ! Diviser par 5 le nombre de require_once présents dans votre code supprimera énormément d’appels disque effectués par votre code (le nombre exact dépend de votre code, en gros position moyenne dans le include path x profondeur de répertoires).
  • utilisez des chemins absolus plutôt que des chemins relatif, ce qui courcircuite l’étape du include_path.

Voici quelques trucs :

1/ Remplacer le premier require_once par un require

Cela vous fait toujours un require_once de moins…

Les deux codes sont fonctionnellement identiques :
require("ClasseMere.class.php");
require_once("ClasseMere.class.php");

et
require_once("ClasseMere.class.php");
require_once("ClasseMere.class.php");

2/ définir en constante le répertoire de base de votre code :
define("CODEBASE", "/data/www/monsite/");
[...]
require(CODEBASE.”ClasseMere.class.php”);

3/ utiliser l’autoload de PHP 5

Si je reprends l’exemple de la doc (dans lequel je remplace require_once par require et auquel je rajoute le chemin absolu) :
<?php
function __autoload($class_name) {
require(CODEBASE.$class_name.'.php');
}
$obj = new MyClass1();
$obj2 = new MyClass2();
?>

Charger un fichier via autoload est plus lent que de le charger directement. Utiliser l’autoload pour ne charger qu’un fichier n’a donc pas de sens (en termes de performances). Par contre, si cela divise par deux le nombre de fichiers chargés, le jeu en vaut largement la chandelle.

4/ Ne pas avoir d’include_path trop long !

Evitez d’avoir plus de 2 entrées dans votre include_path (et si possible une seule).

Posted in French, PHP, Tuning

One Response

  1. Tiger-222

    Article très intéressant !

    Une petite question me vient à l’esprit, est-ce que l’on allègerait le travail en faisant comme ceci :

    require (dirname(__FILE__).’/chemin/fichier.ext’;
    ou encore :
    require (getcwd().’/chemin/fichier.ext’;

    ?

    À moins que la fonction dirname ne mange le peu de ressources économisées ?

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.