10 คะแนน โดย GN⁺ 2025-08-05 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ปรับปรุงประสิทธิภาพของฟังก์ชัน JSON.stringify ในเอนจิน V8 ให้เร็วขึ้นมากกว่าสองเท่า ส่งผลให้การซีเรียลไลซ์ข้อมูลทำได้รวดเร็วยิ่งขึ้น
  • เพิ่มเส้นทางการทำงานแบบเพิ่มประสิทธิภาพสำหรับ อ็อบเจ็กต์ที่ไม่มี side effect โดยตัดตรรกะการตรวจสอบเชิงป้องกันจำนวนมากออกไป จึงได้ความเร็วที่ดีขึ้นมากกับอ็อบเจ็กต์ข้อมูลทั่วไป
  • ในส่วนของ การประมวลผลสตริง มีการใช้วิธีที่ซับซ้อนมากขึ้นทั้งด้านฮาร์ดแวร์และหน่วยความจำ เช่น การแยก 1 ไบต์/2 ไบต์, การใช้ SIMD และการเปลี่ยนโครงสร้างบัฟเฟอร์ชั่วคราว
  • ในกระบวนการ แปลงตัวเลข ได้เปลี่ยนอัลกอริทึมจาก Grisu3 เดิมเป็น Dragonbox ทำให้การแปลงที่รวดเร็วขึ้นเป็นไปได้ในภาพรวมของการเรียก Number.toString() ด้วย
  • แม้ในอาร์กิวเมนต์หรือรูปแบบบางอย่างจะย้อนกลับไปใช้เส้นทางซีเรียลไลซ์ปกติ แต่ใน สถานการณ์การพัฒนาเว็บส่วนใหญ่ จะได้รับผลของการเพิ่มประสิทธิภาพนี้โดยอัตโนมัติ

ภาพรวม

  • JSON.stringify เป็นฟังก์ชันหลักสำหรับ แปลงข้อมูลเป็นสตริงใน JavaScript
  • การเพิ่มประสิทธิภาพของฟังก์ชันนี้ส่งผลดีต่องานสำคัญบนเว็บอย่างการส่งคำขอผ่านเครือข่ายหรือการบันทึกลง localStorage ด้วย
  • ด้วยวิศวกรรมล่าสุดของ V8 ความเร็วของความสามารถนี้ได้รับการปรับปรุง มากกว่าสองเท่า และมีการอธิบายแนวทางการเพิ่มประสิทธิภาพหลักอย่างละเอียด

เส้นทาง Fast Path สำหรับกรณีไม่มี side effect

  • แกนหลักของการเพิ่มประสิทธิภาพคือการใช้ เส้นทางซีเรียลไลซ์แบบเร็ว ที่ใช้ได้เฉพาะใน สถานการณ์ที่ไม่มี side effect
  • ในกรณีเช่นนี้ จะเดินอ็อบเจ็กต์ด้วยโครงสร้างแบบ iterative แทนการเรียกซ้ำแบบ recursive ทำให้ไม่ต้องตรวจสอบ stack overflow และยังลองซีเรียลไลซ์อ็อบเจ็กต์ที่ลึกกว่าเดิมได้
  • เมื่ออ็อบเจ็กต์ข้อมูลมีโครงสร้างเรียบง่าย V8 จะใช้ Fast Path นี้แทนตรรกะทั่วไปที่ช้ากว่า เพื่อลดการตรวจสอบจำนวนมากและเพิ่มความเร็ว

การจัดการรูปแบบสตริงที่หลากหลาย

  • V8 จัดเก็บสตริงต่างกันตามว่าเป็น อักขระ 1 ไบต์/2 ไบต์ (ASCII/ไม่ใช่ ASCII) และถ้ามีอักขระที่ไม่ใช่ ASCII เพียงตัวเดียว ก็จะจัดการทั้งชุดเป็น 2 ไบต์
  • เพื่อประสิทธิภาพในการซีเรียลไลซ์สตริง จึงคอมไพล์ อัลกอริทึมแยกตามชนิดของสตริง ไว้ต่างหาก
  • ระหว่างประมวลผลจำเป็นต้องตรวจสอบชนิด instance ของสตริง ดังนั้นหากพบสตริง 2 ไบต์ ตัวซีเรียลไลเซอร์สำหรับ 2 ไบต์ที่เหมาะสมจะรับช่วงสถานะต่อไป
  • ด้วยเหตุนี้ ภาระจากการสลับเส้นทางตามการเข้ารหัสสตริงจึงแทบไม่มีเลย
  • ผลลัพธ์จะถูกสร้างเป็นบัฟเฟอร์ 1 ไบต์และ 2 ไบต์แยกกัน ก่อนจะรวมเข้าด้วยกันอย่างง่ายในตอนท้าย

