Entries for category Programming

Thu Jun 03 2010

Un point sur les Traits de PHP

et peut être même plus si affinité

Filed under PHP, Programming. 7 comments

Dernièrement, les Traits ont fait leur entrée dans le trunk de PHP. Je ne vais pas les présenter dans leur intégralité puisque la RFC correspondante dont les premières esquisses ont plus de deux ans, est suffisamment claire et détaillée.

Cependant, le document n'est pas très explicite en ce qui concerne l'utilisation d'interfaces et de traits au sein d'une même classe, ainsi que le statut des variables membres et leur accessibilité dans les méthodes des traits. Nous allons voir ce qu'il en est et les conséquences que cela implique.

Avant de commencer, il est vivement recommandé d'avoir lu la RFC. Juste la première partie qui concerne les traits. La seconde partie présente les Grafts que nous n'aborderons pas ici [1].

L'opération de flattening, aka le lissage

L'ordre dans lequel les traits sont manipulés par php est le suivant:

  • Un fichier source est chargé, et comme par hasard, celui-ci contient des traits.
  • Le parser rencontre une déclaration de trait. Ce dernier est pris en compte comme n'importe quelle classe [2]. C'est à dire qu'une structure zend_class_entry est crée et complétée. Seul un flag spécial précise qu'il s'agit d'un trait.
  • Le même parser rencontre une classe qui utilise un trait via le mot clé use ou un trait qui dépend d'un autre trait (on dit qu'il s'agit d'un trait composite). A la fin de la déclaration de la classe, que l'on appelle une client class ou host class, les opérations s'enchainent ainsi:
    • Les méthodes provenant des traits sont triées selon les directives as et insteadof. Une nouvelle table temporaire est construite accueillant ces méthodes candidates à la future intégration dans la classe. Certaines méthodes peuvent être ignorées s'il y a des conflits. On se chope alors un "Warning: Trait method blah has not been applied, because there are collisions with other trait methods".
    • L'opération de flattening (lissage) commence. Pour chaque méthode candidate qui n'est pas redéfinie (overridden) dans la classe client:
      • Vérification du prototype (la signature) par rapport aux éventuelles méthodes des classes parents. S'il y a des différences, et selon la configuration, c'est là qu'on reçoit un "Strict Standards: Declaration of Machin::truc() should be compatible with..".
      • La méthode est dupliquée (son zend_op_array est littéralement copié comme le fait l'extension runkit) et est ajoutée à la table des méthodes de la classe client. Certaines opérations irréversibles se produisent, notamment le fait que la méthode perd toute notion et tout lien avec son trait d'origine [3].
    • Vérification de la bonne conformance de la classe (maintenant dotée de nouvelles méthodes provenant des traits) par rapport aux méthodes abstraites ainsi qu'aux interfaces sensées être implémentées.
  • Le parser continue son job jusqu'à la fin du fichier.

OK, voila une bonne chose de faite.

Sachant que les méthodes des traits sont injectées pratiquement "les yeux fermés", tout se passe comme si celles-ci étaient définies naturellement dans la classe client. Les accès à $this se font exactement de la même manière, rien ne change.

On peut dès lors s'interroger sur certains idiomes qui ne devraient pas tarder à fleurir sur les coins de tables en cette période pré-estivale. En effet, la nature stateless des traits tels qu'implémentés à l'heure actuelle dans php n'est pas sans conséquence quant à l'organisation du code.

L'état des traits

En fait, ils n'en ont pas. On dit qu'ils sont stateless. C'est un choix délibéré et argumenté dans les documents académiques du Software Composition Group (SCG) sur lesquels l'implémentation de php est basée.

Les traits sont uniquement fonctionnels. Ils ne sont pas liés à un quelconque état qui leur serait propre, comme cela peut être le cas dans de l'héritage multiple. Par état, il faut comprendre variables membres. Seule la classe client peut en fournir.

Or, il y a fort à parier que des traits ne manipulant aucun état se feront rares.

Donc, que faire ?

Je vais mettre de coté l'utilisation de variables globales, statiques ou de Singletons. On veut juste faire en sorte que les traits puissent utiliser les variables membres de la classe client. Il n'y a pas 50 possibilités, il y en a juste 2:

  • Accès direct aux variables via $this->
  • Recours à des getters/setters fournis par la classe client

Quoique l'on fasse, ou que je sois, rien ne t'efface, je pense à toi, le trait est dépendant de sa classe client. Accéder directement à une variable avec $this->var peut (pourrait, pourra, pouvait, a pu?) sembler briser l'encapsulation, et nous reviendrons sur ce point. Utiliser des getters et setters est la méthode conseillée par le SCG en particulier parce que leur démonstration et implémentation est basée sur le langage Smalltalk et que de toute manière ils ne peuvent faire autrement.

OK, OK. Il est grand temps qu'on balance un code illustratif car toute cette prose commence à devenir lourdingue ;)

trait T1
{
  public function doSomething() {
    $this->var = 42;
    echo $this->var;
  }
}

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
}

