본문으로 건너뛰기

[cpython] Python re 모듈의 findall, sub, subn 성능 개선: PyList_AppendTakeRef 도입

PR 링크: python/cpython#150943 상태: Merged | 변경: +8 / -8

들어가며

Python의 re 모듈은 정규 표현식을 사용하여 문자열을 검색하고 조작하는 강력한 도구입니다. 특히 re.findall, re.sub, re.subn 함수는 자주 사용되지만, 내부적으로 리스트를 생성하고 항목을 추가하는 과정에서 성능 병목이 발생할 수 있습니다. 이번 PR (gh-150942)은 이러한 리스트 생성 과정을 최적화하여 re 모듈의 전반적인 성능을 향상시키는 것을 목표로 합니다.

기존 코드에서는 PyList_Append 함수를 사용하여 리스트에 항목을 추가한 후, 추가된 항목에 대한 참조를 명시적으로 해제(Py_DECREF)하는 방식이었습니다. 이 과정은 각 항목마다 두 번의 참조 카운트 연산(증가 및 감소)을 유발하며, 특히 스레드 환경에서는 추가적인 잠금(locking) 오버헤드를 발생시킬 수 있습니다. 이 PR은 이러한 비효율을 제거하기 위해 _PyList_AppendTakeRef라는 내부 헬퍼 함수를 도입합니다. 이 함수는 항목의 소유권을 리스트가 직접 가져가도록 하여, 불필요한 참조 카운트 증가 및 감소를 없애고 잠금 오버헤드를 줄여줍니다.

코드 분석

이번 PR의 핵심 변경 사항은 Modules/_sre/sre.c 파일에서 re.findall, re.sub, re.subn 함수의 결과 리스트를 구축하는 방식에 있습니다.

re.findall 최적화

_sre_SRE_Pattern_findall_impl 함수는 re.findall의 구현부입니다. 기존 코드에서는 매칭된 각 항목(item)을 리스트에 추가하기 위해 PyList_Append를 사용하고, 이후 Py_DECREF를 호출하여 참조를 해제했습니다.

Before:

status = PyList_Append(list, item);
Py_DECREF(item);

PyList_Append는 리스트에 새로운 참조를 추가하므로 item의 참조 카운트를 증가시킵니다. 이후 Py_DECREF(item)은 이 증가된 참조를 즉시 해제합니다. 결과적으로 각 항목에 대해 참조 카운트가 한 번 증가했다가 바로 감소하는 불필요한 연산이 발생합니다.

After:

status = _PyList_AppendTakeRef((PyListObject *)list, item);

_PyList_AppendTakeRef 함수는 item의 소유권을 리스트가 직접 넘겨받도록 설계되었습니다. 이는 item의 참조 카운트를 증가시키지 않고 리스트 내부적으로 관리하게 함으로써, Py_DECREF 호출을 생략할 수 있게 합니다. 즉, 각 항목에 대한 참조 카운트 연산이 한 번 줄어드는 효과를 가져옵니다.

re.subre.subn 최적화

pattern_subx 함수는 re.subre.subn의 공통 로직을 처리합니다. 이 함수에서도 매칭된 결과나 치환된 문자열을 리스트에 추가하는 부분에서 동일한 최적화가 적용되었습니다.

Before (re.sub/subn):

status = PyList_Append(list, item);
Py_DECREF(item);

After (re.sub/subn):

status = _PyList_AppendTakeRef((PyListObject *)list, item);

re.subre.subn의 경우, 치환된 문자열(item)이 Py_None이 아닐 때 리스트에 추가하는 로직이 있습니다. 이 부분에서도 마찬가지로 _PyList_AppendTakeRef를 사용하여 참조 카운트 관련 오버헤드를 제거했습니다. 특히 re.subn은 치환된 횟수와 결과 문자열을 튜플로 반환하는데, 이 과정에서도 결과 리스트 구축에 동일한 최적화가 적용됩니다.

왜 이게 좋은가?

이 PR의 주요 개선점은 다음과 같습니다:

  1. 성능 향상: Microbenchmark 결과에 따르면, re.subre.subn은 약 1.11배, re.findall은 약 1.05배 빨라졌습니다. 전체 기하 평균(geomean)으로는 1.06배의 성능 향상을 보였습니다. 이는 특히 대규모 문자열이나 복잡한 정규 표현식을 다룰 때 체감될 수 있는 속도 개선입니다.
  2. 참조 카운트 최적화: PyList_AppendPy_DECREF를 호출하는 것은 각 항목마다 두 번의 참조 카운트 연산을 수행합니다. _PyList_AppendTakeRef는 항목의 소유권을 리스트가 직접 가져가도록 하여 이 과정을 한 번의 연산으로 줄입니다. 이는 CPython의 내부 구현에서 매우 중요한 최적화 기법 중 하나입니다.
  3. 스레드 안전성 향상 (Free-threaded build): CPython의 실험적인 기능인 'free-threaded build' 환경에서는 GIL(Global Interpreter Lock)이 완화되어 여러 스레드가 동시에 파이썬 바이트코드를 실행할 수 있습니다. 이러한 환경에서 리스트와 같은 공유 데이터 구조에 접근할 때는 잠금(locking)이 필수적입니다. 기존의 PyList_Append는 내부적으로 잠금을 사용할 수 있지만, _PyList_AppendTakeRef는 항목의 소유권을 즉시 이전하므로 잠금 획득 및 해제 빈도를 줄여 Free-threaded build 환경에서의 성능을 더욱 향상시킬 수 있습니다.

일반적인 교훈

이 PR은 CPython 내부 구현에서 흔히 발견되는 최적화 패턴을 잘 보여줍니다. 파이썬 객체를 다룰 때, 특히 리스트와 같은 컬렉션에 항목을 추가할 때, 객체의 소유권 이전(ownership transfer)을 활용하는 것은 성능을 크게 향상시킬 수 있는 방법입니다. PyList_Append와 같이 참조 카운트를 증가시키는 함수 대신, _PyList_AppendTakeRef와 같이 참조를 '훔쳐오는(take)' 함수를 사용하면 불필요한 참조 카운트 연산을 제거할 수 있습니다. 이는 CPython 개발자뿐만 아니라, C 확장 모듈을 개발하는 개발자에게도 유용한 패턴입니다.

결론

gh-150942 PR은 re 모듈의 핵심 함수들에서 리스트 생성 방식을 개선하여 눈에 띄는 성능 향상을 이루었습니다. _PyList_AppendTakeRef의 도입은 CPython의 내부 최적화 기법을 잘 보여주는 사례이며, 파이썬의 기본 라이브러리가 지속적으로 발전하고 있음을 증명합니다. 이러한 작은 개선들이 모여 파이썬 생태계 전체의 성능을 향상시키는 밑거름이 됩니다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글