Archives mensuelles : mai 2013

PHP: echo vs print

Ces derniers temps en me baladant nonchalamment sur le Web, j’ai eu l’occasion de voir à de nombreuses reprises des « matches » entre echo et print, certains soutenant (fort heureusement, avec leurs benchmarks à l’appui) qu’echo était plus rapide que print. Nous allons voir ce qu’il en est de notre côté !

Echo et print

Echo n’est pas une fonction ! C’est officiellement une « structure du langage » (language construct) PHP.

Echo, lorsqu’on l’utilise avec des parenthèses, ne prend qu’un seul paramètre :

$prenom = 'Sebastien';
// avec des parenthèses
echo ("Bonjour, " . $prenom);

Notre unique paramètre ici est le résultat de la concaténation de la chaîne de caractères « Bonjour,  » avec la variable $prenom qui contient la valeur Sebastien.

Les virgules nous permettent également le passage de plusieurs paramètres à echo :

$prenom = 'Sebastien';
echo 'Bonjour, ', $prenom, ' !';

Faisons un savant mélange de tout ça :

$prenom = 'Sebastien';
echo "Salut, " . ($prenom . " Ferrandez"), ", ça va ?";

Print n’est pas non plus une fonction, et ce même si print retourne une valeur entière (constante, toujours égale à 1). Print est une aussi construction du langage plus proche de la fonction qu’echo puisqu’elle renvoie une valeur mais la comparaison s’arrête là.

C’est parfaitement inutile, mais de fait rien ne vous interdit de faire le branchement conditionnel suivant :

$prenom = 'Sebastien';
if (1 === print("Bonjour")) {
    echo ",$prenom";
}

ou bien, puisque print renvoie 1 et que 1 == true avec l’égalité dite classique :

$prenom = 'Sebastien';
if (print("Bonjour")) {
    echo ",$prenom";
}

Elephant-PHP

Un benchmark maison

Pour valider la prévalence de l’un de ces language constructs sur l’autre, rien ne vaut un test ! Je suis horripilé par les gens qui propagent des légendes urbaines du développement sans apporter la preuve de ce qu’ils répètent bêtement (« telle fonction est plus rapide que telle autre », « telle chose c’est le Mal ! », « telle autre chose n’est pas optimisée ») !

Voici le programme trivial qui nous sert pour nos tests :

$time_start = microtime(true);

for ($i=0; $i<10000; $i++) {
   print "test";
}

$time_end = microtime(true);
$time = $time_end - $time_start;

echo PHP_EOL . "$time secondes\n";

Evidemment, pour tester echo, vous prendrez soin de remplacer l’appel à print par echo !
Voici la version de php CLI que j’ai utilisé pour mener ces petites investigations sous GNU/Linux Debian sid :

PHP 5.4.4-14 (cli) (built: Mar  4 2013 14:08:43) 
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2012 Zend Technologies

L’idée ici était de faire des boucles avec un nombre d’itérations décuplé. Voici les résultats de ces expérimentations :

100 impressions du mot test :
echo : 0.00029110908508301 secondes
print : 0.00029802322387695 secondes

1000 impressions du mot test :
echo : 0.0025129318237305 secondes
print : 0.0025389194488525 secondes

10000 impressions du mot test :
echo : 0.025437831878662 secondes
print : 0.027919054031372 secondes

100000 impressions du mot test :
echo : 0.30710697174072 secondes
print : 0.23741006851196 secondes

1 million d’impressions du mot test :
echo : 2.6142749786377 secondes
print : 2.5098519325256 secondes

10 millions d’impressions du mot test :
echo : 27.915874958038 secondes
print : 33.695736885071 secondes (quasiment 5 secondes de plus !)

Ces valeurs sont des valeurs moyennes, calculées sur l’ensemble des 5 tests lancés dans chacun des cas et pour chaque language construct. Ce qu’on peut observer à la lumière de ces tests empiriques c’est qu’effectivement, echo est plus rapide en terme de temps d’exécution que print. Cependant, la tendance s’inverse dans certains cas, notamment ici lorsque le nombre d’itérations croît pour atteindre un million d’itérations. La tendance initiale se retrouve sur des nombres très élevés d’itérations (ici, 10 millions).

En conclusion

