Faster JSON parsing with SVE2 on ARM processors
ARM 프로세서에서 SVE2로 더 빠르게 JSON 파싱하기
simdjson이 ARM의 SVE2 `match` 명령으로 JSON 구조 문자를 분류하는 방식을 추가했습니다. Graviton 4·5에서 인덱싱 처리량이 개선됐으며, 전체 파싱 시간은 각각 2~4%, 1~2% 단축됩니다.
- 주제
AI 요약
이 글은 ARM 프로세서의 SVE2 명령을 활용해 simdjson의 JSON 인덱싱 단계를 빠르게 만드는 방법과 실제 벤치마크 결과를 설명합니다. 핵심은 JSON 구조 문자인 쉼표, 콜론, 대괄호, 중괄호를 분류할 때 기존 NEON의 테이블 조회 방식 대신 SVE2의 `match` 명령을 사용하는 것입니다. 장난감 수준의 벤치마크가 아니라 실제 simdjson 파서에 적용한 결과를 측정했다는 점이 이 작업의 중요한 부분입니다.
■ simdjson이 JSON을 인덱싱하는 방식
simdjson은 JSON 문서를 파싱하기 전에 인덱스를 만듭니다. 이 단계에서는 문서를 64바이트 블록으로 나누고, 각 블록에서 JSON 구조 문자와 공백 문자 등의 위치를 64비트 마스크로 계산합니다. 이 마스크를 바탕으로 배열, 객체, 키와 값 같은 JSON 토큰의 위치를 파악한 뒤 다음 파싱 단계에서 사용합니다.
기존 ARM NEON 구현은 16바이트 입력마다 테이블 조회를 수행합니다. 입력 바이트에 3을 더한 뒤 상위 니블을 추출하고, 16바이트 테이블에서 해당 니블에 대응하는 구조 문자를 조회합니다. 조회한 값과 원래 입력이 같으면 해당 바이트가 구조 문자라고 판단합니다. 이 과정은 `vaddq`, `vshrq`, `vqtbl1q`, `vceqq` 같은 NEON intrinsic을 사용하며, 16바이트를 처리하는 데 네 개의 명령이 필요합니다.
이렇게 얻은 네 개의 16바이트 벡터를 하나의 64비트 마스크로 바꾸는 과정도 필요합니다. 각 바이트에 1, 2, 4, 8, 16, 32, 64, 128과 같은 비트 가중치를 적용하고, `addp`를 이용해 인접한 바이트를 세 차례 더합니다. 이 마스크 변환에는 64바이트 블록당 대략 여덟 개의 명령이 추가되며, 공백 문자 마스크 계산과 일부 작업을 공유합니다.
■ SVE2 `match` 명령을 이용한 분류
SVE2에는 입력 벡터와 작은 문자 집합 역할을 하는 두 번째 벡터를 받아, 입력의 각 바이트가 그 집합에 포함되는지를 predicate mask로 반환하는 `match` 명령이 있습니다. 이 명령은 128비트 세그먼트 안에서 동작하므로 문자 집합의 크기는 최대 16바이트입니다. JSON에서 필요한 구조 문자는 여섯 개뿐이므로 충분합니다. NEON에는 이와 같은 명령이 없으며, x64에서 가장 가까운 기능인 SSE4.2의 문자열 비교 명령 `pcmpistrm`은 느린 편이라고 설명합니다.
SVE2용 구현은 16개의 ASCII 바이트를 분류하는 데 사실상 `match` 명령 하나에 가까운 코드로 컴파일될 수 있습니다. 다만 NEON 레지스터에 있는 입력과 테이블을 SVE 레지스터로 옮기기 위해 NEON-SVE bridge인 `svset_neonq_u8`을 사용합니다. SVE 타입의 앞 16바이트가 NEON과 공유된다는 규칙 때문에 이 이동은 실제로 별도 명령 없이 컴파일될 가능성이 있다고 설명합니다.
SVE2 구현의 까다로운 부분은 `match` 결과가 일반 레지스터가 아니라 predicate register에 저장된다는 점입니다. SVE는 벡터 레지스터의 길이가 더 넓을 수 있으므로 predicate를 일반-purpose register로 저렴하게 옮기는 방법을 제공하지 않습니다. 따라서 `svsel`을 이용해 predicate가 설정된 위치에 비트 가중치를 선택하고, 그 결과를 바이트로 물질화합니다. 이후 `svget_neonq_u8`로 NEON 쪽 값을 가져오고, 기존 NEON의 pairwise add를 사용해 네 개의 16비트 결과를 하나의 64비트 마스크로 결합합니다. 즉, 문자 분류 자체는 `match`로 크게 단순화되지만 predicate를 기존 마스크 형식으로 바꾸는 비용은 별도로 감수해야 합니다.
■ 벤치마크 환경과 결과
저자는 simdjson에 포함된 parse benchmark를 사용해 NEON 분류기와 SVE2 `match` 분류기를 비교했습니다. 표준 코퍼스인 22개 JSON 파일 전체 약 24MB를 대상으로 각 파일을 300회 파싱하고 가장 빠른 시간을 기록했습니다. 두 바이너리를 서로 번갈아 실행하고 하나의 CPU 코어에 고정했으며, 각 실행 파일을 세 차례 측정한 뒤 최선의 결과를 사용했습니다. 실행 간 편차는 1% 미만이었습니다. 컴파일러는 GCC 15와 LLVM clang 21, 운영체제는 Ubuntu 26.04였고, 컴파일 옵션은 `-mcpu=native`였습니다.
`match`는 SVE가 아니라 SVE2에 포함된 명령입니다. 따라서 SVE는 지원하지만 SVE2는 지원하지 않는 AWS Graviton 3인 Neoverse V1에서는 이 코드를 실행할 수 없습니다. 측정에는 SVE2를 지원하는 Graviton 4인 Neoverse V2와 Graviton 5인 Neoverse V3를 사용했습니다. 각각 AWS `c8g.2xlarge`와 `c9g.2xlarge` 인스턴스이며, 두 프로세서 모두 128비트 SVE 레지스터를 사용합니다.
인덱싱 단계에서 파일별 성능 향상은 파일의 구조 문자 비중에 따라 달랐습니다. `canada`, `mesh`, `marine_ik`처럼 숫자가 대부분인 파일은 원래 인덱싱 단계의 비용이 낮아 개선 폭도 작았습니다. 반대로 `gsoc-2018`, `random`, `github_events`처럼 JSON 구조가 많은 파일에서 향상 폭이 컸습니다. Graviton 5에서 GCC를 사용한 `canada`와 `mesh`만 2~3% 느려졌으며, 저자는 이를 측정 오차 범위에 가까운 수준으로 설명합니다. 그 외 파일에서는 성능이 저하되지 않았습니다.
절대 처리량으로 보면 Graviton 4의 인덱싱 단계는 clang 기준 4.8GB/s에서 5.3GB/s로, GCC 기준 5.5GB/s에서 5.8GB/s로 증가했습니다. Graviton 5에서는 clang 기준 6.3GB/s에서 6.6GB/s로, GCC 기준 7.1GB/s에서 7.3GB/s로 증가했습니다. Graviton 5보다 Graviton 4에서 상대적인 이득이 더 크게 나타났습니다. 파일별 결과를 종합하면 실제 파서의 인덱싱 단계는 Graviton 4와 Graviton 5에서 3~9% 빨라졌습니다.
■ 전체 파서에 미치는 영향과 배포 방식
simdjson의 두 번째 파싱 단계는 이번 변경의 영향을 받지 않습니다. 따라서 전체 JSON 파싱 시간의 개선 폭은 인덱싱 단계보다 작으며, Graviton 4에서는 2~4%, Graviton 5에서는 1~2%였습니다. 저자는 이미 세밀하게 최적화된 루틴에서 NEON 명령 네 개를 하나의 SVE2 명령으로 대체해 얻은 결과라는 점에서 이를 의미 있는 개선이지만 큰 폭은 아닌 성능 향상으로 설명합니다.
현재 SVE2 구현은 `-mcpu=native` 또는 이에 준하는 옵션으로 빌드할 때만 활성화됩니다. 기본 빌드에서는 모든 ARM 프로세서에 NEON 코드가 사용됩니다. Apple 프로세서와 구형 Graviton 프로세서처럼 SVE2가 없는 환경을 지원하기 위해 simdjson은 NEON 경로로 돌아갑니다. 다음 작업으로는 x64의 여러 instruction set을 선택하는 방식처럼 런타임에 CPU 기능을 확인해, 기본 빌드에서도 SVE2를 지원하는 프로세서에서 자동으로 해당 경로를 선택하도록 만들 계획입니다.
이번 구현은 simdjson pull request 2866에 포함되어 있습니다. SVE2 `match` 분류기 자체는 ARM의 Madhurendra Purbay가 pull request 2863에서 작업했으며, 원문 저자는 predicate 추출에 inline assembly를 사용하는 그의 방식이 조금 더 빠르다고 설명합니다. 반면 본문에서 소개한 구현은 몇 가지 intrinsic만으로 작성할 수 있고 별도의 assembly가 필요하지 않습니다.
■ Lobsters 반응
• @nrposner — 좋은 글입니다! 저는 crowley용 스트리밍 구현을 작업하고 있는데, 아직 SVE2 커널을 만들 여유를 내지 못했습니다. 새로운 주말 프로젝트를 찾았습니다.
원문: Daniel Lemire's blog / 번역·요약: Trawling