class C
{
  use T1, T2;
  
  private $var;
  
  private function getVar() {
    return $this->var;
  }
  private function setVar($value) {
    $this->var = $value;
  }
}

$c = new C;
$c->doSomething();
$c->doSomethingElseButDoItWell(); // please

var_dump($c);

Cela donne, comme attendu:

4243object(C)#1 (1) { ["var":"C":private]=> int(43) }

Dépendances et requirements

La dépendance classe -> trait est simplement exprimée dans le code via le mot clé use. Celle-ci est vérifiée dès la compilation. La dépendance trait -> classe n'est quant à elle pas exprimée du tout dans le code précédent, en tout cas pas avant l'exécution. T1 dépend de la présence de la propriété $var et T2 dépend des getter et setter getVar() et setVar().

La RFC préconise l'usage de méthodes abstraites au sein des traits pour exprimer et vérifier à la compilation cette dépendance trait -> classe. Le document parle de requirements, ce qui est la même chose.

Réécrivons T2 en le bardant de méthodes abstraites afin de se conformer aux bonnes pratiques au paragraphe du dessus:

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
  
  /*private*/ abstract function getVar(); 
  /*private*/ abstract function setVar($value);
}

Ce frêle bout de code est en fait plus profond que ce que l'on pourrait imaginer à première vue, surtout quand on sait que ça compile et que ça fonctionne.

Premièrement, bien que les getters/setters de la classe client soient de visibilité private, cela ne pose pas de problème de les déclarer public dans le trait. La règle des méthodes abstract dit pourtant que la méthode concrète doit avoir une visibilité identique ou moins restrictive que la déclaration abstraite.

Cela fonctionne parce que l'opération de lissage décrite plus haut ne concerne que les méthodes des traits qui ne sont pas redéfinies dans la classe client. Cette redéfinition ne se fait que sur le nom de la méthode. La signature n'intervient pas du tout. La méthode qui porte le même nom dans la classe va complètement masquer celle du trait. Il est donc tout à fait possible de remplacer setVar($value) dans le trait par ceci:

/*private*/ abstract function setVar($value, array $bonus);

On pourrait s'attendre à un beau "Fatal error: Declaration of C::setVar() must be compatible with that of T2::setVar()" car les signatures sont clairement différentes. Et bien non. La phase de lissage rend possible ce comportement.

Exprimer les requirements d'un trait par l'intermédiaire de méthodes abstraites n'est donc que partiel. Seul le nom de la fonction compte. La signature et les éventuels type hintings sont ignorés.

Mais alors pourquoi ça fonctionne quand même? C'est simple, toutes les méthodes abstraites du trait qui ne sont pas masquées par celles de la classe client se retrouvent dans cette même classe toujours avec le statut abstract. Là php peut détecter la non implémentation de ces méthodes et ainsi s'opère la vérification partielle des dépendances trait -> class.

Deuxièmement (le premièrement est 4 ou 5 paragraphes plus haut;) pourquoi ai-je supprimé la visibilité private pour les méthodes abstraites du trait? Après tout, les getter et setter sont privés dans la classe C.

C'est une bien sage question.