เพิ่มประสิทธิภาพการซีเรียลไลซ์สตริงด้วย SIMD

  • สตริงใน JavaScript อาจมีอักขระที่ต้อง escape เมื่อทำ JSON serialization
  • สตริงที่ยาวจะตรวจสอบหลายไบต์พร้อมกันด้วย คำสั่งฮาร์ดแวร์ SIMD (เช่น ARM64 Neon)
  • สตริงสั้นจะใช้ วิธีแบบ SWAR เพื่อประมวลผลหลายอักขระพร้อมกันผ่านการคำนวณบิตในรีจิสเตอร์ทั่วไป
  • ไม่ว่าจะใช้วิธีใด ในกรณีส่วนใหญ่ก็สามารถคัดลอกทั้งสตริงได้อย่างรวดเร็วโดยแทบไม่ต้องแปลงเพิ่มเติม

เพิ่ม Express Lane (เส้นทางความเร็วสูงพิเศษ)

  • แม้อยู่ภายใน Fast Path ก็ยังมีการเพิ่ม Express Lane เพื่อให้ซีเรียลไลซ์ได้ด้วยการคัดลอกคีย์โดยตรง โดยไม่ต้องทำงานซ้ำอย่างการตรวจสอบพร็อพเพอร์ตี
  • ใช้ hidden class flag ของอ็อบเจ็กต์เพื่อตรวจว่าในคีย์ไม่มี Symbol และทั้งหมดเป็น enumerable รวมถึงสามารถซีเรียลไลซ์ได้โดยไม่ต้อง escape จากนั้นทำเครื่องหมายเป็น 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 response หรือการแคชค่าคอนฟิกในงานทั่วไป ผลของการเพิ่มประสิทธิภาพนี้จะถูกใช้โดยอัตโนมัติ

บทสรุป

  • JSON.stringify ได้รับการปรับแนวทางใหม่ในทุกด้านตั้งแต่ การออกแบบพื้นฐาน การจัดการหน่วยความจำ ไปจนถึงการจัดการอักขระ จนทำความเร็วได้ มากกว่า 2 เท่า ตามเกณฑ์ JetStream2 benchmark
  • การปรับปรุงนี้สามารถใช้งานได้ทันทีใน V8 เวอร์ชัน 13.8 (Chrome 138) ขึ้นไป

