본문으로 건너뛰기
Advertisement

실전 벤치마킹

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메모리 할당 위치표준 라이브러리

벤치마킹 원칙:

  1. 워밍업 실행 포함 (JIT, 캐시 효과)
  2. 충분한 반복 횟수 (통계적 의미)
  3. 같은 환경에서 비교 (CPU 클럭, 백그라운드 프로세스)
  4. 실제 데이터와 유사한 입력 사용
Advertisement