timeit — 작은 코드 조각의 실행 시간 측정

timeit — 작은 코드 조각의 실행 시간 측정

작은 파이썬 코드 조각이 얼마나 빨리 실행되는지 측정하고 싶을 때 쓰는 모듈이에요. timeit은 커맨드라인 인터페이스(CLI)와 파이썬에서 호출 가능한 인터페이스(API)를 모두 제공하며, 실행 시간을 측정할 때 빠지기 쉬운 여러 함정을 미리 피해 줍니다. O'Reilly에서 출간된 Python Cookbook 2판의 "알고리즘" 장 앞부분에 실린 Tim Peters의 소개글도 함께 읽어 보면 도움이 돼요.

출처: Python 표준 라이브러리 — timeit

본문

Basic Examples (기본 예시)

커맨드라인 인터페이스로 서로 다른 세 표현식을 비교하는 예시를 볼게요.

$ 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

파이썬 인터페이스로는 이렇게 같은 일을 할 수 있어요.

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

호출 가능한 객체(callable)를 직접 넘겨줄 수도 있어요.

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

다만 timeit()이 반복 횟수를 자동으로 정해 주는 것은 커맨드라인 인터페이스를 쓸 때만이라는 점에 주의하세요. 더 심화된 예시는 아래 "Examples" 절에서 볼 수 있어요.

Python Interface (파이썬 인터페이스)

이 모듈은 편의 함수 세 개와 공개 클래스 하나를 정의합니다.

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

주어진 구문(statement), 셋업 코드, 타이머 함수로 Timer 인스턴스를 만들고, number 회 실행으로 그 인스턴스의 timeit() 메서드를 돌리는 함수예요. 선택 인자 globals는 코드를 실행할 네임스페이스를 지정해요.

버전 3.5에서 변경: 선택 인자 globals가 추가되었습니다.

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

Timer 인스턴스를 만들고, 주어진 repeat 횟수와 number 실행으로 그 인스턴스의 repeat() 메서드를 돌리는 함수예요. globals 인자로 네임스페이스를 지정할 수 있어요.

버전 3.5에서 변경: 선택 인자 globals가 추가되었습니다. / 버전 3.7에서 변경: repeat 기본값이 3에서 5로 바뀌었습니다.

timeit.default_timer()

기본 타이머로, 항상 time.perf_counter()이며 초(float) 단위를 반환해요. 대안으로 time.perf_counter_ns는 나노초 정수를 반환합니다.

버전 3.3에서 변경: time.perf_counter()가 이제 기본 타이머입니다.

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

작은 코드 조각의 실행 속도를 측정하기 위한 클래스예요.

생성자는 측정할 구문, 셋업에 쓸 추가 구문, 타이머 함수를 받아요. 두 구문 모두 기본값은 'pass'이고, 타이머 함수는 플랫폼에 따라 달라져요(모듈 독스트링 참고). stmtsetup은 줄바꿈이나 ;로 구분된 여러 구문을 담을 수도 있는데, 다만 여러 줄 문자열 리터럴(multi-line string literal)은 담을 수 없어요. 구문은 기본적으로 timeit의 네임스페이스 안에서 실행되는데, globals에 네임스페이스를 넘겨 그 동작을 조절할 수 있어요.

첫 구문의 실행 시간을 측정하려면 timeit() 메서드를 쓰면 되고, repeat()autorange()timeit()을 여러 번 호출하기 위한 편의 메서드예요.

셋업의 실행 시간은 전체 측정 시간에서 제외됩니다.

stmtsetup 파라미터에는 인자 없이 호출 가능한 객체도 줄 수 있어요. 이 경우 그 객체들에 대한 호출이 타이머 함수 안에 내장(embed)되고 그게 timeit()에 의해 실행돼요. 이때는 추가 함수 호출 때문에 타이밍 오버헤드가 조금 커진다는 점을 알아 두세요.

버전 3.5에서 변경: 선택 인자 globals가 추가되었습니다.

timeit(number=1000000)

주 구문을 number 회 실행하는 데 걸리는 시간을 측정해요. 셋업 구문을 한 번 실행한 뒤, 주 구문을 여러 번 실행하는 데 걸린 시간을 반환합니다. 기본 타이머는 초를 float로 돌려주고, 인자는 루프를 도는 횟수로 기본값은 백만(1,000,000)이에요. 주 구문, 셋업 구문, 사용할 타이머 함수는 생성자에 전달됩니다.

참고: 기본적으로 timeit()은 타이밍 동안 가비지 컬렉션(GC)을 잠시 꺼 둡니다. 장점은 이렇게 하면 독립적인 측정 결과가 서로 더 잘 비교된다는 점이고, 단점은 GC가 측정 대상 함수 성능의 중요한 요소일 수 있다는 점이에요. 그런 경우라면 셋업 문자열의 첫 구문으로 GC를 다시 켤 수 있어요. 예:

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

autorange(callback=None)

timeit()을 몇 번 호출할지 자동으로 결정해요. 총 시간이 0.2초 이상이 될 때까지 timeit()을 반복 호출하는 편의 함수로, 결과적으로 (number of loops, time taken for that number of loops) 튜플을 반환해요. 시퀀스 1, 2, 5, 10, 20, 50, …에서 점점 큰 횟수로 timeit()을 호출하다가 걸린 시간이 최소 0.2초가 되면 멈춥니다. callback이 주어지고 None이 아니면, 매 시도 후에 callback(number, time_taken) 두 인자로 호출됩니다.

