timeit — Mät exekveringstid för små kodsnuttar

Källkod: Lib/timeit.py


Denna modul ger ett enkelt sätt att tidsinställa små bitar av Python-kod. Den har både ett Kommandoradsgränssnitt och ett callable. Det undviker ett antal vanliga fällor för att mäta exekveringstider. Se även Tim Peters introduktion till kapitlet ”Algorithms” i den andra upplagan av Python Cookbook, utgiven av O’Reilly.

Grundläggande exempel

Följande exempel visar hur Kommandoradsgränssnitt kan användas för att jämföra tre olika uttryck:

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

Detta kan uppnås från Python-gränssnitt med:

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

En anropsbar kan också skickas från Python-gränssnitt:

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

Observera dock att timeit() automatiskt kommer att bestämma antalet repetitioner endast när kommandoradsgränssnittet används. I avsnittet Exempel kan du hitta mer avancerade exempel.

Python-gränssnitt

Modulen definierar tre bekvämlighetsfunktioner och en publik klass:

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

Skapa en instans av Timer med den angivna satsen, setup-koden och timer-funktionen och kör dess timeit()-metod med antal exekveringar. Det valfria argumentet globals anger ett namnrymd där koden ska exekveras.

Ändrad i version 3.5: Den valfria parametern globals har lagts till.

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

Skapa en instans av Timer med den angivna satsen, setup-koden och timer-funktionen och kör dess repeat()-metod med det angivna repeat-antalet och antalet exekveringar. Det valfria argumentet globals anger ett namnrymd där koden ska exekveras.

Ändrad i version 3.5: Den valfria parametern globals har lagts till.

Ändrad i version 3.7: Standardvärdet för repeat har ändrats från 3 till 5.

timeit.default_timer()

Standardtimern, som alltid är time.perf_counter(), returnerar float-sekunder. Ett alternativ, time.perf_counter_ns, returnerar heltalet nanosekunder.

Ändrad i version 3.3: time.perf_counter() är nu standardtimern.

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

Klass för att mäta exekveringshastigheten för små kodsnuttar.

Konstruktören tar ett uttalande som ska tidsbestämmas, ett ytterligare uttalande som används för inställning och en timerfunktion. Båda satserna har 'pass' som standard; timerfunktionen är plattformsberoende (se modulens dokumentsträng). stmt och setup kan också innehålla flera satser åtskilda av ; eller nya rader, så länge de inte innehåller stränglitteraler på flera rader. Satsen kommer som standard att exekveras inom timeits namnrymd; detta beteende kan kontrolleras genom att skicka en namnrymd till globals.

För att mäta exekveringstiden för den första satsen använder du metoden timeit(). Metoderna repeat() och autorange() är bekvämlighetsmetoder för att anropa timeit() flera gånger.

Exekveringstiden för setup är exkluderad från den totala tidsbestämda körningen.

Parametrarna stmt och setup kan också ta objekt som är anropsbara utan argument. Detta kommer att bädda in anrop till dem i en timerfunktion som sedan kommer att utföras av timeit(). Observera att tidsöverskottet är lite större i det här fallet på grund av de extra funktionsanropen.

Ändrad i version 3.5: Den valfria parametern globals har lagts till.

timeit(number=1000000)

Tid antal exekveringar av huvudsatsen. Detta exekverar setup-satsen en gång och returnerar sedan den tid det tar att exekvera huvudsatsen ett antal gånger. Standardtimern returnerar sekunder som en flottör. Argumentet är antalet gånger genom slingan, med standardvärdet en miljon. Main-satsen, setup-satsen och den timerfunktion som ska användas skickas till konstruktören.

Anteckning

Som standard stänger timeit() tillfälligt av garbage collection under tidtagningen. Fördelen med detta tillvägagångssätt är att det gör oberoende tidtagningar mer jämförbara. Nackdelen är att GC kan vara en viktig komponent i prestandan för den funktion som mäts. Om så är fallet kan GC återaktiveras som det första uttalandet i setup-strängen. Till exempel:

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

Bestäm automatiskt hur många gånger du ska anropa timeit().

Detta är en bekvämlighetsfunktion som anropar timeit() upprepade gånger så att den totala tiden >= 0,2 sekunder och returnerar resultatet (antal loopar, tidsåtgång för detta antal loopar). Den anropar timeit() med ökande nummer från sekvensen 1, 2, 5, 10, 20, 50, … tills den totala tiden är minst 0,2 sekunder.

Om callback anges och inte är None, kommer den att anropas efter varje försök med två argument: callback(number, time_taken).

