Entries for category Programming

Thu Jan 07 2010

PHP, Programming

autoload, extension, php, SplClassLoader, standard

2 comments

SplClassLoader php extension benchmarks

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.

Sun Dec 27 2009

PHP, Programming, Tuto

extension, php, test

3 comments

Créer une extension PHP - Part 1: Installation du matos

Intro:

Qui n'a jamais rêvé de ces mondes souterrains, de ces mers lointaines peuplées de légendes ou d'une richesse soudaine qui se conquérerait au détour d'un chemin de la Cordillière des Andes ?

Okee. En fait je voulais juste dire qu'aujourd'hui on va débuter une série d'articles (probablement entrecoupés de conneries à 2 balles) dont la futile existence consiste à vous apporter les bases nécessaires pour que -un jour peut être- vous soyez en mesure d'écrire votre propre extension php en C ou C++.

Inutile de tergiverser, nous allons nous focaliser sur un environnement UNIX. Bien que de gros efforts ont été faits, développer sous Windows pour PHP est encore légèrement chiant, notamment lorsque l'on fait intervenir des librairies tierces. Néanmoins, je ne dis pas que -par chance- il se peut que un jour -par hasard- je complète la série avec un article focalisé sur Windows.

Si tout s'est bien passé, le titre parle d'installation du matos

Il nous faut:

En fait je pense que c'est à peu près tout. De toute façon, si ce n'est pas suffisant, on le saura: ça va merder. Plus tard, on pourra ajouter re2c lorsqu'on abordera (qui sait?) la customisation de la grammaire de php, par exemple autoriser la syntaxe courte pour les array [1,2].

Votre distrib linux contient probablement déjà tout le nécessaire. Si ce n'est pas le cas, vous pouvez surement installer ces tools via le système de packages ou directement via les sources. Ca fonctionne généralement très bien. Sous OSX, il faudra que vous installiez les developer tools c.a.d. Xcode et sa suite, mais rassurez vous, nous n'aurons pas besoins de le lancer.

Voici ce que nous allons faire: On va compiler et installer une version de php qui sera dédiée pour nos expérimentations. Notre extension php sera compilée avec les sources de cette version fraichement installée. Dans un premier temps, nous utiliserons exclusivement la version CLI de php: plus simple à mettre en oeuvre, notamment pour les tests.

Organisation des fichiers

On va bosser chez nous. Nul besoins de polluer /usr, NUL BESOINS D'ETRE ROOT.

$HOME/                <-- /home/machin ou /Users/machin etc
     vrac/            <-- tout sera ici
         php-5.3.1/   <-- les sources de php
         php/         <-- là où sera installé php
         phpext/      <-- le répertoire de nos extensions
               part1/ <-- la première extension

Choper et compiler les sources de php

Ca se passe ici. Je prends PHP 5.3.1 Complete Source Code en tar.gz.

$ cd ~/vrac
$ wget http://fr.php.net/get/php-5.3.1.tar.gz/from/this/mirror

Une fois les sources téléchargées, une petite vérif qui va bien:

$ md5sum php-5.3.1.tar.gz
41fbb368d86acb13fc3519657d277681  php-5.3.1.tar.gz

Sous OSX, si vous n'avez pas installé GNU Coreutils, ceci devrait fonctionner:

$ md5 php-5.3.1.tar.gz 
MD5 (php-5.3.1.tar.gz) = 41fbb368d86acb13fc3519657d277681

OK. C'est bon. On extrait le tarball compressé:

$ tar zxf php-5.3.1.tar.gz
$ cd php-5.3.1

Avant de compiler php, passons rapidos en revue ce que contiennent les sources:

configure <-- configure et génère les Makefile
ext/      <-- les extensions (core et optionnelles)
main/     <-- les sources "basiques" de php (IO, networks...)
sapi/     <-- Server Application Programming Interface (cli, cgi, apache..)
tests/    <-- les fichiers de tests (make test)
TSRM/     <-- Thread Safe Resource Manager
win32/    <-- trucs spécifiques à Windows
Zend/     <-- le Zend Engine (gestion mémoire, opcodes etc)

On va compiler et installer une version ultra light de php cli. Pour voir la liste des options de compilations:

