10 điểm bởi GN⁺ 2025-08-05 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong engine V8, hiệu năng của hàm JSON.stringify đã được nâng lên hơn gấp đôi, giúp cải thiện tốc độ tuần tự hóa dữ liệu
  • Đã bổ sung một đường tối ưu hóa dành cho đối tượng không có tác dụng phụ, nhờ đó có thể bỏ qua nhiều logic kiểm tra phòng thủ và đạt mức tăng tốc lớn với các đối tượng dữ liệu thông thường
  • Ở phần xử lý chuỗi, V8 áp dụng nhiều kỹ thuật tối ưu hóa về phần cứng và bộ nhớ như phân biệt 1 byte/2 byte, tận dụng SIMD và thay đổi cấu trúc bộ đệm tạm
  • Trong quá trình chuyển đổi số, thuật toán Grisu3 trước đây được thay bằng Dragonbox, mở ra khả năng chuyển đổi nhanh hơn cho cả các lệnh gọi Number.toString() nói chung
  • Với một số đối số và hình dạng dữ liệu nhất định, hệ thống sẽ quay lại đường tuần tự hóa thông thường, nhưng trong phần lớn tình huống phát triển web, bạn sẽ tự động được hưởng lợi từ tối ưu hóa này

Tổng quan

  • JSON.stringify là hàm cốt lõi dùng để chuyển dữ liệu thành chuỗi trong JavaScript
  • Việc tăng hiệu năng cho hàm này cũng tác động tích cực đến những tác vụ rất quan trọng trên web như yêu cầu mạng hay lưu vào localStorage
  • Nhờ các cải tiến kỹ thuật mới nhất trong V8, tốc độ của tính năng này đã được cải thiện hơn gấp đôi, và bài viết trình bày chi tiết các hướng tối ưu chính

Đường Fast Path không có tác dụng phụ

  • Trọng tâm của tối ưu hóa là áp dụng đường tuần tự hóa nhanh chỉ dùng được trong các tình huống không có tác dụng phụ (side-effect)
  • Trong các trường hợp như vậy, thay vì dùng đệ quy, V8 duyệt đối tượng bằng cấu trúc lặp (iterative), nên không cần kiểm tra tràn ngăn xếp và cũng có thể thử tuần tự hóa các đối tượng sâu hơn
  • Khi đối tượng dữ liệu đủ đơn giản, V8 sẽ dùng Fast Path này thay cho logic chung chậm hơn, bỏ qua nhiều kiểm tra và tăng tốc đáng kể

Xử lý nhiều kiểu biểu diễn chuỗi

  • V8 lưu trữ chuỗi khác nhau tùy theo ký tự 1 byte/2 byte (ASCII/không phải ASCII); chỉ cần có một ký tự không phải ASCII thì toàn bộ sẽ được quản lý dưới dạng 2 byte
  • Để tăng hiệu năng tuần tự hóa chuỗi, V8 biên dịch các phiên bản thuật toán riêng theo từng loại chuỗi
  • Vì trong lúc xử lý vẫn phải kiểm tra kiểu thực thể chuỗi, nếu phát hiện chuỗi 2 byte thì bộ tuần tự hóa 2 byte phù hợp sẽ tiếp nhận trạng thái hiện tại
  • Nhờ vậy, chi phí chuyển đổi đường xử lý theo kiểu mã hóa chuỗi gần như bằng không
  • Kết quả cuối cùng được tạo bằng cách xây dựng riêng bộ đệm 1 byte và 2 byte rồi chỉ đơn giản hợp nhất chúng ở cuối

Tối ưu hóa tuần tự hóa chuỗi bằng SIMD

  • Chuỗi JavaScript có thể chứa các ký tự cần escape khi tuần tự hóa JSON
  • Với chuỗi dài, V8 dùng các lệnh phần cứng SIMD (như ARM64 Neon) để kiểm tra nhiều byte cùng lúc
  • Với chuỗi ngắn, V8 dùng phương pháp SWAR, xử lý nhiều ký tự đồng thời bằng phép toán bit trong thanh ghi đa dụng
  • Dù theo cách nào, trong đa số trường hợp toàn bộ chuỗi có thể được sao chép rất nhanh mà không cần biến đổi gì thêm

Bổ sung Express Lane (đường siêu tốc)

  • Ngay cả trong Fast Path, V8 cũng bổ sung Express Lane để có thể tuần tự hóa chỉ bằng cách sao chép khóa mà không phải lặp lại các việc như kiểm tra thuộc tính
  • Tận dụng cờ hidden class của đối tượng, nếu khóa không có Symbol, đều là enumerable và có thể được tuần tự hóa mà không cần escape, đối tượng sẽ được đánh dấu là fast-json-iterable
  • Khi tuần tự hóa đối tượng khác có cùng hidden class, V8 sẽ sao chép khóa ngay lập tức mà không cần kiểm tra thêm
  • Kỹ thuật này cũng được áp dụng trong JSON.parse để tăng tốc so sánh khóa

