22

Variant en C++ (part 2 : C++ side)

mar

C’est parti!
Voilà donc une première version C++ de l’implémentation des variants du PHP. Ce n’est pas complet et sans doute pas optimal, mais cela permet déjà de se faire une idée des performances auxquelles ont peut s’attendre et des limites du projet.
C’était sympa cette plongée dans le C++. Je ne suis clairement pas un expert dans le domaine alors toutes amélioration sera bonne à prendre…

Représentation

Un variant est une variable pouvant contenir plusieurs types de donnée mais un seul à un moment t.
L’implémentation la plus simple consiste à utiliser une union.
Voilà une définition d’une union piquée sur Internet :

“Les unions permettent de créer des espaces mémoire où l’on peut interpréter une même donnée de différentes manières”

Ça tombe bien c’est exactement se que l’on veut!

En gros, une union, c’est comme une boite dans laquelle on veut pouvoir loger une variable de n’importe quelle type. Le compilateur se charge alors de s’assurer que la boite sera assez grande pour contenir la variable en donnant à cette boite la taille du type le plus grand. La taille mémoire prise par une instance d’union doit donc être égale ou supérieur à la taille de la plus grosse variable pouvant être contenu dans l’union. C’est clair ou pas ?

Voici donc une première implémentation de notre variant que l’on appellera ebovar.

class ebovar
{
public:
	int var_type;
	union {
		int i;
		float f;
		bool b;
		std::string *s;
		std::map<ebovar , ebovar> *m;
		void *o;
	};
};

var_type var contenir une information permettant de savoir quel est le type de la donnée contenu dans le variant à un instant t. Sans cette variable il nous serait impossible de savoir comment traiter la donnée contenu dans le variant.

La gestion des chaines de caractères (string) et des tableaux associatifs (map) sera laissé à la librairie std.
On remarquera qu’on prévoit dans l’union non pas des instances de ces objets mais des pointeurs. L’intérêt est double, non seulement un pointeur occupe moins de place mémoire qu’une instance de std::string ou std::map mais en plus on évitera d’instancier ces objets à a chaque création d’une instance d’ebovar.
o permettra de contenir un donnée de type objet.

Du fait de la complexité de la gestion des objets et des tableaux associatifs, on va pour le moment exclure ces deux types de données pour y revenir plus tard.

Notre ebovar va donc être défini ainsi

#define EBO_INT				0
#define EBO_FLOAT				1
#define EBO_STRING			2
#define EBO_ERROR				3										// Reservé
#define EBO_BOOL				EBO_INT + 4							// BOOL is a specif INT
#define EBO_NULL				EBO_INT + 8							// NULL is a specif INT 

class ebovar
{
public:
	int var_type;
	union {
		int i;
		float f;
		bool b;
		std::string *s;
	};
};

Construction

J’ai appris plein de trucs ces derniers jours en C++.
Entre autre, la Forme Canonique Orthodoxe de Coplien.

Laquelle conseille de créer au moins ces 4 méthodes suivantes pour chaque class

  • Le constructeur par défaut : appelé lors de la construction d’un objet à défaut d’un meilleur choix.
  • Le constructeur de copie : appelé lorsqu’on passe un objet en paramètre d’un fonction ou lorsque le fonction retourne un objet.
  • l’opérateur d’affectation : appelé lorsqu’on utilise explicitement l’opérateur =
  • Le destructeur

Je ne peux que vous conseiller de suivre les liens fournis ci-dessus vers le site cpp.developpez.com qui sont clairs et limpides pour en savoir plus sur ces 4 méthodes.

L’implémentation que j’en ai faite est la suivante.

/********************************************************************
**
** Forme canonique orthodoxe de Coplien ?
**
********************************************************************/

ebovar::ebovar()																		// Constructeur par defaut
{
	i =0;
	var_type = EBO_NULL;
}

ebovar::ebovar(const ebovar &source)											// Constructeur de copie
{
	var_type = source.var_type;
	switch (var_type)
	{
	case EBO_INT:
	case EBO_NULL:		i = source.i; break;
	case EBO_BOOL:		b = source.i; break;
	case EBO_FLOAT:	f = source.f; break;
	case EBO_STRING:	s = new std::string(*source.s); break;
	}
}