Tillagd i version 3.6.

repeat(repeat=5, number=1000000)

Anropa timeit() några gånger.

Detta är en bekvämlighetsfunktion som anropar timeit() upprepade gånger och returnerar en lista med resultat. Det första argumentet anger hur många gånger timeit() ska anropas. Det andra argumentet anger number-argumentet för timeit().

Anteckning

Det är frestande att beräkna medelvärde och standardavvikelse från resultatvektorn och rapportera dessa. Detta är dock inte särskilt användbart. I ett typiskt fall ger det lägsta värdet en nedre gräns för hur snabbt din maskin kan köra det givna kodavsnittet; högre värden i resultatvektorn orsakas vanligtvis inte av variabilitet i Pythons hastighet, utan av andra processer som stör din tidsnoggrannhet. Så min() av resultatet är förmodligen det enda talet du bör vara intresserad av. Efter det bör du titta på hela vektorn och tillämpa sunt förnuft snarare än statistik.

Ändrad i version 3.7: Standardvärdet för repeat har ändrats från 3 till 5.

print_exc(file=None)

Hjälpmedel för att skriva ut en traceback från den tidsinställda koden.

Typisk användning:

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

Fördelen jämfört med standardtraceback är att källkodsrader i den kompilerade mallen visas. Det valfria argumentet file anger vart spårningen ska skickas; standardvärdet är sys.stderr.

Kommandoradsgränssnitt

När ett program anropas från kommandoraden används följande form:

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

Där följande alternativ förstås:

-n N, --number=N

hur många gånger ska ”statement” utföras

-r N, --repeat=N

hur många gånger timern ska upprepas (standard 5)

-s S, --setup=S

uttalande som ska köras en gång initialt (standard pass)

-p, --process

mäta processtid, inte väggklocktid, med time.process_time() istället för time.perf_counter(), som är standard

Tillagd i version 3.3.

-u, --unit=U

ange en tidsenhet för timerutmatning; kan välja nsec, usec, msec eller sec

Tillagd i version 3.5.

-v, --verbose

skriv ut råa tidsresultat; upprepa för fler siffror precision

-h, --help

skriva ut ett kort användarmeddelande och avsluta

Ett uttalande med flera rader kan anges genom att specificera varje rad som ett separat uttalandeargument; indragna rader är möjliga genom att omsluta ett argument med citattecken och använda inledande mellanslag. Flera -s-alternativ behandlas på liknande sätt.

Om -n inte anges, beräknas ett lämpligt antal loopar genom att prova ökande antal från sekvensen 1, 2, 5, 10, 20, 50, … tills den totala tiden är minst 0,2 sekunder.

default_timer() mätningar kan påverkas av andra program som körs på samma maskin, så det bästa man kan göra när exakt tidtagning är nödvändig är att upprepa tidtagningen några gånger och använda den bästa tiden. Alternativet -r är bra för detta; standardvärdet 5 repetitioner är förmodligen tillräckligt i de flesta fall. Du kan använda time.process_time() för att mäta CPU-tiden.

Anteckning

Det finns en viss baseline overhead förknippad med att köra en pass-sats. Koden här försöker inte dölja det, men du bör vara medveten om det. Den grundläggande kostnaden kan mätas genom att anropa programmet utan argument, och den kan skilja sig mellan olika Python-versioner.

Exempel

Det är möjligt att tillhandahålla en setup-sats som bara körs en gång i början:

$ 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

I utdata finns det tre fält. Loopantalet, som visar hur många gånger satsen kördes per repetition av tidtagningsslingan. Upprepningsantalet (”bäst av 5”), som visar hur många gånger tidtagningsslingan upprepades, och slutligen den tid som satsen i genomsnitt tog i den bästa upprepningen av tidtagningsslingan. Det vill säga den tid som den snabbaste upprepningen tog dividerat med loopantalet.

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

Detsamma kan göras med hjälp av klassen Timer och dess metoder:

>>> 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]

Följande exempel visar hur man tidsbestämmer uttryck som innehåller flera rader. Här jämför vi kostnaden för att använda hasattr() jämfört med try/except för att testa om objektattribut saknas eller finns:

$ 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

För att ge modulen timeit tillgång till funktioner som du definierar kan du skicka en setup-parameter som innehåller en importförklaring:

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

Ett annat alternativ är att skicka globals() till parametern globals, vilket gör att koden exekveras inom ditt aktuella globala namnrymd. Detta kan vara mer praktiskt än att individuellt ange imports:

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