Question 176

Comment chronométrer mon programme ? Comment mesurer précisément le temps ?

Introduction

Il est parfois utile de mesurer une durée entre 2 instants lors de l'exécution du programme, par exemple pour réaliser un chronomètre, pour mesurer les performances d'une fonction ou d'une section de code, pour effectuer une opération à intervalle régulier ou après un temps donné, etc. En fonction de la précision souhaitée, plusieurs méthodes sont possibles.

Cet article présente les méthodes suivantes :

Le contrôle Timer

Ce contrôle permet de déclencher un évènement à intervalle régulier. Il est possible via sa propriété "Interval" de fixer la fréquence de déclenchement à 1 ms mais, en pratique, la précision n'est jamais supérieure à 55 ms dans le meilleur des cas. Sa bonne intégration dans VB et sa simplicité d'utilisation en font néanmoins un outil pratique pour des applications où une précision de l'ordre du dixième de seconde est suffisante.

La fonction Timer

A ne pas confondre avec la fonction "Time", la fonction Timer retourne un Single qui représente le nombre de secondes écoulées depuis minuit. La fonction retourne un nombre en simple précision, limité à 2 décimales. On obtient donc au mieux des centièmes de secondes. Dans la pratique, la précision est même souvent moins bonne. On peut néanmoins l'utiliser quand on n'a pas besoin d'une précision meilleure que le centième voire le dixième de seconde. Attention : il ne faut jamais utiliser la valeur telle quelle, mais toujours utiliser une différence entre 2 appels à cette fonction (à cause du passage à zéro à minuit).

Exemple d'utilisation

    Dim t_start As Single, t_end As Single, elapsed As Single
    Dim i As Long, s As String
    
    t_start = Timer
    For i = 1 To 30000
        s = s & "HELLO "
    Next i
    t_end = Timer
    
    elapsed = CSng(Round(t_end - t_start, 2))
    
    MsgBox "Il faut " & elapsed & " pour faire 30000 concaténations"

L'utilisation dans ce contexte est correcte, car la portion de code mesurée (la boucle de 30000 itérations) mets plus d'une seconde pour s'exécuter. Sur un grand nombre d'itérations, la précision obtenue est suffisante.

La fonction (API) timeGetTime

Cette fonction retourne le "system time" (durée depuis le dernier démarrage de Windows), en millisecondes. Note : la valeur retournée repasse à zéro environ tous les 49 jours. Pour des calculs de durée, on veillera à toujours utiliser non pas le retour de la fonction mais la différence entre les retours de 2 appels successifs (comme pour Timer). La précision par défaut peut varier, mais on peut utiliser les fonctions timeBeginPeriod et timeEndPeriod pour spécifier la précision. La meilleure précision qu'il est possible de spécifier est de 1 ms. En pratique, la résolution est de l'ordre de 16 millisecondes (ce qui signifie qu'il faut 16 millisecondes pour avoir un changement de valeur entre 2 appels successifs à timeGetTime).

Exemple d'utilisation

Private Declare Function timeGetTime Lib "winmm.dll" () As Integer
Private Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Integer) As Integer
Private Declare Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Integer) As Integer


    Dim ret As Integer
    Dim t_start As Long, t_end As Long, elapsed As Long
    Dim i As Long, s As String

    timeBeginPeriod 1
    
    t_start = timeGetTime()
    For i = 1 To 30000
        s = s & "HELLO "
    Next i
    t_end = timeGetTime()
    
    elapsed = t_end - t_start
    
    MsgBox "Il faut " & elapsed & " millisecondes pour faire 30000 concaténations"
    
    timeEndPeriod 1

Cette API offre de bonnes performances. Son utilisation est tout à fait adaptée pour toute application nécessitant une précision de l'ordre d'une dizaine de millisecondes. C'est l'API de choix pour effectuer des mesures de performances. On n'a de toute façon pas besoin d'une résolution supérieure, puisqu'une mesure de performances ne se conçoit que comme la moyenne des mesures d'un grand nombre d'exécutions de ce que l'on veut mesurer.

La fonction (API) GetTickCount

Cette fonction est très proche de la fonction timeGetTime. Elle retourne aussi le nombre de millisecondes écoulées depuis le dernier démarrage du système. Sa résolution théorique est de 1 milliseconde, mais elle est limitée par la résolution du timer du système. Son comportement peut être affectée par des appels à l'API SetSystemTimeAdjustement. On se réfèrera à la documentation (voir section "Aller plus loin") pour plus de détails.

En pratique, la résolution est de l'odre de 16 milliseconde, comme pout timeGetTime (ce qui signifie qu'il faut 16 millisecondes pour avoir un changement de valeur entre 2 appels successifs à GetTickCount).

Exemple d'utilisation

Private Declare Function GetTickCount Lib "kernel32" () As Long
' Effectue une pause de sleep_interval millisecondes
Private Sub MySleep(ByVal sleep_interval As Long)
    Dim start_time As Long

    start_time = GetTickCount
    While start_time + sleep_interval > GetTickCount
        DoEvents
    Wend
End Sub

La fonction (API) QueryPerformanceCounter : timer à très haute résolution