1 ความคิดเห็น

 
GN⁺ 2025-08-05
ความคิดเห็นใน Hacker News
  • รู้สึกว่าการเข้ารหัส JSON เป็นคอขวดใหญ่ของการสื่อสารระหว่างโปรเซสใน NodeJS

    • สุดท้ายแล้วคนส่วนใหญ่มักพยายามย้ายงานไปยังเธรดอื่นเพื่อลดการหน่วงของ event loop แต่กลับพบว่าภาระ CPU ของเธรดหลักเพิ่มขึ้นเป็น 3 เท่าอยู่ดี
    • เห็นตัวอย่างที่ stringify อาร์เรย์ทีละตัวบ่อยมาก และดูเหมือนภายในเองก็ใช้แนวทางคล้ายกัน
    • อยากให้ทีม V8 เสริมความแข็งแกร่งในส่วนนี้มากขึ้น
    • สงสัยว่าสำหรับชุดข้อมูลบางประเภทจะประมวลผลได้โดยไม่ bail out หรือไม่ และเรื่องการจัดการ CString เป็นอย่างไร รวมถึงสนใจว่าความสามารถ faststr จะกลับมาหรือเปล่า
    • ตอนที่เริ่มวิเคราะห์ประสิทธิภาพของ Node ครั้งแรกเมื่อปีก่อน JSON.stringify เป็นหนึ่งในปัจจัยหลักที่สุดที่ฉุดประสิทธิภาพของบริการ Node
      • ต้องใช้ stringify กับคีย์ของดิกชันนารี และ apollo/express ก็ซีเรียลไลซ์ทั้ง response เป็นสตริงทีเดียวแทนที่จะสตรีม
      • ถ้ามาจากฝั่ง JVM หรือ Go จะรู้สึกว่าส่วนนี้ใน Node ดูค่อนข้างสมัครเล่น
    • Python ก็มีปัญหาเดียวกัน
      • คิดว่าน่าจะดีถ้ามี IPC primitive ที่มีประสิทธิภาพอยู่ใต้ API ระดับสูงสำหรับแพตเทิร์นทั่วไป
    • เห็นด้วยว่าการเข้ารหัส JSON เป็นอุปสรรคใหญ่ต่อการสื่อสาร
      • สงสัยว่าทั่วโลกแล้วโอเวอร์เฮดการคำนวณจากการประมวลผล JSON ในการสื่อสารมีขนาดแค่ไหน และถ้าส่ง bytes ในรูปแบบคงที่หรือรูปแบบที่ parse ได้มีประสิทธิภาพกว่าตรง ๆ (เช่น ASN.1) จะดีกว่าหรือไม่
    • ไม่เห็นด้วยกับการที่ทีม V8 จะมาลงแรงกับส่วนนี้มากนัก และแนะนำให้นักพัฒนาที่มีปัญหาแบบนี้ไปหาเครื่องมืออื่น
      • คิดว่า Node/V8 ไม่ค่อยเหมาะกับงานแบ็กเอนด์หรือปัญหาการคำนวณประสิทธิภาพสูง
      • Javascript ถูกออกแบบมาสำหรับการใช้งานบนเว็บและคงจะเป็นแบบนั้นไปอีกนาน ดังนั้นทีม V8 ไม่จำเป็นต้องแก้ปัญหานี้ให้
      • ทีม Typescript ก็หันไปใช้ Go แล้ว และยังสามารถทำระบบแปลงข้ามภาษาอัตโนมัติได้ด้วย
    • แทบมีอยู่ครั้งเดียวเท่านั้นที่การ offload งานไปให้ Worker ช่วยประหยัดเวลาได้มากกว่าต้นทุนจากการ serialize/deserialize
      • ถ้าข้อมูลใหญ่ ต้นทุนการส่งข้อความที่ทั้งใหญ่และแพงก็จะกินประโยชน์จากการขนานงานไปพอ ๆ กัน
  • น่าทึ่งมากว่าตลอดราว 10 ปีที่ผ่านมา ประสิทธิภาพการซีเรียลไลซ์ตัวเลขทศนิยมลอยตัวพัฒนาไปได้มากแค่ไหน

    • กระบวนการแปลงค่า IEEE floating point เป็นสตริง decimal UTF-8 แล้วแปลงกลับนั้นไม่เพียงช้า แต่ยังเปราะบางมาก
      • เพราะค่าที่แทนได้อย่างแม่นยำในเลขฐานสองกับฐานสิบไม่เหมือนกัน จึงอาจเกิดความคลาดเคลื่อนเล็กน้อยได้
  • มีคนบอกว่าใน JSON.stringify ถ้ามีพารามิเตอร์ replacer หรือ space จะไม่เข้า fast path

    • ถ้าอย่างนั้นการใช้ JSON.stringify(data, null, 0) ยังเข้า fast path ได้ไหม หรือว่าพารามิเตอร์ต้องเป็น undefined เท่านั้น
  • อัลกอริทึม SWAR escaping[1] คล้ายมากกับที่เคยทำไว้ใน Folly JSON[2]

  • ไม่ได้สงสัยคุณค่าของงานนี้ แต่ยังอยากรู้ปัญหาเชิงรูปธรรมหรือข้อมูลจริงที่ทำให้ JSON.stringify ครองสัดส่วน runtime ในระบบนิเวศ V8 จริง ๆ

    • ไม่จำเป็นต้องกินสัดส่วนเวลาแบบท่วมท้นเสมอไป เพราะมันถูกเรียกใช้กับหน้าหลายร้อยล้านหน้าทุกวัน ดังนั้นผลด้านการประหยัดพลังงานในระดับโลกก็น่าจะมากพอสมควร
  • คิดว่าประสิทธิภาพของ v8 ยังได้รับคำชมน้อยเกินไป ทุกวันนี้ JS เร็วขึ้นมหาศาลแล้ว

    • น่าทึ่งจริง ๆ คิดว่านี่เป็นตัวอย่างที่ดีของคำว่า “ถ้ามีเงิน 1 พันล้านดอลลาร์ ก็แก้ปัญหาอะไรก็ได้”
      • ต่อไปก็อยากให้ JS วิวัฒน์เป็นอะไรแบบ “strict”, “stricter” มากขึ้น เพื่อให้เป็นภาษาที่คอมไพล์/JIT ได้ง่ายและเรียบง่ายกว่าเดิม
    • ในอีกด้านหนึ่ง v8 ถูก optimize จนสุดทางเกินไป จนทั่วโลกอาจมีคนที่เข้าใจภายในมันจริง ๆ แค่ราว 100 คน และนักพัฒนาส่วนใหญ่ก็คงคิดว่า “ทำไม JS ของฉันไม่เร็วล่ะ?”
  • อยากรู้ว่าเมื่อเทียบกับ ecosystem อื่นแล้วนี่ถือว่าโดดเด่นแค่ไหน

    • ทำ JSON serialization มานานกว่าสิบปี แต่แทบไม่เคยกังวลเรื่องนี้เพราะมันเร็วมากอยู่แล้ว
    • simdjson ทำได้ระดับ GB ต่อวินาทีต่อคอร์ และเมื่อคิดรวมเรื่อง prefetching/branch prediction แล้ว ก็รู้สึกว่าการ serialize JSON ใน workload จริงส่วนใหญ่เล็กน้อยจนมองข้ามได้
    • จุดอ่อนที่สุดของ JSON คือโอเวอร์เฮดด้าน IO ต่อให้ serializer จะเร็วแค่ไหน ถ้ายังต้องเขียน blob ขนาด 100MB ลง storage ทุกครั้งก็แทบไม่มีประโยชน์
  • “No indexed properties on objects” — บอกว่า fast path ถูก optimize มาเฉพาะสำหรับอ็อบเจ็กต์ที่ใช้คีย์แบบสตริงทั่วไป ดังนั้นถ้ามีพร็อพเพอร์ตีแบบ index คล้าย array ก็จะกลับไปใช้ slow path

    • สงสัยว่าเพราะอะไร
    • หมายความว่าอ็อบเจ็กต์ที่มีคีย์หน้าตาเหมือน integer จะถูก serialize เป็น JSON array งั้นหรือ? ไม่น่าใช่มั้ง...?
  • ชอบแนวทาง segmented buffer มาก เมื่อก่อนต้องใช้ไลบรารีฝั่ง userland อย่าง fast-json-stringify มาทำ rope trick เอง แต่ตอนนี้เป็น native แล้วเลยดีกว่ามาก

    • อยากรู้ว่าคนเจอเงื่อนไข bailout (เช่น replacer, space, .toJSON() แบบกำหนดเอง) บ่อยแค่ไหน และในกรณีแบบนี้จะ fallback ไปเส้นทางช้าทันทีเลยหรือเปล่า
  • V8 ยอดเยี่ยมมาก แต่เพราะตัว JS เองหรือเปล่าที่ทำให้ประสิทธิภาพยังสู้ LuaJIT หรือ JVM ไม่ได้

    • JVM ใช้เวลา warmup นานก็จริง แต่ถึงอย่างนั้นก็ยังดีกว่า JS
    • ต้นเหตุคือ JS เอง คิดว่า V8 ก้าวหน้ากว่า luajit และ JVM มาก
      • Java มีข้อจำกัดด้านเวลาจริงน้อยกว่า (มีคอมไพเลอร์) และนั่นคือข้อได้เปรียบของมัน
    • overhead ของ JS จำนวนมากมาจากความเป็น dynamic
      • asm.js ห้ามพฤติกรรม dynamic อย่างการเปลี่ยน shape ของอ็อบเจ็กต์ จึงข้ามเช็กจำนวนมากได้
    • ขอคัดค้านคำว่า “แม้แต่ JVM” เพราะ JVM เองก็อยู่ระดับท็อปอยู่แล้ว