10P by GN⁺ 2일전 | ★ favorite | 댓글 1개
  • 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진수로 정확히 표현 가능한 값이 다르기에 미세한 오차가 생길 수 있음
  • JSON.stringify에서 replacer나 space 인자가 있으면 fast path가 적용되지 않는다고 함

    • 그렇다면 JSON.stringify(data, null, 0)을 써도 fast path가 가능한지 아니면 인자가 undefined이어야만 되는지 궁금함
  • SWAR escaping 알고리즘[1]이 Folly JSON에 구현했던 것과 아주 비슷함[2]

  • 작업 자체 가치는 의심하지 않지만, 실제 V8 생태계에서 JSON.stringify가 런타임을 지배했던 구체적 문제나 데이터가 더 궁금함

    • 반드시 실행 시간 비중이 압도적일 필요는 없고, 매일 수억 페이지에서 호출되는 상황이니 전 세계적 전력 절감 효과는 상당할 것임
  • v8의 성능이 충분히 칭찬받지 못한다고 생각함, 요즘 JS가 어마어마하게 빨라졌음

    • 정말 감탄스러움, “10억 달러면 아무 문제도 풀 수 있다”의 좋은 예라고 생각함
      • 앞으로 JS가 “strict”, “stricter”처럼 더 진화해서 컴파일/JIT에 쉽고 단순한 언어가 되면 좋겠음
    • 반면, v8은 너무 극한까지 최적화돼서 전 세계에 제대로 내부를 이해하는 사람이 100명 남짓이고, 대부분 개발자는 “왜 내 JS는 안 빠르지?”라는 마음일 것이라고 느낌
  • 다른 생태계와 비교해서 이게 얼마나 뛰어난 것인지 궁금함

    • 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은 최고 수준임