timeit — Mesurer le temps d'exécution de fragments de code

Code source : Lib/timeit.py


Ce module fournit une façon simple de mesurer le temps d'exécution de fragments de code Python. Il expose une Interface en ligne de commande ainsi qu'une interface Python. Ce module permet d'éviter un certain nombre de problèmes classiques liés à la mesure des temps d'exécution. Voir par exemple à ce sujet l'introduction par Tim Peters du chapitre « Algorithmes » dans la seconde édition du livre Python Cookbook, aux éditions O'Reilly.

Exemples simples

L'exemple suivant illustre l'utilisation de l'Interface en ligne de commande afin de comparer trois expressions différentes :

$ python3 -m timeit '"-".join(str(n) for n in range(100))'
10000 loops, best of 5: 30.2 usec per loop
$ python3 -m timeit '"-".join([str(n) for n in range(100)])'
10000 loops, best of 5: 27.5 usec per loop
$ python3 -m timeit '"-".join(map(str, range(100)))'
10000 loops, best of 5: 23.2 usec per loop

L'Interface Python peut être utilisée aux mêmes fins avec :

>>> import timeit
>>> timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
0.3018611848820001
>>> timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000)
0.2727368790656328
>>> timeit.timeit('"-".join(map(str, range(100)))', number=10000)
0.23702679807320237

Un objet appelable peut également être passé en argument à l'Interface Python :

>>> timeit.timeit(lambda: "-".join(map(str, range(100))), number=10000)
0.19665591977536678

Notez cependant que timeit() détermine automatiquement le nombre de répétitions seulement lorsque l'interface en ligne de commande est utilisée. Vous pouvez trouver des exemples d'usages avancés dans la section Exemples.

Interface Python

Ce module définit une classe publique ainsi que trois fonctions destinées à simplifier son usage :

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)

Crée une instance d'objet Timer à partir de l'instruction donnée, du code setup et de la fonction timer, puis exécute sa méthode timeit() à number reprises. L'argument optionnel globals spécifie un espace de nommage dans lequel exécuter le code.

Modifié dans la version 3.5: Le paramètre optionnel globals a été ajouté.

timeit.repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None)

Crée une instance d'objet Timer à partir de l'instruction donnée, du code setup et de la fonction timer, puis exécute sa méthode repeat() à number reprises, repeat fois. L'argument optionnel globals spécifie un espace de nommage dans lequel exécuter le code.

Modifié dans la version 3.5: Le paramètre optionnel globals a été ajouté.

Modifié dans la version 3.7: La valeur par défaut de repeat est passée de 3 à 5.

timeit.default_timer()

Le minuteur par défaut, qui est toujours time.perf_counter().

Modifié dans la version 3.3: time.perf_counter() est désormais le minuteur par défaut.

class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>, globals=None)

Classe permettant de mesurer le temps d'exécution de fragments de code.

Ce constructeur prend en argument une instruction dont le temps d'exécution doit être mesuré, une instruction additionnelle de mise en place et une fonction de chronométrage. Les deux instructions valent 'pass' par défaut; la fonction de chronométrage dépend de la plateforme d'exécution (se référer au doc string du module). stmt et setup peuvent contenir plusieurs instructions séparées par des ; ou des sauts de lignes tant qu'ils ne comportent pas de littéraux sur plusieurs lignes. L'instruction est exécutée dans l'espace de nommage de timeit par défaut ; ce comportement peut être modifié en passant un espace de nommage au paramètre globals.

Pour mesurer le temps d'exécution de la première instruction, utilisez la méthode timeit(). Les méthodes repeat() et autorange() sont des méthodes d'agrément permettant d'appeler timeit() à plusieurs reprises.

Le temps d'exécution de setup n'est pas pris en compte dans le temps global d'exécution.

Les paramètres stmt et setup peuvent également recevoir des objets appelables sans argument. Ceci transforme alors les appels à ces objets en fonction de chronométrage qui seront exécutées par timeit(). Notez que le surcoût lié à la mesure du temps d'exécution dans ce cas est légèrement supérieur en raisons des appels de fonction supplémentaires.

Modifié dans la version 3.5: Le paramètre optionnel globals a été ajouté.

timeit(number=1000000)