La réponse est que l'on veut éviter cet effet indésirable: "Fatal error: Abstract function T2::getVar() cannot be declared private". C'est une conséquence du fait que les traits sont traités (c'est le cas de le dire) par php comme de simples classes lors de la compilation. Ce point fut brièvement abordé plus haut ici. Or, une méthode abstraite ne peut être déclarée private car php considère que celle-ci n'est pas accessible depuis des classes dérivées et qu'il est donc impossible d'y apporter une implémentation. Quelque part, ça a du sens...

De l'existentialisme

Jusqu'ici, nos getters et setters font leur job. On n'en demande pas plus. Pourtant, un sérieux problème se profile à l'horizon.

C::getVar() retourne une copie de la variable membre $var. Sa valeur n'est pas systématiquement dupliquée, php faisant du copy-on-write, mais nous restons dans une sémantique de copie. Toute modification opérée sur ce que retourne getVar() n'aura pas de répercussion sur la véritable variable membre. Cela peut être tout à fait justifié et suffisant.

Cependant, prenons un peu de recul. Nous avons affaire à un getter privé dont l'appel se fait d'une méthode pleinement intégrée à la classe et qui pourrait tout à fait être privée elle aussi. Nous évoluons dans un no man's land à la frontière de l'encapsulation, l'indirection et l'abstraction. Le fameux triptyque des mots en ion.

Le trait pourrait avoir besoins de manipuler autre chose qu'un simple scalaire, par exemple un array. Va-t-il falloir définir toujours plus de setters pour toujours plus d'opérations telles que l'ajout, l'insertion ou le retrait d'éléments? Faut-il gonfler la classe client en méthodes annexes à faible valeur ajoutée, annulant au passage les bénéfices en copy'n'paste sensés provenir de l'utilisation même du trait?

Une solution à mi-chemin entre rien du tout, c'est à dire accéder directement à la variable membre via $this->var, et une belle série de getters/setters serait de recourir à un simple accesseur retournant une référence sur la variable. Coté trait, l'affectation classique = ou par référence =& permet de controller la nature de la liaison suivant l'opération que l'on désire effectuer, ce qui laisse à php la possibilité de faire son job routinier de passage (retour) par copie.

Il ne reste plus qu'à dégainer les variables temporaires. Oui, car comme chacun sait, php est très frileux quand il s'agit de déréférencer automatiquement les retours de fonctions. Vous aurez beau bidouiller en ajoutant des parenthèses ou des accolades, seule la dernière ligne n'est pas une erreur dans les constructions suivantes:

$this->func()[]
$this->func()()
++$this->func()
$this->func() = 
$this->func()->chain()

C'est navrant, et c'est la syntaxe qui n'est pas bonne, le problème détecté étant un parse error dans la plupart des cas. Pour contourner ce problème, il faudra d'abord récupérer le retour de la fonction dans une variable puis travailler à partir de celle-ci.

Traits et Interfaces

Nous allons clore cet article par un exemple qui réunit tout ce qui a été vu: faire endosser à un trait l'implémentation d'une interface héritée de la classe client. Cette construction est à mon sens potentiellement utile pour les auteurs de libraires et frameworks.

Une telle classe client peut être utilisée dans un contexte polymorphique ou de type hinting sans reposer pour autant sur une hiérarchie parfois bancale de classes abstraites, ni sur la présence d'adaptateurs spécifiques et verbeux dans le cadre d'une composition ou aggrégation.

On choisit d'implémenter ArrayAccess qui est automatiquement disponible dans php car fournie par la SPL.

/*
interface ArrayAccess
{
  public function offsetGet($offset);
  public function offsetSet($offset, $value);
  public function offsetExists($offset);
  public function offsetUnset($offset);
} */

trait ArrayAccessImpl
{
  abstract function &data();
  
  public function offsetGet($offset) {
    $d = $this->data();
    return $d[$offset];
  }
  
  public function offsetSet($offset, $value) {
    $d = &$this->data();
    $offset === null ? $d[] = $value : $d[$offset] = $value;
  }
  
  public function offsetExists($offset) {
    return array_key_exists($offset, $this->data());
  }
  
  public function offsetUnset($offset) {
    $d = &$this->data();
    unset($d[$offset]);
  }
}

class MyClass implements ArrayAccess
                     //, Iterator // laissé en exercice
{
  private $data = array(
    'a' => 3.14,
    'b' => null,
    'c' => true,
  );
  
  use ArrayAccessImpl;
  
  private function &data() {
    return $this->data;
  }
}

$c = new MyClass;

var_dump(isset($c['b']));
$c[] = $c['a'];
unset($c['c']);
var_dump($c);

Note: l'utilité plus que douteuse de MyClass n'est pas fortuite, il s'agit juste d'un code d'exemple (ma spécialité;)

Conclusions

Comme d'hab, RAS?

Traits're in da trunk, mais cela ne veut pas dire pour autant qu'ils seront dans le futur php, ni sous leur forme actuelle. Cependant, vu l'avancement du projet, le contraire serait étonnant.

Si tout se passe comme prévu, alors j'ai hâte de voir comment ils seront employés à l'avenir. Il s'agit d'un réel nouveau paradigme pour composer du code OOP. Par exemple, pour valider la pertinence des traits, le SCG a réimplémenté toute la hiérarchie des Collections de Smalltalk.

Je salue mageekguy pour son billet Et si on tirait des traits ? qui est à l'origine de mon soudain regain d'intérêt.

 

[1] Le statut des grafts est bien plus hypothétique puisqu'aucune implémentation n'existe à l'heure actuelle.

[2] Un trait peut déclarer des variables membres ou dériver d'une classe. Cela n'est pas interdit (pour l'instant?). Il ne peut cependant pas implémenter une interface. Cela résulte en une fatal error.

[3] Les éventuelles variables membres déclarées dans le trait auquel la méthode appartenait sont purement et simplement ignorées.

Tags php, traits
Thu Apr 08 2010

Boost Phoenix 2.0 tips

Lazy lazy evaluation + bind to overloaded member

Filed under C++, NoteToMyself, Programming. 0 comment

Quelques tips pour Boost.Phoenix2 glanés dans la sueur et dans le sang (dans la doc aussi).

Higher higher order function

On a vite fait d'imbriquer les algorithms de Phoenix, mais il ne faut pas oublier qu'ils sont déjà des higher order functions. Leur prédicats doivent être évalués de manière lazy et même lazy lazy si je puis dire.

Un code vaut mieux qu'un long schema à main levée. Dans ce qui suit, j'ai deux std::vector de int. Je veux afficher les éléments du premier vector qui ne sont pas dans le second. Un code à la con qui a juste valeur d'exemple. Dedans y est glissée une erreur grossière. Saurez-vous la trouver ? Votre compilo saura la trouver lui, et il se fera un plaisir de générer environ 50ko de message d'erreur :)

#include <iostream>
#include <vector>
#include <algorithm>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/home/phoenix/scope/local_variable.hpp>
#include <boost/spirit/home/phoenix/statement/if.hpp>
#include <boost/spirit/home/phoenix/scope/let.hpp>
#include <boost/spirit/home/phoenix/container.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_algorithm.hpp>

int main()
{
    using boost::phoenix::arg_names::arg1;
    using boost::phoenix::local_names::_a;
    using boost::phoenix::ref;
    using boost::phoenix::let;
    using boost::phoenix::if_;
    using boost::phoenix::find_if;
    using boost::phoenix::end;
	
    std::vector<int> a;
    std::vector<int> b;
	
    a.push_back(1);
    a.push_back(2);
    a.push_back(3);
	
    b.push_back(2);
	
    std::for_each(a.begin(), a.end(),
        let(_a = arg1)
        [
            if_(end(ref(b)) == find_if(ref(b), arg1 == _a))
            [
                std::cout << _a << std::endl
            ]
        ]
    );
	
    return 0;
}

Le problème est que find_if attend un prédicat, dont on pourrait penser qu'il est tout à fait correcte en tant que composite phoenix arg1 == _a. Mais le bloc du find_if est déjà un composite, autrement dit une higher order function. Son prédicat doit être évalué de manière lazy lazy et non pas lazy -tout court.

La solution est fournie par phoenix: le composite lambda va permettre de transformer le prédicat récalcitrant en une higher higher order function. Le tour est joué:

std::for_each(a.begin(), a.end(),
    let(_a = arg1)
    [
        // lambda[arg1 == _a]
        if_(end(ref(b)) == find_if(ref(b), lambda[arg1 == _a]))
        [
            std::cout << _a << std::endl
        ]
    ]
);

Bind to overloaded member function

Qui n'a jamais été tenté de binder à gogo avec phoenix, tellement c'est facile ? Le prob survient le jour où on tente désespérément de binder une fonction qui a des overloads. le compilo se plaint avec un message du genre bind(<unresolved overloaded function type>, ..25 lignes de types phoenix..).

Example. Je cherche à binder std::map<K,V>::find. On va prendre int pour K et V. m est une instance quelconque de ce type map:

typedef std::map<int, int> map_t;

bind(&map_t::find, ref(m), arg1);

Pas bon. map_t::find a plusieurs overloads. Le compilo ne sait pas lequel choisir. Il faut le guider, après tout, c'est nous les boss dans l'histoire!

Méthode one liner:

bind(static_cast<
        map_t::const_iterator (map_t::*)(int const&) const
     >(&map_t::find), ref(m), arg1);

On utilise ce bon vieux static_cast.

Autre méthode, passer par un pointer sur fonction membre:


map_t::const_iterator (map_t::*find_f)(int const&) const = &map_t::find;

bind(find_f, ref(m), arg1);

Ca fait son job. On n'en demande pas plus.

Conclusion

RAS

Fri Mar 19 2010

r8t, un moteur de template pour C++ à base de javascript

Google V8 inside

Filed under C++, Programming. 2 comments

L'idée de base, mais vraiment de base, consiste à clamer à qui veut bien l'entendre que JSON est un format idéal pour passer des données à un système de Vue, comprendre View, dans un MVC. Ce format a tout ce qu'il faut: simple, compact, expressif et connu de tous, ou presque.

Le constat de base, mais vraiment de base (oui vous savez), c'est qu'il y a très peu de systèmes de template pour C++ qui ont un penchant web, c'est à dire principalement orientés génération de HTML. Je ne sais pas pourquoi, peut être parce que tout le monde s'en fout ? ;) Toujours est-il que lorsque je me suis mis en recherche d'un tel système de template, pour des besoins perso, je n'ai rien trouvé d'autre que Clearsilver et google-ctemplate. Il me fallait un truc standalone et léger. J'aurai pu chercher un peu plus, mais je me suis arrêté là, pas vraiment convaincu.