Nous avons pu vérifier avec quelques exemples concrets que oui, echo est plus rapide que print. Evidemment, nous le voyons d’autant plus que nous poussons ces deux language constructs dans leurs derniers retranchements, avec un nombre d’itérations épouvantable (qui fait ça en pratique, hein ?).

J’ai tendance à penser que lorsqu’on en est arrivé à tenter de gagner des millisecondes sur un appel à une fonction ou un language construct PHP, c’est qu’on n’a plus rien à faire au niveau de la qualité du code produit et malheureusement, l’expérience m’a souvent montré qu’avant de s’attacher à optimiser des appels comme ceux-ci, les développeurs feraient mieux de s’attacher à produire du code de qualité (conforme aux principes SOLID, par exemple) !

Quelques liens

Documentation PHP : echo
PHP Benchmark http://www.phpbench.com/ [anglais]

Introduction à MongoDB

Dans le cadre des cours de bases de données que j’enseigne au CNAM, j’ai décidé pour ma dernière séance de l’année 2013-2013 de faire une présentation de MongoDB à mes élèves.

Sans aller trop en détail dans la technique (on n’y parle pas de sharding, de map-reduce ou de GridFs), l’idée était ici de leur présenter cet aspect des bases de données qui trouve de plus en plus d’écho dans la communauté des développeurs d’applications en ligne.

snapshot2

Nous avons passé l’année à parler de schéma conceptuel, d’E/R, de DDL, de DML, il me fallait aussi présenter une vision alternative des bases de données car connaître l’état de l’art technique est, en ce qui me concerne, une qualité requise chez tout concepteur qui prétend l’être.

Vous pouvez télécharger cette présentation au format PDF.

MySQL et InnoDB : activer les fichiers au niveau table avec innodb_file_per_table

Fichiers de base d’InnoDB

Par défaut, votre configuration pour les tables utilisant le moteur InnoDB avec MySQL est la suivante :

  • un fichier ibdata1 constamment alimenté est créé
  • deux fichiers de log (ib_logfile0 et ib_logfile1) sont crées

Vous pouvez les voir en faisant : 

sebastien.ferrandez@sebastien$ ls -l /var/lib/mysql
total 28736
-rw-rw---- 1 mysql mysql 18874368 mai   21 10:17 ibdata1
-rw-rw---- 1 mysql mysql  5242880 mai   21 10:17 ib_logfile0
-rw-rw---- 1 mysql mysql  5242880 mai   20 15:58 ib_logfile1

Evidemment, il faudra au besoin modifier le chemin si vous avez customisé votre configuration MySQL. Le problème de ce fichier ibdata1 en mode append est qu’il a tendance à grossir très vite et à atteindre des tailles problématiques (de nature à remettre en cause le bon fonctionnement de MySQL) : sur ma machine locale, le mien faisait 3.4G ! Il m’a fallu dumper mes bases de données à des fins de sauvegarde, faire en sorte de ne plus avoir ce fichier monolithique mais plusieurs (par base de données et par table), redémarrer MySQL et rejouer mon script SQL pour remettre en place les données dont j’avais besoin rapidement.

mysql

Pour effectuer ce découpage propre par table, nous allons nous servir de la directive de configuration innodb_file_per_table. Comme je le disais plus haut, celle-ci n’est pas activée par défaut :

mysql> SHOW VARIABLES LIKE 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | OFF   |
+-----------------------+-------+

Pour l’activer, il vous suffit d’en faire mention dans votre fichier my.cnf (situé par défaut sur mon installation dans /etc/mysql/my.cnf), dans la section réservée à mysqld (par exemple du côté du commentaire « Fine-tuning ») et évidemment il vous faudra redémarrer le démon mysqld pour prendre en compte ce changement.

Voici l’option de configuration à rajouter :

[mysqld]
#
# * Fine Tuning
#
innodb_file_per_table   = 1

Vous pouvez également écrire tout simplement « innodb_file_per_table », ceci fonctionnera. Redémarrez ensuite le service MySQL :

sudo service mysql restart

Connectez-vous en ligne de commande à votre MySQL et retapez la commande donnée ci-dessus, vous devriez avoir du nouveau :

mysql> SHOW VARIABLES LIKE 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON    |
+-----------------------+-------+
1 row in set (0.00 sec)

