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()

The default timer, which is always time.perf_counter(), returns float seconds. An alternative, time.perf_counter_ns, returns integer nanoseconds.

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)

Time number executions of the main statement. This executes the setup statement once, and then returns the time it takes to execute the main statement a number of times. The default timer returns seconds as a float. The argument is the number of times through the loop, defaulting to one million. The main statement, the setup statement and the timer function to be used are passed to the constructor.

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().

This is a convenience function that calls timeit() repeatedly so that the total time >= 0.2 second, returning the eventual (number of loops, time taken for that number of loops). It calls timeit() with increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2 seconds.

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

specify a time unit for timer output; can select nsec, usec, msec, or 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()))