• การติดตั้งแพ็กเกจของ Bun ทำงานได้เร็วมากเมื่อเทียบกับแพ็กเกจแมเนเจอร์แบบเดิม
  • หัวใจสำคัญของความเร็วคือ แนวทางแบบระบบโปรแกรมมิง และ การลดจำนวน system call ให้เหลือน้อยที่สุด
  • เพิ่มประสิทธิภาพผ่านกลยุทธ์เชิงลึก เช่น การเขียนโค้ดเนทีฟด้วยภาษา Zig, การใช้ไบนารีแคช, และการปรับแต่งให้เหมาะกับแต่ละ OS
  • แม้แต่ในขั้นตอน แตกไฟล์ tarball และคัดลอกไฟล์ ก็มีการนำวิธีประสิทธิภาพสูงที่อาศัยคุณลักษณะของฮาร์ดแวร์มาใช้
  • ปรับปรุง โครงสร้างข้อมูล อย่าง dependency graph และ lockfile เพื่อเพิ่มประสิทธิภาพของ CPU cache และการเข้าถึงหน่วยความจำ

ทำไม Bun Install ถึงเร็ว

  • bun install ของ Bun ให้ประสิทธิภาพการติดตั้งแพ็กเกจโดยเฉลี่ย เร็วกว่า npm 7 เท่า, pnpm 4 เท่า, และ yarn 17 เท่า
  • นี่ไม่ใช่แค่ผลจากเบนช์มาร์ก แต่เกิดจากการ มองปัญหาการติดตั้งแพ็กเกจในมุมของระบบโปรแกรมมิงแทนที่จะเป็น JavaScript
  • มีการปรับแต่งประสิทธิภาพอย่างจริงจังในหลายชั้น ทั้งการลด system call, การแคช manifest แบบไบนารี, การปรับแต่งการแตก tarball, และการคัดลอกไฟล์แบบเนทีฟของแต่ละ OS

ข้อจำกัดของสถาปัตยกรรม Node.js และแพ็กเกจแมเนเจอร์

  • หลังการเปิดตัว Node.js ในปี 2009 โมเดล asynchronous IO ที่อิง event loop และ thread pool ก็ถูกนำมาใช้กับแพ็กเกจแมเนเจอร์ด้วย
  • ในเวลานั้น ข้อจำกัดของฮาร์ดแวร์ เช่น ดิสก์ช้าและเครือข่ายช้า ทำให้แนวทาง asynchronous IO และการเรียก system call ถี่ ๆ เป็นทางเลือกที่สมเหตุสมผล
  • แต่ในระบบสมัยใหม่ NVMe SSD, เครือข่ายความเร็วสูง, และ CPU ประสิทธิภาพสูง กลายเป็นเรื่องปกติ และคอขวดที่แท้จริงไม่ใช่ IO แต่คือ overhead ของ system call

ต้นทุนของ system call และ mode switch

  • เมื่อโปรแกรมร้องขอการทำงานอย่างการอ่านไฟล์ จะต้อง สลับจาก user mode ไปเป็น kernel mode และกระบวนการนี้กิน CPU cycle ราคาแพงมาก (1000~1500 cycles)
  • การติดตั้งแพ็กเกจโดยพื้นฐานต้องใช้ system call ตั้งแต่หลักหมื่นถึงหลักแสนครั้งขึ้นไป ทำให้แค่ต้นทุนในการสลับงานก็ใช้เวลา CPU ไปหลายวินาทีแล้ว
  • ตัวอย่างเช่น เมื่อติดตั้ง React และ dependency ของมัน npm ใช้ system call ราว 1 ล้านครั้ง, yarn 4 ล้านครั้ง, pnpm 5 แสนครั้ง, ส่วน bun ใช้ 1.6 แสนครั้ง