Thuật toán double-to-string nhanh hơn

  • Quá trình chuyển số thành chuỗi cũng diễn ra rất thường xuyên và có độ phức tạp cao
  • Việc thay thế thuật toán Grisu3 cũ bằng Dragonbox cũng giúp tăng hiệu năng cho toàn bộ các lệnh gọi Number.prototype.toString()

Tối ưu hóa cấu trúc bộ đệm tạm

  • Trước đây khi xây dựng chuỗi, V8 dùng một bộ đệm liên tục duy nhất, nên mỗi khi thiếu chỗ lại phát sinh chi phí sao chép toàn bộ dữ liệu
  • Cách mới dùng cấu trúc bộ đệm phân đoạn (segmented), nối nhiều bộ đệm nhỏ theo nhu cầu
  • Nhờ đó, khi hết chỗ, hệ thống chỉ cần cấp phát bộ đệm mới mà không phải sao chép toàn bộ

Giới hạn

  • Fast Path chỉ hoạt động cho việc tuần tự hóa dữ liệu đơn giản
  • Nếu không thỏa các điều kiện dưới đây thì hệ thống sẽ dùng đường xử lý thông thường
    • Không được dùng đối số replacer hoặc space (không hỗ trợ pretty-print hay biến đổi)
    • Phải là đối tượng đơn giản, không có phương thức tùy biến toJSON
    • Nếu có thuộc tính dựa trên chỉ số, hệ thống sẽ chuyển sang đường chậm
    • Chuỗi đặc biệt như ConsString không được xử lý theo đường này
  • Trong hầu hết các trường hợp phổ biến như tuần tự hóa dữ liệu, tạo phản hồi API hay cache cấu hình, tối ưu hóa này sẽ tự động được áp dụng

Kết luận

  • JSON.stringify đã được tái cấu trúc cách tiếp cận trên toàn bộ bề mặt, từ thiết kế nền tảng đến xử lý bộ nhớ và xử lý ký tự, qua đó đạt mức tăng tốc hơn 2 lần theo benchmark JetStream2
  • Các cải tiến này có thể trải nghiệm ngay từ V8 phiên bản 13.8 (Chrome 138) trở lên

