
64개 코어를 쓴 Python 프로세스가 초당 65.7GB의 JSON을 처리했다. fastsimdjson의 지연 읽기 기능을 사용한 결과다.
컴퓨터 과학자 대니얼 르미르(Daniel Lemire)는 Python 3.14.6에서 JSON 처리 속도를 비교했다. 필요한 값만 읽고 여러 코어를 활용하려는 Python 개발자에게 참고가 될 실험이다.
무엇을 비교했나?
simdjson은 JSON을 빠르게 읽는 라이브러리다. fastsimdjson은 이를 Python에서 쓰도록 만든 바인딩이다. 필요한 값만 골라 읽는 방식은 필요한 물건만 꺼내는 서랍을 떠올리면 쉽다. 여기서는 JSON을 파싱하되 접근한 값에 대해서만 Python 객체를 만든다.
실험에는 GitHub 공개 저장소의 활동 기록을 모으는 GH Archive의 2024년 3월 13일 8시간치 자료를 썼다. 압축을 풀면 7.6GB이고 이벤트는 220만 건이었다. 이벤트 하나는 평균 약 3.4kB의 JSON 객체였으며, 그중 일부 필드만 필요했다. 새로 열린 풀 리퀘스트가 어느 프로그래밍 언어의 저장소를 대상으로 했는지 세었다.
그 8시간 동안 열린 풀 리퀘스트는 65,269건이었다. TypeScript 저장소가 11,565건, JavaScript가 7,778건, Python이 7,057건, Java가 4,668건이었다. 언어가 지정되지 않은 저장소를 대상으로 한 건은 약 5,857건이었다.
여러 스레드에서 어떻게 셌나?
이 코드는 이벤트마다 호출되는 one_event 함수가 스레드별로 카운터를 유지한다. 각 스레드는 자기 카운터만 갱신하고, 처리가 끝나면 메인 스레드가 결과를 합친다. map_many는 요청한 스레드 수만큼 이벤트 처리를 병렬로 수행한다.
import collections, threading
import fastsimdjson
local = threading.local()
parts = []
def one_event(e):
acc = getattr(local, "acc", None)
if acc is None: # first event seen by this thread
acc = local.acc = (collections.Counter(), collections.Counter())
parts.append(acc)
t = e["type"]
acc[0][t] += 1
if t == "PullRequestEvent":
p = e["payload"]
if p["action"] == "opened":
acc[1][p["pull_request"]["base"]["repo"]["language"]] += 1
data = open("events.json", "rb").read()
fastsimdjson.map_many(one_event, data, format="lines", threads=64, lazy=True)
types, languages = collections.Counter(), collections.Counter()
for t, lang in parts:
types.update(t)
languages.update(lang)
lazy=True를 쓰면 함수에는 사전 대신 지연 뷰가 전달된다. 데이터는 파싱하지만, 실제로 접근하는 값에 대해서만 Python 객체를 만든다. 이 방식은 simdjson의 다른 Python 바인딩인 pysimdjson에서 타일러 케네디(Tyler Kennedy)가 먼저 개발했다.
어떤 조건에서 얼마나 빨랐나?
비교 대상은 Python 표준 라이브러리의 json, orjson, msgspec, fastsimdjson이었다. msgspec은 필요한 필드만 선언한 형식 구조로 JSON을 읽을 수 있어, 실험에서는 이 기능을 활용했다. 각 방법의 결과가 같은지 확인했다.
단일 코어 결과는 초당 처리한 GB다.
| 방법 | GB/s |
|---|---|
json(표준 라이브러리) | 0.18 |
orjson | 0.42 |
fastsimdjson map_many | 0.63 |
msgspec(형식 구조) | 1.29 |
fastsimdjson map_many(지연 읽기) | 2.13 |
표준 라이브러리는 8시간치 자료를 처리하는 데 42초가 걸렸다. fastsimdjson의 지연 읽기 방식은 3.6초가 걸렸다. 르미르는 json, orjson, 지연 읽기를 쓰지 않은 fastsimdjson처럼 모든 값을 Python 객체로 만드는 방식에서는, 곧바로 버릴 객체를 만드는 데 시간이 많이 든다고 설명한다.
64개 코어를 한 프로세스에서 쓴 결과는 다음과 같다. 자유 스레드 Python(free-threaded Python)은 전역 인터프리터 잠금(GIL)이 없어 여러 스레드가 동시에 Python 코드를 실행할 수 있는 빌드다. json과 msgspec에는 스레드 풀을 썼고, fastsimdjson에는 threads=64를 지정했다. 모든 스레드가 입력 데이터를 공유했다.
| 방법 | GB/s |
|---|---|
json(표준 라이브러리), 64개 스레드 | 3.17 |
msgspec(형식 구조), 64개 스레드 | 24.6 |
fastsimdjson map_many, 64개 스레드 | 29.8 |
fastsimdjson map_many(지연 읽기), 64개 스레드 | 65.7 |
실험에서는 7.6GB를 약 0.12초 만에 처리했다. 단일 코어 사용 때보다 31배 빨랐고, 64개 스레드를 쓴 msgspec의 초당 25GB보다 높은 처리량이었다.
결과를 적용할 때 확인할 것은?
이 수치는 인텔 제온 골드 6548N 프로세서 2개를 장착한 64코어 서버, Python 3.14.6, fastsimdjson 0.6.0, orjson 3.13.0, msgspec 0.22.0에서 측정했다. 데이터는 타이머를 시작하기 전에 메모리에서 압축을 풀었다. 가장 느린 단일 스레드 파서와 64개 스레드의 표준 라이브러리는 한 번 측정했고, 나머지는 일곱 번 실행한 결과 중 가장 좋은 값을 보고했다.
fastsimdjson 0.6.0은 PyPI에서 설치할 수 있다. 실험 코드와 벤치마크 자료도 공개돼 있다. 다만 원문은 압축 해제 전 파일 크기나 다른 서버에서의 처리량을 밝히지 않는다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요