Parsing JSON at tens of gigabytes per second… in Python
Python에서 초당 수십 GB의 JSON을 파싱하기
Python 3.14의 GIL 없는 빌드와 simdjson 바인딩 fastsimdjson을 사용해 7.6GB의 GitHub 이벤트 JSON을 처리한 벤치마크입니다. 64개 스레드에서 초당 66GB를 기록했으며, 필요한 필드만 Python 객체로 만드는 지연 뷰가 성능에 큰 영향을 줍니다.
- 주제
에디터 노트
초당 66GB라는 수치는 Python 코드가 직접 JSON 파서를 구현한 결과가 아니라, simdjson 네이티브 라이브러리를 Python에서 병렬로 호출한 결과입니다. 다만 이 벤치마크의 질문은 순수 Python 파서의 속도보다 free-threaded Python에서 네이티브 확장과 스레드별 집계가 함께 얼마나 처리량을 내는지에 있습니다. 필요한 필드만 Python 객체로 만드는 지연 뷰가 단일 코어에서도 표준 라이브러리보다 빠른 점은 사용 방식의 영향도 보여줍니다. 66GB/s는 압축 해제된 데이터가 메모리에 있는 상태에서 가장 좋은 실행 결과입니다. 따라서 저장 장치 읽기나 압축 해제까지 포함한 처리량으로 읽으면 안 됩니다. 댓글에서는 제목이 순수 Python 구현처럼 들린다는 비판과 네이티브 확장도 Python 생태계에서 쓰는 방식이라는 반론이 맞섭니다. 벤치마크에 pysimdjson이 빠졌다는 지적도 남아 있어 바인딩 간 비교까지 이 수치만으로 판단할 수는 없습니다.
AI 요약
Python 3.14는 GIL(Global Interpreter Lock)을 제거한 free-threaded 빌드 python3.14t를 제공합니다. Daniel Lemire는 여러 스레드가 Python 코드를 동시에 실행하는 이 환경에서 JSON 처리 성능을 측정했습니다. 대상은 GitHub 공개 저장소의 이벤트를 시간별 NDJSON 파일로 모으는 GH Archive 데이터입니다. 2024년 3월 13일 중 8시간 분량을 골랐으며 압축을 푼 데이터는 7.6GB, 이벤트는 220만 건입니다. 이벤트 종류를 세고 새로 열린 pull request가 향한 저장소의 언어를 집계합니다. 이벤트 하나는 평균 3.4KB이고 필요한 필드는 일부뿐입니다.
결과로 새 pull request 65,269건을 찾았습니다. TypeScript 저장소가 11,565건, JavaScript가 7,778건, Python이 7,057건, Java가 4,668건이었습니다. 언어 정보가 없는 저장소를 대상으로 한 pull request는 약 5,857건입니다.
스레드별 집계와 지연 뷰
fastsimdjson은 simdjson의 Python 바인딩입니다. map_many 함수는 이벤트마다 사용자 함수를 호출하며 지정한 수의 스레드로 작업을 나눕니다. 각 스레드는 자체 Counter 두 개를 만들고 이벤트 종류와 pull request 대상 언어를 따로 집계합니다. 공유 카운터를 여러 스레드가 동시에 갱신하지 않으므로 이벤트 처리 중 잠금 경쟁을 피합니다. 처리가 끝나면 주 스레드가 스레드별 집계 결과를 합칩니다.
lazy=True를 지정하면 이벤트 전체를 Python 딕셔너리로 만들지 않고 지연 뷰를 전달합니다. JSON은 파싱하지만 프로그램이 접근한 값에 대해서만 Python 객체를 생성합니다. 글은 이 방식을 다른 simdjson 바인딩 pysimdjson에서 Tyler Kennedy가 처음 개발했다고 설명합니다.
비교 대상은 표준 라이브러리 json, orjson, msgspec입니다. msgspec은 필요한 필드만 선언한 타입 구조로 JSON을 디코딩할 수 있어 지연 뷰와 비슷한 이점이 있으며, 벤치마크에서는 필요한 필드만 선언하는 이점을 인정했습니다. 모든 구현에서 집계 결과가 같은지 확인했습니다.
측정 결과와 조건
실험 장비는 Intel Xeon Gold 6548N 프로세서 두 개를 장착한 서버이며 총 64코어를 사용했습니다. Python은 3.14.6이고 fastsimdjson 0.6.0, orjson 3.13.0, msgspec 0.22.0을 썼습니다. 데이터는 타이머를 시작하기 전에 메모리에 압축 해제했습니다. 빠른 방식은 일곱 번 실행해 가장 좋은 결과를 보고했고, 단일 스레드 파서와 64개 스레드로 실행한 표준 라이브러리에는 한 번의 결과를 썼습니다.
단일 코어에서는 표준 라이브러리가 42초 걸렸고 fastsimdjson의 지연 뷰는 3.6초 걸렸습니다. json, orjson, 지연 뷰를 끈 fastsimdjson처럼 전체 데이터를 Python 객체로 만드는 방식은 사용하지 않는 객체까지 생성하느라 느렸다고 설명합니다. 64코어를 모두 사용하면 fastsimdjson은 한 프로세스에서 약 0.12초 만에 7.6GB를 처리해 초당 66GB를 기록했습니다. 단일 코어 실행보다 31배 빠르며, 64개 스레드로 실행한 msgspec의 초당 25GB보다 높은 수치입니다. fastsimdjson 0.6.0은 PyPI에서 제공되며 벤치마크 코드도 공개했습니다.
Lobsters 반응
- @Student — 비판하는 분들을 위한 재미있는 사실입니다. Daniel Lemire는 simdjson의 원저자 중 한 명입니다.
- @nrposner — 몇 달 전 그의 오래된 블로그 글을 봤는데, "잠깐, 이건 맞지 않는 것 같은데. 이 사람이 누구지?"라고 생각했습니다. 블로그 이름을 다시 읽고 "SIMDJSON의 그 사람인가?" 싶어 더 찾아봤고, 그가 맞고 제가 틀렸다는 걸 확인했습니다. 종종 있는 일입니다.
- @zimpenfish — Python에서 초당 수십 GB의 JSON을 파싱할 수 있느냐고요? simdjson이 실제 파싱 작업을 하는데 그렇다고 말하는 건 다소 오해를 부르는 표현 같습니다. "초당 수십 GB를 파싱하는 라이브러리를 쓰면 초당 수십 GB의 JSON을 파싱할 수 있나요?"가 훨씬 덜 낚시성 있는 제목이겠지만요.
- @fanf — 흥미로운 점은 프로그래머가 유용한 작업을 하면서도 성능을 Python 수준으로 떨어뜨리지 않도록 빠른 라이브러리용 Python API를 설계하는 방법입니다. 지연 객체 생성에 대한 모호한 언급을 넘어 구현 방식을 설명했다면 흥미로웠을 겁니다. 가장 가까운 비교 대상일 것 같은 pysimdjson도 벤치마크에 포함하는 편이 더 낫지 않을까요?
- @yossarian — Python 커뮤니티에서 "Python으로"라는 말은 Python 런타임과 상호작용하는 라이브러리 코드까지 가리키는 경우가 흔하다고 생각합니다. 네이티브 확장도 포함합니다. 표준 라이브러리의 JSON 파서도 실제 작업은 C에서 합니다. 네이티브 확장도 상당한 성능 비용을 치르므로 이 표현은 공정하고 합리적이라고 봅니다.
- @colonelpanic — Python 블로거들이 FFI를 이해했으면 좋겠습니다. Python으로 하는 게 아니라 C++로 구현하고 Python 인터페이스를 붙인 겁니다.
- @yossarian — Python 개발자들이 Python 확장을 작성한다는 점을 알고 있다고 보는 편이 호의적입니다. "Python으로"는 네이티브 코드를 Python에서 사용한다는 뜻이라고 생각합니다.
- @nrposner — 일반적인 문제 제기에는 공감하지만 Lemire는 C++로 작성한 원래 SIMDJSON 논문의 두 저자 중 한 명입니다. 패키지가 일반 Python 코드로 작성되지 않았다는 사실을 모르는 블로거의 사례가 아닙니다. 그는 새 free-threaded 인터프리터가 고도로 병렬화된 확장 모듈과 얼마나 잘 작동하는지 측정했습니다. 결과는 꽤 좋고 생태계에도 도움이 됩니다. 그래도 다른 언어로 라이브러리를 쓰고 Python 인터페이스를 붙이는 방식은 제가 Python을 사용하는 방법이기도 합니다.
- @vbernat — 표준 라이브러리의 상당 부분도 C로 작성됐습니다. 그것도 Python인가요?
- @nedbat — "Python으로 X를 했다"에는 두 가지 뜻이 있습니다. 순수 Python 코드만 써서 예상보다 빠르게 만드는 것, 또는 그냥 Python 코드를 작성해 필요한 일을 충분히 빠르게 처리하는 것입니다. 이 글은 두 번째 의미에 해당합니다. 그 경우에는 "Python에서 X를 했다"고 말할 수도 있겠습니다.
- @nedbat — 사람들은 자기 블로그가 웹에서 어떻게 보이는지 확인하지 않나요? 왜 이렇게 줄을 짧게 끊었죠?
- @nrposner — 아주 좋네요. msgspec의 성능에도 계속 감탄하고 있습니다.
- @Halkcyon — msgspec은 관리가 중단된 것 아닌가요? 처리 오류가 꽤 있었는데 해결되지 않았다고 읽은 기억이 있습니다.
- @nrposner — 최근 커밋이 많고 불과 한 시간 전에도 버그 수정이 있었습니다. 다만 이슈가 많이 남아 있기는 합니다.
원문: Daniel Lemire's blog / 번역·요약: Trawling