WebAssembly ทำให้ JavaScript ทำงานได้เร็วขึ้นอย่างไร
(bytecodealliance.org)บทนำ
-
เวลาเรียกใช้ JS ในเบราว์เซอร์ มักทำงานได้เร็วเพราะเอนจิน JS ของเบราว์เซอร์ถูกปรับแต่งมาอย่างดี แต่ทุกวันนี้ก็มีการใช้ JS ในสภาพแวดล้อมอื่นมากขึ้นด้วย (serverless, เกมคอนโซล, iOS เป็นต้น)
-
WASM คือเทคโนโลยีที่ช่วยให้รัน JS ได้เร็วในรันไทม์เหล่านี้
วิธีการทำงาน
-
หากมีเอนจิน JS โค้ด JS จะถูกแปลงเป็นไบต์โค้ดผ่าน interpreter และ JIT compiler เป็นต้น
-
ในสภาพแวดล้อมที่ไม่มีเอนจิน JS จำเป็นต้องแจกจ่ายเอนจิน JS ไปพร้อมกับโค้ด แต่หากแจกจ่ายเอนจิน JS เป็นโมดูล WASM ก็จะทำให้พกพาไปใช้ได้ในหลายสภาพแวดล้อม
-
โค้ด JS จะทำงานอยู่ภายในเอนจิน JS ที่ถูกแยกไว้ภายในเอนจิน WASM
-
เอนจิน JS ที่เอนจิน WASM ใช้คือ SpiderMonkey ซึ่ง Firefox ก็ใช้ตัวนี้เช่นกัน
-
WASM ไม่สามารถสร้าง machine code ได้ด้วยตัวเอง จึงต้องผ่านการคอมไพล์ด้วย JS
-
แต่เพราะใช้ JIT ไม่ได้ ปกติแล้ว WASM จึงควรจะช้า แล้ว WASM ทำให้การรัน JS “เร็วขึ้น” ได้อย่างไรกันแน่?
ใช้ WASM ที่ไหนบ้าง
การใช้ JS บน iOS (หรือสภาพแวดล้อมที่ใช้ JIT ไม่ได้)
- เกมคอนโซล, แอป iOS ที่ไม่มีสิทธิพิเศษ, สมาร์ตทีวี ฯลฯ ใช้ JIT ไม่ได้ด้วยเหตุผลด้านความปลอดภัย
(→ มีการพูดราวกับว่าปัญหาด้านความปลอดภัยของการคอมไพล์แบบ JIT เป็นเรื่องแน่นอนอยู่แล้ว แต่ถึงลองหาดูก็ยังไม่ค่อยเข้าใจเหตุผลชัดเจนนัก)
- ดังนั้นในที่เหล่านี้จึงต้องใช้ interpreter แต่จริง ๆ แล้วแอปที่รันบนแพลตฟอร์มแบบนี้มักทำงานยาวนานและมีโค้ดจำนวนมาก จึงควรหลีกเลี่ยงการช้าลงจากการใช้ interpreter
- ถ้าอยากใช้ JS โดยหลีกเลี่ยงปัญหาประสิทธิภาพตกจาก interpreter จะทำอย่างไร?
การใช้ JS ใน serverless
- สภาพแวดล้อม serverless มี JIT ก็จริง แต่มีปัญหาเรื่อง cold start ที่นาน ทำให้ latency สูงขึ้น (แค่โหลดเอนจินก็อย่างน้อย 5 ms)
- มีเทคนิคเพิ่มประสิทธิภาพเพื่อซ่อนเวลา cold start แต่ยิ่งชั้นเครือข่ายดีขึ้น (เช่น QUIC) ความหมายของเทคนิคเหล่านี้ก็ลดลง อีกทั้งเมื่อรันฟังก์ชัน serverless หลายตัวพร้อมกัน เทคนิคเหล่านี้ก็แทบไม่ค่อยมีประโยชน์
- อาจหลีกเลี่ยงเวลา cold start ได้ด้วยการนำ instance กลับมาใช้ซ้ำ แต่ก็หมายความว่าจะมีการแชร์สถานะระหว่างคำขอ ซึ่งกลายเป็นความเสี่ยงด้านความปลอดภัย
- ด้วยเหตุนี้ ในทางปฏิบัติจึงมีหลายกรณีที่ไม่ทำตาม best practice และยัดหลายอย่างไว้ในฟังก์ชัน serverless เดียว
- กล่าวคือ ถ้าแก้ปัญหา cold start ได้ ก็ไม่จำเป็นต้องใช้เทคนิคต่าง ๆ เพื่อหลีกเลี่ยงมัน และปัญหาหลายอย่างก็จะถูกแก้ไปด้วย
- WASM ห่อหุ้มและแยก JS ออกไว้ โค้ดของ WASM เองก็สั้นและเรียบง่าย จึงตรวจสอบได้ง่ายและลดความเสี่ยงด้านความปลอดภัยได้ด้วย
เอนจิน JS ใช้เวลาไปมากที่ไหน
เฟสเริ่มต้น
- (การเริ่มต้นเอนจิน) ส่วนนี้เกี่ยวข้องกับ serverless เอนจินต้องเตรียมตัวเองและเพิ่มฟังก์ชัน built-in เข้าไปในสภาพแวดล้อม นี่คือหนึ่งในเหตุผลที่ cold start ของ serverless ช้า
- (การเริ่มต้นแอปพลิเคชัน) แยกวิเคราะห์ฟังก์ชันเป็นไบต์โค้ด, จัดสรรหน่วยความจำให้ตัวแปร, กำหนดค่าให้ตัวแปร
เฟสรันไทม์
- ตั้งแต่ช่วงนี้เป็นต้นไป throughput จะได้รับผลจากหลายเงื่อนไข
- ใช้ language features แบบใดบ้าง
- โค้ดมีพฤติกรรมที่คาดเดาได้จากมุมมองของเอนจิน JS หรือไม่
- ใช้โครงสร้างข้อมูลประเภทใด
- โค้ดรันนานพอที่จะได้ประโยชน์จาก optimizing compiler ของเอนจิน JS หรือไม่
การทำให้เอนจิน JS เร็วขึ้นหมายถึงการทำให้ทั้งเฟสเริ่มต้นและเฟสรันไทม์เร็วขึ้น กล่าวให้ชัดคือ ลดเวลาที่ใช้ในการเริ่มต้น และเพิ่ม throughput หรือความเร็วในการประมวลผลโค้ดระหว่างรันไทม์
ลดเวลาเริ่มต้น
-
WASM ใช้ pre-initializer ที่ชื่อ Wizer เพื่อลดเวลาเริ่มต้น (สำหรับแอปขนาดเล็ก JS on WASM เร็วกว่า JS isolate ราว 13 เท่า)
-
ในขั้นตอน build ก่อนแจกจ่ายโค้ด pre-initializer จะลองรันโค้ด JS ทั้งหมดจนถึงขั้นเริ่มต้นหนึ่งรอบ
-
ทำให้โค้ด JS ถูกเก็บเป็นไบต์โค้ดอยู่ใน linear memory ของเอนจิน JS และการจัดสรรหน่วยความจำก็เสร็จแล้ว
-
จากนั้นก็คัดลอกสิ่งนี้ไปใส่ไว้ใน data section ของ WASM ตามเดิม
-
-
เมื่อเอนจิน JS ถูก instantiate ก็จะเข้าถึงข้อมูลทั้งหมดใน data section ได้ หากต้องการหน่วยความจำเฉพาะส่วนก็เพียงคัดลอกจาก data section มาใช้ จึงไม่ต้องเสียเวลาเริ่มต้น และนี่จึงถูกเรียกว่า pre-initialization
-
ปัจจุบัน data section ถูกแนบไว้กับโมดูลเดียวกับเอนจิน JS แต่ในอนาคตมีแผนจะใช้ module linking เพื่อแยก data section ออกเป็นโมดูลต่างหาก ทำให้หลายแอปพลิเคชันแชร์เอนจิน JS ร่วมกันได้
-
และจริง ๆ แล้วเทคนิค pre-initialization นี้ไม่ได้จำเป็นต้องจำกัดอยู่แค่เอนจิน JS แต่เป็นแนวคิดที่ใช้ได้กับรันไทม์ใดก็ได้ เช่น Python, Ruby, Lua เป็นต้น
เพิ่ม throughput
-
หากโค้ด JS รันเพียงช่วงเวลาสั้น ๆ อยู่แล้วก็จะไม่ผ่าน JIT อยู่ดี ดังนั้น throughput ของ WASM ก็จะใกล้เคียงกับบนเบราว์เซอร์ แต่สำหรับโค้ดที่รันยาว ความต่างของ throughput จากการมีหรือไม่มี JIT จะมาก
-
WASM ใช้ JIT ไม่ได้ จึงเลือกใช้การคอมไพล์แบบ AOT (ahead-of-time) แทน โดยดึงเทคนิคที่นำมาจาก JIT ได้ก็เอามาใช้
-
หนึ่งในเทคนิคเพิ่มประสิทธิภาพของ JIT คือ inline caching คือการเก็บชิ้นส่วนโค้ดที่เคยรันไว้แล้วนำกลับมาใช้ซ้ำ
-
ใน WASM มีการเตรียมรูปแบบการใช้งานที่พบบ่อยใน JS ไว้เป็น stub เช่น การเข้าถึง property ของอ็อบเจ็กต์
-
ปกติแล้วการเข้าถึง property ของอ็อบเจ็กต์อย่างถูกต้องต้องมีข้อมูล shape และ offset ซึ่งไม่สามารถรู้ได้ล่วงหน้าด้วย AOT
-
แต่สามารถเตรียม stub ที่เข้าถึง property โดยรับ shape และ offset เป็นพารามิเตอร์ไว้ล่วงหน้าได้ และ stub นี้ก็สามารถนำกลับมาใช้ซ้ำได้หลายจุด
-
-
WASM เตรียม common patterns เหล่านี้ไว้เป็น stub ทั้งหมด โดยไม่เกี่ยวว่าโค้ด JS จริงมีหน้าตาอย่างไร วิธีนี้ช่วยลด machine code ที่เอนจิน JS ต้องสร้าง ลดเวลาเริ่มต้น และทำให้ cache locality ดีขึ้น
-
มีการยืนยันแล้วว่าเพียงเตรียม stub ไว้ 2kb ก็ครอบคลุมโค้ด JS จริงได้ประมาณ 95%
-
เทคนิคแบบนี้เป็นการเพิ่มประสิทธิภาพแบบ ahead-of-time กล่าวคือ ปรับให้เหมาะโดยไม่รู้เนื้อหาโค้ดล่วงหน้า (ไม่มี profiling) ดังนั้นถ้าทำ profiling เพิ่มเติม ก็ยังอาจมีพื้นที่ให้เพิ่มประสิทธิภาพได้มากขึ้นเหมือน JIT
- แต่ตัว profiling เองก็ไม่ใช่เรื่องง่าย จึงยังอยู่ระหว่างพัฒนา
2 ความคิดเห็น
เกี่ยวกับประเด็นความปลอดภัยของ JIT เคยมีการกล่าวถึงเรื่องนี้ไว้ในบทความบล็อกของทีม MS Edge ที่เคยแนะนำไว้ที่นี่ก่อนหน้านี้ โดยพื้นฐานแล้วเอนจิน JIT มีความซับซ้อน จึงไม่เพียงแต่เพิ่มพื้นผิวการโจมตีเท่านั้น แต่ยังดูเหมือนว่าวิธีการอย่าง Speculative Optimization ที่ JIT ใช้เพื่อเพิ่มประสิทธิภาพ ก็มีแนวโน้มทำให้เกิดปัญหาความปลอดภัยบางรูปแบบซ้ำๆ ด้วย ด้วยเหตุนี้จึงมีการบอกว่าสัดส่วนของช่องโหว่ความปลอดภัยที่เกี่ยวข้องกับ JIT ในเว็บเบราว์เซอร์นั้นค่อนข้างสูง
https://th.news.hada.io/topic?id=4771
https://microsoftedge.github.io/edgevr/posts/Super-Duper-Secure-Mode/
https://docs.google.com/spreadsheets/d/…
โอ้ ขอบคุณมากครับ! แต่ผมดันไม่ได้ลองหาใน GeekNews เองเลย