ebovar &ebovar::operator=(const ebovar &source)								// Opérateur d'affectation
{
	var_type = source.var_type;
	switch (var_type)
	{
	case EBO_INT:
	case EBO_NULL:		i = source.i; break;
	case EBO_BOOL:		b = source.i; break;
	case EBO_FLOAT:	f = source.f; break;
	case EBO_STRING:	delete s; s = new std::string(*source.s); break;
	case EBO_ARRAY:	delete m; m = new std::map<ebovar , ebovar>(*source.m); break;
	}

	return *this ;
}

ebovar::~ebovar()																		// Destructeur
{
	switch (var_type)
	{
	case EBO_STRING:	delete s; break;
	}

	i = 0;
	var_type = EBO_NULL;
}

Les autres constructeurs permettent de créer un ebovar à partir d’une donnée fournie

/********************************************************************
**
** Constructeur de typage
**
********************************************************************/

ebovar::ebovar(int data)															// Entier
{
	i = data;
	var_type = EBO_INT;
}

ebovar::ebovar(float data)															// Flottant
{
	f = data;
	var_type = EBO_FLOAT;
}

ebovar::ebovar(bool data)															// Boolean
{
	b = data;
	var_type = EBO_BOOL;
}

ebovar::ebovar(const char *data)													// String à partir d'un char*
{
	s = new std::string(data);
	var_type = EBO_STRING;
}

ebovar::ebovar(std::string data)													// String à partir d'un string
{
	s = new std::string(data);
	var_type = EBO_STRING;
}

A ce stade, il est possible d’initialiser les variables de type ebovar…

int main(int _argc, char **_argv)
{
	ebovar _x = 100;
	ebovar _y = 12.2f;
	ebovar _chaine = "Hello World !";
	ebovar _copy = _y;

	return 0;
}

… mais ne rien faire d’autre que les initialiser ou les recopier.

Surcharge des opérateurs

Le C++ c’est pas mal ! On peut surcharger les opérateurs.
C’est tant mieux car un fois les opérateurs surchargés afin de leur donner le même fonctionnement que les opérateurs du PHP, la conversion PHP vers C++ devient nettement plus facile.

Voici les explications concernant l’opérateur + mais évidement cela est plus ou moins la même chose pour tous les opérateurs.

Déclaration pour l’opérateur +
	ebovar &operator += (const ebovar &);
	ebovar &operator += (const int);
	friend ebovar operator + (const ebovar &, const ebovar &);
	friend ebovar operator + (const ebovar &, const int);
	friend ebovar operator + (const int, const ebovar &);

Plusieurs interrogations pourrait (doivent même) vous traverser l’esprit.

  • Pourquoi surcharger l’opérateur avec des types int?
  • A quoi sert const?
  • C’est quoi friend?
Pourquoi surcharger l’opérateur avec des type int?

Il y a au moins 2 bonnes raisons pour surcharger les opérateurs arithmétiques avec le type int

La première est que cela va éviter la construction d’une variable ebovar lorsqu’on réalise une opération avec une valeur immédiate.
Prenons par exemple le cas suivant:

 	ebovar _a;
	a += 5;

Par défaut le compilateur ne va pas savoir comment additionner un entier à un ebovar.
On pourrait simplement laisser le compilateur convertir 5 en ebovar, mais cette conversion sera couteuse en temps CPU.
L’autre solution c’est de prévoir le cas en surchargeant les opérateurs arithmétiques avec les combinaisons de constantes les plus courantes. C’est pour cela qu’on prévoit ces surcharges additionnelles.

La deuxième bonne raison pour réaliser explicitement cette surcharge, je la garde pour plus tard ;)
Je vous l’expliquerai bientôt (un autre jour). Cette raison est également liée à une notion de performance… mais j’en dit pas plus.

A quoi sert const?

Là c’est du lourd !!!
Disons qu’il existe 4 façons de transmettre un objet à un opérateur.

  • Soit on lui envoie une copie de l’objet : … += (ebovar a);
  • Soit on lui envoie une référence de l’objet : … += (ebovar &a);
  • Soit on lui envoie un objet const : … += (const ebovar a);
  • Soit on lui envoie une référence const : … += (const ebovar &a);

C’est quoi la différence ?
Lorsqu’on envoie une copie, l’objet est dupliqué.
Le clone de l’objet est initialisé avec le constructeur de copie. Il s’agit donc d’une opération assez couteuse.

Lorsqu’on envoie une référence de l’objet, on passe un pointeur sur l’objet (sauf que ce pointeur est masqué. c’est une référence !!!).
C’est nettement mieux, car on n’a plus besoin de copier l’objet. Il convient évidement de ne pas modifier les objets passés en argument dans ce cas, sinon on modifie l’objet envoyé à l’opérateur.