$ ./configure --help

Ce qui nous intéresse est ceci:

$ ./configure --prefix=$HOME/vrac/php \
  --with-config-file-path=$HOME/vrac/php \
  --disable-all --enable-cli

Ensuite, si tout s'est bien passé, les dernières lignes doivent ressembler à ça:

Generating files
updating cache ./config.cache
creating ./config.status
creating php5.spec
creating main/build-defs.h
creating scripts/phpize
creating scripts/man1/phpize.1
creating scripts/php-config
creating scripts/man1/php-config.1
creating sapi/cli/php.1
creating main/php_config.h
creating main/internal_functions.c
creating main/internal_functions_cli.c
+--------------------------------------------------------------------+
| License:                                                           |
| This software is subject to the PHP License, available in this     |
| distribution in the file LICENSE.  By continuing this installation |
| process, you are bound by the terms of this license agreement.     |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point.                            |
+--------------------------------------------------------------------+

Thank you for using PHP.

On enchaine avec un grand classique:

$ make

La compilation est rapide puisque notre configuration est très light. Encore une fois, si tout est OK, on doit voir un truc du genre:

Build complete.
Don't forget to run 'make test'.

Avant d'installer les fichiers, on obtempère en lançant les tests. Ca ne coute rien. A si, ça coute pas mal de temps. C'est bien plus long que la compilation...

$ make test

Là sur ma machine, j'obtiens ceci:

=====================================================================
TEST RESULT SUMMARY
---------------------------------------------------------------------
Exts skipped    :   71
Exts tested     :    7
---------------------------------------------------------------------

