Python 3.12의 성능 향상 원리를 분석합니다. 특수화 적응 인터프리터, 컴프리헨션 인라인화, immortal objects, asyncio 최적화 등 CPython 내부를 다룹니다.
CPython의 성능 개선은 Mark Shannon의 제안서(Shannon Plan)로 시작된 장기 프로젝트입니다. Python 3.11에서 평균 25%의 속도 향상을 달성한 이후, 3.12에서도 이 흐름이 계속되고 있습니다.
Python 3.10 - 기준선
Python 3.11 - 약 25% 향상 (Faster CPython Phase 1)
Python 3.12 - 약 5% 추가 향상
Python 3.13 - free-threaded 모드에서 1~8% 오버헤드 (실험적)3.12의 5% 향상은 수치로는 작아 보이지만, 3.11의 대폭 개선 이후에 달성한 것이라는 점에서 의미가 있습니다. 더 중요한 것은 이 향상이 특정 코드 패턴에 집중되어 있어, 해당 패턴을 많이 사용하는 코드에서는 체감이 클 수 있다는 점입니다.
Python 3.11에서 도입된 특수화 적응 인터프리터는 3.12에서 더욱 발전했습니다. 이 메커니즘의 핵심 아이디어는 바이트코드를 실행 중에 관찰된 타입에 맞게 특수화하는 것입니다.
CPython은 바이트코드를 실행하면서, 특정 연산이 반복적으로 같은 타입의 피연산자를 받는 것을 감지합니다. 이때 범용 바이트코드를 해당 타입에 최적화된 특수 바이트코드로 교체합니다.
일반 바이트코드: BINARY_ADD
- 실행할 때마다 타입 체크 필요
- int + int, float + float, str + str 모두 같은 경로
관찰: x + y에서 x, y가 항상 int
특수화: BINARY_ADD_INT
- int 전용 빠른 경로 사용
- 타입 체크 생략
- 실패 시 일반 경로로 폴백Python 3.12는 다음 영역에서 새로운 특수화를 추가했습니다.
FOR_ITER_LIST - list 이터레이션 특수화
FOR_ITER_TUPLE - tuple 이터레이션 특수화
FOR_ITER_RANGE - range 이터레이션 특수화
FOR_ITER_GEN - 제너레이터 이터레이션 특수화
SEND_GEN - 제너레이터 send 특수화
TO_BOOL - 불리언 변환 특수화
CONTAINS_OP_SET - set의 in 연산 특수화
CONTAINS_OP_DICT - dict의 in 연산 특수화이 특수화 덕분에 반복문(for loop)과 멤버십 테스트(in 연산)의 성능이 향상되었습니다.
Python 3.12의 가장 주목할 만한 최적화 중 하나는 컴프리헨션(comprehension)의 인라인화입니다.
Python 3.11 이하에서 리스트, 딕셔너리, 셋 컴프리헨션과 제너레이터 표현식은 암묵적으로 중첩된 함수로 구현되었습니다.
# 이 코드는
result = [x * 2 for x in range(10)]
# 내부적으로 이렇게 동작했음
def _comprehension(iterable):
result = []
for x in iterable:
result.append(x * 2)
return result
result = _comprehension(range(10))이 방식에는 두 가지 비용이 있었습니다. 함수 객체를 생성하고 호출하는 오버헤드, 그리고 별도의 스코프로 인한 변수 접근 비용입니다.
Python 3.12에서는 컴프리헨션이 인라인으로 실행됩니다. 별도의 함수 호출 없이 현재 프레임 내에서 직접 실행됩니다.
3.11: 컴프리헨션 = 함수 생성 + 호출 + 결과 반환
3.12: 컴프리헨션 = 현재 프레임에서 직접 실행
성능 향상: 컴프리헨션 실행 속도 약 11% 향상인라인화로 인해 한 가지 동작이 변경되었습니다. 3.11 이하에서 컴프리헨션의 반복 변수는 별도 스코프에 있어서 외부로 누출되지 않았지만, 3.12에서는 인라인 실행이므로 이론적으로 변수 누출이 가능합니다. 다만 CPython은 호환성을 위해 반복 변수가 외부로 누출되지 않도록 처리합니다.
Python 3.12는 "불멸 객체(Immortal Objects)"라는 개념을 도입했습니다. 이는 free-threaded Python을 위한 사전 작업이기도 합니다.
CPython은 참조 카운팅(reference counting)으로 메모리를 관리합니다. 모든 객체는 자신을 참조하는 변수의 수를 추적하며, 참조 수가 0이 되면 해제됩니다.
문제는 None, True, False, 작은 정수(-5~256) 같은 객체는 프로그램 전체에서 매우 빈번하게 참조되므로, 참조 카운터가 불필요하게 자주 변경된다는 것입니다. 이는 캐시 무효화를 유발하고, 멀티스레드 환경에서는 경쟁 조건(race condition)의 원인이 됩니다.
불멸 객체는 참조 카운터를 특수한 값으로 설정하여, 증가/감소 연산을 건너뛰도록 합니다.
일반 객체:
Py_INCREF(obj) -> refcount++
Py_DECREF(obj) -> refcount--, if 0: dealloc
불멸 객체 (None, True, False, 작은 정수 등):
Py_INCREF(obj) -> no-op (아무 것도 하지 않음)
Py_DECREF(obj) -> no-op
결과: 캐시 친화적, 멀티스레드 안전이 변화는 사용자 코드에 직접적인 영향을 주지 않지만, CPython 내부적으로 성능 향상과 free-threaded 모드의 기반을 제공합니다.
Python 3.12는 asyncio 패키지의 성능을 상당히 개선했습니다. 일부 벤치마크에서는 75%의 속도 향상이 보고되었습니다.
1. Task 생성 최적화
- Task 객체 생성 비용 감소
- eager task factory 도입
2. 이벤트 루프 최적화
- 셀렉터 호출 빈도 감소
- 콜백 스케줄링 효율화
3. Future 최적화
- Future.result() 빠른 경로 추가
- 완료된 Future에 대한 await 비용 감소Python 3.12에서 도입된 asyncio.eager_task_factory는 코루틴이 첫 번째 await 지점에 도달하기 전에 완료될 수 있는 경우, Task 객체 생성을 건너뛰는 최적화입니다.
import asyncio
async def cached_get(key: str) -> str:
# 캐시 히트 시 await 없이 즉시 반환
if key in cache:
return cache[key]
# 캐시 미스 시에만 비동기 I/O
result = await fetch_from_db(key)
cache[key] = result
return result
async def main():
# eager task factory 설정
loop = asyncio.get_event_loop()
loop.set_task_factory(asyncio.eager_task_factory)
# 캐시 히트 시 Task 생성 오버헤드 없이 즉시 결과 반환
result = await cached_get("user:123")eager task factory는 많은 코루틴이 동기적으로 완료되는 패턴(캐시 히트, 조건 분기에서의 조기 반환 등)에서 효과적입니다. 모든 코루틴이 실제 I/O를 수행하는 경우에는 효과가 제한적입니다.
Python 3.12는 새로운 디버깅/프로파일링 API인 sys.monitoring을 도입했습니다(PEP 669). 기존의 sys.settrace와 sys.setprofile을 대체하는 것이 목적입니다.
sys.settrace는 모든 바이트코드 실행마다 콜백을 호출합니다. 디버거나 프로파일러가 활성화되면 전체 프로그램의 실행 속도가 크게 저하됩니다.
import sys
# 모니터링 도구 ID 등록
MY_TOOL = sys.monitoring.DEBUGGER_ID
# 이벤트 콜백 등록
def on_line(code, line_number):
print("Line " + str(line_number) + " in " + code.co_filename)
return sys.monitoring.DISABLE # 이 위치에서 더 이상 모니터링하지 않음
# 모니터링 활성화
sys.monitoring.use_tool_id(MY_TOOL, "my_debugger")
sys.monitoring.set_events(MY_TOOL, sys.monitoring.events.LINE)
sys.monitoring.register_callback(
MY_TOOL,
sys.monitoring.events.LINE,
on_line
)핵심 차이점은 이벤트 기반이라는 것입니다. 관심 있는 이벤트만 선택적으로 모니터링하고, 특정 위치에서 모니터링을 비활성화할 수 있습니다. 이로 인해 모니터링 오버헤드가 대폭 감소합니다.
sys.settrace:
- 모든 줄 실행마다 콜백
- 프로그램 전체 속도 저하 (3~10배)
sys.monitoring:
- 선택적 이벤트 모니터링
- 동적 활성화/비활성화
- 모니터링 오버헤드: 거의 0 (이벤트 미발생 시)Python 3.12는 Linux perf 프로파일러와의 통합을 지원합니다. 기존에는 perf로 Python 프로그램을 프로파일링하면, C 레벨의 함수만 보였습니다. 3.12에서는 Python 함수 이름이 perf 출력에 직접 표시됩니다.
# Python 3.12에서 perf 지원 활성화
python3.12 -X perf my_script.py
# 또는 환경 변수로
PYTHONPERFSUPPORT=1 python3.12 my_script.py
# perf로 프로파일링
perf record -g -p $PID
perf report이 기능은 프로덕션 환경에서 Python 애플리케이션의 성능 병목을 분석할 때 유용합니다.
실제 코드 패턴에서의 성능 변화를 측정합니다.
import timeit
# 컴프리헨션 성능
comprehension_code = """
result = [x * 2 for x in range(10000)]
"""
# for 루프 성능
loop_code = """
result = []
for x in range(10000):
result.append(x * 2)
"""
# 딕셔너리 컴프리헨션
dict_comp_code = """
result = {str(k): v * 2 for k, v in enumerate(range(10000))}
"""
# 멤버십 테스트
membership_code = """
s = set(range(10000))
result = [x for x in range(20000) if x in s]
"""
# 각 벤치마크 실행
for name, code in [
("list comprehension", comprehension_code),
("for loop", loop_code),
("dict comprehension", dict_comp_code),
("membership test", membership_code),
]:
time = timeit.timeit(code, number=1000)
print(name + ": " + str(round(time, 3)) + "s")성능 비교는 동일한 하드웨어, 동일한 OS, 동일한 부하 상태에서 수행해야 의미가 있습니다. pyperformance 벤치마크 스위트를 사용하면 더 신뢰할 수 있는 결과를 얻을 수 있습니다.
Python 3.12의 성능 향상은 특정 기법이 아니라 여러 최적화의 누적 결과입니다.
7장에서는 Python 3.13의 가장 혁신적인 변화인 free-threaded Python(GIL 제거)을 다룹니다. PEP 703의 배경, free-threaded 빌드의 설치와 사용법, 그리고 실제 멀티스레드 성능 테스트를 살펴봅니다.
이 글이 도움이 되셨나요?
Python 3.13의 free-threaded 모드를 심층 분석합니다. GIL의 역사와 문제점, PEP 703의 설계, free-threaded 빌드의 설치와 실전 멀티스레드 성능을 다룹니다.
Python 3.13에서 도입된 PyREPL의 구문 강조, 멀티라인 편집, 자동완성, 히스토리 관리 등 현대적 REPL 기능을 실전 예시와 함께 다룹니다.
Python 3.13에 도입된 실험적 JIT 컴파일러를 분석합니다. copy-and-patch 기법의 원리, Tier 2 최적화 파이프라인, 빌드와 활성화 방법, 성능 벤치마크를 다룹니다.