C'est là que je me suis souvenu de l'idée de base: JSON. En fait ça tombait bien puisque j'utilisais déjà le moteur javascript V8 dans mon projet. Cet engine, initialement conçu par google pour le navigateur Chrome, est open source et son API est en C++. L'idée qui a commencé à émerger est la suivante:

  • On balance du "JSON" au système de template
  • La logique de présentation est controllée par javascript
  • Le système crache du text en retour (principalement du HTML)

Sympa. Reste plus qu'à implémenter ça.

Le JSON, expédié avec une technique à base de Boost.Variant récursif que j'ai présenté dans ce billet Simple modélisation de JSON en C++.

Le javascript qui controle la logique de présentation est embarqué dans les templates sous une forme proche d'un mix de Django et de la syntaxe alternative de php.

{% for (i in posts) : %}
  <div class="post">
    <h2>{%= posts[i].title %}</h2>
    ...
  </div>
{% end %}

Le principe c'est que tout ce qui est entre {% ... %} est grosso modo du javascript qui sera inchangé avant de le donner au moteur js. Une phase de parsing transforme le fichier de template (ou n'importe quel text) en une forme intermédiaire qui sera consumée par V8. Pour l'exemple ci-dessus, cela donne quelque chose proche de ceci:

for (i in posts) {__pr('  <div class="post">\n    <h2>');
__p(posts[i].title);__pr('</h2>\n    ...\n  </div>\n');};