Mesure le temps number exécution de l'instruction principale. Ceci exécute l'instruction de mise en place une seule fois puis renvoie un flottant correspondant au temps nécessaire à l'exécution de l'instruction principale à plusieurs reprises, mesuré en secondes. L'argument correspond au nombre d'itérations dans la boucle, par défaut un million. L'instruction principale, l'instruction de mise en place et la fonction de chronométrage utilisée sont passées au constructeur.

Note

Par défaut, timeit() désactive temporairement le ramasse-miettes pendant le chronométrage. Cette approche a l'avantage de permettre de comparer des mesures indépendantes. L'inconvénient de cette méthode est que le ramasse-miettes peut avoir un impact significatif sur les performances de la fonction étudiée. Dans ce cas, le ramasse-miettes peut être réactivé en première instruction de la chaîne setup. Par exemple :

timeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit()
autorange(callback=None)

Détermine automatiquement combien de fois appeler timeit().

Cette fonction d'agrément appelle timeit() à plusieurs reprises jusqu'à ce que le temps total écoulé soit supérieur à 0,2 secondes et renvoie le couple (nombre de boucles, temps nécessaire pour exécuter ce nombre de boucles). Elle appelle timeit() avec un nombre d'itérations croissant selon la séquence 1, 2, 5, 10, 20, 50, … jusqu'à ce que le temps d'exécution dépasse 0,2 secondes.

Si callback est spécifié et n'est pas None, elle est appelée après chaque itération avec deux arguments (numéro de l'itération et temps écoulé) : callback(number, time_taken).

Nouveau dans la version 3.6.

repeat(repeat=5, number=1000000)

Appelle timeit() plusieurs fois.

Cette fonction d'agrément appelle timeit() à plusieurs reprises et renvoie une liste de résultats. Le premier argument spécifie le nombre d'appels à timeit(). Le second argument spécifie l'argument number de timeit().

Note

Il est tentant de vouloir calculer la moyenne et l'écart-type des résultats et notifier ces valeurs. Ce n'est cependant pas très utile. En pratique, la valeur la plus basse donne une estimation basse de la vitesse maximale à laquelle votre machine peut exécuter le fragment de code spécifié ; les valeurs hautes de la liste sont typiquement provoquées non pas par une variabilité de la vitesse d'exécution de Python, mais par d'autres processus interférant avec la précision du chronométrage. Le min() du résultat est probablement la seule valeur à laquelle vous devriez vous intéresser. Pour aller plus loin, vous devriez regarder l'intégralité des résultats et utiliser le bon sens plutôt que les statistiques.

Modifié dans la version 3.7: La valeur par défaut de repeat est passée de 3 à 5.

print_exc(file=None)

Outil permettant d'afficher la trace du code chronométré.

Usage typique :

t = Timer(...)       # outside the try/except
try:
    t.timeit(...)    # or t.repeat(...)
except Exception:
    t.print_exc()

L'avantage par rapport à la trace standard est que les lignes sources du code compilé sont affichées. Le paramètre optionnel file définit l'endroit où la trace est envoyée, par défaut sys.stderr.

Interface en ligne de commande

Lorsque le module est appelé comme un programme en ligne de commande, la syntaxe suivante est utilisée :

python -m timeit [-n N] [-r N] [-u U] [-s S] [-h] [statement ...]

Les options suivantes sont gérées :

-n N, --number=N

nombre d'exécutions de l'instruction statement

-r N, --repeat=N

nombre de répétitions du chronomètre (5 par défaut)

-s S, --setup=S

instruction exécutée une seule fois à l'initialisation (pass par défaut)

-p, --process

mesure le temps au niveau du processus et non au niveau du système, en utilisant time.process_time() plutôt que time.perf_counter() qui est utilisée par défaut

Nouveau dans la version 3.3.

-u, --unit=U

spécifie l'unité de temps utilisée pour la sortie du chronomètre (parmi nsec, usec, msec ou sec)

Nouveau dans la version 3.5.

-v, --verbose

affiche les temps d'exécutions bruts, répéter pour plus de précision

-h, --help

affiche un court message d'aide puis quitte

Une instruction sur plusieurs lignes peut être donnée en entrée en spécifiant chaque ligne comme un argument séparé. Indenter une ligne est possible en encadrant l'argument de guillemets et en le préfixant par des espaces. Plusieurs -s sont gérées de la même façon.

Si -n n'est pas donnée, un nombre de boucles approprié est calculé en essayant des nombres croissants de la séquence 1, 2, 5, 10, 20, 50, ... jusqu'à ce que le temps total d'exécution dépasse 0,2 secondes.

