프로파일링
cProfile, line_profiler, memory_profiler로 병목을 찾고 성능을 측정합니다.
설치
pip install line-profiler memory-profiler pyinstrument
cProfile — CPU 프로파일러 (표준 라이브러리)
import cProfile
import pstats
import io
def slow_function():
total = 0
for i in range(1_000_000):
total += i ** 2
return total
def process_data(data: list[int]) -> list[int]:
return [slow_function() for _ in data]
# 방법 1: 커맨드라인
# python -m cProfile -s cumulative script.py
# 방법 2: 코드 내 직접 프로파일링
profiler = cProfile.Profile()
profiler.enable()
result = process_data([1, 2, 3])
profiler.disable()
# 결과 출력 (상위 20개 함수, 누적 시간 기준 정렬)
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.strip_dirs()
stats.sort_stats("cumulative")
stats.print_stats(20)
print(stream.getvalue())
# 방법 3: 컨텍스트 매니저
with cProfile.Profile() as pr:
result = process_data([1, 2, 3])
pr.print_stats(sort="cumulative")
line_profiler — 줄 단위 프로파일러
# @profile 데코레이터 추가 후 kernprof로 실행
# kernprof -l -v script.py
@profile # type: ignore # kernprof가 주입하는 데코레이터
def compute_stats(numbers: list[float]) -> dict:
total = sum(numbers) # 이 줄에 얼마나 걸리나?
mean = total / len(numbers)
variance = sum((x - mean) ** 2 for x in numbers) / len(numbers)
std_dev = variance ** 0.5
sorted_nums = sorted(numbers)
median = sorted_nums[len(sorted_nums) // 2]
return {"mean": mean, "std": std_dev, "median": median}
# 코드에서 직접 사용
from line_profiler import LineProfiler
def target_function(data):
result = []
for item in data:
result.append(item ** 2)
return result
lp = LineProfiler()
lp_wrapper = lp(target_function)
lp_wrapper(list(range(10000)))
lp.print_stats()
memory_profiler — 메모리 프로파일러
# @profile 데코레이터 추가 후 실행
# python -m memory_profiler script.py
@profile # type: ignore
def memory_hungry():
# 메모리 사용량 추적
big_list = [i for i in range(1_000_000)] # 약 8MB
big_dict = {str(i): i for i in range(100_000)} # 약 50MB
del big_list # 해제
return big_dict
# 코드에서 직접 사용
from memory_profiler import memory_usage
def my_func():
return [i ** 2 for i in range(1_000_000)]
# 함수 실행 중 최대 메모리 사용량 (MB)
mem = memory_usage((my_func,), interval=0.1)
print(f"최대 메모리: {max(mem):.1f} MB")
print(f"최소 메모리: {min(mem):.1f} MB")
pyinstrument — 통계 기반 프로파일러 (시각화)
from pyinstrument import Profiler
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 컨텍스트 매니저
with Profiler() as profiler:
fibonacci(35)
profiler.print() # 콘솔 출력 (flame graph 형태)
# profiler.open_in_browser() # 브라우저에서 시각화
# FastAPI / Django 미들웨어로 통합
# 커맨드라인: python -m pyinstrument script.py
tracemalloc — 메모리 할당 추적 (표준 라이브러리)
import tracemalloc
def create_objects():
data = [{"id": i, "value": i ** 2} for i in range(100_000)]
return data
# 메모리 스냅샷 비교
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
result = create_objects()
snapshot2 = tracemalloc.take_snapshot()
# 메모리 증가량 상위 10개
top_stats = snapshot2.compare_to(snapshot1, "lineno")
for stat in top_stats[:10]:
print(stat)
tracemalloc.stop()
프로파일링 워크플로우
# 실전 패턴: 느린 코드 → 프로파일 → 최적화 → 재측정
import time
import cProfile
import pstats
from functools import wraps
from typing import Callable
def timeit(func: Callable) -> Callable:
"""실행 시간 측정 데코레이터"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[{func.__name__}] {elapsed:.4f}s")
return result
return wrapper
@timeit
def before_optimization(n: int) -> list[int]:
result = []
for i in range(n):
if str(i) not in [str(j) for j in range(i)]: # O(n²) — 매우 느림
result.append(i)
return result
@timeit
def after_optimization(n: int) -> list[int]:
seen = set() # O(1) 조회
result = []
for i in range(n):
s = str(i)
if s not in seen:
seen.add(s)
result.append(i)
return result
정리
| 도구 | 측정 대상 | 용도 |
|---|---|---|
cProfile | 함수별 CPU 시간 | 병목 함수 탐색 |
line_profiler | 줄별 CPU 시간 | 함수 내 상세 분석 |
memory_profiler | 줄별 메모리 증가 | 메모리 누수 탐색 |
pyinstrument | 통계 기반 CPU | 시각화, 빠른 파악 |
tracemalloc | 객체별 메모리 할당 | 할당 위치 추적 |
측정 없는 최적화는 추측일 뿐입니다. 항상 프로파일 먼저, 최적화 나중에.