실전 벤치마킹
timeit, pytest-benchmark로 코드 성능을 정확히 측정하고 Big-O 감각을 키웁니다.
설치
pip install pytest pytest-benchmark
timeit — 마이크로 벤치마크
import timeit
# ── 기본 사용 ─────────────────────────────────────────────
# 방법 1: 커맨드라인
# python -m timeit "sum(range(1000))"
# python -m timeit -n 10000 -r 5 "'-'.join(str(i) for i in range(100))"
# 방법 2: 코드 내 사용
t = timeit.timeit("sum(range(1000))", number=10_000)
print(f"10000회 실행: {t:.4f}s")
print(f"1회 평균: {t / 10_000 * 1e6:.2f}µs")
# 방법 3: repeat (여러 번 측정)
results = timeit.repeat(
stmt="[x**2 for x in range(100)]",
repeat=5, # 5회 반복
number=10_000, # 매 반복당 10000회 실행
)
print(f"최솟값: {min(results):.4f}s")
print(f"평균값: {sum(results)/len(results):.4f}s")
# 방법 4: 함수 직접 전달
def target():
return sorted(range(1000), reverse=True)
t = timeit.timeit(target, number=10_000)
print(f"{t:.4f}s")
# 방법 5: setup 코드 포함
t = timeit.timeit(
stmt="bisect.bisect_left(data, 500)",
setup="import bisect; data = list(range(1000))",
number=1_000_000,
)
print(f"bisect: {t:.4f}s")
pytest-benchmark — 테스트 통합 벤치마크
# tests/test_benchmark.py
import pytest
def bubble_sort(arr):
arr = arr.copy()
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def insertion_sort(arr):
arr = arr.copy()
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
@pytest.fixture
def sample_data():
import random
return [random.randint(0, 1000) for _ in range(500)]
def test_bubble_sort(benchmark, sample_data):
result = benchmark(bubble_sort, sample_data)
assert result == sorted(sample_data)
def test_insertion_sort(benchmark, benchmark_rounds_warmup=1, **kwargs):
import random
data = [random.randint(0, 1000) for _ in range(500)]
result = benchmark(insertion_sort, data)
assert result == sorted(data)
def test_builtin_sort(benchmark, sample_data):
result = benchmark(sorted, sample_data)
assert result == sorted(sample_data)
# pytest tests/ --benchmark-only
# pytest tests/ --benchmark-compare # 이전 결과와 비교
# pytest tests/ --benchmark-save=baseline # 결과 저장
# pytest tests/ --benchmark-histogram # 히스토그램 저장
Big-O 실측 비교
import timeit
import random
# ── O(n) vs O(n log n) vs O(n²) ─────────────────────────
def measure_complexity():
sizes = [100, 500, 1000, 5000, 10000]
print(f"{'크기':>8} {'O(n) list.count':>16} {'O(n log n) sorted':>18} {'O(n²) bubble':>14}")
print("-" * 60)
for n in sizes:
data = [random.randint(0, n) for _ in range(n)]
# O(n): 선형 탐색
t_linear = timeit.timeit(lambda: data.count(n // 2), number=100)
# O(n log n): 정렬
t_nlogn = timeit.timeit(lambda: sorted(data), number=100)
# O(n²): 버블 정렬
def bubble(arr):
arr = arr[:]
for i in range(len(arr)):
for j in range(len(arr) - i - 1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
t_n2 = timeit.timeit(lambda: bubble(data), number=10)
print(f"{n:>8} {t_linear*1000:>14.2f}ms {t_nlogn*1000:>16.2f}ms {t_n2*1000:>12.2f}ms")
measure_complexity()
# ── 탐색 복잡도 비교 ──────────────────────────────────────
import bisect
def compare_search(n: int = 100_000):
data_list = list(range(n))
data_set = set(data_list)
target = n - 1
t_list = timeit.timeit(lambda: target in data_list, number=10_000)
t_set = timeit.timeit(lambda: target in data_set, number=10_000)
t_bisect = timeit.timeit(
lambda: bisect.bisect_left(data_list, target) < len(data_list),
number=10_000,
)
print(f"list O(n): {t_list:.4f}s")
print(f"set O(1): {t_set:.6f}s ({t_list/t_set:.0f}x faster)")
print(f"bisect O(logn): {t_bisect:.6f}s ({t_list/t_bisect:.0f}x faster)")
compare_search()
메모리 프로파일 기반 벤치마크
from memory_profiler import memory_usage
import tracemalloc
def benchmark_memory(func, *args, **kwargs):
"""함수 실행 전후 메모리 측정"""
# 실행 중 메모리 추적
mem_usage = memory_usage((func, args, kwargs), interval=0.01, retval=True)
memory_timeline, result = mem_usage[:-1], mem_usage[-1]
peak_mem = max(memory_timeline) if memory_timeline else 0
base_mem = memory_timeline[0] if memory_timeline else 0
print(f"기본 메모리: {base_mem:.1f} MB")
print(f"최대 메모리: {peak_mem:.1f} MB")
print(f"증가량: {peak_mem - base_mem:.1f} MB")
return result
# tracemalloc — 할당 위치 추적
def trace_allocations(func, *args):
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
result = func(*args)
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, "lineno")
print("\n=== 메모리 할당 상위 5개 ===")
for stat in top_stats[:5]:
print(f" {stat}")
tracemalloc.stop()
return result
성능 회귀 테스트
# 성능 임계값 설정으로 회귀 방지
import pytest
import time
def fast_function(n: int) -> int:
return sum(range(n))
def test_performance_budget():
"""성능 예산: 100ms 이내"""
start = time.perf_counter()
result = fast_function(1_000_000)
elapsed = time.perf_counter() - start
assert result == 499_999_500_000
assert elapsed < 0.1, f"성능 임계값 초과: {elapsed:.3f}s > 0.1s"
# pytest-benchmark 활용 임계값
def test_with_benchmark(benchmark):
result = benchmark.pedantic(
fast_function,
args=(1_000_000,),
rounds=10,
warmup_rounds=2,
)
assert result == 499_999_500_000
# benchmark.stats.mean < 0.01 # 평균 10ms 이내
정리
| 도구 | 용도 | 특징 |
|---|---|---|
timeit | 단순 표현식/함수 | 표준 라이브러리, 정확한 CPU 시간 |
pytest-benchmark | 테스트 통합 | 통계, 비교, CI 연동 |
memory_profiler | 메모리 사용량 | MB 단위 추적 |
tracemalloc | 메모리 할당 위치 | 표준 라이브러리 |
벤치마킹 원칙:
- 워밍업 실행 포함 (JIT, 캐시 효과)
- 충분한 반복 횟수 (통계적 의미)
- 같은 환경에서 비교 (CPU 클럭, 백그라운드 프로세스)
- 실제 데이터와 유사한 입력 사용