Autoload avec modération
Par Laurentj le mardi, février 21 2012, 16:08 - Technologies Web - Lien permanent
Dans un billet de Loïc d'Anterroches qui cherche à comprendre pourquoi Symfony 2 est très lent dans les benchs qu'il a fait, j'ai appris via un commentaire que Symfony 2 chargeait par défaut tout un tas de truc (avant d'arriver à l’exécution même du code propre à la page), et notamment donc, des "autoloaders" pour chaque composant. D'ailleurs, un "conseil" de la part d'un "expert" SF2, indique qu'il faut désactiver des choses, comme par exemple l'autoloader pour swiftMailer, qui serait l'une des causes de ralentissement. La conséquence ne m'étonne guère au final. Par contre que SF2, soit disant un framework performant, le fasse par défaut, m'a étonné, voir même stupéfié.
Pourquoi diable configurer l'autoload de manière à ce qu'il puisse trouver une classe... qui n'est que rarement utilisé ?? Sauf cas très spécifique (je cherche encore), en effet, il est rare qu'on envoi un mail à chaque visite d'une page du site. Par exemple sur un site e-commerce, on va envoyer un mail lors de la confirmation de commande. Et cette page de confirmation de commande est une page rarement "visitée", en regard du nombre de "hits" sur les pages permettant de parcourir le catalogue produit par exemple.
Admettons que sur 100 000 page visitées, un seule page va envoyer un mail. Cela veut donc dire que, dans 99,999% des cas, la préparation de l'autoload pour trouver les classes de swiftMailer, ne sert strictement à rien. Déjà à ce niveau, on perd en temps d’exécution (celui du chargement de l'autoloader de swiftmailer), même si c'est quelques milli-secondes (allez, disons micro-secondes), et on perd en occupation mémoire, même si c'est quelques dizaines d'octets. Cependant, quand tout ça est multiplié par le nombre de requêtes par jour ou simultanée, ça peut commencer à devenir significatif.
Alors on me répond, "oui mais bon, c'est mieux que de faire un require pour chaque page". Certes. Mais n'empêche, rien que de charger l'autoload, ça bouffe du CPU et de la mémoire pour rien, à chaque page.
Et ce n'est pas tout. L'autoload de swiftmailer, une fois chargé, est rarement actif. Il va manger encore plus du CPU et de la mémoire que ce que je disais. Comment ? tout simplement, quasiement à chaque fois que PHP tente de charger une classe. Car lors de ce processus, PHP va demander à tout les autoloaders si ils ne veulent pas essayer de charger le fichier qui contient la définition de la classe qu'il demande. Quand un framework charge, rien que pour un projet vide, des autoloadeurs à tout va, qui eux même vont faire un, deux, dix, 20 tests sur le nom de la classe ou faire des manipulations de chaîne dans tout les sens sur ce nom de classe (un autoloader compatible PSR0, n'est pas anodin en ce sens...), tout ça prend du temps, forcément. Multipliez ça par le nombre de classes utilisées dans chaque page, et par le nombre de page chargée, le temps passé dans l'autoload commence à ne pas devenir anodin (vis à vis du "temps de latence" du framework dont parle Loic), avec une part significative des autoloaders pour les composants utilisées rarement. Et on en arrive a des perfs catastrophiques, même pour un hello world.
Bref, l'autoload, c'est bien, ça évite au développeur de faire des require/require_once dans tous les sens. Mais il ne faut pas en abuser.
Dans Jelix, voici ce que je fais :
- pour les classes de jelix utilisées à chaque "page" ou presque : require. Utiliser l'autoload n'a pas de sens pour des classes utilisées tout le temps. Les classes sont chargées directement, PHP n'a pas à faire appel aux autoloaders.
- pour les classes rarement utilisées : require. Le développeur va utiliser les classes en question une fois de temps en temps, il n'y a donc pas de raison de les déclarer pour l'autoload, Le développeur peut très bien prendre 10 secondes à écrire un require là où il en a besoin (ce qui n'empêche pas le fichier inclus de configurer l'autoload pour les classes que le développeur est alors susceptible d'utiliser). Il peut même utiliser l'objet jClasses pour indiquer de charger la classe de tel module, sans avoir à connaitre le chemin.
- Pour toutes les autres, c'est à dire les classes utilisées souvent mais pas tout le temps : autoload.
On a ainsi un bon compromis entre performance et facilité de développement.
PS : l'utilisation de l'autoload pour toutes les classes dans SF2 n'explique probablement pas totalement les problèmes de performances...
Commentaires
Un petit coup de XHprof et il devient très facile de savoir à quoi s'en tenir précisément ;).
Ton problème est réel, je le comprends mieux que sur twitter, mais finalement ton problème n'est pas dans l'autoload lui même. Pour une classe rarement utilisée, le require ne gagne quasiment rien par rapport à un autoload simple.
Le problème que tu soumets c'est ce délire d'avoir des fonctions personnalisées pour l'autoload de chaque bloc logiciel voire de chaque classe.
La solution ce n'est pas le require, c'est d'arrêter les frais avec ces chargeurs de classe personnalisés et personnalisables. Java et Python gèrent très bien ça tous seuls avec une correspondance simple entre le chemin fichier et le chemin logiciel.
Si chaque classe X\Y\Z est dans le fichier X/Y/Z.php, tu peux charger swift mailer ou tout ce que tu veux en autoload, ça ne prendra pas de CPU si tu ne les utilises pas, et pas grand chose quand tu les utilises.
Pour gérer les délires projet il y a déjà une notion de include_path qui existe. Il suffit de faire un arbre général pour PEAR et les libs plus ou moins standards, un arbre pour le framework utilisé et toutes ses dépendances, un arbre pour l'application, et un arbre local pour les surcharges spécifiques. Ca fait au pire 4 recherches pour trouver le bon fichier. Avec un APC bien configuré ça ne coute virtuellement rien. En gros :
set_include_path( "/usr/lib/php" .PATH_SEPARATOR."/path/to/myframework" .PATH_SEPARATOR."/path/to/myapp" .PATH_SEPARATOR."/path/to/myoverrides" ); spl_autoload_extensions(".php"); spl_autoload_register();Rien de plus, et surtout aucun autre appel à spl_autoload_register qui ne soit franchement motivé avec raison.
Ce qu'il faut jeter ce n'est pas l'autoload, c'est l'idée de brancher plein de gestionnaires de chargement "magiques" à chaque fois qu'on ajoute une lib ou un bloc logiciel. Ca c'est mal, et Dieu vous le fera payer.
Je rejoins partiellement Eric D sur ce point.
Partiellement, car je pense aussi qu'on est en plein délire dans les autoload : quand j'utilise une fonction de 3 lignes dans les "bootstrap" de mes framework maison, sensio (zend n'est pas mieux) utilise 5 classes rien que dans le composant de base de Symfony.
Mais, la force de symfony est de pouvoir prendre des projets tiers (ou bundle) avec leur propre logique d'architecture (cas typique de SwiftMailer ou l'autoloader joue le role de bootstrap pour configurer les injections de dépendances) sans avoir a se soucier d'initialiser tel ou tel sous-composant du bundle avant de l'utiliser.
Certes, cela se fait au détriment du chargement d'une classe de 70 lignes et de l'enregistrement d'une méthode dans la stack spl_autoload
Personnellement, je reproche à Symfony la gestion des dépendances, où la terre entière est chargé a chaque fois qu'on utilise un service. Si une des méthodes d'un service à une dépendance sur un autre composant, celui ci est injecté lors de la construction du service. C'est propre, facile a tester, mais si ce composant n'est utilisé que dans une seule méthode, il se retrouve instancié a chaque fois que j'utilise ce service, et il est possible que ce composant instancie d'autres composants dont il dépend. Bref, toute personne qui a eu le malheurs de faire un print_r (ou var_dump) de quoi que ce soit sur symfony comprendra de quoi je parle.
J'espère qu'avec php 5.4 et les traits, Sensio trouvera quelque chose de plus propre, où l'utilisation d'une fonction getComposantXyz dans le service instanciera si nécessaire le dit composant au moment où le service en a besoin.
@Eric : je te rejoins sur ton idée. D'ailleurs, Jelix essaye de limiter au maximum la multiplicité des autoloaders. En gros, il y en a un pour le core, et un pour les modules, avec chacun leurs listes de mapping. Mais comme l'a dis Jeremy, dans pas mal de framework, on en a une multitude, chaque module/composant proposant son autoloader.
Sinon, je vais chipoter : qu'on ait un seul autoloader avec un mapping ou l'utilisation de includepath, plus il y a de classes plus il y a de vérifications pour trouver le bon fichier. Donc pour moi, un require pour une classe rarement utilisée reste plus efficace que si PHP doit la trouver via un autoloader ou include_path. Il n'y a pas de recherche, et cela ne contribue pas à l'alourdissement des recherches pour les autres classes (pour un includepath, PHP doit faire pas mal d'accès disque, c'est pas anodin).
Le include path (ou classpath, sys.path ou autre suivant les langages) c'est la base assez habituelle. Il ne faut pas l'augmenter à l'infini mais si il est bien ordonné et qu'on limite à 4 ou 5 les chemins de recherche, c'est souvent acceptable.
Logiquement pour ton mailer, ça devrait se trouver dans un répertoire des libs système (paquet installé hors du framework) ou dans les libs du framework. Aucune raison de rajouter un chemin spécifique.