Bien ! Notre modification est donc bien prise en compte ! Si je crée une nouvelle base de données et une nouvelle table :

mysql> create database lolcats; use lolcats;
Query OK, 1 row affected (0.00 sec)

Database changed
mysql> create table cat(id tinyint unsigned primary key, name varchar(20), age tinyint unsigned);
Query OK, 0 rows affected (0.04 sec)

Je retrouve bien tout ça dans mon répertoire MySQL :

sebastien.ferrandez@sebastien$ sudo ls -l /var/lib/mysql/lolcats/
total 116
-rw-rw---- 1 mysql mysql  8614 mai   21 10:44 cat.frm
-rw-rw---- 1 mysql mysql 98304 mai   21 10:44 cat.ibd
-rw-rw---- 1 mysql mysql    65 mai   21 10:43 db.opt

C’est bien le signe que le découpage « un fichier par table » est désormais en place !

Bénéficier du découpage avec une base de données déjà implantée

Si vous avez déjà une base de données et que vous vous apercevez qu’ibdata1 a grossi et qu’il est temps de passer à un fichier par table, procédez comme suit :

Créez un répertoire s’apprêtant à recueillir votre sauvegarde :

sebastien.ferrandez@sebastien:$ mkdir -p $HOME/mysql/backups/avant_filepertable

Faites une sauvegarde de l’intégralité de vos données :

sebastien.ferrandez@sebastien:$ mysqldump -u root -p --all-databases > $HOME/mysql/backups/avant_filepertable/all_databases.sql

Arrêtez le démon MySQL :

sebastien.ferrandez@sebastien:~$ sudo service mysql stop
[ ok ] Stopping MySQL database server: mysqld.

Naturellement, rajoutez votre option dans my.cnf :

[mysqld]
#
# * Fine tuning
#
innodb_file_per_table

Redémarrez MySQL :

sebastien.ferrandez@sebastien:~$ sudo service mysql start
[ ok ] Starting MySQL database server: mysqld ..
[info] Checking for tables which need an upgrade, are corrupt or were 
not closed cleanly..

A l’issue de ce redémarrage, pensez à valider la bonne prise en compte de cette option à l’aide du SHOW VARIABLES montré un peu plus haut dans ce billet.
Supprimez vos fichiers (à vous de voir si vous souhaitez conserver les logs) :