Lorsqu’on envoie un objet de type const, on indique au compilateur que l’objet qu’on envoie ne sera pas modifié (car il est… constant).
Dans ce cas, le compilateur n’autorisera aucune modification de l’objet ni aucun appel à une méthode de l’objet qui serait éventuellement susceptible de modifier l’objet.
Comme le compilateur n’est pas stupide, il va décidé tout seul comme un grand de remplacer l’envoie de l’objet par une référence puisqu’il sait que l’objet ne pourra pas être modifié.

Lorsqu’on envoie une référence de type const, on indique au compilateur que l’objet référencé qu’on envoie ne sera pas modifié.
Finalement, le résultat (si j’ai bien compris) sera le même que pour le cas précédent.
Mais pour une raison de lecture, il est cohérent de d’envoyer à un opérateur une référence de type const.

Remarque : pour les entiers (int) il est inutile d’envoyer une référence car un int n’est pas plus volumineux qu’une référence. Par contre il peut être cohérent de déclarer la variable comme étant const puisque notre opérateur n’est pas supposé modifier notre copie de int. Ceci dit, même si l’opérateur venait a modifier la copie de notre entier, cela n’aurait aucun impact puisque c’est justement une copie.

Pour plus d’information sur const, je vous invite à lire l’article d’Openclassrooms qui est très bien fait sur le sujet.

C’est quoi friend?

Bon là, j’avoue que j’en sait rien. Je vais chercher et j’écrierai ce paragraphe… un jour… peut-être…

L’implémentation de l’opérateur +

Voilà ci-dessous l’implémentation de la surcharge de l’opérateur +.

	ebovar &ebovar::operator += (const ebovar &right_op)
	{
		int ope = ((var_type & 3) < < 2) + (right_op.var_type & 3);
		switch(ope)
		{
		case EBO_INT_INT:			i += right_op.i; break;
		case EBO_INT_FLOAT:		castIntToFloat((float)i + right_op.f); break;
		case EBO_INT_STRING:		castIntToFloat(i + right_op.getFloat()); break;
		case EBO_FLOAT_INT:		f += (float)right_op.i; break;
		case EBO_FLOAT_FLOAT:	f += right_op.f; break;
		case EBO_FLOAT_STRING:	f += right_op.getFloat(); break;
		case EBO_STRING_INT:		castStringToFloat(getFloat() + (float)right_op.i); break;
		case EBO_STRING_FLOAT:	castStringToFloat(getFloat() + f); break;
		case EBO_STRING_STRING:	castStringToFloat(getFloat() + right_op.getFloat()); break;
		default:
			printf("Error : can't apply operator += to this vartype (%s)\n", right_op.getType().c_str());
			break;
		}
		return *this;
	}

	ebovar &ebovar::operator += (const int ivalue)
	{
		switch(var_type)
		{
		case EBO_INT:				i += ivalue; break;
		case EBO_FLOAT:			f += (float)ivalue; break;
		case EBO_STRING:			castStringToFloat(getFloat() + (float)ivalue); break;
		default:
			printf("Error : can't apply operator += to this vartype (%s)\n", getType().c_str());
			break;
		}
		return *this;
	}

	ebovar operator + (const ebovar &left_op, const ebovar &right_op)
	{
		int ope = ((left_op.var_type & 3) << 2) + (right_op.var_type & 3);
		switch(ope)
		{
		case EBO_INT_INT:			return (ebovar)(left_op.i + right_op.i); break;
		case EBO_INT_FLOAT:		return (ebovar)(left_op.i + right_op.f); break;
		case EBO_INT_STRING:		return (ebovar)(left_op.i + right_op.getFloat()); break;
		case EBO_FLOAT_INT:		return (ebovar)(left_op.f + right_op.i); break;
		case EBO_FLOAT_FLOAT:	return (ebovar)(left_op.f + right_op.f); break;
		case EBO_FLOAT_STRING:	return (ebovar)(left_op.f + right_op.getFloat()); break;
		case EBO_STRING_INT:		return (ebovar)(left_op.getFloat() + right_op.i); break;
		case EBO_STRING_FLOAT:	return (ebovar)(left_op.getFloat() + right_op.f); break;
		case EBO_STRING_STRING:	return (ebovar)(left_op.getFloat() + right_op.getFloat()); break;
		default:
			printf("Error : can't apply operator + to this vartype (%s + %s)\n", left_op.getType().c_str(), right_op.getType().c_str());
			break;
		}
	}

	ebovar operator + (const ebovar &left_op, const int ivalue)
	{
		switch(left_op.var_type)
		{
		case EBO_INT:       return (ebovar)(left_op.i + ivalue); break;
		case EBO_FLOAT:     return (ebovar)(left_op.f + ivalue);	break;
		case EBO_STRING:    return (ebovar)(left_op.getFloat() + ivalue); break;
		default:
			printf("Error : can't apply operator + to this vartype (%s)\n", left_op.getType().c_str());
			break;
		}
	}

	ebovar operator + (const int ivalue, const ebovar &right_op)
	{
		switch(right_op.var_type)
		{
		case EBO_INT:			return (ebovar)(ivalue + right_op.i); break;
		case EBO_FLOAT:		return (ebovar)(ivalue + right_op.f); break;
		case EBO_STRING:		return (ebovar)(ivalue + right_op.getFloat()); break;
		default:
			printf("Error : can't apply operator + to this vartype (%s)\n", right_op.getType().c_str());
			break;
		}
	}

