TypeScript บน LLVM

Perry ลดระดับภาษาที่ออกแบบมาสำหรับ JIT engine ให้เป็น LLVM IR ได้ อย่างไร — monomorphization, NaN-boxing, inline lowering — และ ทำไมมันถึงทิ้ง Cranelift

ทำไมต้อง LLVM สำหรับ TypeScript?

คอมไพเลอร์แบบ ahead-of-time อยู่ในระบอบที่ต่างจาก JIT JIT คอมไพล์ ในขณะที่ผู้ใช้รอ ดังนั้น latency ของการคอมไพล์จึงเป็นข้อจำกัด คอมไพเลอร์ AOT อย่าง Perry คอมไพล์เพียงครั้งเดียว — บนเครื่องของ นักพัฒนาหรือใน CI — แล้วไบนารีก็ถูกรันนับล้านครั้งหลังจากนั้น ความ ไม่สมมาตรนั้นเองคือจุดที่ optimizer ตัวหนักคุ้มค่าที่จะใช้

LLVM มาพร้อมงานด้าน middle-end กว่าสองทศวรรษ: loop vectorization, loop-invariant code motion, global value numbering, sparse conditional constant propagation, aggressive inlining, alias analysis หน้าที่ของ Perry คือส่งมอบ IR ที่เครื่องจักรนั้นปรับแต่ง ได้จริงให้มัน — ซึ่งเป็นจุดที่ข้อมูล type ของ TypeScript เข้ามามี บทบาท

ไปป์ไลน์การ lowering

ซอร์สถูก parse ด้วย SWC จากนั้นถูก lower ลงสู่ typed high-level IR (HIR) ที่ซึ่งการตัดสินใจที่น่าสนใจเกิดขึ้นก่อนที่ LLVM จะได้เห็น โค้ดด้วยซ้ำ:

  • Monomorphization ฟังก์ชันและคลาส generic ถูกทำให้เฉพาะเจาะจงต่อ instantiation ที่ เป็น concrete แต่ละตัว เป็นกลยุทธ์เดียวกับที่ Rust และ C++ ใช้ Stack<number> และ Stack<string> กลายเป็นฟังก์ชันอิสระสองตัวที่มี type เต็มรูปแบบ — ทำให้ optimizer ทำงานกับ concrete type แทนที่จะเป็น generic dispatch blob และ generic ไม่มีต้นทุนตอนรันไทม์เลย
  • Static dispatch เมื่อ type ของ receiver รู้แล้วตอนคอมไพล์ การเรียก method จะถูก คอมไพล์เป็นการเรียกโดยตรงที่ LLVM สามารถ inline ได้ ไม่ใช่การ ค้นหาแบบ hash-table
  • Direct field access field ของ object ถูก resolve เป็น index ตอนคอมไพล์ ทำให้การอ่าน property เป็นการโหลดแบบ fixed-offset — ไม่ใช่การค้นหาแบบ dictionary

NaN-boxing และ inline lowering

เมื่อค่าเป็นแบบ dynamic Perry ใช้ NaN-boxing: ทุกค่าเป็น word ขนาด 64 บิต ตัวเลข double ถูกเก็บโดยตรง; object, string, boolean, null และ undefined ถูกเข้ารหัสลง ในรูปแบบบิตที่ไม่ได้ใช้ของ quiet NaN ตามมาตรฐาน IEEE 754 ตัวเลข ไม่มีต้นทุนเลย — ไม่มีการ boxing ไม่มีการจัดสรรหน่วยความจำสำหรับ การคำนวณ

ข้อแม้คือการดำเนินการกับค่าที่ไม่ใช่ตัวเลขต้องใช้ลำดับบิตแบบ unpack-operate-repack ถ้าลำดับเหล่านั้นอยู่ในรูปแบบการเรียกเข้าไป ยัง runtime ที่คอมไพล์แยกต่างหาก LLVM จะเห็นมันเป็นกล่องดำทึบและ ไม่สามารถปรับแต่งข้ามมันได้ ดังนั้น Perry จึง emit การดำเนินการที่ hot — การโหลด property, method dispatch, การจัดสรร object — เป็น LLVM IR แบบ inline ที่ optimizer สามารถรวมและลดรูปได้ ตัวอย่างเช่น การจัดสรร object ถูกคอมไพล์ลงไปเป็น inline thread-local bump allocation:

