[cpython] Python 문자열 split/splitlines 성능 개선: _PyList_AppendTakeRef 도입
PR 링크: python/cpython#155922 상태: Merged | 변경: +5 / -11
들어가며
Python의 문자열 처리 기능은 매우 자주 사용됩니다. 특히 split()과 splitlines() 함수는 문자열을 특정 구분자나 줄바꿈 문자를 기준으로 분리하는 데 필수적입니다. 하지만 이 함수들이 내부적으로 어떻게 동작하는지에 따라 성능 차이가 발생할 수 있습니다. 최근 CPython 레포지토리의 PR gh-150942는 stringlib의 split 및 splitlines 함수에서 발생하는 성능 병목 현상을 해결하기 위한 중요한 최적화를 포함하고 있습니다. 이 PR은 PyList_Append 대신 _PyList_AppendTakeRef를 사용하여 불필요한 참조 카운트 연산을 제거함으로써, 특히 많은 수의 조각으로 문자열을 분리할 때 성능을 향상시킵니다.
이 글에서는 해당 PR의 코드 변경 사항을 자세히 살펴보고, 왜 이러한 변경이 성능 향상으로 이어지는지, 그리고 이 최적화가 우리에게 주는 일반적인 교훈은 무엇인지 알아보겠습니다.
코드 분석
이번 PR의 핵심 변경 사항은 Objects/stringlib/split.h 파일에 집중되어 있습니다. 이 파일은 CPython 내부에서 문자열 분리 로직을 구현하는 매크로들을 정의하고 있습니다. 주요 변경점은 SPLIT_ADD와 SPLIT_APPEND 매크로에서 PyList_Append를 호출하는 부분을 _PyList_AppendTakeRef로 대체한 것입니다.
SPLIT_ADD 매크로 변경
SPLIT_ADD 매크로는 문자열 분리 시 생성된 각 조각(substring)을 결과 리스트에 추가하는 역할을 합니다. 원래 코드에서는 PyList_Append를 사용한 후, Py_DECREF를 호출하여 참조 카운트를 감소시켰습니다.
Before:
#define SPLIT_ADD(data, left, right) { \
sub = STRINGLIB_NEW((data) + (left), \
(right) - (left)); \
if (sub == NULL) \
goto onError; \
if (PyList_Append(list, sub)) { \
Py_DECREF(sub); \
goto onError; \
} \
else \
Py_DECREF(sub);
After:
#define SPLIT_ADD(data, left, right) { \
sub = STRINGLIB_NEW((data) + (left), \
(right) - (left)); \
if (sub == NULL) \
goto onError; \
if (_PyList_AppendTakeRef((PyListObject *)list, sub)) \
goto onError;
변경 후에는 _PyList_AppendTakeRef 함수를 사용합니다. 이 함수는 CPython 3.11부터 도입된 함수로, 리스트에 객체를 추가할 때 해당 객체의 소유권을 가져갑니다. 즉, _PyList_AppendTakeRef가 성공적으로 호출되면, 함수 내부에서 참조 카운트 증가를 처리해주므로, 호출자 측에서 별도로 Py_DECREF를 호출할 필요가 없어집니다. 이는 PyList_Append 호출 후 Py_DECREF(sub)를 하는 불필요한 참조 카운트 감소 및 증가 연산(내부적으로 PyList_Append는 Py_INCREF를 호출함)을 제거합니다.
SPLIT_APPEND 매크로 변경
SPLIT_APPEND 매크로는 splitlines() 함수에서 각 줄을 추가할 때 사용됩니다. 이 매크로 역시 SPLIT_ADD와 유사한 로직을 가지고 있으며, 동일하게 PyList_Append와 Py_DECREF 쌍을 _PyList_AppendTakeRef로 대체했습니다.
Before:
#define SPLIT_APPEND(data, left, right) {
sub = STRINGLIB_NEW((data) + (left),
(right) - (left));
if (sub == NULL)
goto onError;
if (count < MAX_PREALLOC) {
PyList_SET_ITEM(list, count, sub);
} else {
if (PyList_Append(list, sub)) {
Py_DECREF(sub);
goto onError;
}
else
Py_DECREF(sub);
}
count++; }
After:
#define SPLIT_APPEND(data, left, right) {
sub = STRINGLIB_NEW((data) + (left),
(right) - (left));
if (sub == NULL)
goto onError;
if (count < MAX_PREALLOC) {
PyList_SET_ITEM(list, count, sub);
} else {
if (_PyList_AppendTakeRef((PyListObject *)list, sub))
goto onError;
}
count++; }
이 변경 역시 PyList_Append 호출 후 Py_DECREF를 하는 과정을 제거하여, 객체 소유권 이전 방식을 통해 참조 카운트 연산을 최적화합니다. MAX_PREALLOC (기본값 12)보다 많은 항목을 추가할 때 이 경로가 사용됩니다.
_PyList_AppendTakeRef의 중요성
_PyList_AppendTakeRef 함수는 CPython 3.11에서 도입된 내부 API로, 리스트에 새로운 항목을 추가할 때 해당 항목의 참조 카운트를 직접 관리하지 않고, 객체의 소유권을 리스트로 이전시키는 방식으로 동작합니다. 이는 다음과 같은 이점을 제공합니다:
- 참조 카운트 오버헤드 감소:
PyList_Append는 내부적으로Py_INCREF를 호출하여 리스트에 추가되는 객체의 참조 카운트를 증가시킵니다. 이후Py_DECREF를 호출하여 원래 참조 카운트를 감소시키는데, 이는 불필요한 참조 카운트 증감 연산입니다._PyList_AppendTakeRef는 이 과정을 생략하고 객체의 소유권만 이전하므로, 이중 참조 카운트 연산을 제거합니다. - GIL 경합 감소 (Free-threaded build): CPython의 free-threaded 빌드에서는 리스트와 같은 내장 타입에 대한 접근 시 전역 인터프리터 잠금(GIL) 경합이 발생할 수 있습니다.
_PyList_AppendTakeRef는 내부적으로 리스트의ob_item배열에 직접 접근하여 항목을 추가하므로,PyList_Append가 수행하는 잠금 및 해제 과정을 줄여 GIL 경합을 완화할 수 있습니다.
이 PR은 split, rsplit, splitlines 함수에서 str, bytes, bytearray 타입을 모두 지원하며, 결과 리스트가 함수 내에서 지역적으로 할당되고 반환될 때까지 유지되므로 이러한 최적화의 효과를 극대화합니다.
왜 이게 좋은가?
이번 PR은 명확한 성능 향상을 가져왔습니다. 제공된 마이크로벤치마크 결과는 다음과 같습니다:
str_split_10k_pieces: `(
참고 자료
- https://docs.python.org/3/c-api/list.html#c.PyList_Append
- https://docs.python.org/3/c-api/list.html#c.Py_DECREF
- https://docs.python.org/3/c-api/list.html#_PyList_AppendTakeRef
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [cpython] CPython `PyFloat_Pack/Unpack2` 최적화: 네이티브 `_Float16` 활용으로 성능 향상
- [cpython] CPython 성능 최적화: re.split의 리스트 빌드 과정 개선하기
- [cpython] CPython 최적화: functools.reduce 대신 Union 직접 사용하기
- [cpython] CPython unicodedata.normalize() 최적화: Py_UCS4 버퍼 직접 조작으로 성능 향상
- [cpython] Python re 모듈의 findall, sub, subn 성능 개선: PyList_AppendTakeRef 도입
PR Analysis 의 다른글
- 이전글 [sglang] SGLang의 DeepSeek DSA 모델 최적화: Skip-TopK 레이어의 KV 캐시 효율화
- 현재글 : [cpython] Python 문자열 split/splitlines 성능 개선: _PyList_AppendTakeRef 도입
- 다음글 [cpython] CPython 성능 최적화: re.split의 리스트 빌드 과정 개선하기
댓글