Le parser est écrit avec la librairie Boost.Spirit Classic. __p() pour "print" et __pr() pour "print raw" sont des fonctions javascript dont l'implémentation est en C++ et qui permettent de controller la sortie textuelle finale, par exemple en appliquant des filtres d'échappement automatique pour du HTML. Ces filtres sont inspirés des modifiers de google-ctemplate. Du genre {%:h:s= comment %} pour un filtre h qui échappe du HTML et un autre s qui permet de wrapper des snippets entre des éléments <pre>.

Ce qui est sympathique c'est qu'il est possible de transformer des sources javascript textuelles en un byte code propre à V8. C'est dans l'API. On peut donc mettre en cache des templates pre compilées, un peu comme le fait APC pour php, mais ça dépote encore plus! J'avais effectué quelques benchmarks qui laissaient penser que ce moteur de template à base de V8 était plus véloce que php+APC. Il faudrait que je mesure ça plus sérieusement.

A vrai dire ce projet de système de template a été codé à l'arrache courant novembre 2009. Un simple proof of concept à l'origine. Les dernières librairies de Boost notamment Spirit 2 (ça tue!), Phoenix 2 et Fusion 2 (OK, beaucoup de 2) m'ont donné envie de réécrire un tel système. J'héberge le truc sur github. Je lui ai donné le joli nom de r8t, réunion de rat et V8. Pour l'instant, c'est succinct. Pourquoi rat? Parce que ;)

Thu Jan 07 2010

SplClassLoader php extension benchmarks

With ApacheBench

Filed under PHP, Programming. 3 comments

UPDATE (2010-01-09T22....Z): a small change (yet to be properly benchmarked) yields linear performance gains: the extension brings about 30% more RPS than the user code in all tests. /UPDATE