ความต่างของแนวทางระหว่างแพ็กเกจแมเนเจอร์เดิมกับ Bun

  • npm, pnpm, yarn ล้วน สร้างอยู่บน Node.js จึงต้องรัน JavaScript ผ่านหลายชั้นของ abstraction เช่น libuv, event loop, thread pool และตัวกลางสำหรับ system call
  • ระหว่างทางมีต้นทุนสะสมจากการแปลงอาร์กิวเมนต์, คิวของ worker pool, การกระจายงานใน event loop, และ system call อย่าง futex (การซิงก์ล็อก)
  • ผลลัพธ์คือ แพ็กเกจแมเนเจอร์ที่สร้างด้วย Node.js มีข้อจำกัดเชิงโครงสร้าง ทำให้ยากจะรีดประสิทธิภาพให้ใกล้เคียงเนทีฟจริง ๆ

Bun: เอนจินติดตั้งแบบเนทีฟที่เขียนด้วย Zig

  • Bun เรียกใช้ system call โดยตรงด้วยภาษา Zig จึงข้ามทั้ง JavaScript engine และ abstraction layer ทั้งหมด
  • ตัวอย่างเช่น การอ่านไฟล์ในโค้ด Zig สามารถเรียก openat() ได้โดยตรงและรับข้อมูลกลับมาได้ทันที
  • เพราะฉะนั้น กระบวนการอ่านไฟล์นับหมื่นไฟล์จึงทำงานได้เร็วมากโดยไม่ต้องผ่าน thread pool, event loop หรือขั้นตอนแปลงข้อมูลเพิ่มเติม
  • จากเบนช์มาร์ก Bun สามารถอ่าน package.json ได้ 146,057 ไฟล์ต่อวินาที ขณะที่ Node.js ช้ากว่ามาก โดยอยู่แถว 60,000 ไฟล์ต่อวินาที

การจัดการ dependency และการปรับแต่ง DNS

  • เมื่อรัน bun install Bun จะวิเคราะห์ dependency ไปพร้อมกับทริกเกอร์ DNS prefetch แบบ asynchronous
  • ตัวอย่างเช่น บน macOS จะใช้ async DNS API ที่ไม่เป็นทางการของ Apple (getaddrinfo_async_start()) เพื่อรองรับ การทำงานเครือข่ายพร้อมกันโดยไม่บล็อกเธรด
  • ส่วนแพ็กเกจแมเนเจอร์แบบเดิมอาศัย thread pool ของ libuv ซึ่งภายในยังรันโค้ดแบบบล็อกกิงจริงอยู่ จึงสิ้นเปลืองทรัพยากรโดยไม่จำเป็น

การแคช manifest ของแพ็กเกจแบบไบนารี

  • npm และตัวอื่น ๆ แคช manifest เป็น JSON แต่ Bun จะ พาร์สหนึ่งครั้งแล้วแปลงผลลัพธ์นั้นเป็นไบนารี (.npm) เพื่อเก็บไว้
  • วิธีนี้ช่วย ลดความซ้ำซ้อนของสตริงและ overhead จากการพาร์ส และในหน่วยความจำจริงก็สามารถเข้าถึงค่าต่าง ๆ ได้ทันทีด้วยการคำนวณ offset โดยไม่ต้องสร้างอ็อบเจ็กต์ใหม่, พาร์สใหม่ หรือทำ garbage collection
  • ใช้ header อย่าง ETag และ If-None-Match เพื่อตรวจเฉพาะการเปลี่ยนแปลง ทำให้ตรวจสอบความเป็นปัจจุบันได้โดยไม่ต้องพาร์สข้อมูลที่ไม่จำเป็น
  • จากเบนช์มาร์ก การติดตั้งจากแคชของ Bun ยังเร็วกว่า npm แบบ fresh install เสียอีก

ประสิทธิภาพในการจัดการ Tarball (ไฟล์บีบอัด)

  • แพ็กเกจแมเนเจอร์ทั่วไปจะรับ tarball แบบสตรีม ทำให้เมื่อบัฟเฟอร์หน่วยความจำไม่พอ จะเกิดการจัดสรรใหม่ คัดลอก และปรับขนาดซ้ำ ๆ อย่างต่อเนื่อง
  • Bun จะ รับ tarball มาทั้งก้อนก่อนแล้วค่อยแตกไฟล์ และใช้ 4 ไบต์สุดท้ายของ gzip เพื่อทราบขนาดหลังคลายบีบอัดล่วงหน้า ทำให้ จองหน่วยความจำเพียงครั้งเดียว
  • ยังใช้ libdeflate และเทคนิคอื่นเพื่อคลายบีบอัดได้เร็ว พร้อมตัดการคัดลอกซ้ำและการปรับขนาดที่ไม่จำเป็นออกทั้งหมด

