순수 파이썬 jq가 C 바인딩보다 40배 빠른 이유

발행: (2026년 6월 11일 PM 03:13 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

어제 나는 purejq라는 jq 패키지를 PyPI에 올리는 작업을 했습니다. (C 바인딩 버전과 비교)

벤치마크

워크로드purejqjq (C 바인딩)
필드 접근 (스트림)9 ms368 ms
필터 + 카운트55 ms442 ms
맵 + 집계18 ms444 ms
group_by112 ms704 ms
변환 + 정렬136 ms899 ms

Pure Python 구현이 C 확장보다 7~40배 빠릅니다. 처음엔 이 수치가 이상하게 보였지만 tools/bench.py --verify 로 확인했습니다.

C 바인딩은 실제 jq를 감싸고 있으며, jq는 오직 JSON만을 다룹니다. 따라서 매 호출마다 다음과 같은 과정을 거칩니다.

dicts → JSON 텍스트 → C 파서 → jq 평가 → JSON 텍스트 → dicts

이 왕복은 10만 개의 작은 객체에 대해 약 350~450 ms가 소요됩니다.
purejq는 이 과정을 완전히 생략합니다. jq 프로그램을 한 번 Python 코드로 컴파일해 두고, 이후에는 직렬화 없이 바로 객체에 작동합니다.

import purejq

prog = purejq.compile(
    "group_by(.team) | map({team: .[0].team, n: length})"
)
prog.first(data)   # 객체에 직접 작동, 직렬화 없음

이 교훈은 jq에만 국한되지 않습니다. C 라이브러리를 임베드할 때, 언어 경계를 건너는 비용을 피할 수 있다면 큰 성능 향상을 기대할 수 있습니다.

예를 들어 93 MB 파일(1 백만 객체) 전체를 처리했을 때의 결과는 다음과 같습니다.

워크로드purejq CLIjq 1.8.1 (바이너리)
단일 조회0.51 s1.68 s
필터 + 카운트1.08 s1.96 s
group_by2.32 s3.89 s

여기에도 트릭은 없습니다. 단순히 연산량 차이일 뿐입니다. 큰 파일에서는 대부분의 시간이 JSON 파싱에 소비됩니다.

예상보다 더 큰 영향을 준 요소들

  • 한 번 컴파일, 여러 번 실행
    프로그램이 중첩된 Python 클로저가 되면서, 평가 단계에서 AST를 다시 탐색하지 않습니다.

  • 정적 바인딩
    프로그램이 select 같은 함수를 재정의하지 않으면, 호출이 런타임이 아니라 컴파일 타임에 해결됩니다.

  • 단일 출력 전용 빠른 경로
    .score * 2 + 1 처럼 정확히 하나의 값을 반환하는 식은 제너레이터가 아닌 일반 함수 호출로 컴파일됩니다. 키가 고정된 객체 리터럴도 제너레이터를 건너뛰죠.

  • C에 정렬을 맡기기
    정렬 키가 모두 문자열이거나 숫자이면 sort_by / group_by / unique가 Python 내장 sort로 바로 넘어갑니다. 이 최적화만으로도 정렬 중심 워크로드에서 5배 정도 빨라집니다.

  • PyPy와의 시너지
    Pure Python 구현이므로 PyPy에서도 바로 동작합니다. 무거운 워크로드에서는 2~9배 추가 가속을 얻을 수 있습니다(예: map+aggregate가 18 ms → 2 ms).

jq라서 빠른 거다”라고 말하기는 쉽지만, 실제로는 jq 공식 테스트 스위트를 vendoring하고 있다는 점을 강조하고 싶습니다. --verify 출력 결과를 보면 확인할 수 있습니다.

pip install purejq
  • 레포지토리: https://github.com/adam2go/purejq — 이슈와 PR을 언제든 환영합니다. 특히 Pyodide나 PyPy에서 테스트해 보시면 좋습니다.
0 조회
Back to Blog

관련 글

더 보기 »