Les mesures de default_timer() peuvent être altérées par d'autres programmes s'exécutant sur la même machine. La meilleure approche lorsqu'un chronométrage exact est nécessaire est de répéter celui-ci à plusieurs reprises et considérer le meilleur temps. L'option -r est adaptée à ce fonctionnement, les cinq répétitions par défaut suffisent probablement dans la plupart des cas. Vous pouvez utiliser time.process_time() pour mesurer le temps processeur.

Note

Il existe un surcoût minimal associé à l'exécution de l'instruction pass. Le code présenté ici ne tente pas de le masquer, mais vous devez être conscient de son existence. Ce surcoût minimal peut être mesuré en invoquant le programme sans argument ; il peut différer en fonction des versions de Python.

Exemples

Il est possible de fournir une instruction de mise en place exécutée une seule fois au début du chronométrage :

$ python -m timeit -s 'text = "sample string"; char = "g"'  'char in text'
5000000 loops, best of 5: 0.0877 usec per loop
$ python -m timeit -s 'text = "sample string"; char = "g"'  'text.find(char)'
1000000 loops, best of 5: 0.342 usec per loop

In the output, there are three fields. The loop count, which tells you how many times the statement body was run per timing loop repetition. The repetition count ('best of 5') which tells you how many times the timing loop was repeated, and finally the time the statement body took on average within the best repetition of the timing loop. That is, the time the fastest repetition took divided by the loop count.

>>> import timeit
>>> timeit.timeit('char in text', setup='text = "sample string"; char = "g"')
0.41440500499993504
>>> timeit.timeit('text.find(char)', setup='text = "sample string"; char = "g"')
1.7246671520006203

La même chose peut être réalisée en utilisant la classe Timer et ses méthodes :

>>> import timeit
>>> t = timeit.Timer('char in text', setup='text = "sample string"; char = "g"')
>>> t.timeit()
0.3955516149999312
>>> t.repeat()
[0.40183617287970225, 0.37027556854118704, 0.38344867356679524, 0.3712595970846668, 0.37866875250654886]

Les exemples qui suivent montrent comment chronométrer des expressions sur plusieurs lignes. Nous comparons ici le coût d'utilisation de hasattr() par rapport à try/except pour tester la présence ou l'absence d'attributs d'un objet :

$ python -m timeit 'try:' '  str.__bool__' 'except AttributeError:' '  pass'
20000 loops, best of 5: 15.7 usec per loop
$ python -m timeit 'if hasattr(str, "__bool__"): pass'
50000 loops, best of 5: 4.26 usec per loop

$ python -m timeit 'try:' '  int.__bool__' 'except AttributeError:' '  pass'
200000 loops, best of 5: 1.43 usec per loop
$ python -m timeit 'if hasattr(int, "__bool__"): pass'
100000 loops, best of 5: 2.23 usec per loop
>>> import timeit
>>> # attribute is missing
>>> s = """\
... try:
...     str.__bool__
... except AttributeError:
...     pass
... """
>>> timeit.timeit(stmt=s, number=100000)
0.9138244460009446
>>> s = "if hasattr(str, '__bool__'): pass"
>>> timeit.timeit(stmt=s, number=100000)
0.5829014980008651
>>>
>>> # attribute is present
>>> s = """\
... try:
...     int.__bool__
... except AttributeError:
...     pass
... """
>>> timeit.timeit(stmt=s, number=100000)
0.04215312199994514
>>> s = "if hasattr(int, '__bool__'): pass"
>>> timeit.timeit(stmt=s, number=100000)
0.08588060699912603

Afin de permettre à timeit d'accéder aux fonctions que vous avez définies, vous pouvez passer au paramètre setup une instruction d'importation :

def test():
    """Stupid test function"""
    L = [i for i in range(100)]

if __name__ == '__main__':
    import timeit
    print(timeit.timeit("test()", setup="from __main__ import test"))

Une autre possibilité est de passer globals() au paramètre globals, ceci qui exécutera le code dans l'espace de nommage global courant. Cela peut être plus pratique que de spécifier manuellement des importations :

def f(x):
    return x**2
def g(x):
    return x**4
def h(x):
    return x**8

import timeit
print(timeit.timeit('[func(42) for func in (f,g,h)]', globals=globals()))