Chronométrer les temps d'exécution à l'aide de la fonction clock_gettime

Une fois l'efficacité d'un algorithme démontrée théoriquement, il est souvent nécessaire de comparer la performance réelle d'implantations concurrentes. Pour ce faire, le temps d'exécution de chaque implantation doit être déterminé empiriquement.

Si la tâche peut paraître simple à première vue, celle-ci se révèle être relativement délicate dans la pratique. En effet, puisque les mesures sont effectuées sur des systèmes d'exploitation préemptifs, toutes les approches fondées sur des lectures directes de l'horloge système doivent être écartées.

Heureusement, les systèmes d'exploitation UNIX définissent généralement la fonction clock_gettime qui offre une solution élégante à ce problème. Cette dernière permet d'obtenir soit le temps d'exécution du processus ou du thread courant.

Une interface plus intéressante

Si l'usage de la fonction clock_gettime est très simple, les manipulations requises afin de calculer le temps d'exécution d'un segment de code particulier sont plus délicates, des additions et soustractions de timespec étant nécessaires. D'où l'intérêt de placer ces dernières derrières une interface simple et cohérente.

Entre donc en scène la classe Chrono dont la seule tâche est de rendre l'utilisation de la fonction clock_gettime aussi conviviale que possible dans des projets en C++.

class Chrono {
private:
    bool stopped;
    timespec start_time; /**< Chronometer's start time. */
    timespec total_time; /**< Cumulative chronometer's running time. */
    int clock_id;        /**< Chronometer underlying clock id. */

public:
    //! \brief Default chronometer's constructor.
    //!
    //! Build a chronometer mesuring the process execution time.
    Chrono();

    //! \brief Chronometer's constructor.
    //! \param clock The clock type used by the chronometer.
    //!
    //! Build a chronometer, specifying the underlying clock. The underlying
    //! chronometer's clock is used to determine whether the chronometer
    //! will count time for the current thread or for the whole process.
    Chrono(Clock::Type);

    //! \brief Start chronometer.
    void start() throw(std::runtime_error);

    //! \brief Stop chronometer.
    //! \post The elapsed process or thread execution time since last call to
    //! the start method is added to the chronometer's total time.
    void stop() throw(std::runtime_error);

    //! \brief Reset chronometer's time.
    //! \post The chronometer's total time is set to zero.
    void reset();

    //! \brief Retrieve chronometer's second count.
    //! \return The second count.
    long long nsec() const throw (std::logic_error);

    //! \brief Retrieve chronometer's nanosecond count.
    //! \return The nanosecond count.
    long long sec() const throw (std::logic_error);
};

Le comportement de la classe Chrono est l'image d'un chronomètre. Une fois instanciée, un appel à la fonction start démarre le chronomètre alors qu'un appel à la fonction stop l'arrête — rien de bien étonnant.

Tout comme pour un chronomètre standard, le temps d'exécution écoulé entre chaque paire d'appels aux méthodes start et stop est cumulé. Pour réinitialiser le chronomètre, la méthode reset doit être appelée.

Le second constructeur permet de spécifier quelle horloge utiliser: l'horloge du processus ou du thread courant.

Voici un exemple d'utilisation de la classe Chrono pour mesurer le temps d'exécution de l'algorithme de tri de la librairie standard du C++.

#include <vector>
#include <algorithm>

#include "chronometer.hpp"

using namespace benchmark;
using namespace std;

int main() {

    Chrono chrono(Clock::Thread);
    vector<int> v;

    for (int i = 0; i < 1000000; i++)
        v.push_back(i);

    random_shuffle(v.begin(), v.end());

    chrono.start();
    sort(v.begin(), v.end());
    chrono.stop();

    cout << chrono.sec() << ":" << chrono.nsec() << endl;

    return 0;
}

Implantation

Si l'implantation de cette classe est simple, deux fonctions méritent quelques explications.

La première fonction, timespec_delta, permet de la calculer la différence entre deux timespec. Une simple soustraction entre les attributs du timespec ne suffit pas car il faut tenir compte d'une éventuelle valeur négative résultant de la soustraction des nanosecondes; auquel cas une retenue doit être effectuée au compte des secondes pour être ajoutée au compte des nanosecondes.

timespec timespec_delta(const timespec& t1, const timespec& t2) {
    timespec result;

    if ((t2.tv_nsec - t1.tv_nsec) < 0 /* ns */) {
        result.tv_sec = t2.tv_sec - t1.tv_sec - 1;
        result.tv_nsec = 1000000000 /* ns */ + t2.tv_nsec - t1.tv_nsec;
    } else {
        result.tv_sec = t2.tv_sec - t1.tv_sec;
        result.tv_nsec = t2.tv_nsec - t1.tv_nsec;
    }

    return result;
}

La seconde fonction, timespec_add, permet d'additionner deux timespec. Pour la même raison qu'une simple soustraction entre les attributs du timespec ne suffisait pas pour la fonction timespec_delta, une simple addition des attributs est insuffisante. Il faut en effet tenir compte d'un éventuel débordement du compte des nanosecondes; auquel cas une retenue doit être effectuée au compte des nanosecondes pour être ajoutée au compte des secondes.

timespec timespec_add(const timespec& t1, const timespec& t2) {
    timespec result;

    if ((t1.tv_nsec + t2.tv_nsec) > 1000000000 /* ns */) {
        result.tv_sec = t1.tv_sec + t2.tv_sec + 1;
        result.tv_nsec = t1.tv_nsec + t2.tv_nsec - 1000000000 /* ns */;
    } else {
        result.tv_sec = t1.tv_sec + t2.tv_sec;
        result.tv_nsec = t1.tv_nsec + t2.tv_nsec;
    }

    return result;
}

Le reste de l'implantation peut être consulté en ligne. Sous Linux, le code utilisant la fonction clock_gettime doit être lié contre librt.

Il est possible d'implanter l'équivalent de la classe Chrono sous Windows à l'aide des fonctions GetProcessTimes et GetThreadTimes. La résolution de ces dernières laisse toutefois à désirer (1/64s).