Number of tests : 10899              5974
Tests skipped   : 4925 ( 45.2%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :   28 (  0.3%) (  0.5%)
Expected fail   :    6 (  0.1%) (  0.1%)
Tests passed    : 5940 ( 54.5%) ( 99.4%)
---------------------------------------------------------------------
Time taken      :  306 seconds
=====================================================================

La plupart des tests non skippés sont passés (99.4%). C'est bon. S'il y avait eu un sérieux problème, comme ça m'est déjà arrivé, c'est l'ensemble des tests qui auraient échoué.

On continue en installant les fichiers. Rappellez-vous, il seront installés dans $HOME/vrac/php. Encore une fois, pas la peine d'être root puisque nous restons dans notre home.

$ make install

OK, les fichiers ont normalement été installés dans $HOME/vrac/php. Sous OSX, php cli est nommé php.dSYM. On va le renommer en php. Sans changer de répertoire (toujours sous php-5.3.1), on fait:

$ mv ../php/bin/php.dSYM ../php/bin/php

Pour être sur que toute cette daube fonctionne, quoi de mieux que de tester notre php fraichement installé?

$ $HOME/vrac/php/bin/php --version
PHP 5.3.1 (cli) (built: Dec 26 2009 22:32:35) 
Copyright (c) 1997-2009 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2009 Zend Technologies

Si vous n'obtenez pas quelque chose de fortement similaire, alors il y a un prob! Je conseillerai dans ces cas là de prendre un café et une clope, puis de reprendre à zéro. Pour rappel, quand on est dans php-5.3.1 ceci permet de cleaner le chantier:

$ make clean

Ne pas hésiter à checker par 3 fois les paramètres passés à configure.

RAS pour php. (N)ext

Ce premier billet ne s'occupe que de l'installation des trucs nécessaires. Nous allons tout de même compiler une extension absolument bidon et la tester via le système de tests fourni par php. Ca nous sera utile pour la suite. Les sources sont sur github. A vous de vous démerder pour que part1 soit placé dans $HOME/vrac/phpext. Ceci fonctionne:

$ cd ~/vrac
$ git clone git://github.com/metagoto/phpext.git

Comme vous pouvez le voir, les sources de cette extension nommée part1 sont en dehors du source tree de php. On va donc utiliser le tool phpize que nous venons juste de compiler: ce dernier se charge de créer un fichier configure pour notre extension.

$ cd ~/vrac/phpext/part1
$ $HOME/vrac/php/bin/phpize 
$ ./configure --with-php-config=$HOME/vrac/php/bin/php-config

Ensuite, on build, mais on n'installe pas car ça ne sert à rien pour ce qu'on veut faire.

$ make
...
Build complete.
Don't forget to run 'make test'.

Et on ne lance pas les tests! Tout du moins, on ne va pas faire ce qui nous est proposé. Pour tester notre extension, la commande utilisée est plus subtile.

Tester une extension

Le lecteur consciencieux aura remarqué la présence d'un répertoire part1/tests. Dedans y figure un misérable fichier nommé 001.phpt. Ce dernier est au format compatible avec le lanceur de tests de php qui par chance a été créé lors de notre précédent appel à ./configure. Ce fichier est nommé run-tests.php.

Aussi, si tout s'est bien déroulé, notre extension compilée a été placée dans le répertoire part1/modules sous le nom part1.so. Il va donc falloir que php charge cette extension puis lance l'unique test 001.phpt. Nous sommes toujours dans le répertoire part1. La commande est la suivante:

$ TEST_PHP_EXECUTABLE=$HOME/vrac/php/bin/php \
  $HOME/vrac/php/bin/php run-tests.php \
  -d "extension=$HOME/vrac/phpext/part1/modules/part1.so"

Là le test est immédiat. Si ça passe, et ça DOIT:

=====================================================================
PASS part1 test [tests/001.phpt] 
=====================================================================

Maintenant, que fait la commande bizarre que nous venons d'exécuter ? En gros, on exécute le fichier run-tests.php avec notre php. Habituellement, ce fichier est lancé via un make test. Sauf que là nous ne sommes pas dans un source tree de php, nous sommes dans le source tree de notre extension. run-tests.php a besoins de savoir explicitement quel est l'exécutable utilisé pour les tests, c'est pourquoi on définit la variable TEST_PHP_EXECUTABLE. Aussi, nous voulons que php charge notre extension. C'est chose faite en passant l'argument -d qui permet de définir un couple clé/valeur au format ini.

Mais qu'y a-t-il dans part1/tests/001.phpt? Ceci:

--TEST--
part1 test
--SKIPIF--
<?php if (!extension_loaded('part1')) die('part1 not loaded'); ?>
--FILE--
<?php 
part1_test();
?>
--EXPECT--
test from part1

Ca me parait assez explicite. Le test est skippé si l'extension part1 est absente. Oui c'est notre extension! Le test à proprement parler consiste à exécuter la fonction part1_test(). Il s'agit de la seule fonction exportée de notre extension. Cette fonction se contente juste de faire l'équivalent d'un print 'test from part1'.

Dacodac. Nous avons maintenant en main les bases pour compiler, charger et tester des extensions php.

Pour moi c'est RAS.

Sat Dec 19 2009

Programming

php, standard

0 comment

A propos du PHP Standards Working Group

L'arrivée de php 5.3 et son support pour les namespaces, les lambda functions et autres joyeusetés a le mérite de fédérer au sein du PHP Standards Working Group le gratin des lead developers des "grands" frameworks que sont Zend Framework, Symfony, PEAR, Solar, j'en passe et des meilleurs.

De l'interopérabilité Technique

Un des objectifs du Working Group est de s'accorder sur une interopérabilité technique (technical interoperability). De quoi s'agit-il exactement ? Personne ne le sait, c'est pourquoi dans un premier temps tout du moins, les protagonistes se focalisent sur une interopérabilité du mécanisme d'autoloading dans un contexte namespaces-aware.

PSR-0 Final Proposal

Le Working Group a publié le PSR-0 Final Proposal concernant l'autoloading. Ce document décrit les conditions que les auteurs de librairies et frameworks se doivent de respecter en terme de convention de nommage et de hiérarchie de fichiers afin d'assurer cette interopérabilité. Au passage, on notera les conditions sévères qu'il est nécessaire de respecter pour espérer avoir un droit de vote ou même de participation aux débats.

En résumé, la convention est la suivante:

\<Vendor Name>\(<Namespace>\)*<Class Name>

Dans la partie \<Vendor Name>\(<Namespace>\)*, les séparateurs de namespaces seront transformés en DIRECTORY_SEPARATOR. Pour la partie <Class Name>, les underscores seront transformés en DIRECTORY_SEPARATOR. Cette dernière partie est donc backward compatible avec la convention à la Horde / PEAR.

The standards we set here should be the lowest common denominator for painless autoloader interoperability.

Et voici l'implémentation proposée de SplClassLoader

J'affirme que non seulement ce standard est inutile, mais qu'en plus il pose plus de problèmes qu'il n'en résoud.

Larry Garfield de Drupal ou Robert Lemke de TYPO3/FLOW3, tous deux membres du Working Group, expriment leur inquiétudes quant à la possible adhésion de leur projet respectif au seul PSR-0 (comprendre, la faisabilité même). Voir leurs interventions ici et ici. Le Working Groupe s'est mis dans la tête de ne travailler qu'avec une seule implémentation d'autoloader. A partir de ce moment là, soit un projet est compatible avec la convention de nommage et la structure de fichiers, soit il ne l'est pas. Au delà d'une convention, c'est toute une architecture logicielle qui est impactée, comme le montrent Garfield et Lemke.

Afin de bien comprendre, et de rigoler un peu, prenons le cas de TYPO3. Chez eux, pour un composant donné, les fichiers de classes sont localisés dans un sous répertoire "Classes" mais ce dernier n'apparait bien évidemment pas explicitement dans le nom complètement qualifié:

class: \TYPO3\Fluid\Core\Parser
file : Packages/Framework/TYPO3/Fluid/Classes/Core/Parser.php

Fluid/
     Classes/
     Core/
         Parser.php
     Meta/
         Package.xml
     Documentation/
         Manual/
               en/
                 Index.xml    

TYPO3 doit donc être entièrement repensé et réécrit. Dommage pour eux. En revanche, Symfony, Doctrine ou ZF s'en sortent bien. Il faut dire que leur représentants sont influents. Par exemple, Jonathan H. Wage (Doctrine - Sensio) est à l'origine de SplClassLoader. Matthew Weier O'Phinney (ZF - Zend) a réussi à imposer la règle spéciale qui consiste à transformer les underscores dans les noms de class en DIRECTORY_SEPARATOR. Cela complexifie fortement l'implémentation de SplClassLoader, mais bon, il s'agit de Zend tout de même!

Ce qu'il faudrait faire

La seule chose importante pour une interopérabilité d'autoloading est l'unicité de la partie <Vendor Name> dans:

\<Vendor Name>\(<Namespace>\)*<Class Name>

Tout le reste n'est que contraintes inutiles. Si une librairie X veut mettre toutes ses classes dans un sous répertoire Classes, elle n'a qu'à fournir sa propre implémentation pour spl_autoload_register(). Dans un contexte de namespaces, l'unicité de <Vendor Name> suffit.

Au passage, les constantes __DIR__ et __NAMESPACE__ permettent potentiellement de se passer d'include_path lors des opérations d'inclusion de fichiers. Quoiqu'il en soit, les auteurs de la librairie X ont tout le loisir d'optimiser au maximum leur implémentation et se passer, par exemple, de la règles des underscores dans les noms de classes.

Mais quid d'une possible implémentation en C de SplClassLoader ? Et bien tant mieux! Rien n'empêche un projet d'adhérer à la lettre au PSR-0. Ce n'est aucunement incompatible avec la présence de plusieurs autoloader. SplClassLoader utilise déja spl_autoload_register.

Sun Nov 29 2009

Programming

boost, c++, json, variant

1 comment

Simple modélisation de JSON en C++

Il existe un certain nombre de parsers et autres librairies en C++ pour traiter du JSON (voir en bas de la page). Il manquerait Boost.PropertyTree qui a récemment été intégré dans boost.

En ce qui me concerne, je n'ai que très rarement besoins de manipuler du JSON de fond en comble, c'est à dire, par exemple, parser une chaine en JSON ou écrire dans un fichier avec ce format. Ce qui m'est utile en revanche, est de balancer des valeurs JSON-esques dans un container qui servira ensuite d'input à d'autres composants. C'est vague, je sais, c'est fait pour.

Le container en question est donc sommé d'accepter des strings, tableaux, objets, booléens etc. Bref, tout ce qui fait l'intérêt de JSON. Une des techniques que j'utilise est basée sur Boost.Variant. L'intérêt de Boost.Variant par rapport à Boost.Any est qu'il est type safe. Un mécanisme de visiteur permet de manipuler précisément les types wrappés, ce qui est utile lorsque le container en question n'est qu'un container de "passage" entre composants. L'autre intérêt de variant est sa propension à être récursif, tout comme JSON. Ca tombe bien.

Dans le code qui suit, json_type est le type généric qui modélise une valeur JSON. json_array, json_object sont des json_type. En plus, on accepte des std::string, bool etc. Ce n'est pas une implémentation de JSON fidèle au bit près. Comme je le disais, je veux juste du JSON-esque:

typedef boost::make_recursive_variant
<
    int
  , double
  , bool
  , std::string
  , std::unordered_map<std::string, boost::recursive_variant_> // object
  , std::vector<boost::recursive_variant_> // array

>::type json_type; // notre type generic

typedef std::unordered_map<std::string, json_type> json_object;

typedef std::vector<json_type> json_array;

On notera l'utilisation de std::unordered_map. Un boost::unordered_map ferait tout autant l'affaire. boost::make_recursive_variant permet de rendre le variant récursif. Il accepte dès lors sans broncher un std::vector<boost::recursive_variant_>. Vous aurez compris qu'il s'agit de la modélisation du array JSON. Un typedef qui va bien permet de déclarer json_array à partir de std::vector<json_type>. La boucle est bouclée.

A l'utilisation, ça donne ce genre de chose:

json_array arr {3.14, 21, true}; //initializer list goodies (C++0x)
  
json_object obj;
obj["key"] = 42;
obj["str"] = std::string("hello");
obj["arr"] = arr;

Ok, mais faire ça ne sert à rien. Effectivement. Rappelez-vous, il s'agit d'un container de passage. Un autre composant va devoir se démerder (encore une fois, sans broncher) pour faire quelque chose d'utile avec. C'est là que boost::static_visitor entre en jeu. On va pouvoir traiter finement les types wrappés (std::string, int, std::vector...). Le principe est le suivant:

// un type T quelconque (pseudo code)
struct json_visitor : public boost::static_visitor<T>
{
    T operator()(const json_object& val) const
    {
        //...
    }

    T operator()(const json_array& val) const
    {
        //...
    }

    T operator()(const std::string& val) const
    {
        //...
    }
    
    // ...
};

// on applique le visitor à notre json_type
boost::apply_visitor(json_visitor(), json_type);

Par exemple, faire un output au format JSON de notre json_type. On va déclarer un visitor dérivant de boost::static_visitor<std::string> qui se chargera de formater le tout:

struct json_conv_visitor : public boost::static_visitor<std::string>
{
    std::string operator()(const json_object& val) const
    {
        std::ostringstream os;
        os << "{";
        for (json_object::const_iterator i(val.begin()), e(val.end())
            ; i != e; ++i)
        {
            os << i->first << ": "
               << boost::apply_visitor(json_conv_visitor(), i->second)
               << ", ";
        }
        os << "}";
        return os.str();
    }

    std::string operator()(const json_array& val) const
    {
        std::ostringstream os;
        os << "[";
        for (json_array::const_iterator i(val.begin()), e(val.end())
            ; i != e; ++i)
        {
            os << boost::apply_visitor(json_conv_visitor(), *i) 
               << ", ";
        }
        os << "]";
        return os.str();
    }

    std::string operator()(const std::string& val) const
    {
        return std::string("\"").append(val).append("\"");
    }
    
    template<typename T> // le reste est refilé à boost::lexical_cast
    std::string operator()(const T& val) const
    {
        return boost::lexical_cast<std::string>(val);
    }
};

Appliqué à l'exemple plus haut, ça retourne:

{str: "hello", key: 42, arr: [3.1400000000000001, 21, 1, ], }

Voila, le code complet est dispo sur github ici. Si ça vous intéresse, servez-vous. Un de ces 4, on parlera d'une manière de parser une chaine JSON pour filler ce genre de variant, sans utiliser Spirit. Sur ce, a+

« newer older »