Il y a assez peu de chose à dire sur l'implémentation.
Elle n'appelle aucun commentaire si ce n'est l'instruction

	int ope = ((left_op.var_type & 3) < < 2) + (right_op.var_type & 3);

qui a pour objectif de retourner un identifiant entier représentant les deux types d'opérandes.

Les autres opérateurs

Les autres opérateurs arithmétiques ou de comparaison s'implémentent plus ou moins de la même façon.
Dans mon code, j'ai utilisé des macros pour déclarer tous ces opérateurs sans avoir besoin de dupliquer le code à la main.

Benchmark

Fibonacci

Bien. Nous avons à présent suffisamment de matière pour convertir un petit programme PHP à la main et voir comment cela fonctionne.
J'ai choisi un petit programme trouvé sur Internet qui calcule la suite de fibonacci.

Le code PHP est le suivant :

#!/opt/php/bin/php
<?
function fib($n)
{
    if ($n <= 2)
        return 1;
    else
        return fib($n-1) + fib($n-2);
}

$n = 36;

echo "fib $n = " . fib($n) . "\n";
?>

La version ebola qui pourrait facilement être produite automatiquement :

#include <string>
#include <stdio.h>

#include "e-lib/ebovar.h"
#include "e-lib/e-std.h"

/********************************************************************
**
** Main EBOLA
**
********************************************************************/

ebovar fib(ebovar _n)
{
    if (_n <= 2)
        return 1;
    else
        return fib(_n - 1) + fib(_n - 2);
}

int main(int _argc, char **_argv)
{
	ebovar _n = 36;

	echo ("fib " < < _n << " = " << fib(_n) << "\n");
}

e-lib/ebovar.h contient la class ebovar.
e-lib/e-std.h contient la déclaratino de la fonction echo.

Remarque : tous les codes sources sont disponibles. Le liens est fourni en bas de ce post.

Résultats
PHP
real	0m12.402s
user	0m12.253s
sys	0m0.148s
Ebola
real	0m1.047s
user	0m1.028s
sys	0m0.016s
C++
real	0m0.046s
user	0m0.044s
sys	0m0.000s

Ebola est 12 fois plus rapide que le PHP.
Le C++ quant à lui est 269 fois plus rapide que le PHP sur ce type de calcul.

C’est chaud non ?

Conclusion, Code source et Remerciements

Il est clair a présent que les variants ça moule, donc c’est nul :)

On comprend mieux pourquoi facebook après avoir essayé de convertir ses scripts PHP en C++ a finalement décidé de créer le langage Hack, qui n’est rien d’autre qu’une version améliorée du PHP gérant les variables typées.

Vous pouvez retrouvez les sources de la version actuelle des variants d’ebola ainsi que les scripts de bench dans l’archive téléchargeable ici.
Dans cette archive vous trouverez, tous le code ebola actuel, le benchmark en PHP et celui en C++.

Pour compiler le script ebola utilisez la commande suivante

g++ -O3 ebola.cpp e-lib/ebovar.cpp e-lib/e-std.cpp

Enfin, je me dois de remercier chaleureusement les utilisateurs du forum C++ de developpez.com qui m’ont fortement aider à aboutir au résultat publié aujourd’hui.

Répondre

Human control : 3 + 3 =