LLVM IR — inline bump allocation
%off_ptr = getelementptr i8, ptr %state, i64 8
%offset  = load i64, ptr %off_ptr        ; current bump offset
%new_off = add i64 %offset, 96           ; headers + 8 fields
%sz_ptr  = getelementptr i8, ptr %state, i64 16
%size    = load i64, ptr %sz_ptr         ; block capacity
%fits    = icmp ule i64 %new_off, %size
br i1 %fits, label %fast, label %slow

ทำไมไม่ใช้ Cranelift?

backend แรกของ Perry คือ Cranelift — codegen เบื้องหลัง wasmtime ที่สร้างมาเพื่อการคอมไพล์ที่เร็วและคาดเดาได้ มันเป็นจุดเริ่มต้นที่ ถูกต้อง และยังคงเป็นตัวเลือกที่ยอดเยี่ยมสำหรับ JIT และรันไทม์แบบ sandbox สองสิ่งที่บังคับให้ต้องเปลี่ยน:

  • เพดานของ optimizer Cranelift ถูกออกแบบให้เป็นคอมไพเลอร์แบบ single-tier ที่เร็วโดย เจตนา: “สร้างโค้ดที่ใช้ได้เร็ว” ซึ่งเป็นข้อแลกเปลี่ยน ที่ถูกต้องสำหรับ JIT แต่ผิดสำหรับคอมไพเลอร์ AOT ที่จุดขายคือ ประสิทธิภาพเนทีฟระดับสูงสุด
  • arm64_32 Apple Watch ใช้ ABI (คำสั่ง 64 บิต, pointer 32 บิต) ที่ Cranelift ไม่ รองรับ เพื่อให้ watchOS มีอยู่เป็นเป้าหมายได้ ต้องใช้ LLVM — และการดูแล backend สองตัวหมายถึงชุดของ bug, test และ performance baseline ที่ต้องดูแลสองชุด

การย้ายครั้งนี้ไม่ได้มาฟรี ๆ: release แรกที่ใช้ LLVM อย่างเดียว ทำให้เบนช์มาร์กบางตัวถดถอยลงถึง 70 เท่า เพราะการดำเนินการที่ hot ในตอนแรกต้องผ่านการเรียก runtime helper ที่ทึบแสง การกู้คืน — inline lowering, bump allocator ด้านบน, ขอบเขตการ inline ที่ดีขึ้น — ทำให้ backend แซงตัวเลขของ Cranelift ได้ และเมื่อทุกอย่างลงตัว แล้ว Perry ก็เอาชนะ Node.js ได้ในทุกเบนช์มาร์กของชุดทดสอบ โดยเร็ว กว่า 1.7 ถึง 24.6 เท่า พร้อมกับเสมอกันสองรายการ (เมษายน 2026) โพสต์วิเคราะห์แบบเต็มน่าอ่าน: จาก Cranelift สู่ LLVM: Perry เร็วขึ้น 24 เท่าได้อย่างไร.

เจาะลึกยิ่งขึ้น

หน้าโครงสร้างภายในคอมไพเลอร์ ครอบคลุม NaN-boxing, monomorphization และ static dispatch อย่าง ละเอียดมากขึ้น บนบล็อก ปรับแต่งทุกอย่าง: หนึ่งสัปดาห์, 68 รุ่น, และ JSON เร็วขึ้น 547 เท่า พาไปดูงานปรับแต่งทีละ release และ Generational GC, Lazy JSON และเบนช์มาร์กที่ทนต่อการตรวจสอบ อธิบายว่าระเบียบวิธีเบนช์มาร์กทำงานอย่างไร (RUNS=11, ค่ากลาง + p95) สำหรับภาพรวมที่ใหญ่กว่านี้ เริ่มที่ภาพรวม คอมไพเลอร์ TypeScript เนทีฟ.

ดูเอาต์พุตด้วยตัวเอง

perry compile main.ts — โค้ดเครื่องเนทีฟ ไม่มี engine ติดมาด้วย