JSON.stringify를 두 배 이상 빠르게 만든 방법
(v8.dev)- V8 엔진에서 JSON.stringify 함수의 성능을 두 배 이상 높여, 데이터 직렬화 속도 개선 효과를 얻음
- 부작용 없는 객체를 위한 최적화 경로를 도입해 다수의 방어적인 검사 로직을 생략함으로써 일반적인 데이터 객체에서 큰 속도 향상을 이룸
- 문자열 처리 시 1바이트/2바이트 구분, SIMD 활용, 임시 버퍼 구조 변경 등 하드웨어와 메모리 측면 전반에서 고도화된 방법을 적용
- 숫자 변환 과정에서는 기존 Grisu3 알고리듬을 Dragonbox로 교체하여 Number.toString() 호출 전반에서도 신속한 변환 가능성 확보
- 일부 인자·형태에서는 일반 직렬화 경로로 돌아가지만, 대부분 웹 개발 상황에서는 자동으로 최적화 효과를 누릴 수 있음
개요
-
JSON.stringify
는 자바스크립트에서 데이터를 문자열로 변환하는 핵심 함수 - 이 함수의 성능 향상은 네트워크 요청이나 localStorage 저장 등 웹에서 매우 중요한 작업들에도 긍정적 영향을 줌
- 최신 V8 엔지니어링을 통해 이 기능의 속도가 두 배 이상 개선되었으며, 주요 최적화 방안을 상세히 소개함
부작용 없는 Fast Path 경로
- 최적화의 핵심은 부작용(side-effect)이 없는 상황에서만 사용할 수 있는 빠른 직렬화 경로 적용
- 이런 상황에서는 재귀가 아니라 반복적(iterative) 구조로 객체 순회함으로써, 스택 오버플로우 검사가 필요 없고, 더 깊은 객체 직렬화 시도도 가능함
- 데이터 객체가 단순할 때 V8은 느린 일반 로직 대신 이 Fast Path를 활용해 많은 검사를 생략하고 속도를 올림
다양한 문자열 표현 처리
- V8은 1바이트/2바이트 문자(ASCII/비-ASCII) 에 따라 문자열을 다르게 저장하며, 하나라도 비-ASCII가 있으면 전체가 2바이트로 관리됨
- 문자열 직렬화 성능을 위해 문자열 타입별로 별도의 알고리듬 버전을 만들어 컴파일함
- 처리 중에 문자열 인스턴스 타입을 확인해야 하므로, 2바이트 문자열이 감지되면 적합한 2바이트 직렬화기가 상태를 넘겨받음
- 덕분에 문자열 인코딩별 경로 전환 부하는 사실상 없음
- 결과는 1바이트, 2바이트 버퍼를 각각 만든 뒤 마지막에 단순히 병합함
SIMD로 문자열 직렬화 최적화
- 자바스크립트 문자열에는 JSON 직렬화 시 이스케이프가 필요한 문자가 포함될 수 있음
- 긴 문자열은 SIMD 하드웨어 명령(ARM64 Neon 등)으로 여러 바이트를 한 번에 검사함
- 짧은 문자열은 SWAR 방식으로, 범용 레지스터에서 비트 연산을 통해 여러 문자를 동시에 처리함
- 어떤 방식이든, 대부분의 경우 별다른 변환 없이 전체 문자열을 빠르게 복사 가능함
Express Lane(초고속 경로) 추가
- Fast Path 내에서도 프로퍼티 검사 등 반복적인 작업 없이 키 복사만으로 직렬화가 가능하도록 Express Lane 마련함
- 오브젝트의 hidden class 플래그를 활용해 키에 Symbol이 없고, 모두 enumerable 및 이스케이프 필요 없이 직렬화된 경우 'fast-json-iterable'로 마킹함
- 동일한 hidden class를 갖는 다른 객체 직렬화 시 별도 검사 없이 바로 키 복사 수행
- 이 기법은
JSON.parse
에서도 빠른 키 비교에 응용
더 빠른 double-to-string 알고리듬
- 숫자를 문자열로 변환하는 과정도 높은 빈도와 복잡성을 가짐
-
기존 Grisu3 알고리듬을 Dragonbox로 교체해,
Number.prototype.toString()
전체 호출에서도 성능향상 효과가 발생함
임시 버퍼 구조 최적화
- 문자열 빌드 시 기존에는 단일 연속 버퍼를 사용해, 공간이 부족할 때마다 전체 복사 작업이 발생하는 과부하가 있었음
- 새로운 방법은 분할(segmented) 버퍼 구조로, 여러 소규모 버퍼를 필요에 따라 이어붙임
- 덕분에 공간 부족 시 전체 복사 필요 없이 새로운 버퍼 할당만 진행됨
한계
- Fast Path는 단순한 데이터 직렬화에 한해 동작함
- 아래 조건에 부합하지 않을 경우 일반 경로 사용
- replacer 혹은 space 인자 사용 불가(Pretty-Print, 변형 불가)
- toJSON 커스텀 메서드 없는 단순 객체여야 함
- 인덱스 기반 프로퍼티가 있으면 느린 경로로 이동
- ConsString 등 특수 문자열은 처리하지 않음
- 대부분 데이터 직렬화, API 응답 생성, 설정 캐싱 등 일반적인 용도엔 자동으로 최적화 효과가 적용됨
결론
-
JSON.stringify
의 기본 설계부터 메모리 처리, 문자 처리까지 전 영역에서 접근 방식을 재구성하여 JetStream2 벤치마크 기준 2배 이상의 속도 상승을 달성함 - 해당 개선사항은 V8 버전 13.8(Chrome 138) 이상에서 바로 경험 가능함
Hacker News 의견
-
JSON 인코딩이 NodeJS에서 프로세스 간 통신에 큰 장애물임을 느낌
- 대부분 결국엔 이벤트 루프 지연을 줄이고자 작업을 다른 스레드로 넘기려고 시도하지만, 메인 스레드의 CPU 부하는 결국 3배가 됨을 알게 됨
- 배열을 하나씩 stringify하는 예시도 많이 봄, 내부적으로도 이런 방식이 적용되는 듯함
- V8 팀이 이 부분을 더 강화해줬으면 하는 바람이 있음
- 일부 데이터 셋에 대해 bail out 없이 처리할 수 있는지, 또는 CString 처리 문제는 어찌되는지 궁금함, faststr 기능이 부활하는지도 관심 있음
- 작년에 Node 퍼포먼스 분석을 처음 했을 때, JSON.stringify가 Node 서비스에서 성능을 가로막는 가장 큰 요인 중 하나였음
- 딕셔너리 키로 stringify를 써야 하고, apollo/express는 전체 응답을 한 번에 문자열로 직렬화해서 스트리밍하지 않음
- JVM이나 Go에서 온 입장에서는 Node에서 이런 부분이 꽤 아마추어같이 느껴졌음
- Python도 똑같은 문제를 가짐
- 일반 패턴을 위한 높은 수준의 API 위에서 효율적인 IPC 프리미티브가 있으면 좋겠다고 생각함
- JSON 인코딩이 통신에 큰 장애라는 의견에 공감함
- 전 세계적으로 통신에서 JSON 처리로 인한 연산 오버헤드가 얼마나 큰지, bytes를 고정 포맷 등 더 파싱 효율적인 방식(예: ASN.1)으로 그냥 보내면 더 나은지 궁금함
- V8팀이 이 부분을 더 적극적으로 강화하는 데에는 반대하며, 이 문제가 있는 개발자들이 다른 도구를 찾기를 추천함
- Node/V8은 백엔드나 고성능 계산 문제에 그다지 적합하지 않다고 생각함
- Javascript는 웹 사용에 맞춰져 오래 그럴 것이므로, V8팀이 이런 문제를 해결해 줄 필요는 없음
- Typescript 팀도 Go로 전향했고, 언어 간 변환 자동화도 가능함
- Worker로 작업을 오프로드해서 직렬화/역직렬화 시간보다 세이브한 시간이 많았던 경우는 거의 단 한 번뿐이었음
- 데이터가 크면 크고 비싼 메시지 전달이 병렬화 이익과 맞먹게 됨
-
최근 10여 년간 부동소수점 숫자 직렬화 성능이 얼마나 향상됐는지 아주 놀라웠음
- IEEE 부동소수점 값을 decimal UTF-8 문자열로 바꿨다 다시 역변환하는 과정은 느릴 뿐 아니라 매우 불안정함
- 2진수와 10진수로 정확히 표현 가능한 값이 다르기에 미세한 오차가 생길 수 있음
- IEEE 부동소수점 값을 decimal UTF-8 문자열로 바꿨다 다시 역변환하는 과정은 느릴 뿐 아니라 매우 불안정함
-
JSON.stringify
에서 replacer나 space 인자가 있으면 fast path가 적용되지 않는다고 함- 그렇다면
JSON.stringify(data, null, 0)
을 써도 fast path가 가능한지 아니면 인자가 undefined이어야만 되는지 궁금함
- 그렇다면
-
SWAR escaping 알고리즘[1]이 Folly JSON에 구현했던 것과 아주 비슷함[2]
- Folly는 4바이트가 아니라 8바이트 단위로 처리하고, escape가 필요한 첫 위치도 반환해 fast path에서 오버헤드를 최소화함
- [1] https://source.chromium.org/chromium/_/…
- [2] https://github.com/facebook/folly/…
-
작업 자체 가치는 의심하지 않지만, 실제 V8 생태계에서 JSON.stringify가 런타임을 지배했던 구체적 문제나 데이터가 더 궁금함
- 반드시 실행 시간 비중이 압도적일 필요는 없고, 매일 수억 페이지에서 호출되는 상황이니 전 세계적 전력 절감 효과는 상당할 것임
-
v8의 성능이 충분히 칭찬받지 못한다고 생각함, 요즘 JS가 어마어마하게 빨라졌음
- 정말 감탄스러움, “10억 달러면 아무 문제도 풀 수 있다”의 좋은 예라고 생각함
- 앞으로 JS가 “strict”, “stricter”처럼 더 진화해서 컴파일/JIT에 쉽고 단순한 언어가 되면 좋겠음
- 반면, v8은 너무 극한까지 최적화돼서 전 세계에 제대로 내부를 이해하는 사람이 100명 남짓이고, 대부분 개발자는 “왜 내 JS는 안 빠르지?”라는 마음일 것이라고 느낌
- 정말 감탄스러움, “10억 달러면 아무 문제도 풀 수 있다”의 좋은 예라고 생각함
-
다른 생태계와 비교해서 이게 얼마나 뛰어난 것인지 궁금함
- 10년 넘게 JSON 직렬화를 해왔지만 너무 빨라서 걱정해본 적이 거의 없음
- simdjson은 코어당 초당 GB 처리 가능하며, 프리페칭/분기예측 등 최적화 감안하면 JSON 직렬화는 대부분 현실 workload에서 무시해도 될 수준이라 생각함
- JSON의 가장 큰 단점은 IO 오버헤드임, serializer 속도가 아무리 빨라도 100MB blob을 매번 저장소에 저장해야 하면 무용지물임
-
“No indexed properties on objects” — fast path는 일반적인 문자열 기반 키(key) 객체에만 최적화돼 있다고 하는데, array-like 인덱스 속성이 있으면 slow path로 돌아감
- 이유가 무엇인지 궁금함
- integer처럼 보이는 키가 있는 객체는 JSON 배열로 직렬화된다는 뜻일까? 설마 그런가…?
-
segmented buffer 방식이 마음에 듦, 예전엔 fast-json-stringify 같은 userland 라이브러리로 rope 트릭을 직접 짰어야 했는데 이젠 네이티브라 훨씬 좋음
- bailout 조건(예: replacer, space, 커스텀 .toJSON()) 많이 경험하는지 궁금함, 이런 경우 느린 경로로 바로 fallback되는 것인지 문의함
-
V8은 탁월하지만, JS 자체 때문인지 LuaJIT이나 JVM보다 성능에서 부족함
- JVM은 워밍업 시간이 길긴 하지만, 그럼에도 불구하고 JS보다는 나음
- JS가 원인임, V8이 luajit과 JVM보다 훨씬 진보됐다고 생각함
- Java는 실시간 제약이 적고(컴파일러가 있음) 그 점이 장점임
- 많은 JS overhead는 dynamic 특성 때문임
- asm.js는 객체의 shape 변경 같은 동적 동작을 금지해서 많은 체크를 스킵할 수 있었음
- “JVM조차”라는 표현에 이의를 제기함, JVM은 최고 수준임