- Porffor เป็นโปรเจกต์วิจัยที่คอมไพล์ JavaScript ล่วงหน้า แทนที่จะทำตอนรัน ให้กลายเป็น WebAssembly และไบนารีเนทีฟ
- ด้วยแนวทางที่ไม่รวมอินเทอร์พรีเตอร์เข้าไปด้วย จึงตั้งเป้าให้เอาต์พุต เล็กและเร็วกว่า 10–30 เท่า เมื่อเทียบกับโปรเจกต์ JS→Wasm ที่มีอยู่
- แม้ในบิลด์เนทีฟก็ไม่แพ็กเกจรันไทม์เข้าไป ทำให้ขนาดไบนารีอาจลดลงได้สูงสุด 1000 เท่า โดยตัวอย่างลดจากราว 90MB เหลือน้อยกว่า 100KB
- เขียนด้วย JS และไม่มี
evalพร้อมชูโครงสร้างที่ รองรับ TypeScript แบบเนทีฟ โดยไม่ต้องมีขั้นตอนบิลด์แยกต่างหาก - AOT เอื้อต่อการเพิ่มประสิทธิภาพด้วยการวิเคราะห์แบบสแตติกและการคอมไพล์ก่อนรัน แต่การประเมิน JS แบบไดนามิกอย่าง
evalทำได้ยาก และยังอยู่ในช่วงเริ่มต้นที่ JS จำนวนมากยังทำงานไม่ได้
รูปแบบการรันที่ Porffor สร้างขึ้น
- Porffor เป็นโปรเจกต์วิจัยที่คอมไพล์ JavaScript แบบ Ahead-of-Time ไปเป็น WebAssembly และ ไบนารีเนทีฟ
- ไบนารี TypeScript ที่คอมไพล์ด้วย Porffor เป็นตัวให้บริการหน้านี้
- ถูกเขียนขึ้นโดยคำนึงถึง AOT ตั้งแต่ต้น จึงมีโครงสร้างที่พยายามทำการเพิ่มประสิทธิภาพซึ่งทำได้ยากในรูปแบบการรัน JS แบบเดิม
ความแตกต่างระหว่างเอาต์พุต WebAssembly และเนทีฟ
-
JS → Wasm
- เอาต์พุต WebAssembly ของ Porffor เล็กและเร็วกว่า 10–30 เท่า เมื่อเทียบกับโปรเจกต์ JS→Wasm ที่มีอยู่
- ความแตกต่างหลักอยู่ที่การคอมไพล์ JS โดยตรง และไม่บันเดิลอินเทอร์พรีเตอร์เข้าไป
- การรัน JS บน Wasm ทำให้รันในแซนด์บ็อกซ์ได้ แต่อาจมีการสูญเสียประสิทธิภาพมาก Porffor จึงมุ่งลดต้นทุนนี้
- ตัวอย่างการใช้งานที่เป็นไปได้:
- โฮสติง JS ฝั่งเซิร์ฟเวอร์: ในเอดจ์รันไทม์ การใช้ Wasm sandboxing สามารถให้การรันที่ปลอดภัยโดยไม่ต้องแยกสภาพแวดล้อมมากเกินไป
- โอเวอร์เฮดที่ต่ำของ AOT ทำให้มีโอกาสรันลูกค้าได้มากขึ้นบนฮาร์ดแวร์เดียวกันเมื่อเทียบกับ JIT โดยสูญเสียประสิทธิภาพน้อยที่สุด
- ความต้านทานต่อการวิศวกรรมย้อนกลับ: สำหรับ JS ที่อ่อนไหว โค้ดที่คอมไพล์แล้วอาจทำวิศวกรรมย้อนกลับได้ยากกว่าการทำ obfuscation
-
JS → Native
- เพราะคอมไพล์ JS จริง ๆ โดยไม่แพ็กเกจรันไทม์เข้าไป ขนาดไบนารีจึงอาจเล็กลงได้สูงสุด 1000 เท่า
- ขนาดตัวอย่างคือประมาณ 90MB → น้อยกว่า 100KB
- ภายในจะคอมไพล์ JS เป็น C แล้วคอมไพล์เป็นเนทีฟ ดังนั้นที่ใดใช้ C ได้ ก็ใช้ JS ได้
- ตัวอย่างการใช้งานที่เป็นไปได้:
- การรัน JS ที่รวดเร็ว บนอุปกรณ์ฝังตัว เกมคอนโซล ฯลฯ
- แอป JS CLI ขนาดเล็ก ที่คอมไพล์เป็นไฟล์รันแบบคลิกเดียวขนาดต่ำกว่า 1MB
ข้อดีและข้อจำกัดของ AOT
- อินเทอร์พรีเตอร์แบบดั้งเดิมหรือหลายขั้นตอนของ JIT ต้องหาสมดุลระหว่างเวลาเริ่มต้นกับประสิทธิภาพของ JS
- AOT คอมไพล์ก่อนแล้วค่อยรัน ดังนั้นความเร็วในการคอมไพล์จึงสำคัญต่อประสบการณ์นักพัฒนา แต่ไม่ส่งผลต่อประสบการณ์ผู้ใช้
- วิธีนี้เปิดช่องให้ทำ การเพิ่มประสิทธิภาพบนฐานการวิเคราะห์แบบสแตติก คล้ายกับ C++ และ Rust
- ข้อเสียหลักคือไม่มีการประเมิน JS แบบไดนามิกอย่าง
evalและต้องสร้างเอนจิน JS ขึ้นมาใหม่ - เนื่องจากยังอยู่ในช่วงเริ่มต้น JS จำนวนมากจึงยังทำงานไม่ได้ แต่กำลังมีการปรับปรุงอย่างต่อเนื่อง
- เพื่อติดตามความคืบหน้าด้านความเข้ากันได้กับ ECMAScript จะรันชุดทดสอบทางการ Test262 ในทุกคอมมิต
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
Oliver ผู้พัฒนาหลักของ Porffor ประกาศว่า จะทำงานกับ Porffor แบบเต็มเวลา: https://x.com/canadahonk/status/1818347311417938237
https://news.ycombinator.com/user?id=defunkt
เคยคิดเรื่องคล้าย ๆ กัน แต่เห็นว่ายากที่จะทำให้ JavaScript มีประสิทธิภาพดีกว่านี้มาก ๆ ได้ บางทีทางที่ดีที่สุดอาจเป็นแค่การทรานสไพล์ JS ไปเป็น การเรียก V8 C++ เท่านั้น
การปรับแต่งประสิทธิภาพที่เจ๋งจริง ๆ จะเกิดขึ้นเมื่อคอมไพล์ TypeScript หรือสิ่งที่ใกล้เคียงกัน การใช้ type จะให้ประโยชน์มาก และส่วนที่ไม่มี type โดยพื้นฐานก็จะตกกลับไปเป็นการเรียก JS ที่ช้า interface สามารถลดรูปเป็น virtual function table หรือ direct call ได้ และอาจทำงานบน struct แทน map ได้ด้วย อาจมี type
IntกับFloatและลดระดับเป็นNumberเมื่อจำเป็น พร้อมทั้งเก็บไว้ใน register ได้ปัญหาหลักคือ ทั้ง TS และ V8 เป็นเป้าหมายที่ไม่เป็นมาตรฐานและเปลี่ยนแปลงเร็ว โปรเจกต์แบบนี้ต้องมีทีมใหญ่ถึงจะทำได้ และการรักษาความเข้ากันได้ก็กลายเป็นงานอีกงานหนึ่งไปเลย
ตัวอย่างง่าย ๆ คือ TypeScript ไม่แยกจำนวนเต็มกับเลขทศนิยม แต่ถือว่าทั้งหมดเป็น number ดังนั้นการเข้าถึง array ทุกครั้งจึงต้องมีการแปลงชนิด ถ้า TypeScript ถูกออกแบบมาเพื่อช่วยการคอมไพล์แบบสแตติก ก็คงมีการแยกแบบนี้
ปัญหาที่ใหญ่กว่าคือ structural subtyping ของ TypeScript คุณสมบัตินี้ทำให้ compiler แทบเป็นไปไม่ได้ที่จะระบุโครงสร้างทางกายภาพของอาร์กิวเมนต์ที่ไม่ใช่ primitive ซึ่งถูกส่งเข้าไปในฟังก์ชันแบบสแตติก JIT สามารถวิเคราะห์ shape แบบไดนามิกได้ ดังนั้นการเข้าถึงทุก field อาจมีประสิทธิภาพแย่กว่า JIT
มีงานจำนวนมากที่ทำเครื่องมือวิเคราะห์ type แบบสแตติกสำหรับ JS และสามารถวิเคราะห์ได้อย่างละเอียดมาก ตัวอย่างที่นึกออกคือ TAJS แม้จะค่อนข้างเก่าแล้ว
อย่างน้อย TypeScript ถ้าสามารถระบุ type อย่าง
integerได้ก็น่าจะดี แม้ในการคอมไพล์ TS→JS ทั่วไปจะปฏิบัติกับconst val: intเหมือนกับconst val: numberทุกประการ แต่ runtime สมัยใหม่ที่เข้าใจ TS ก็น่าจะใช้ข้อมูลเพิ่มเติมนั้นได้สงสัยว่า syntax อย่าง
const counter: Numberจะเป็นที่ยอมรับได้หรือไม่ไม่รู้ว่าเว็บไซต์เปลี่ยนไปแล้ว หรือว่าผมพลาดอะไรไป
ที่ windmill.dev เวลาผู้ใช้ deploy โค้ด จะใช้ Bun build เพื่อรวมสคริปต์และ dependency ทั้งหมดเป็นไฟล์ JS เดียว แล้วโหลดไฟล์นั้นเพื่อปรับปรุง cold start และการใช้หน่วยความจำ เนื่องจากขนาด bundle จึงเก็บผลลัพธ์ไว้ใน S3
ถ้าสามารถ bundle ทุกอย่างเป็น native ได้ เกมจะเปลี่ยนไปโดยสิ้นเชิง ต่อให้ cold start ของ Bun ดีแค่ไหน ก็ยากจะชนะการรัน native โดยตรงจาก binary ขนาดเล็ก
ดีใจที่ได้เห็น JS runtime มากขึ้นเข้าถึง Wasm โครงการนี้ทำให้นึกถึง Static Hermes ซึ่งเป็น JS engine ของ Facebook สำหรับเพิ่มความเร็ว iOS·Android ในโปรเจกต์ React Native
ทั้งคู่ตั้งเป้าให้สอดคล้องกับ JS test262 โดย Porffor รองรับทั้งเอาต์พุตแบบเนทีฟและ Wasm ขณะที่ Static Hermes ตอนนี้เน้นเอาต์พุตแบบเนทีฟเป็นหลัก Porffor เขียนด้วย JS ล้วนและมุ่งไปในทิศทางที่สามารถคอมไพล์ตัวเองได้ ส่วน Static Hermes พึ่งพา LLVM Porffor ยังรองรับ async/promise/await ได้จำกัด ขณะที่ Static Hermes รองรับพร้อมข้อจำกัดบางอย่าง Static Hermes เขียนด้วย C++ ส่วน Porffor เขียนด้วย JS เป็นหลัก ทั้งคู่รองรับ TypeScript แต่ Static Hermes transpile TS AST เป็น Flow ส่วน Porffor รองรับแบบเนทีฟ Static Hermes มีอินเทอร์พรีเตอร์สำรองสำหรับสถานการณ์ JS ที่คอมไพล์ยากอย่าง
evalส่วน Porffor รองรับเฉพาะการคอมไพล์ล่วงหน้าเท่านั้นโดยรวมแล้วคาดหวังว่าโปรเจกต์นี้จะได้รับแรงส่งและทำให้ JavaScript engine บน edge เร็วขึ้นได้หรือไม่ ฝากไว้ในฐานะ Syrus จาก Wasmer
https://github.com/facebook/hermes/discussions/1137
https://github.com/tc39/test262
https://wasmer.io
อย่างไรก็ตาม นั่นไม่ใช่จุดโฟกัสของเรา และเรามุ่งเน้น React Native เป็นหลัก ในสภาพแวดล้อมนั้น WASM ไม่ค่อยมีความหมาย
ฟีเจอร์ที่สำคัญที่สุดของ Static Hermes คือ type checker ที่รับประกันความถูกต้องด้าน runtime Porffor น่าสนใจมาก และผมติดตามมาสักพักแล้ว พร้อมเอาใจช่วยให้ไปได้ดี
เป็นแนวทางคล้ายกับ Kiesel: https://kiesel.dev/
Array.prototype.filter,Math.sin,atobนั้นคอมไพล์ตัวเองได้บางส่วนช่วงหลัง Porffor ก็เริ่มรองรับ async/promise/await ขั้นพื้นฐานแล้ว ยังทำได้ไม่ค่อยดีนัก
JavaScript มี subset ที่คอมไพล์ได้ง่าย และส่วนที่ยากคือ long tail ที่อยู่นอกเหนือจากนั้น ถึงอย่างนั้นก็น่าสนใจที่มีการวิจัยว่า ขอบเขตอยู่ตรงไหน และจะได้ประโยชน์มากแค่ไหนจาก subset นั้น
ชอบจริง ๆ ที่รองรับ
String.blinkการที่นักพัฒนามี อารมณ์ขันและความขี้เล่น เป็นสัญญาณที่ดีเสมอการ implement ก็เล็กน้อยระดับ
function() { return "" + this + ""; }ดังนั้นถึง ECMAScript host จะไม่ใช่เว็บเบราว์เซอร์ก็ยังน่า implement ในกรณีนั้นจะเป็น optional ผมไม่คิดว่านี่เกี่ยวกับ “อารมณ์ขันหรือความขี้เล่น”String.blinkอยู่ใน test262 ดังนั้นถ้าจะบรรลุเป้าหมายของโปรเจกต์ ก็แทบจะต้องรองรับผมสงสัยว่ามี nuance อะไรที่พลาดไป ทำไม “JS engine แบบคอมไพล์ล่วงหน้า” ถึงเป็นคำอธิบายที่ดีกว่า “คอมไพเลอร์ JS-to-Wasm” ถ้าเป็นแค่กลยุทธ์การ framing ก็ไม่เป็นไร
ระบบเวอร์ชัน ที่อธิบายไว้ตรงนี้ดูน่าสงสัยเล็กน้อย
ถ้าการเปลี่ยนแปลงบางอย่างทำให้เกิด regression ในบางเทสต์ของ Test262 หมายเลขเวอร์ชันก็อาจย้อนกลับไปด้วย กล่าวคือ Porffor ไม่สามารถมีทั้งหมายเลขเวอร์ชันที่เพิ่มขึ้นแบบ monotonic และความสามารถที่การเปลี่ยนแปลงที่จำเป็นอาจก่อให้เกิด regression ใน Test262 ได้พร้อมกัน
https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...
ในภาษาเวลส์แปลว่า “สีม่วง”
รู้สึกสดใหม่ที่เห็น JS engine หลายตัวสำหรับการใช้งานหลากหลาย
เคยทำงานเพิ่ม API ที่เข้ากันได้กับ Node ให้กับ quickjs ผ่าน llrt เพื่อฝังปลั๊กอินในแอปพลิเคชัน
https://github.com/awslabs/llrt