[CPython] JIT float 연산 최적화 — 유일 참조 피연산자 재사용
PR 링크: python/cpython#146307 상태: Merged | 변경: +2349 / -1231
들어가며
CPython의 float 이진 연산(+, -, *)은 매번 새로운 PyFloatObject를 heap에 할당한다. 수치 계산이 많은 루프에서는 이 할당-해제 비용이 상당하다. 이 PR은 Tier 2 JIT optimizer에서 피연산자의 참조 카운트가 1(유일 참조)일 때, 새 객체를 만드는 대신 기존 객체의 ob_fval을 직접 변경하는 최적화를 추가한다.
핵심 코드 분석
새로운 micro-op 정의
Before (기존 _BINARY_OP_ADD_FLOAT):
// 항상 새 PyFloatObject 할당
PyObject *result = PyFloat_FromDouble(a + b);
After (_BINARY_OP_ADD_FLOAT_INPLACE):
tier2 op(_BINARY_OP_ADD_FLOAT_INPLACE, (left, right -- res, l, r)) {
PyObject *left_o = PyStackRef_AsPyObjectBorrow(left);
PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
assert(PyFloat_CheckExact(left_o));
assert(_PyObject_IsUniquelyReferenced(left_o));
double dres = ((PyFloatObject *)left_o)->ob_fval
+ ((PyFloatObject *)right_o)->ob_fval;
((PyFloatObject *)left_o)->ob_fval = dres;
res = left;
l = PyStackRef_NULL; // borrowed로 표시 -> POP_TOP_NOP
r = right;
INPUTS_DEAD();
}
_PyObject_IsUniquelyReferenced로 왼쪽 피연산자가 유일 참조인지 확인한 후, ob_fval을 직접 변경한다. 새 객체 할당이 완전히 제거된다. 변경된 객체는 l = PyStackRef_NULL로 borrowed 상태가 되어 이후 _POP_TOP_FLOAT가 _POP_TOP_NOP로 치환된다.
RIGHT 변형
tier2 op(_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT, (left, right -- res, l, r)) {
FLOAT_INPLACE_OP(left, right, right, +);
res = right;
l = left;
r = PyStackRef_NULL;
INPUTS_DEAD();
}
덧셈과 곱셈은 교환법칙이 성립하므로, 오른쪽 피연산자가 유일 참조일 때 사용하는 _RIGHT 변형도 제공한다. 뺄셈은 a - b에서 b만 유일 참조일 때 b.ob_fval = a - b로 처리하는 별도 변형이 있다.
Unary 연산
tier2 op(_UNARY_NEGATIVE_FLOAT_INPLACE, (value -- res, v)) {
PyObject *val_o = PyStackRef_AsPyObjectBorrow(value);
assert(_PyObject_IsUniquelyReferenced(val_o));
double dres = -((PyFloatObject *)val_o)->ob_fval;
((PyFloatObject *)val_o)->ob_fval = dres;
res = value;
v = PyStackRef_NULL;
INPUTS_DEAD();
}
단항 부정(-x)에도 같은 최적화가 적용된다.
왜 이게 좋은가
마이크로벤치마크 결과:
| 표현식 | 기존 (ns/iter) | 최적화 (ns/iter) | 속도 향상 |
|---|---|---|---|
total += a*b + c |
24.0 | 11.5 | 2.1x |
total += a + b |
16.9 | 11.0 | 1.5x |
total += a*b + c*d |
28.5 | 18.3 | 1.6x |
실제 벤치마크인 pyperformance nbody에서 19% 성능 향상(60.6ms -> 49.0ms)을 달성했다.
핵심 원리는 간단하다. a * b의 결과로 생성된 float 객체는 그 줄의 연산이 끝나면 버려진다. 참조 카운트가 1이므로 안전하게 재사용할 수 있고, heap 할당/해제 한 쌍을 제거한다. 체이닝 연산(a*b + c)에서는 중간 결과가 연쇄적으로 재사용되어 효과가 배가된다.
정리
_BINARY_OP_{ADD,SUBTRACT,MULTIPLY}_FLOAT_INPLACE및_RIGHT변형 6개의 micro-op이 추가되었다_UNARY_NEGATIVE_FLOAT_INPLACE1개가 추가되었다- Tier 2 optimizer가
PyJitRef_IsUnique검사 후 자동으로 이 micro-op을 선택한다 - JIT only 최적화로, interpreter 모드에는 영향이 없다
- 향후 나눗셈, int 연산, complex 연산으로 확장 가능하다
참고 자료
- 이슈 #146306 -- 최적화 제안 이슈
- CPython JIT 설계 -- PEP 744: JIT Compilation
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
PR Analysis 의 다른글
- 이전글 [Open WebUI] asyncio.to_thread로 heartbeat DB 쓰기 이벤트 루프 블로킹 해소
- 현재글 : [CPython] JIT float 연산 최적화 — 유일 참조 피연산자 재사용
- 다음글 [Gradio] 백엔드 프로파일링 및 벤치마크 인프라 구축
댓글