sebastien.ferrandez@sebastien:$ sudo rm -fr /var/lib/mysql/*

Installez de nouvelles tables système. Attention, les messages que j’ai mis en gras nécessitent votre attention !

sebastien.ferrandez@sebastien:$ sudo /usr/bin/mysql_install_db
Installing MySQL system tables...
OK
Filling help tables...
OK

To start mysqld at boot time you have to copy
support-files/mysql.server to the right place for your system

PLEASE REMEMBER TO SET A PASSWORD FOR THE MySQL root USER !
To do so, start the server, then issue the following commands:

/usr/bin/mysqladmin -u root password 'new-password'
/usr/bin/mysqladmin -u root -h sebastien password 'new-password'

Alternatively you can run:
/usr/bin/mysql_secure_installation

which will also give you the option of removing the test
databases and anonymous user created by default.  This is
strongly recommended for production servers.

Je vous conseille de passer par :

sudo /usr/bin/mysql_secure_installation

Voici les options que j’ai personnellement choisi :

Change the root password? [Y/n] n
 ... skipping.

Remove anonymous users? [Y/n] n
 ... skipping.
Disallow root login remotely? [Y/n]
 ... Success!

Remove test database and access to it? [Y/n]
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reload privilege tables now? [Y/n]

Ré-injectez vos données sauvegardées dans votre base qui maintenant sait découper par table :

sebastien.ferrandez@sebastien:~$ mysql -u root -p < $HOME/mysql/backups/avant_filepertable/all_databases.sql

Et validez que les tables que vous avez recrée se trouvent bien dans les répertoires correspondant à vos bases de données :

sebastien.ferrandez@sebastien:~$ sudo ls -l /var/lib/mysql/lolcats
total 116
-rw-rw---- 1 mysql mysql    65 mai   22 09:31 db.opt
-rw-rw---- 1 mysql mysql  8556 mai   22 10:02 cat.frm
-rw-rw---- 1 mysql mysql 98304 mai   22 10:02 cat.ibd

La table cat que j’ai crée se retrouve bien dans le répertoire correspondant à la base de données lolcats dont elle fait partie, l’opération est un succès !

Kit de premiers secours

Problèmes pouvant survenir au shutdown du démon MySQL

Si jamais vous n’arrivez pas à stopper MySQL, il s’agit sans doute d’un problème lié à l’utilisateur ‘debian-sys-maint’ du à la suppression des tables.
Voici comment le résoudre :

Récupérez d’abord le mot de passe courant de cet utilisateur (c’est une installation locale, je vous donne le mot de passe sans crainte) :

sebastien.ferrandez@sebastien:$ sudo grep password /etc/mysql/debian.cnf
password = JZe6ZMa9bMK4aqfm

Ensuite mettez-lui les bons privilèges dans votre ligne de commande MySQL :

mysql> GRANT ALL PRIVILEGES ON *.* TO 'debian-sys-maint'@'localhost' IDENTIFIED BY 'JZe6ZMa9bMK4aqfm' WITH GRANT OPTION;
Query OK, 0 rows affected (0.00 sec)

Changer le mot de passe root

Pour mettre p@$sW0rd! comme mot de passe à l’utilisateur root

mysqladmin -u root password p@$sW0rd!

Pour aller plus loin…

http://dev.mysql.com/doc/refman/5.0/fr/innodb-configuration.html
http://dev.mysql.com/doc/refman/5.5/en/innodb-multiple-tablespaces.html (anglais)

MongoDB : un exemple de map-reduce

Le concept de map-reduce (on lit parfois les termes de paradigme de programmation ou de design pattern associés à map-reduce) n’a rien de nouveau : il s’inspire grandement des principes de base de la programmation fonctionnelle (i.e, par usage de fonctions mathématiques).

Comme son nom l’indique, map-reduce se compose de deux fonctions :

  • map, qui passe à reduce une clé et des données à traiter
  • reduce qui, pour chaque clé donnée par map, va opérer une réduction sur les données en provenance de map selon une logique que vous allez devoir écrire.

Map-reduce a connu un renouveau avec l’apparition des data stores de très grande capacité (on parle ici en pétaoctets) sur le web et doit en grande partie son retour sous les feux de la rampe à Google qui s’en est servi (et s’en sert toujours) pour distribuer/paralléliser les traitements, sur des clusters de machines. Le principe est simple: découper un gros problème en plusieurs petits, résolus par distribution à des nœuds fils (réducteurs) qui ensuite remontent le résultat de ces opérations aux nœuds de niveau supérieur, et ce récursivement jusqu’à remonter au la racine de l’arbre, qui a initié le calcul et en récupère donc le résultat final.

Ici nous n’aurons par cette notion de master nodes / worker nodes car nous travaillons sur une seule instance de MongoDB, située sur un seul serveur (ma machine) et non sur une architecture basée sur des shards ou des replica sets.

Image provenant de http://www.pal-blog.de/

Image provenant de PAL-Blog [http://www.pal-blog.de]


Map traverse tous les documents de votre collection MongoDB et va pousser des infos à reduce selon une clé définie. Passons à la pratique sans plus tarder…

Notre collection de test

Nous allons constituer une collection dénommée commandes et qui va contenir les commandes passées sur notre site web. Ces commandes seront évidemment des documents dont la structure est la suivante :

  • userid : l’identifiant de l’utilisateur à l’origine de la commande
  • date : la date de passage (ou de validation, pourquoi pas) de la commande
  • codepost : le code postal de la ville de résidence de l’utilisateur
  • articles : la liste des articles commandés (il y en a autant que souhaité)
  • totalttc : le prix TTC de la totalité de la commande
  • totalht : le prix HT de la totalité de la commande
  • tva : le montant de TVA applicable à la commande
sebastien.ferrandez@sebastien:~$ mongo
 MongoDB shell version: 2.0.6
 connecting to: test
>db.createCollection('commandes');
 { "ok" : 1 }
>show collections;
 commandes
 system.indexes

>db.commandes.insert({userid: 54845, date: new Date("Apr 28, 2013"), codepost:13100, articles:[{id:1,nom:'livre',prix:29.90}, {id:9, nom:'eponge', prix:2.90}], totalttc:32.80, tva:5.38, totalht:27.42});

>db.commandes.insert({userid: 54846, date: new Date("Apr 29, 2013"),codepost:13290, articles:[{id:45,nom:'robinet',prix:69.90}, {id:9, nom:'laitx6', prix:9.90}], totalttc:79.80, tva:13.08, totalht:66.72});

>db.commandes.insert({userid: 54847, date: new Date("Apr 30, 2013"),codepost:13008, articles:[{id:76,nom:'clavier',prix:49.90}, {id:2, nom:'fromage', prix:1.50}], totalttc:51.40, tva:8.42, totalht:42.98});

>db.commandes.insert({userid: 54848, date: new Date("Apr 28, 2013"),codepost:13600, articles:[{id:2987,nom:'presse',prix:2}], totalttc:2, tva:0.33, totalht:1.67});

>db.commandes.insert({userid: 54848, date: new Date("Apr 29, 2013"),codepost:13600, articles:[{id:2988,nom:'presse',prix:5.90}], totalttc:5.90, tva:0.97, totalht:4.93});

>db.commandes.insert({userid: 54848, date: new Date("Apr 30, 2013"),codepost:13600, articles:[{id:3989,nom:'presse',prix:1.20}], totalttc:1.20, tva:0.20, totalht:1});

>db.commandes.insert({userid: 54847, date: new Date("Apr 25, 2013"),codepost:13008, articles:[{id:2987,nom:'presse',prix:2}], totalttc:2, tva:0.33, totalht:1.67});

Structure du JSON

Voici la structure de l’un de nos documents :

{
    "_id": ObjectId("517fb463b53bb7169584f3c7"),
    "userid": 54847,
    "date": ISODate("2013-04-29T22:00:00Z"),
    "codepost": 13008,
    "articles": [
        {
            "id": 76,
            "nom": "clavier",
            "prix": 49.9
        },
        {
            "id": 2,
            "nom": "fromage",
            "prix": 1.5
        }
    ],
    "totalttc": 51.4,
    "tva": 8.42,
    "totalht": 42.98
}

Le JavaScript de notre map-reduce

Maintenant, nous allons écrire le map-reduce qui va opérer sur ces données; notre but est d’afficher, par code postal, le chiffre d’affaire généré par notre site en ligne. Créons donc un fichier mapred.js qui contiendra notre MR :

use commandes;

map = function() {
	emit(this['codepost'], {totalttc: this['totalttc']});
	};

reduce = function(cle, valeur) {
			var s = {somme : 0};
			valeur.forEach(function(article) {
				s.somme += article.totalttc;
			});
			return s;
		};

db.commandes.mapReduce(map, reduce, {out:'total_cmdes_par_ville'});

db.total_cmdes_par_ville.find();

And…action !

Nous injectons notre fichier JS dans Mongo

sebastien.ferrandez@sebastien:~$ mongo < /tmp/mapred.js

Et voici le résultat produit :

MongoDB shell version: 2.0.6
connecting to: test
function () {
    emit(this.codepost, {totalttc:this.totalttc});
}
function (cle, valeur) {
    var s = {somme:0};
    valeur.forEach(function (article) {s.somme += article.totalttc;});
    return s;
}
{
        "result" : "total_cmdes_par_ville",
        "timeMillis" : 33,
        "counts" : {
                "input" : 7,
                "emit" : 7,
                "reduce" : 2,
                "output" : 4
        },
        "ok" : 1,
}
{ "_id" : 13008, "value" : { "somme" : 53.4 } }
{ "_id" : 13100, "value" : { "totalttc" : 32.8 } }
{ "_id" : 13290, "value" : { "totalttc" : 79.8 } }
{ "_id" : 13600, "value" : { "somme" : 9.1 } }
bye

La fonction map va émettre (emit) des paires clé/valeur sur lesquelles reduce va travailler. Pour faire simple, reduce va faire la somme de tous les montants TTC des commandes pour un code postal donné. Une collection est créée pour abriter le résultat de l’exécution de map-reduce : nous avons décider de la nommer total_cmdes_par_ville. Elle est persistante, elle ne sera pas détruite une fois que vous vous déconnectez du shell MongoDB.