1 bình luận

 
GN⁺ 2025-08-05
Ý kiến Hacker News
  • Cảm thấy việc mã hóa JSON là một nút thắt lớn trong giao tiếp giữa các tiến trình trên NodeJS

    • Cuối cùng, phần lớn đều cố chuyển công việc sang luồng khác để giảm độ trễ event loop, nhưng rồi lại nhận ra tải CPU của luồng chính rốt cuộc tăng gấp 3 lần
    • Cũng thấy nhiều ví dụ stringify từng phần tử mảng một, có vẻ bên trong cũng áp dụng cách này
    • Hy vọng đội V8 sẽ tiếp tục cải thiện mạnh hơn ở phần này
    • Tò mò không biết với một số tập dữ liệu có thể xử lý mà không cần bail out hay không, hoặc vấn đề xử lý CString thì thế nào, cũng quan tâm liệu tính năng faststr có quay lại không
    • Năm ngoái khi lần đầu phân tích hiệu năng Node, JSON.stringify là một trong những yếu tố lớn nhất chặn hiệu năng của dịch vụ Node
      • Phải dùng stringify cho key của dictionary, còn apollo/express thì serialize toàn bộ phản hồi thành một chuỗi cùng lúc thay vì stream
      • Từ góc nhìn người từng làm JVM hay Go, mấy chỗ này trên Node trông khá nghiệp dư
    • Python cũng gặp đúng vấn đề này
      • Tôi nghĩ sẽ rất tốt nếu có các primitive IPC hiệu quả nằm dưới một API cấp cao dành cho các pattern phổ biến
    • Đồng ý với ý kiến rằng mã hóa JSON là một rào cản lớn đối với giao tiếp
      • Tò mò không biết trên toàn cầu overhead tính toán do xử lý JSON trong giao tiếp lớn đến mức nào, và liệu cứ gửi bytes theo một định dạng cố định hay dễ parse hơn (ví dụ: ASN.1) có tốt hơn không
    • Phản đối việc đội V8 tích cực tối ưu mạnh hơn cho phần này, và khuyên các lập trình viên gặp vấn đề này nên tìm công cụ khác
      • Tôi không nghĩ Node/V8 phù hợp lắm cho backend hay các bài toán tính toán hiệu năng cao
      • Javascript được thiết kế cho web và sẽ còn như vậy lâu dài, nên đội V8 không nhất thiết phải giải quyết kiểu vấn đề này
      • Cả đội Typescript cũng đã chuyển sang Go, và việc tự động hóa chuyển đổi giữa các ngôn ngữ cũng khả thi
    • Hầu như chỉ có đúng một lần tôi offload công việc sang Worker mà thời gian tiết kiệm được lớn hơn thời gian serialize/deserialize
      • Khi dữ liệu lớn, chi phí truyền message lớn và đắt đỏ gần như triệt tiêu lợi ích từ song song hóa
  • Tôi rất ngạc nhiên về việc hiệu năng serialize số dấu phẩy động đã cải thiện nhiều đến mức nào trong khoảng hơn 10 năm gần đây

    • Việc chuyển giá trị dấu phẩy động IEEE thành chuỗi thập phân UTF-8 rồi chuyển ngược lại không chỉ chậm mà còn rất mong manh
      • Vì các giá trị có thể được biểu diễn chính xác trong hệ nhị phân và hệ thập phân là khác nhau, nên có thể phát sinh sai số rất nhỏ
  • Có nói rằng nếu JSON.stringify có đối số replacer hoặc space thì fast path sẽ không được áp dụng

    • Vậy thì dùng JSON.stringify(data, null, 0) có còn vào fast path được không, hay đối số phải là undefined mới được?
  • Thuật toán escaping SWAR[1] rất giống với thứ tôi từng triển khai trong Folly JSON[2]

  • Tôi không nghi ngờ giá trị của công việc này, nhưng muốn biết rõ hơn các vấn đề hay dữ liệu cụ thể trong hệ sinh thái V8 nơi JSON.stringify thực sự chi phối runtime

    • Không nhất thiết tỷ trọng thời gian chạy phải áp đảo hoàn toàn, nhưng vì nó được gọi trên hàng trăm triệu trang mỗi ngày nên hiệu quả tiết kiệm điện trên quy mô toàn cầu chắc sẽ rất đáng kể
  • Tôi nghĩ hiệu năng của v8 chưa được khen ngợi đủ mức, JS dạo này đã nhanh lên khủng khiếp

    • Thật sự rất ấn tượng, tôi xem đây là một ví dụ hay cho câu “có một tỷ đô thì giải được mọi vấn đề”
      • Mong rằng sau này JS sẽ tiếp tục tiến hóa kiểu “strict”, “stricter” để trở thành một ngôn ngữ dễ và đơn giản hơn cho compile/JIT
    • Mặt khác, v8 đã được tối ưu đến mức cực hạn, đến nỗi trên toàn thế giới có lẽ chỉ còn cỡ 100 người thật sự hiểu bên trong nó, còn đa số lập trình viên thì chỉ nghĩ “sao JS của mình không nhanh?”
  • Tò mò không biết việc này xuất sắc đến mức nào nếu so với các hệ sinh thái khác

    • Tôi đã làm JSON serialization hơn 10 năm nhưng hầu như chưa bao giờ phải lo về nó vì nó quá nhanh
    • simdjson có thể xử lý mức GB mỗi giây trên mỗi core, và nếu tính cả các tối ưu như prefetching/dự đoán nhánh thì tôi nghĩ JSON serialization trong phần lớn workload thực tế gần như có thể bỏ qua
    • Nhược điểm lớn nhất của JSON là overhead IO, serializer có nhanh đến đâu mà cứ phải ghi blob 100MB vào storage mỗi lần thì cũng vô ích
  • “No indexed properties on objects” — có vẻ fast path chỉ tối ưu cho object thông thường với key dạng chuỗi, còn nếu có thuộc tính chỉ mục kiểu array-like thì sẽ quay về slow path

    • Tò mò không biết lý do là gì
    • Điều đó có nghĩa là object có key trông giống integer sẽ được serialize thành mảng JSON sao? Chắc không đến mức vậy chứ...?
  • Tôi thích cách dùng segmented buffer, ngày xưa phải tự dùng mấy thư viện userland như fast-json-stringify để làm trò rope, giờ có native thì tốt hơn nhiều

    • Tò mò không biết các điều kiện bailout (ví dụ: replacer, space, .toJSON() tùy biến) có thường gặp không, và trong các trường hợp đó có rơi thẳng sang đường chậm hay không
  • V8 rất xuất sắc, nhưng có vẻ vì bản thân JS mà hiệu năng vẫn kém hơn LuaJIT hay JVM

    • JVM đúng là có thời gian warm-up dài, nhưng dù vậy vẫn tốt hơn JS
    • Nguyên nhân là JS, tôi nghĩ V8 tiến bộ hơn LuaJIT và JVM rất nhiều
      • Java có ít ràng buộc thời gian thực hơn (và có compiler), đó là lợi thế
    • Phần lớn overhead của JS đến từ tính động
      • asm.js cấm các hành vi động như thay đổi shape của object nên có thể bỏ qua rất nhiều kiểm tra
    • Tôi phản đối cách nói “ngay cả JVM”, JVM là hàng đỉnh cao nhất