L'API Windows met à la disposition des programmeurs un set d'API regroupées sous le nom de "high-resolution performance counter" (compteurs à haute résolution). Ces fonctions permettent de mesurer des durées ou intervalles de durées avec une très grande précision. La précision réelle finale dépend de la vitesse du processeur de la machine. Sur une machine récente à 3.4 Ghz, la précision théorique du compteur est de 1/3.391.640.000, ce qui donne une résolution de environ 0,3 nanoseconde... Sur une machine plus modeste à 1 Ghz, on atteint encore une résolution de 1 nanoseconde (1.E-9 seconde). A noter que l'exécution de cette fonction (l'appel en lui même) prend de une à deux microsecondes sur une machine à 3,4 Ghz.

Note : la documentation indique que certains hardwares peuvent ne pas supporter cette API. Dans la pratique, nous n'avons jamais rencontré ce cas. Ils semblent être implémentés sur tous les processeurs plus récents qu'un 80386.

Il faut signaler que peu d'applications nécessitent de connaitre ou de mesurer le temps avec une telle précision. L'utilisation de cet ensemble de fonctions est probablement à réserver pour des cas très particuliers, notamment d'interfaçage ou de contrôle avec du matériel ou des périphériques demandant un pilotage ou un monitoring ultra-précis. On pourra aussi l'utiliser pour réaliser des mesures de performances, en gardant à l'esprit que la mesure doit se faire sur un grand nombre d'exécutions.

La manipulation de ces API n'est pas très compliquée, mais nécessite l'emploi de structures de données particulières pour le stockage précis de très grands nombres.

Exemple d'utilisation : implémentation d'une Classe Chronomètre

Enregistrer le code suivant dans une classe : chrono.cls

Option Explicit

Private Type LARGE_INTEGER
    LowPart As Long
    HighPart As Long
End Type

Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As LARGE_INTEGER) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As LARGE_INTEGER) As Long
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

Private liStart As LARGE_INTEGER
Private liStop As LARGE_INTEGER
Private liFrequency As LARGE_INTEGER

Public Enum CounterUnit
    Second = 1
    Millisecond = 2
    microsecond = 3
    nanosecond = 4
End Enum

Private m_ResultUnit As CounterUnit

Public Property Let ResultUnit(ur As CounterUnit)
    m_ResultUnit = ur
End Property

Public Property Get ResultUnit() As CounterUnit

        ResultUnit = m_ResultUnit
End Property

Public Sub TimerStart()

    QueryPerformanceCounter liStart
End Sub

Public Sub TimerStop()

    QueryPerformanceCounter liStop
End Sub

Public Function getTimeElapsed() As Double
    Dim cuStart As Currency
    Dim cuStop As Currency
    Dim cuFreq As Currency
    Dim v As Double
    
    QueryPerformanceFrequency liFrequency
    
    cuStart = LargeIntToCurrency(liStart)
    cuStop = LargeIntToCurrency(liStop)
    cuFreq = LargeIntToCurrency(liFrequency)
    ' elapsed time
    v = CDbl(cuStop - cuStart) / CDbl(cuFreq)
    Select Case ResultUnit
        Case Second
            getTimeElapsed = v
        Case Millisecond
            getTimeElapsed = v * 1000#
        Case microsecond
            getTimeElapsed = v * 1000000#
        Case nanosecond
            getTimeElapsed = v * 1000000000#
    End Select
End Function

Private Function LargeIntToCurrency(liInput As LARGE_INTEGER) As Currency

    CopyMemory LargeIntToCurrency, liInput, LenB(liInput)
    LargeIntToCurrency = LargeIntToCurrency * 10000
End Function

Et pour l'utiliser, rien de plus simple :

    Dim c As New chrono
    Dim i As Integer, s As String

    c.TimerStart
    
    For i = 1 To 30000
        s = s & "HELLO "
    Next i
    
    c.TimerStop
    
    c.ResultUnit = microsecond
    
    MsgBox "Temps écoulé : " & c.getTimeElapsed & " microsecondes."

A noter qu'on peut aussi utiliser directement le type Currency au lieu de la structure LARGE_INTEGER :

Private Declare Function QueryPerformanceCounter Lib "Kernel32" (X As Currency) As Boolean
Private Declare Function QueryPerformanceFrequency Lib "Kernel32" (X As Currency) As Boolean
                             
Sub Test()
    Dim Ctr1 As Currency, Ctr2 As Currency, Freq As Currency
    Dim i As Long, n As Double
    
    QueryPerformanceCounter Ctr1
    For i = 1 To 10000
        n = Sqr(2)
    Next i
    QueryPerformanceCounter Ctr2
    QueryPerformanceFrequency Freq
    
    MsgBox "Temps écoulé : " & ((Ctr2 - Ctr1) / Freq) * 1000 & " millisecondes"
    
End Sub

Pour aller plus loin

Voir aussi :

Date de publication : 13 septembre 2007
Dernière modification : 06 mars 2008
Rubriques : Divers
Mots-clés : Chronomètre, mesure, performances, temps, benchmark, performance, précision, temps, durée, GetTickCount, timeGetTime, QueryPerformanceCounter