La question de savoir combien le temps requis pour réaliser une opération ou une groupe d'opérations dans un programme donné revient souvent en période de développement. Des questions comme mon programme me semble lent, mais pourquoi? reviennent régulièrement, surtout pour qui commence à rédiger des programmes ayant des besoins en calcul plus imposants. Cet article ne s'intéressera pas à toutes les considérations propres à l'optimisation des programmes, à l'étude de la complexité des algorithmes et au repérage des zones susceptibles de consommer du temps de traitement dans un programme, mais donnera quelques trucs simples pour vérifier le temps requis pour réaliser une ou plusieurs opérations. Nous n'irons pas jusqu'à montrer comment garantir la précision extrême des mesures, mais nous verrons comment prendre le pouls d'un programme de manière raisonnablement pratique dans la plupart des cas. Si vos besoins en précision vont au-delà de ce qu'expose ce texte d'introduction, vous avez besoin d'outils spécialisés et de techniques plus sophistiquées que ce qui peut être proposé dans un article aussi simple que celui-ci. |
Sorte de TL;DR Si vous ne cherchez qu'un truc rapide pour mesurer la vitesse d'exécution d'une fonction en C++, voici ce que je fais en pratique (code C++ 14) :
|
Il existe plusieurs raisons de vouloir mesurer le temps pour un programme, parmi lesquelles on trouve :
Si vous avez des besoins précis qui ne sont pas couverts ici, alors n'hésitez pas à creuser plus loin par vous-mêmes. Construisez des bancs d'essai réalistes et utiles. Par exemple, si vous devez entreposer un bloc de 100 Ko de données sur un support média chaque seconde pendant 3 heures sans jamais en manquer un seul, alors assurez-vous d'avoir validé votre capacité de le faire avant de passer en production: sauvegarder 100 Ko une fois est une chose simple mais, sur une période très longue, un tas de choses peuvent se passer qui risquent de faire en sorte qu'une des écritures, en chemin, ne soit pas faite au bon moment.
Assurez-vous que le test n'interfère pas avec ce qui est mesuré. Les résultats des tests seront habituellement rendus disponibles aux testeurs à travers des entrées/ sorties (console, fichier, peu importe) et ces opérations prennent beaucoup de temps, et du temps plus ou moins stable par-dessus le marché (beaucoup de facteurs interveiennent dans une entrée/ sortie). Évitez les allocations/ déallocations de mémoire à l'intérieur du tronçon de temps mesuré, à moins que ces opérations ne fassent partie du test, car ces opérations prennent un temps fortement dépendant du contexte.
Évitez d'interférer avec la séquence de tests: les les fenêtres modales (p. ex. : MessageBox()) et les lectures au clavier (p. ex. : lire une touche pour continuer) sont, dans la majorité, des cas, des pratiques inefficaces pour suivre le déroulement des opérations. Mieux vaut une batterie de tests très automatisés et une trace lisible a posteriori (dans un fichier, par exemple : voir plus bas) qu'un(e) humain(e) obligé(e) de cliquer Ok sans arrêt pendant des heures et des heures – ou même des minutes et des minutes; soyons honnêtes, c'est très abrutissant d'en être réduit(e) à cliquer sur Ok (clic clic clic...).
Développez le réflexe de conserver la trace de vos mesures et de vos tests dans des fichiers plutôt que de les faire apparaître à la console. Ceci vous permettra de procéder à des analyses plus approfondies des données obtenues sans vous forcer à rester inutilement attentive ou attentif pendant que les tests s'exécutent à toute allure. Utiliser un chiffrier électronique et regarder des courbes avec un boncafé est bien plus productif que de rester fixé(e) devant un écran console à voir défiler des entiers.
Faites plusieurs tests; se limiter à un seul test n'est pas significatif. Si vous voulez mesurer le temps d'exécution d'une fonction qui prend en général quelques millisecondes à se compléter, alors répétez le test quelques centaines de milliers de fois (avec une répétitive, évidemment) et assurez-vous de récupérer les résultats avec précision.
Enfin, prenez soin de garder des statistiques pertinentes. Le temps moyen requis pour une opération donne une image globale intéressante, et le plus petit temps requis pour réaliser une opération est aussi chose utile, mais il arrivera fréquemment que vous soyezs intéressé(e) par le temps maximal requis pour réaliser l'opération mesurée: si vous avez des contraintes strictes à respecter, c'est celui-là qui vous fera mal.
L'idée de base est simple. Supposons qu'on veuille mesurer le temps t requis pour exécuter la fonction f(), alors l'algorithme de base pour prendre une mesure sera :
avant ← maintenant()
f()
après ← maintenant()
écoulé ← après - avant
Dans la mesure où écoulé>=0 après exécution de cet algorithme, on devrait avoir obtenu le temps requis pour exécuter f().
Nous souhaitons que écoulé>=0 parce que nous présumons que le temps avance pendant que f() s'exécute, mais nous savons que les nombres d'un type donné sont situées entre deux bornes: si la valeur de avant est très près de la borne supérieure du type de données de maintenant(), alors il est possible que après-avant soit négatif pour cause de débordement. Dans ce cas, on peut réaliser des calculs fins pour savoir le nombre d'unités de temps passées entre avant et après, ou simplement rejeter la donnée.
Si écoulé==0, il faut se demander si l'unité de mesure choisie pour le test est suffisamment fine.
De manière plus globale, si on veut réaliser NTESTS tests et garder (a) le meilleur temps, (b) le pire temps et (c) le temps moyen, alors l'algorithme global ressemblera à ceci :
NTESTS ← ...
cpt ← 0
temps_total ← 0
temps_min ← 0
temps_max ← 0
Tant que cpt < NTESTS Faire
avant ← maintenant()
f()
après ← maintenant()
écoulé ← après - avant
Si écoulé >= 0 Alors
temps_total ← temps_total + écoulé
Si cpt == 0 Alors
temps_min ← écoulé
temps_max ← écoulé
Sinon
Si temps < temps_min Alors
temps_min ← écoulé
Si temps > temps_max Alors
temps_max ← écoulé
cpt ← cpt + 1
moyenne ← temps_total / NTESTS
Le test en soi est constitué des lignes en caractères gras. Les autres opérations sont des opérations de contrôle et de compilation de statistiques. On présume ici que temps_total ne subira pas de débordements, et on ne conserve que les temps qui ne sont pas négatifs.
On incrémentera habituellement cpt même si le temps de la mesure a été rejeté, pour éviter une boucle infinie en situation (pour le moins irrégulière) où tous les échantillons de temps saisis seraient invalides. Si le nombre de tests conservés doit absolument être celui fixé par NTESTS, alors on aura le choix de traiter les débordements de la variable temps avec rigueur, ou encore d'incrémenter cpt seulement dans le cas où la valeur de temps est valide.
Évidemment, pour pouvoir implémenter un tel algorithme, il faut savoir quels sont les outils disponibles pour saisir le temps courant. Il en existe beaucoup, on s'en doute, selon les langages de programmation et selon les plateformes.
Depuis C++ 11, nous sommes avantageusement servis par les outils de l'en-tête standard <chrono>, qui sont maintenant bien en place dans les compilateurs les plus importants.
//
// Opération à mesurer
//
void f() {
//
// Insérez le code désiré
//
}
#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;
int main() {
const int NTESTS = 100'000; // choisi avec soin (!)
system_clock::duration temps_total = {},
temps_min = {},
temps_max = {};
for(int cpt = 0; cpt < NTESTS;) {
auto avant = system_clock::now();
f();
auto apres = system_clock::now();
auto ecoule = duration_cast<milliseconds>(apres - avant);
if (ecoule.count() >= 0) {
temps_total += ecoule;
if (!cpt)
temps_min = temps_max = ecoule;
else
{
if (ecoule < temps_min)
temps_min = ecoule;
if (ecoule > temps_max)
temps_max = ecoule;
}
++cpt;
}
}
auto moyenne = temps_total.count() / static_cast<double>(NTESTS);
cout << "[ Statistiques pour un appel à f() ]" << endl
<< "Temps minimal: " << temps_min.count() << " ms." << endl
<< "Temps maximal: " << temps_max.count() << " ms." << endl
<< "Temps moyen: " << moyenne << " ms." << endl;
}
Si votre compilateur n'est pas à jour, vous avez tout de même accès aux outils du langage C, utiles encore avec C++ 03.
La fonction la plus connue pour saisir le temps courant en C ou en C++ est la fonction std::time(), rendue disponible dans les fichiers d'en-tête <time.h> (langage C) et <ctime> (langage C++). Cette fonction retourne le nombre de secondes depuis le 1er janvier 1970 à minuit (00:00:00), et n'offre donc qu'une précision très grossière.
La fonction std::time() retourne un std::time_t, qui correspond habituellement à un entier. Elle prend en paramètre l'adresse d'un std::time_t (qui peut être 0 ou nullptr, chaque fois au sens de pointeur nul). Si on passe un pointeur de std::time_t non nul à std::time(), alors la valeur insérée par std::time() dans la variable pointée sera la même que celle retournée par la fonction.
//
// Opération à mesurer. Probablement longue puisque nous
// mesurerons son temps d'exécution en secondes.
//
void f() {
//
// Insérez le code désiré
//
}
//
// Pas nécessaire, mais va alléger l'affichage dans main() en
// nous y évitant de convertir chaque time_t en int de manière
// manuelle dans les affichages avec std::cout
//
#include <ostream>
#include <ctime>
#include <iostream>
using namespace std;
ostream& operator<<(ostream &os, time_t t) {
return os << static_cast<int>(t); }
}
int main() {
const int NTESTS = 100000; // choisi avec soin (!)
time_t temps_total = 0,
temps_min = 0,
temps_max = 0;
for(int cpt = 0; cpt < NTESTS;) {
auto avant = time(nullptr);
f();
auto apres = time(nullptr);
auto ecoule = apres - avant;
if (ecoule >= 0)
{
temps_total += ecoule;
if (!cpt)
temps_min = temps_max = ecoule;
else
{
if (ecoule < temps_min)
temps_min = ecoule;
if (ecoule > temps_max)
temps_max = ecoule;
}
++cpt;
}
}
double moyenne = temps_total / static_cast<double>(NTESTS);
cout << "[ Statistiques pour un appel à f() ]" << endl
<< "Temps minimal: " << temps_min << " secondes" << endl
<< "Temps maximal: " << temps_max << " secondes" << endl
<< "Temps moyen: " << moyenne << " secondes" << endl;
}
Pour une précision plus grande tout en restant dans les fonctions standards de C et de C++ (donc dans le code indépendant de la plateforme), la fonction std::clock() de type std::clock_t retourne une valeur (entière) représentant le temps écoulé depuis le lancement du processus dans lequel l'appel est réalisé. Le temps est calculé en ombre de secondes multiplié par CLOCKS_PER_SEC, qui dépend de considérations matérielles (souvent, la valeur de CLOCKS_PER_SEC sera 1000, mais il ne faut pas compter là-dessus).
Un exemple utilisant cette fonction irait à peu près comme suit.
//
// Opération à mesurer.
//
void f() {
//
// Insérez le code désiré
//
}
#include <ctime>
#include <iostream>
using namespace std;
int main() {
const int NTESTS = 100000; // choisi avec soin (!)
clock_t temps_total = 0,
temps_min = 0,
temps_max = 0;
for (int cpt = 0; cpt < NTESTS; ) {
clock_t avant = clock();
f();
clock_t apres = clock();
clock_t ecoule = apres - avant;
if (ecoule >= 0)
{
temps_total += ecoule;
if (!cpt)
temps_min = temps_max = ecoule;
else
{
if (ecoule < temps_min)
temps_min = ecoule;
if (ecoule > temps_max)
temps_max = ecoule;
}
}
}
double moyenne = temps_total / static_cast<double>(NTESTS);
cout << "[ Statistiques pour un appel à f() ]" << endl
<< "Temps minimal: " << temps_min << ' ' << CLOCKS_PER_SEC << "ièmes de seconde" << endl
<< "Temps maximal: " << temps_max << ' ' << CLOCKS_PER_SEC << "ièmes de seconde" << endl
<< "Temps moyen: " << moyenne << ' ' << CLOCKS_PER_SEC << "ièmes de seconde" << endl;
}
On obtient avec clock() des mesures plus précises qu'avec time(), évidemment. Si clock() est incapable d'obtenir les métriques requises pour faire son travail, alors elle retourne -1, ce qui pourrait être validé en début de programme ou à l'aide d'un singleton.
#ifndef VALIDEUR_CLOCK_H
#define VALIDEUR_CLOCK_H
#include "Incopiable.h"
#include <ctime>
//
// Valider automatiquement l'usage de clock()
//
class ValideurClock
: Incopiable
{
ValideurClock() {
if (std::clock() == -1) exit(-1); // BOUM!
}
// le singleton
static ValideurClock singleton;
};
#include "ValideurClock.h"
ValideurClock ValideurClock::singleton;
Sous Java, on peut obtenir le temps écoulé (en millisecondes) depuis le lancement du processus en appelant la méthode de classe currentTimeMillis() de la classe System.
public class Z {
//
// Opération à mesurer.
//
static class Démo {
public void f() {
//
// Insérez le code désiré
//
}
}
public static void main(String [] args) {
final int NTESTS = 100000; // choisi avec soin (!)
long temps_total = 0,
temps_min = 0,
temps_max = 0;
for(int cpt = 0; cpt < NTESTS; ) {
Démo démo = new Démo();
long avant = System.currentTimeMillis();
démo.f();
long après = System.currentTimeMillis();
long écoulé = après - avant;
if (écoulé >= 0) {
temps_total += écoulé;
if (cpt == 0) {
temps_min = temps_max = écoulé;
} else {
if (écoulé < temps_min) {
temps_min = écoulé;
}
if (écoulé > temps_max) {
temps_max = écoulé;
}
}
++cpt;
}
}
double moyenne = temps_total / (double) NTESTS;
System.out.println("[ Statistiques pour un appel à f() ]");
System.out.println("Temps minimal: " + temps_min + " millisecondes");
System.out.println("Temps maximal: " + temps_max + " millisecondes");
System.out.println("Temps moyen: " + moyenne + " millisecondes");
}
}
On le voit, les différences entre Java, C et C++ sont petites, et on s'y retrouve facilement de l'un à l'autre.
Avec les langages .NET, on peut obtenir le temps écoulé à partir d'une instance de la classe Stopwatch, prise dans l'espace nommé System.Diagnostics.
using System;
namespace z
{
public class Z
{
//
// Opération à mesurer.
//
class Démo
{
public void f()
{
//
// Insérez le code désiré
//
}
}
public static void Main()
{
const int NTESTS = 100000; // choisi avec soin (!)
long temps_total = 0,
temps_min = 0,
temps_max = 0;
for (int cpt = 0; cpt < NTESTS; )
{
Démo démo = new Démo();
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
demo.f();
watch.Stop();
long écoulé = watch.Elapsed;
if (temps >= 0)
{
temps_total += écoulé;
if (cpt == 0)
temps_min = temps_max = écoulé;
else
{
if (temps < temps_min)
temps_min = écoulé;
if (temps > temps_max)
temps_max = écoulé;
}
++cpt;
}
}
double moyenne = temps_total / (double) NTESTS;
Console.WriteLine("[ Statistiques pour un appel à f() ]");
Console.WriteLine("Temps minimal: {0} millisecondes", temps_min);
Console.WriteLine("Temps maximal: {0} millisecondes", temps_max);
Console.WriteLine("Temps moyen: {0} millisecondes", moyenne);
}
}
}
On le voit encore une fois, les différences entre C#, Java, C et C++ sont petites, et on s'y retrouve facilement de l'un à l'autre.
Parfois, on peut souhaiter avoir recours à des outils de plus haute précision que ceux proposés par notre langage de programmation de manière portable. Il est fréquent qu'on doive alors avoir recours à des outils spécifiques au système d'exploitation.
Sous Win32, par exemple, on peut obtenir une mesure plus préciser qu'avec std::clock() en sollicitant la paire de fonctions QueryPerformanceCounter(), qui donne une mesure à haute précision en fonction de la fréquence d'un compteur rafraîchi très souvent, et QueryPerformanceFrequency(), qui indique la fréquence de ce compteur. Chaque plateforme aura son propre standard, ses propres types de données et ses propres fonctions.
Un exemple d'utilisation de ces fonctions suit. Pour plus de détails, consultez la documentation en ligne.
#include <iostream>
using namespace std;
#include <windows.h>
void f() {
Sleep(1500); // exemple simplet
}
int main() {
LARGE_INTEGER freq;
if (!QueryPerformanceFrequency(&freq)) {
cerr << "Zut, ça ne marche pas!" << endl;
exit (-1);
}
cout << "La fonction f() prend...";
LARGE_INTEGER avant, apres;
QueryPerformanceCounter(&avant);
f();
QueryPerformanceCounter(&apres);
cout << static_cast<double>(apres.QuadPart - avant.QuadPart) / freq.QuadPart * 1000.0;
cout << " millisecondes à s'exécuter" << endl;
}
Si les mesures dont vous avez besoin nécessitent de tels outils, alors utilisez-les. Si possible, évidemment, isolez-les de manière à pouvoir écrire vos bancs d'essai une seule fois peu importe la plateforme.
En C++, il est plus simple et plus élégant encore d'avoir recours à des mécanismes RAII pour mesurer le passage du temps. Pour les besoins de l'exemple qui suit, nous utiliserons std::clock(), fonction utilisée plus haut dans d'autres exemples. Au besoin, remplacez cette fonction par d'autres mécanismes; la technique restera essentiellement la même.
Pour cet exemple un peu naïf, nous aurons recours à une petite classe nommée MesureDeTemps. Nous saisirons aussi à la construction le nom de la fonction à tester et le flux dans lequel projeter les résultats des mesures de performance saisies. |
|
Une instance de MesureDeTemps saisira, dans son constructeur, le moment présent. Dans son destructeur, elle saisira à nouveau le moment présent, puis fera la différence entre ces deux valeurs et affichera le temps écoulé sur le flux en sortie choisi par le code client. |
|
Mesurer le temps d'exécution d'une fonction f() quelconque à l'aide d'une instance de MesureDeTemps devient d'une simplicité élémentaire. Le code dont on veut mesurer la vitesse d'exécution doit être entouré d'une paire d'accolades, délimitant ainsi la portée des entités qui y sont déclarées. Une instance de MesureDeTemps doit être déclarée dans ce bloc, juste avant le code à tester, et doit n'être suivie que de ce test, pour éviter de polluer les résultats obtenus. Le destructeur de l'instance de MesureDeTemps sera invoqué à la fin de la portée ce qui provoquera l'affichage du résultat de la mesure.
// ...
int main() {
// ...bla bla...
{
MesureDeTemps mt{"f()", cout};
f();
} // mt est détruit: affichage!
// ...bla bla...
}
Note rapide : ce qui suit était auparavant un texte se trouvant ailleurs sur le site, ayant été préparé dans le cadre d'un cours sur le parallélisme. Je le fusionnerai éventuellement de manière plus harmonieuse avec ce qui précède. D'ici lors, je vous prie de tolérer toute redondance et tout dédoublement d'information – je manque de temps.
Notez que les explications sont essentiellement présentées pour la version C++ 03, mais je vous propose aussi une version pour C++ 11 plus bas.
Ce qui suit montre comment il est possible de mettre en place un système RAII de mesure du temps écoulé par l'exécution d'un sous-programme, à l'aide d'outils de votre choix, tout en gardant la strate client portable.
L'idée derrière une démarche de mesure de performance d'un sous-programme (ou d'un programme entier) est qu'il est souvent utile, voire nécessaire, de valider des hypothèses de travail. Par exemple, est-ce qu'une optimisation donnée mène à des résultats concluants (car, soyons honnêtes, il arrive qu'une optimisation locale réduise les performances globales, tout comme il arrive que ce qui semblait être une optimisation entraîne des coûts inattendus, allant ainsi à l'encontre de la démarche d'optimisation)?
Parfois aussi, il arrive qu'on approche un problème selon des approches philosophiquement et qualitativement différentes, comme par exemple de manière séquentielle et de manière parallèle; les mérites de l'une et de l'autre des approches valent souvent la peine qu'on les mesure : une solution parallèle à un problème donné est-elle préférable à une solution séquentielle au même problème, malgré les coûts de la synchronisation encourus? Si oui, quels sont les paramètres environnementaux qui influencent ces gains (nombre de processeurs, nombre de threads, quantité de mémoire disponible, etc.)? À partir de quelle complexité d'échantillon à traiter voit-on des gains apparaître?
Parfois encore, on souhaitera comparer deux techniques, comme par exemple une synchronisation des accès concurrents à une zone de transit à l'aide de sections critiques, typiquement locales au processus, ou à l'aide d'un outil système comme le sont typiquement les mutex implémentés dans les systèmes d'exploitation les plus connus. Sachant que la contention influence l'impact de ces deux approches sur la performance d'ensemble du système, des métriques permettent de faire des choix éclairés.
L'optique de portabilité privilégiée ici repose sur un raisonnement relativement simple : personne n'a véritablement de temps à perdre. Si nous savons assurer la portabilité du code client de nos outils, toute migration du code client d'une plateforme à l'autre se trouvera par la suite allégée. L'alternative est d'utiliser le code local à la plateforme choisie directement dans le code client; cela fonctionne, mais implique des efforts plus importants pour toute migration ultérieure. Ça fonctionne aussi, bien entendu; le choix dépend de vous. Avis aux intéressé(e)s. Ce qui suit suppose que vous avez un intérêt pour cette démarche.
Fichier functional_extension.h | |
---|---|
L'optique de base proposée ici va comme suit :
Le problème qui nous attend est donc :
Les autres problèmes (identifier un référentiel, spécifier une action à prendre lorsque le calcul du temps écoulé a été fait) sont plus simples à régler et ne demandent pas de techniques avancées. Tel que le montre le code à droite, pour identifier le type de retour d'un foncteur ayant zéro, un ou deux paramètres (on pourrait faire de même avec plus de paramètres, mais c'est sans intérêt sur le plan théorique), on peut y arriver en injectant des types internes et publics à titre documentaire. Ici, les types dont il faut dériver pour y arriver sont nullary_function (type maison pour un foncteur sans paramètre), std::unary_function ou std::binary_function (ces deux derniers étant des foncteurs standards de <functional>). Pour intégrer harmonieusement fonctions et foncteurs, la solution la plus élégante présentement est d'avoir recours aux traits, représentés ici par nullary_function_traits et unary_function_traits. C++ 11 offre decltype() qui permet de simplifier cette technique. Le code requis pour définir ces strates d'isolation est visible à droite. Remarquez la spécialisation des traits sur la base de la signature des fonctions... Avec cela, le type nullary_function_traits<std::clock>::return_type est std::clock_t, et le type de l'opérateur () d'un foncteur comme
...est std::time_t. |
|
Fichier Minuterie.h | |
---|---|
Armé de ces outils, il devient envisageable de définir une minuterie RAII capable d'utiliser diverses sortes d'opérations unaires. La classe Minuterie, à droite, en est un exemple. Elle déduit le type de sont attribut avant_ du type retourné par timeOp, qu'il s'agisse d'une fonction ou d'une foncteur, et utilise une opération nulle (un no-op) par défaut comme action à réaliser lorsque la mesure du temps écoulé sera faite. Évidemment, il est probable que vous souhaitiez faire quelque chose de plus... signifiant, disons-le ainsi, lorsqu'une instance de Minuterie sera finalisée. Dans cette illustration, j'ai choisi d'utiliser une opération binaire (deux opérandes) au nom générique InfoOp comme opération à réaliser lors de la finalisation d'une Minuterie, et un exemple d'une telle opération est le foncteur afficher_temps_clock. Remarquez que la saisie du temps avant dans le constructeur de Minuterie se fait entre les accolades du constructeur, pas dans la séquence de préconstruction des attributs. Cette décision assure que la saisie du temps présent (à l'aide d'un TimeOp) soit la dernière étape de la construction d'une Minuterie, donc que l'initialisation des autres attributs n'interférera pas dans la captation des métriques. Vous comprendrez qu'il est important qu'on ne dérive pas de Minuterie, à moins que le constructeur de l'enfant ne fasse partie de ce qui doit être mesuré. |
|
Fichier precise_clock.h | |
---|---|
Un exemple d'implémentation d'outil de mesure non-portable mais harmonisé à notre infrastructure de saisie de métriques serait la classe precise_clock visible à droite. Elle repose sur les fonctions de saisie du temps écoulé précises d'un système d'exploitation spécifique, fonctions qui ne respectent pas les usages génériques que nous avons adoptés (des opérateurs nullaires retournant une mesure de temps qu'il est possible de manipuler comme un nombre). L'exemple montre donc comment sont encapsulées ces fonctions précises mais délinquantes (au sens de notre cadre de travail). On pourrait aller plus loin et rendre l'en-tête complètement portable à l'aide de l'idiome pImpl. |
|
Fichier precise_clock.cpp | |
---|---|
L'implémentation du code d'affichage suit le même modèle que celui utilisé pour la classe afficher_temps_clock, plus haut. Encore une fois, plusieurs raffinements pourraient être apportés ici. |
|
Reste à voir comment il est possible d'utiliser ces outils.
Fichier Principal.cpp (version un peu troup lourde) | |
---|---|
Une utilisation possible est celle proposée à droite. Vous remarquerez la lourdeur de la déclaration des variables de type Minuterie. Cette lourdeur tient du fait que le type d'une variable doit être connu du compilateur, dans le but de réserver suffisamment d'espace pour la loger. Ici, la classe étant générique, son type est paramétrique, et l'écriture de ce type est... disons déplaisante, mais surtout (en pratique) trop lourde pour être utilisée par le commun des mortels (et même par des spécialistes). La compréhension de trop de détails techniques est requise pour bien utiliser cette classe dans sa forme actuelle (oui, même les parenthèses autour de l'instanciation du precise_clock de la 2e instance de Minuterie sont nécessaires... On ne veut pas savoir tout ça pour être en mesure de faire des tests de performance!). |
|
Ne reste plus qu'à insérer le code à tester dans les fonctions comme f() et g().
Le langage C++ a beaucoup évolué entre C++ 03 et C++ 11, et les pratiques qui se sont développées et codifiées au fil des ans ont mené au développement d'outils qui simplifient beaucoup l'écriture.
Voici donc une version C++ 11 de l'exemple précédent; là où ce dernier était relativement complexe, ce qui suit sera beaucoup plus simple, en plus d'être plus portable et plus court.
Examinons, un fichier à la fois, ce que nous dit cette version. Je ne répéterai pas les explications déjà offertes, me limitant à une explication sommaire des raffinements que nous permet le nouveau standard.
Fichier functional_extension.h (version précédente) | |
---|---|
La raison d'être principale de ce fichier dans la version précédente était la définition de traits pour déduire aisément le type de retour de l'opération permettant de quantifier le concept de « maintenant ». Avec C++ 11, certains des traits les plus fréquemment rencontrés dans la pratique ont été définis de manière standard dans un fichier standard très utile et nommé <type_traits>. L'un de ces traits, std::result_of, permet précisément de déduire cette information, comme nous le verrons un peu plus bas. Ainsi, pour cette version, le fichier functional_extension.h n'est tout simplement pas requis. |
|
Fichier Minuterie.h (version précédente) | |
---|---|
N'ayant plus besoin de types internes particuliers, j'ai aplani la hiérarchie des afficheurs. Je n'ai entre autres plus recours à std::binary_function, par exemple, qui est obsolète avec C++ 11. Le type de retour d'un timeOp pour une Minuterie donnée est exprimé en termes du trait standard std::result_of. |
|
Fichier Minuterie.cpp (version précédente) | |
Peu de changements ici, outre peut-être le recours à auto pour réduire le couplage entre les expressions et le type de leur résultat. |
|
Fichier precise_clock.h (version précédente) | |
---|---|
Ce fichier est de beaucoup allégé si on le compare au précédent du fait que C++ 11 supporte officiellement le type long long, ce qui permet d'y avoir recours et d'éliminer <windows.h> du portrait. Notre en-tête est maintenant pleinement portable, le code non-portable ayant été relégué à un fichier source (ci-dessous). |
|
Fichier precise_clock.cpp (version précédente) | |
---|---|
Ce fichier a dû croître en taille en comparaison avec son prédécesseur, contenant maintenant les définitions de quelques méthodes, mais c'est un bien petit prix à payer pour la portabilité accrue des en-têtes qui en résulte. Remarquez le retour à la syntaxe unifiée des fonctions pour l'opérateur () d'un precise_clock. Voyez-vous quel est le gain ici? |
|
Fichier Principal.cpp (version précédente) | |
---|---|
Enfin, le code de test demeure tout aussi simple qu'auparavant. Ces gains en écriture et en simplicité ont été obtenus à coût zéro. |
|
Charmant, n'est-ce pas?
Pour alléger un peu plus la syntaxe de création d'une Minuterie<M,A> donnée, on peut avoir recours à une fonction génétratrice. Par exemple :
//
// version simple mais pas si mal
//
template <class NM, class A>
Minuterie<M,A> minuterie(M m, A a) {
return { m, a };
}
Cette fonction fait presque de la magie. En effet, elle permet d'éviter complètement d'engluer le code de test avec des déclarations de types complexes pour le système de minuterie. Le code de test devient tout simplement :
//
// ...
//
template <class F, class M, class A>
void tester(M mesure, F fct, A aff)
{
auto minu = minuterie(mesure, aff);
fct();
}
void f(); // fonction quelconque qu'on souhaite tester
void g(); // idem
int main() {
tester(system_clock::now, f, afficher_temps{cout});
tester(precise_clock(), g, afficher_temps_precise_clock{cout});
}
Plus simple, plus clair, et sans perte d'efficacité.
Avec l'avènement des templates variadiques, on peut maintenant généraliser le concept de tester le temps d'exécution d'une fonction étant donné un ensemble de paramètres. Par exemple :
template <class M, class F, class ... Args>
typename M::duration tester(F f, Args && ... args) {
auto avant = M::now();
f(forward<Args>(args)...);
return apres - M::now();
}
... ce qui pourrait s'utiliser comme ceci :
// ...
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;
int f(double, string);
int main() {
cout << duration_cast<milliseconds>(tester<system_clock>(3.14159, "Yo")) << endl;
}
On pourrait, si la valeur de retour de la fonction testée est importante, adapter le tout pour retourner une paire faite du temps écoulé et de cette valeur, comme suit :
#include <utility>
template <class M, class F, class ... Args>
auto tester(F f, Args && ... args) -> std::pair<decltype(f(forward<Args>(args)...)), typename M::duration> {
auto avant = M::now();
auto res = f(forward<Args>(args)...);
return std::make_pair(res, apres - M::now());
}
... ou encore, depuis C++ 14, comme suit :
#include <utility>
template <class M, class F, class ... Args>
auto tester(F f, Args && ... args) {
auto avant = M::now();
auto res = f(forward<Args>(args)...);
return std::make_pair(res, M::now() - avant);
}
... ce qui pourrait s'utiliser comme ceci :
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;
int f(double, string);
int main() {
auto res = tester<system_clock>(3.14159, "Yo"));
cout << "Resultat: " << res.first << ", temps: " << duration_cast<milliseconds>(res.second) << endl;
}
Voilà.
Quelques liens pour enrichir le propos.