การปรับแต่ง dependency graph และโครงสร้างข้อมูล

  • แพ็กเกจแมเนเจอร์เดิมสร้างต้นไม้ dependency ด้วย JavaScript object และ pointer ทำให้หน่วยความจำกระจายตัวแบบสุ่มและเกิด CPU cache miss บ่อย (ปัญหา pointer chasing)
  • Bun ใช้แพตเทิร์น Structure of Arrays (SoA) โดยเก็บแพ็กเกจทั้งหมด สตริงทั้งหมด และ dependency ทั้งหมดไว้ในบล็อกหน่วยความจำต่อเนื่องขนาดใหญ่
    • การเข้าถึงแบบ offset/length ทำให้ CPU สามารถอ่านหลายแพ็กเกจพร้อมกันเป็นหน่วย cache line ได้ง่ายขึ้น (โครงสร้างที่เป็นมิตรกับแคช)
    • lockfile ก็ถูกจัดเก็บให้สอดคล้องกับแพตเทิร์น SoA แทน JSON/YAML เพื่อให้ตัดสตริงซ้ำและเข้าถึงหน่วยความจำแบบลำดับได้ง่าย
  • เคยมีการนำ lockfile แบบไบนารี (bun.lockb) มาใช้เชิงทดลองด้วย แต่ภายหลังเปลี่ยนกลับเป็นฟอร์แมตข้อความธรรมดาที่อ่านง่ายกว่า เพราะการทำงานร่วมกันผ่าน Git แย่ลง

การปรับแต่งการคัดลอกไฟล์ตามแต่ละ OS

macOS

  • ใช้ clonefile: โคลนทั้งไดเรกทอรีด้วยวิธี Copy-On-Write ผ่าน system call เพียงครั้งเดียว
  • ช่วยลดการใช้พื้นที่ดิสก์ซ้ำซ้อนและเพิ่มความเร็วในการติดตั้งสูงสุด
  • หาก clonefile ล้มเหลว จะ fallback เป็น per-directory cloning แล้วค่อย fallback ต่อไปที่ copyfile

Linux

  • ลองใช้ hard link ก่อน: สร้างเพียง reference ใหม่ให้ไฟล์เดิมโดยไม่ต้องสร้างไฟล์ใหม่จริง ๆ (ไม่มีการย้ายข้อมูลบนดิสก์)
  • หากใช้ hard link ไม่ได้ บน Btrfs/XFS จะใช้ ioctl_ficlone เพื่อทำ Copy-On-Write
  • จากนั้นจึง fallback ไปที่ copy_file_range, sendfile และสุดท้ายเป็นการคัดลอกแบบ copyfile ทั่วไป

สรุป

  • Bun ก้าวข้ามข้อจำกัดด้านประสิทธิภาพแบบดั้งเดิมของแพ็กเกจแมเนเจอร์ ผ่านการลด system call, การใช้โครงสร้างไบนารี, การปรับแต่งตาม OS และการปรับปรุงโครงสร้างข้อมูล
  • ส่งผลให้ไม่เพียงติดตั้งได้เร็วมาก แต่ยังมีประสิทธิภาพที่ดีขึ้นทั้งด้านหน่วยความจำและ CPU
  • สามารถนำไปใช้กับโปรเจกต์ได้โดยยังรักษาความเข้ากันได้ไว้ โดยไม่จำเป็นต้องเปลี่ยนรันไทม์แยกต่างหากเมื่อเทียบกับแมเนเจอร์ที่อิง Node.js เดิม
  • มอบประสบการณ์ที่ย่นเวลาติดตั้งจากระดับหลายนาทีในโค้ดเบสขนาดใหญ่ ให้เหลือเพียงหลักมิลลิวินาทีถึงไม่กี่วินาที
  • นี่เป็นตัวอย่างชั้นดีของการปรับแต่งให้เหมาะกับระดับระบบ ฮาร์ดแวร์ และ OS ที่มีคุณค่าสำหรับการศึกษาและอ้างอิง

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น