Ok, the C implementation of SplClassLoader is here on github. I am afraid I won't have much time in the coming days to follow what is going on. Anyway I took the time to do some benchmarks with ApacheBench. I will present the results in this post.

In order to ease things, I wrote a python script that generates a dummy directories subtree filed with php files containing blank classes with their proper namespace. The script also generates a php file named batch_instances.php (by default) which is responsible for instantiating all the generated classes in a row. It looks like that:

$a = new \vendor0\AA;
$a = new \vendor0\AAA;
$a = new \vendor0\B_B;
$a = new \vendor0\B_BB;
$a = new \vendor0\sub0\AA;
$a = new \vendor0\sub0\AAA;
...
$a = new \vendor1\sub0\AA;
$a = new \vendor1\sub0\AAA;
...
$a = new \vendor1\sub1\sub1\AA;
$a = new \vendor1\sub1\sub1\AAA;
$a = new \vendor1\sub1\sub1\B_B;
$a = new \vendor1\sub1\sub1\B_BB;

That php file is then included for the various benchmarks. The python script (fs_gen.py) is located under tests/for_bench but it is not integrated with the extension tests suite.

Setup

  • MacBook Pro 2.53ghz 4gb ram
  • nginx 0.8.27
  • PHP_5_3_FPM trunk compiled with --disable-all --enable-maintainer-zts. Unix sockets.

Note: APC was not loaded (nor any other accelerator.. I would have to install it btw).

Protocol

I did 3 kind of tests:

  • SplClassLoader() used without namespaces nor path arguments. We have to configure the include_path.
  • SplClassLoader('vendorN') where N goes from 0 to 3 at maximum. It means we have several 'vendor' namespaces. Note that the second argument (the path) is not passed to the constructor.
  • SplClassLoader('vendorN','/pathN') where the path is absolute. The php include_path is not used.

For example, the second test bootstrap file looks like this:

<?php
// 'vendorN' directories are under lib/
set_include_path(
   __DIR__.PATH_SEPARATOR
  .__DIR__.DIRECTORY_SEPARATOR.'lib'.PATH_SEPARATOR
  .get_include_path() // mine is empty btw
);

$v0 = new SplClassLoader('vendor0');
$v0->register();

$v1 = new SplClassLoader('vendor1');
$v1->register();

include 'batch_instances.php';

Since both implementations declare the same class SplClassLoader be it a user script or an extension, the php implementation has been renamed to SplClassLoaderUser.

I noted each result returned by ApacheBench. For each test, a few warm ups then 10 runs in a row. Results in the following tables are in requests per second. Also, the lowest and the highest numbers were removed prior calculating the mean.

The exact arguments passed to ab are not known at that time ;) Depending on the number of classes loaded, sometimes it was -n 3000 or -n 1000 but always -c 1 and of course always the same for a given test.

Results

24 classes under a unique 'vendor0' namespace containing several sub namespaces:

        test 1     test 2      test 3
        simple     with ns     with ns+path

user    496        429         449
ext     554        543         576
ratio   1.117      1.266       1.282

70 classes under two namespaces containing several sub namespaces:

        test 1     test 2      test 3
        simple     with ns     with ns+path

user    221        182         192
ext     232        228         246
ratio   1.050      1.252       1.281

Note: there were no underscore characters in the classes names used for the above bench.

126 classes under two namespaces containing several sub namespaces:

        test 1     test 2      test 3
        simple     with ns     with ns+path

user    119         99         104
ext     126        124         133
ratio   1.059      1.253       1.279

Conclusions

Results are indeed extremely regular. In fact I performed several other tests with different directories layouts but ratios were roughly the sames.

Internally, the C implementation does nothing more than the php implementation. spl_autoload_register() is called through the Zend Engine. There is no magic door. When more computations are involved as in the test number 3 (comparing namespaces and appending paths) the C implementation is ahead.

In fact these numbers suggest that we better have to pass both a namespace and an absolute path to the SplClassLoader constructor when using the extension. On the contrary the php implementation performs better with no argument at all, ie. no namespace comparison. Low level string manipulations are faster in C. All in all, both implementations call include() through the ZE.

One advantage of course concerning the extension is that the class is registered at module initialization (MINIT). The class remains in the engine between requests (if the sapi allows it).

PS. Why did I write that extension? I was looking for a simple (no dependencies) extension to illustrate a tutorial I am supposed to write (second part) for this blog (in french). I came up with SplClassLoader but I am by no way a ZE specialist.