버전 3.6에서 추가되었습니다.

repeat(repeat=5, number=1000000)

timeit()을 여러 번 호출해요. timeit()을 반복 호출하고 결과 목록을 반환하는 편의 함수로, 첫 번째 인자는 timeit()을 호출할 횟수, 두 번째 인자는 timeit()number 인자예요.

참고: 결과 벡터에서 평균과 표준편차를 계산해 보고하는 게 유혹적이지만, 그건 별로 유용하지 않아요. 전형적인 경우 가장 낮은 값이 여러분의 머신이 해당 코드 조각을 얼마나 빨리 돌릴 수 있는지의 하한선을 알려주고, 결과 벡터의 더 높은 값들은 대개 파이썬 속도의 변동 때문이 아니라 다른 프로세스가 타이밍 정확도를 방해해서 생겨요. 그래서 결과의 min()이 아마도 관심을 가질 유일한 숫자예요. 그 뒤에는 전체 벡터를 보고 통계보다는 상식적으로 판단하는 게 좋아요.

버전 3.7에서 변경: repeat 기본값이 3에서 5로 바뀌었습니다.

print_exc(file=None)

측정된 코드에서 traceback을 출력하는 헬퍼예요. 전형적인 사용법은 이렇습니다.

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

표준 traceback보다 나은 점은 컴파일된 템플릿의 소스 줄이 표시된다는 거예요. 선택 인자 file은 traceback이 출력될 곳을 지정하며 기본값은 sys.stderr입니다.

Command-Line Interface (커맨드라인 인터페이스)

프로그램으로서 커맨드라인에서 호출하면 다음과 같은 형태를 써요.

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

각 옵션은 이렇게 이해됩니다.

  • -n N, --number=N — 'statement'를 실행할 횟수
  • -r N, --repeat=N — 타이머를 반복할 횟수 (기본 5)
  • -s S, --setup=S — 처음에 한 번 실행할 구문 (기본 pass)
  • -p, --process — 벽시계(wallclock) 시간 대신 프로세스 시간을 측정. 기본인 time.perf_counter() 대신 time.process_time()을 사용
    • 버전 3.3에서 추가
  • -u, --unit=U — 타이머 출력의 시간 단위 지정. nsec, usec, msec, sec 중에서 선택
    • 버전 3.5에서 추가
  • -v, --verbose — 원시 타이밍 결과 출력. 반복하면 더 많은 자릿수 정밀도로 표시
  • -h, --help — 짧은 사용법 메시지를 출력하고 종료

여러 줄로 된 구문은 각 줄을 별도의 statement 인자로 지정해서 줄 수 있어요. 들여쓰기가 있는 줄은 인자를 따옴표로 감싸고 앞에 공백을 넣으면 됩니다. 여러 개의 -s 옵션도 비슷하게 처리돼요.

-n이 주어지지 않으면, 시퀀스 1, 2, 5, 10, 20, 50, …에서 점점 큰 숫자를 시도하다가 총 시간이 0.2초 이상이 될 때 적절한 루프 횟수가 계산됩니다.

default_timer() 측정은 같은 머신에서 돌아가는 다른 프로그램의 영향을 받을 수 있어요. 그래서 정확한 타이밍이 필요할 때는 타이밍을 몇 번 반복하고 가장 좋은 시간을 쓰는 게 최선이에요. -r 옵션이 여기에 좋고, 기본 5회 반복이 대부분의 경우 충분할 거예요. CPU 시간을 측정하려면 time.process_time()을 쓸 수 있어요.

참고: pass 구문을 실행하는 데도 일정한 기준 오버헤드(baseline overhead)가 따라옵니다. 아래 코드는 그걸 숨기려 하지 않으니 여러분이 인지하고 있어야 해요. 이 기준 오버헤드는 인자 없이 프로그램을 호출해 측정할 수 있고, 파이썬 버전마다 다를 수 있어요.

Examples (예시)

처음에 딱 한 번만 실행되는 셋업 구문을 제공하는 것도 가능해요.

$ 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

출력에는 세 가지 필드가 있어요. 루프 횟수(loop count)는 타이밍 루프 반복 1회당 구문 본문이 실행된 횟수이고, 반복 횟수('best of 5')는 타이밍 루프가 몇 번 반복됐는지이며, 마지막으로 타이밍 루프의 가장 좋은 반복 안에서 구문 본문이 평균적으로 걸린 시간이에요. 즉 가장 빠른 반복이 걸린 시간을 루프 횟수로 나눈 값이죠.

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

같은 일을 Timer 클래스와 그 메서드로도 할 수 있어요.

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

다음 예시들은 여러 줄로 된 표현식의 시간을 어떻게 측정하는지 보여줘요. 여기서는 객체 속성의 존재 여부를 검사할 때 hasattr()을 쓰는 것과 try/except를 쓰는 것의 비용을 비교해요.

$ 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

여러분이 정의한 함수에 timeit 모듈이 접근하게 하려면, import 구문을 담은 setup 파라미터를 넘기면 돼요.

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

또 다른 방법은 globals 파라미터에 globals()를 넘기는 거예요. 그러면 코드가 여러분의 현재 전역 네임스페이스 안에서 실행되죠. import를 하나씩 지정하는 것보다 이 방식이 더 편리할 수 있어요.

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