[cpython] Python statistics.fmean() 성능 최적화: itertools.compress를 활용한 오버헤드 제거
PR 링크: python/cpython#148875 상태: Merged | 변경: +None / -None
들어가며
Python의 statistics.fmean() 함수는 부동 소수점 산술 평균을 계산할 때 math.fsum()을 사용하여 정밀도를 유지하면서도 빠른 속도를 제공하는 유용한 도구입니다. 하지만 최근 CPython 메인 레포지토리에 이 함수의 성능을 한 단계 더 끌어올리는 흥미로운 PR이 머지되었습니다.
이 PR의 핵심은 데이터의 개수를 세는 방식(Counting strategy)의 개선입니다. 특히 길이를 미리 알 수 없는 이터레이터(Iterator)가 입력으로 들어왔을 때, 기존 방식이 가졌던 불필요한 객체 생성 오버헤드를 어떻게 제거했는지 상세히 살펴보겠습니다.
문제 상황: 이터레이터의 길이를 재는 비용
fmean은 평균을 구하기 위해 합계(total) / 개수(n)를 계산해야 합니다. 입력 데이터가 리스트처럼 __len__을 구현하고 있다면 len(data)로 즉시 개수를 알 수 있지만, 제너레이터나 이터레이터의 경우 끝까지 순회해보기 전에는 그 개수를 알 수 없습니다.
기존의 statistics.fmean은 이 문제를 해결하기 위해 다음과 같은 트릭을 사용했습니다.
Before (기존 코드)
# Lib/statistics.py
try:
n = len(data)
except TypeError:
# Handle iterators that do not define __len__().
counter = count()
total = fsum(map(itemgetter(0), zip(data, counter)))
n = next(counter)
위 코드의 흐름을 분석해봅시다.
zip(data, counter)를 호출하여 데이터와 숫자를 묶습니다. 예:(val1, 0), (val2, 1), ...map(itemgetter(0), ...)를 통해 다시 값만 추출합니다.fsum이 이 값을 소비하면서 합계를 구합니다.- 마지막에
next(counter)를 호출하여 총 몇 개의 아이템이 지나갔는지 확인합니다.
이 방식은 영리해 보이지만, 치명적인 단점이 있습니다. 매 루프마다 zip에 의해 새로운 튜플 객체가 생성되고, itemgetter에 의해 인덱싱이 발생하며, 다시 튜플이 파괴되는 과정이 반복됩니다. 이는 순수하게 합계만 구하면 되는 작업에 비해 상당한 CPU 사이클을 낭비하게 만듭니다.
개선된 방식: itertools.compress의 재발견
이번 PR에서는 itertools.compress를 활용하여 이 오버헤드를 획기적으로 줄였습니다.
After (변경 후 코드)
# Lib/statistics.py
# ... (생략) ...
from itertools import compress, count, groupby, repeat
# ... (생략) ...
try:
n = len(data)
except TypeError:
# Handle iterators that do not define __len__().
counter = count(1)
total = fsum(compress(data, counter))
n = next(counter) - 1
무엇이 달라졌나요?
itertools.compress사용:compress(data, selectors)는selectors의 요소가 참(True)인 경우에만data의 요소를 반환합니다. 여기서counter는count(1)로 시작하므로 모든 숫자가 참(True)으로 취급됩니다. 즉,data의 모든 요소를 그대로 통과시킵니다.- 객체 생성 오버헤드 제거:
zip과 달리compress는 내부적으로 튜플을 생성하지 않습니다. 단순히 두 이터레이터를 동시에 소비하면서 조건에 맞는 값만 넘겨줄 뿐입니다. - 인덱싱 제거:
itemgetter(0)와 같은 추가적인 함수 호출이나 인덱싱 과정이 사라졌습니다.
리뷰어의 피드백: 코드 스타일 유지
이 PR의 리뷰 과정에서 Shrey-N은 기술적인 로직 외에도 코드의 가독성과 유지보수성을 위한 피드백을 남겼습니다.
"Hiya, Keeping the imports in alphabetical order would be better..."
기존 코드에서 compress를 추가할 때 알파벳 순서를 고려하지 않았던 점을 지적했고, 최종 코드는 compress, count, groupby, repeat 순으로 정렬되어 반영되었습니다. 이는 거대한 오픈소스 프로젝트에서 일관된 스타일을 유지하는 것이 얼마나 중요한지 보여주는 대목입니다.
왜 이게 좋은 최적화인가?
1. 성능 수치 (Benchmark)
PR 작성자가 제시한 벤치마크 결과는 놀랍습니다.
- Before: 18.7 usec per loop
- After: 11.4 usec per loop
- 성능 향상: 약 39% 속도 개선
단순히 라이브러리 함수 하나를 바꿨을 뿐인데, 이터레이터를 처리하는 속도가 비약적으로 상승했습니다.
2. 일반적인 교훈
이 최적화는 파이썬 고성능 코딩의 핵심 원칙을 잘 보여줍니다.
- Avoid Tuple Packing/Unpacking in Loops: 루프 내부에서 발생하는 암묵적인 객체 생성은 파이썬에서 가장 큰 성능 저하 원인 중 하나입니다.
- Leverage C-implemented Itertools:
itertools모듈의 함수들은 C 언어로 고도로 최적화되어 있습니다. 파이썬 레벨에서zip+map을 조합하는 것보다 전용 도구인compress를 사용하는 것이 훨씬 빠릅니다.
마치며
이번 statistics.fmean()의 개선 사례는 "동작하는 코드"를 넘어 "효율적인 코드"로 가는 길을 잘 보여줍니다. 우리가 무심코 사용하는 zip이나 map이 때로는 불필요한 비용을 발생시킬 수 있다는 점을 인지하고, 표준 라이브러리가 제공하는 다양한 도구들을 적재적소에 활용하는 안목을 길러야겠습니다.
참고 자료
- https://docs.python.org/3/library/statistics.html#statistics.fmean
- https://docs.python.org/3/library/itertools.html#itertools.compress
- https://docs.python.org/3/library/math.html#math.fsum
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
PR Analysis 의 다른글
- 이전글 [triton] Triton Gluon Attention 커널의 Autotuning을 통한 성능 최적화 분석
- 현재글 : [cpython] Python statistics.fmean() 성능 최적화: itertools.compress를 활용한 오버헤드 제거
- 다음글 [ACE-Step-1.5] ACE-Step에 파동대역 보정(DCW) 샘플러 훅 추가: SNR-t 편향 개선
댓글