2 คะแนน โดย GN⁺ 2024-07-31 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 ความคิดเห็น

 
GN⁺ 2024-07-31
ความคิดเห็นจาก Hacker News
  • Oliver ผู้พัฒนาหลักของ Porffor ประกาศว่า จะทำงานกับ Porffor แบบเต็มเวลา: https://x.com/canadahonk/status/1818347311417938237

    • ว่ากันว่า defunkt ผู้ร่วมก่อตั้ง GitHub และอดีต CEO จะให้เงินทุนสำหรับโปรเจกต์ในอนาคตที่ยังไม่ได้เปิดเผย
      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 ช่วยได้น้อยกว่าที่คิด เพราะมันไม่ได้ถูกออกแบบมาเพื่อจุดประสงค์นั้นตั้งแต่แรก
      ตัวอย่างง่าย ๆ คือ TypeScript ไม่แยกจำนวนเต็มกับเลขทศนิยม แต่ถือว่าทั้งหมดเป็น number ดังนั้นการเข้าถึง array ทุกครั้งจึงต้องมีการแปลงชนิด ถ้า TypeScript ถูกออกแบบมาเพื่อช่วยการคอมไพล์แบบสแตติก ก็คงมีการแยกแบบนี้
      ปัญหาที่ใหญ่กว่าคือ structural subtyping ของ TypeScript คุณสมบัตินี้ทำให้ compiler แทบเป็นไปไม่ได้ที่จะระบุโครงสร้างทางกายภาพของอาร์กิวเมนต์ที่ไม่ใช่ primitive ซึ่งถูกส่งเข้าไปในฟังก์ชันแบบสแตติก JIT สามารถวิเคราะห์ shape แบบไดนามิกได้ ดังนั้นการเข้าถึงทุก field อาจมีประสิทธิภาพแย่กว่า JIT
    • ในฐานะ contributor ของ Porffor ไม่เห็นด้วย JavaScript เองก็ยังมีพื้นที่ให้ปรับปรุงได้ไม่น้อยใน ช่วง compile time
      มีงานจำนวนมากที่ทำเครื่องมือวิเคราะห์ type แบบสแตติกสำหรับ JS และสามารถวิเคราะห์ได้อย่างละเอียดมาก ตัวอย่างที่นึกออกคือ TAJS แม้จะค่อนข้างเก่าแล้ว
    • โปรเจกต์ที่เกี่ยวข้องกับไอเดียนี้อยู่บ้างคือ AssemblyScript: https://www.assemblyscript.org
    • ECMAScript 4 เคยเป็นความพยายามที่จะเพิ่ม type ที่ดีกว่าเดิมให้ภาษา แต่น่าเสียดายที่ล้มเหลวไปนานแล้ว
      อย่างน้อย 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 ขนาดเล็ก

    • ในฐานะ developer เห็นด้วย ดูเหมือนเป็น use case ที่น่าสนใจ ที่ Porffor อาจช่วยได้ หวังว่าสักวันจะได้คุยกัน
  • ดีใจที่ได้เห็น 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

    • ขอเสริมว่า Static Hermes รองรับความสามารถในการ คอมไพล์ JS เป็น WASM อย่างเต็มรูปแบบ มี LLVM backend เดิมอยู่แล้ว จึงได้ความสามารถนี้มาแทบฟรี ดูตัวอย่างได้ที่ https://x.com/tmikov/status/1706138872412074204
      อย่างไรก็ตาม นั่นไม่ใช่จุดโฟกัสของเรา และเรามุ่งเน้น React Native เป็นหลัก ในสภาพแวดล้อมนั้น WASM ไม่ค่อยมีความหมาย
      ฟีเจอร์ที่สำคัญที่สุดของ Static Hermes คือ type checker ที่รับประกันความถูกต้องด้าน runtime Porffor น่าสนใจมาก และผมติดตามมาสักพักแล้ว พร้อมเอาใจช่วยให้ไปได้ดี
    • ในฐานะผู้มีส่วนร่วมกับ Porffor ผมมองว่าเป็นการเปรียบเทียบที่ดี แต่ในทางเทคนิค Porffor ก็รองรับ Promise เช่นกัน เพียงแต่มันทำงานแบบ synchronous
      เป็นแนวทางคล้ายกับ Kiesel: https://kiesel.dev/
    • มีข้อแก้ไขเล็กน้อยบางอย่าง Porffor ยังไม่ได้ self-hosting อย่างสมบูรณ์ แต่คาดว่าน่าจะทำได้ อย่างไรก็ตาม built-in อย่าง Array.prototype.filter, Math.sin, atob นั้นคอมไพล์ตัวเองได้บางส่วน
      ช่วงหลัง Porffor ก็เริ่มรองรับ async/promise/await ขั้นพื้นฐานแล้ว ยังทำได้ไม่ค่อยดีนัก
    • ดูเหมือนพูดให้การพึ่งพา LLVM ฟังดูเป็นเรื่องไม่ดี
  • JavaScript มี subset ที่คอมไพล์ได้ง่าย และส่วนที่ยากคือ long tail ที่อยู่นอกเหนือจากนั้น ถึงอย่างนั้นก็น่าสนใจที่มีการวิจัยว่า ขอบเขตอยู่ตรงไหน และจะได้ประโยชน์มากแค่ไหนจาก subset นั้น

  • ชอบจริง ๆ ที่รองรับ String.blink การที่นักพัฒนามี อารมณ์ขันและความขี้เล่น เป็นสัญญาณที่ดีเสมอ

    • ถ้าตั้งใจให้ “ECMAScript host ทำตัวเหมือนเว็บเบราว์เซอร์” ก็ต้องรองรับอยู่แล้ว เพราะเป็นส่วนหนึ่งของสเปก: https://tc39.es/ecma262/multipage/additional-ecmascript-feat...
      การ implement ก็เล็กน้อยระดับ function() { return "" + this + ""; } ดังนั้นถึง ECMAScript host จะไม่ใช่เว็บเบราว์เซอร์ก็ยังน่า implement ในกรณีนั้นจะเป็น optional ผมไม่คิดว่านี่เกี่ยวกับ “อารมณ์ขันหรือความขี้เล่น”
    • String.blink อยู่ใน test262 ดังนั้นถ้าจะบรรลุเป้าหมายของโปรเจกต์ ก็แทบจะต้องรองรับ
  • ผมสงสัยว่ามี nuance อะไรที่พลาดไป ทำไม “JS engine แบบคอมไพล์ล่วงหน้า” ถึงเป็นคำอธิบายที่ดีกว่า “คอมไพเลอร์ JS-to-Wasm” ถ้าเป็นแค่กลยุทธ์การ framing ก็ไม่เป็นไร

    • มีโปรเจกต์ที่ทำ JS-to-WASM โดย bundle JS interpreter อยู่แล้ว ดังนั้นสำนวนนี้อาจตั้งใจทำให้ความแตกต่างจากแนวทางนั้นชัดขึ้น
  • ระบบเวอร์ชัน ที่อธิบายไว้ตรงนี้ดูน่าสงสัยเล็กน้อย
    ถ้าการเปลี่ยนแปลงบางอย่างทำให้เกิด regression ในบางเทสต์ของ Test262 หมายเลขเวอร์ชันก็อาจย้อนกลับไปด้วย กล่าวคือ Porffor ไม่สามารถมีทั้งหมายเลขเวอร์ชันที่เพิ่มขึ้นแบบ monotonic และความสามารถที่การเปลี่ยนแปลงที่จำเป็นอาจก่อให้เกิด regression ใน Test262 ได้พร้อมกัน
    https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...

    • น่าจะตั้งใจว่าการทำงานที่ทำให้เกิด regression ใน Test262 จะเป็นงานชั่วคราวและทำใน branch แยก จากนั้น merge เข้า main เฉพาะเมื่อรวมการแก้ไขทั้งหมดที่จำเป็นเพื่อกำจัด regression แล้วเท่านั้น หมายเลขเวอร์ชันใหม่ก็ใช้หลังจาก merge นั้นเกิดขึ้นเท่านั้นก็พอ
  • ในภาษาเวลส์แปลว่า “สีม่วง

    • รากศัพท์มาจากภาษากรีกที่แปลว่าสีม่วง และคำอังกฤษที่มีรากเดียวกันซึ่งน่าจะพบได้บ่อยที่สุดคือ porphyry ซึ่งเป็นแร่สีม่วง
  • รู้สึกสดใหม่ที่เห็น JS engine หลายตัวสำหรับการใช้งานหลากหลาย
    เคยทำงานเพิ่ม API ที่เข้ากันได้กับ Node ให้กับ quickjs ผ่าน llrt เพื่อฝังปลั๊กอินในแอปพลิเคชัน
    https://github.com/awslabs/llrt