3 คะแนน โดย GN⁺ 2026-01-03 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • วิเคราะห์ข้อจำกัดด้านประสิทธิภาพของ Bundler พร้อมเปรียบเทียบเหตุผลที่ตัวจัดการแพ็กเกจ Python อย่าง uv ทำงานได้รวดเร็ว
  • ความเร็วของ uv ไม่ได้มาจากการใช้ภาษา Rust แต่เกิดจากการออกแบบเชิงโครงสร้าง เช่น การดาวน์โหลดแบบขนาน, แคชส่วนกลาง, การจัดการ dependency ที่อิงเมทาดาทา
  • Bundler มีข้อจำกัดในการประมวลผลแบบขนาน เพราะ ผูกกระบวนการดาวน์โหลดและติดตั้งเข้าด้วยกัน แต่หากแยกสองส่วนนี้ออกจากกันก็มีโอกาสปรับปรุงได้มาก
  • สามารถลดความซ้ำซ้อนระหว่าง RubyGems กับ Bundler ได้ด้วย การรวมแคชส่วนกลาง, การติดตั้งด้วย hardlink, การรวมตัวแก้ dependency แบบ PubGrub
  • แม้ไม่ต้องเขียนใหม่ทั้งระบบด้วยภาษาอื่น การปรับปรุงประสิทธิภาพส่วนใหญ่ก็ทำได้ภายในโค้ด Ruby และอาจเข้าใกล้ความเร็วระดับ uv ได้

การเปรียบเทียบประสิทธิภาพระหว่าง Bundler กับ uv

  • เริ่มจากคำถามใน RailsWorld ว่า “ทำไม Bundler ถึงไม่เร็วเท่า uv?” จึงมีการตรวจสอบคอขวดด้านประสิทธิภาพของ Bundler
  • ผู้เขียนมั่นใจว่า Bundler สามารถไปถึงระดับความเร็วของ uv ได้ และย้ำว่า ความต่างด้านประสิทธิภาพเป็นปัญหาเรื่องการออกแบบ ไม่ใช่ภาษา
  • อ้างถึงบทความของ Andrew Nesbitt “How uv got so fast” เพื่อนำแนวทางการเพิ่มประสิทธิภาพหลักของ uv มาวิเคราะห์ว่าสามารถใช้กับ Bundler ได้หรือไม่

ต้องเขียนใหม่ด้วย Rust หรือไม่

  • แม้ uv จะเขียนด้วย Rust จริง แต่ สาเหตุหลักของความเร็วไม่ใช่ตัว Rust เอง
  • หากสามารถกำจัดคอขวดของ Bundler ได้จนเหลือเพียง “การเขียนใหม่ด้วย Rust” เป็นทางปรับปรุงเดียว นั่นก็ถือว่าประสบความสำเร็จ
  • การเขียนใหม่ด้วย Rust ให้อิสระในการทดลองออกแบบโดยไม่ต้องแบกรับข้อจำกัดด้านความเข้ากันได้เดิม แต่ไม่ใช่เงื่อนไขจำเป็น

คอขวดเชิงโครงสร้างของ Bundler

  • Bundler ผูกรวมการดาวน์โหลด gem และการติดตั้งไว้ในเมธอดเดียว ทำให้ไม่สามารถดาวน์โหลดแบบขนานได้
    • ในโค้ดตัวอย่าง เมธอด install เรียก fetch_gem_if_not_cached และ install ต่อเนื่องกัน
    • ด้วยเหตุนี้ gem ที่มีความสัมพันธ์แบบพึ่งพากัน (a -> b -> c) จึงติดตั้งได้แบบลำดับเท่านั้น
  • จากการทดลอง หากมี dependency จะใช้เวลามากกว่า 9 วินาที แต่ gem ที่เป็นอิสระต่อกัน (d, e, f) สามารถดาวน์โหลดแบบขนานและเสร็จภายใน 4 วินาที
  • หากแยกการดาวน์โหลดออกจากการติดตั้ง ก็ยังคงกฎ dependency ไว้ได้พร้อมเปิดทางให้ประมวลผลแบบขนาน
    • มีข้อเสนอให้แยกเป็น 4 ขั้นตอน (ดาวน์โหลด → แตกไฟล์ → คอมไพล์ → ติดตั้ง)
    • สำหรับ gem ที่เป็น Ruby ล้วน สามารถผ่อนเงื่อนไขลำดับการติดตั้ง dependency เพื่อเพิ่มความเร็วได้อีก

การเพิ่มประสิทธิภาพแคชและการติดตั้ง

  • แนวทางของ uv เรื่องแคชส่วนกลางและการติดตั้งด้วย hardlink สามารถนำมาใช้กับ Bundler ได้เช่นกัน
    • ปัจจุบัน Bundler และ RubyGems ใช้แคชแยกกันตามเวอร์ชันของ Ruby
    • จึงควรรวมเป็นแคชที่ใช้ร่วมกันบนพื้นฐานของ $XDG_CACHE_HOME
    • การติดตั้งด้วย hardlink จะทำได้หลังจากรวมแคชแล้ว
  • แม้ Bundler จะใช้ตัวแก้ dependency แบบ PubGrub อยู่แล้ว แต่ RubyGems ยังใช้ molinillo
    • การรวมตัวแก้ของทั้งสองระบบคือกุญแจสำคัญในการลดหนี้ทางเทคนิค

ความเป็นไปได้ในการนำแนวทางเพิ่มประสิทธิภาพที่เกี่ยวกับ Rust มาปรับใช้

  • Zero-copy deserialization อาจนำมาปรับใช้ได้บางส่วนในขั้นตอน parse YAML ของ RubyGems
  • GVL (Global VM Lock) ของ Ruby ไม่ได้เป็นข้อจำกัดใหญ่ต่อการประมวลผลแบบขนานในงานที่เน้น IO
    • การทำงานของ IO และ ZLIB จะปล่อย GVL จึงสามารถรันแบบขนานได้
    • อย่างไรก็ตาม การเขียนไฟล์ขนาดเล็กมี overhead จากการจัดการ GVL ซึ่งทำให้ประสิทธิภาพลดลง
    • ขณะนี้มีงานปรับปรุงเรื่องนี้อยู่ภายใน Ruby
  • การเพิ่มประสิทธิภาพการเปรียบเทียบเวอร์ชัน: uv เข้ารหัสเวอร์ชันเป็นจำนวนเต็ม u64 เพื่อให้เปรียบเทียบได้เร็วขึ้น
    • ฝั่ง Ruby ก็สามารถแปลง Gem::Version ให้เป็นจำนวนเต็มเพื่อปรับปรุงประสิทธิภาพของตัวแก้ dependency ได้
    • เคยมีความพยายามรีแฟกเตอร์ในลักษณะนี้แล้ว แต่ถูกพักไว้เพราะปัญหาความเข้ากันได้ย้อนหลัง

บทสรุปและแผนถัดไป

  • ความเร็วของ uv มาจากการออกแบบที่ตัดงานที่ไม่จำเป็นออกไปมากกว่าตัวภาษา และ Bundler ก็สามารถพัฒนาไปในทิศทางเดียวกันได้
  • RubyGems และ Bundler มีโครงสร้างการจัดการแพ็กเกจสมัยใหม่อยู่แล้ว ทำให้การไปถึงระดับความเร็วของ uv เป็นเรื่องที่เป็นไปได้จริง
  • ความท้าทายใหญ่ที่สุดคือโค้ดแบบ legacy และการรักษาความเข้ากันได้
  • แม้ไม่ต้องเขียนใหม่ด้วย Rust แต่การเพิ่มประสิทธิภาพ 99% สามารถทำได้ภายในโค้ด Ruby ส่วนอีก 1% ที่เหลือมีผลเพียงเล็กน้อย
  • ในบทความถัดไป ผู้เขียนจะพูดถึงการทำ profiling จริงของ Bundler และ RubyGems รวมถึงสาเหตุของคอขวดอย่างเจาะจง

2 ความคิดเห็น

 
iolothebard 2026-01-06

พูดนั้นง่าย โชว์โค้ดมาเลย!

 
GN⁺ 2026-01-03
ความเห็นจาก Hacker News
  • ผมไม่ได้รู้โครงสร้างของ Bundler ลึกมากนัก แต่คิดว่าการปรับปรุงที่ใหญ่ที่สุดคือการนำ การออกแบบแคชของ uv มาใช้
    หนึ่งในเหตุผลสำคัญที่ uv เร็วคือโครงสร้างแคช และสิ่งนี้ก็สามารถทำซ้ำได้ในภาษาและ ecosystem อื่น ๆ
    อย่างไรก็ตาม ส่วนที่ละเลย upper bound ของ requires-python ไม่ได้ทำไปเพื่อประสิทธิภาพ แต่เพื่อ การแก้ dependency ที่ดีกว่า
    ตัวอย่างเช่น ถ้าโปรเจกต์ต้องการ Python 3.8 ขึ้นไป แต่ dependency ตัวหนึ่งกำหนดข้อจำกัด <4 ก็จะทำให้ติดตั้งบน Python 4 ไม่ได้
    uv จะแก้ปัญหาสำหรับทุกเวอร์ชันที่รองรับอยู่แล้ว ดังนั้นการไม่สนใจ upper bound จึงแทบไม่ได้ช่วยประหยัดเวลา
    ดูการอภิปรายที่เกี่ยวข้องได้ใน ฟอรัม Python Discuss

  • หลัง PEP 658 Simple Repository API ของ Python ก็ให้เมทาดาทาโดยตรงได้แล้ว และ RubyGems.org ก็มีข้อมูลคล้ายกันอยู่แล้ว
    แต่เราจะรู้ได้ว่าเป็น native extension หรือไม่ ก็ต่อเมื่อแตกไฟล์ gem ออกมา
    จึงมีข้อเสนอว่าถ้าเพิ่มข้อมูลนี้เข้าไปในเมทาดาทาของ RubyGems.org โดยตรง ก็น่าจะทำให้ทำ dependency install tree แบบขนานได้ทั้งหมด

    • ผมก็คิดเหมือนกัน แต่มีความเป็นไปได้ที่ข้อมูลใน gemspec กับเมทาดาทาของ RubyGems.org จะไม่ตรงกัน
      ตอนที่เคยทำงานกับ RubyGems.org ผมจำได้ว่าเมทาดาทาถูกดึงออกมาตามแต่ละเวอร์ชัน
      ถ้าจะทำก็ต้องประมวลผล gemspec ของเวอร์ชันเก่าใหม่ ซึ่งอาจเป็น การเปลี่ยนเมทาดาทาที่มีความเสี่ยง
      เพราะงั้นคงใช้กับเวอร์ชันเก่าได้ยาก แต่ต่อไปข้างหน้าน่าจะปรับปรุงให้รู้ลำดับการติดตั้งได้โดยไม่ต้อง unpack
  • ผมชอบที่ Aaron โฟกัสกับ การปรับปรุงอัลกอริทึมที่ใช้งานได้จริง มากกว่าการเขียน Bundler ใหม่ด้วย Rust

    • การเพิ่มความเร็วก็เป็นเรื่องดี แต่สำหรับผม ฟีเจอร์ที่จัดการการติดตั้ง Ruby เองได้สำคัญกว่า
      สภาพแวดล้อมที่มีเครื่องมือจัดการหลายตัวกับ Ruby หลายเวอร์ชันปนกันเป็น สภาพแวดล้อมที่ชวนสับสน ซึ่งน่าหงุดหงิดมาก
    • อาจเพราะ Aaron อยู่กับ Shopify จึงไม่ได้พูดถึงโปรเจกต์ gem.coop เลย ทำให้รู้สึกซับซ้อนอยู่เหมือนกัน
      ปัญหาไม่ได้มีแค่เรื่องความเร็ว แต่เป็นเรื่อง อำนาจการควบคุมและทิศทางของ ecosystem
      ตลอด 10 ปีที่ผ่านมา Ruby เน้นเรื่องความเร็ว แต่จริง ๆ แล้วคุณภาพเอกสารกับการดูแลชุมชนสำคัญกว่า
      ถึงเวลาที่ต้องคิดอย่างจริงจังว่าทำไมภาษานี้ถึงถดถอย และต้องผลักดันไอเดียที่หลากหลายมากขึ้น
  • มีบทความที่เกี่ยวข้องล่าสุดคือ How uv got so fast (ธันวาคม 2025, 457 คอมเมนต์)

  • ถ้าจะทำให้ RubyGems เร็วขึ้น กุญแจสำคัญคือการทำ รายการไฟล์ของแต่ละ gem ให้เป็น registry/database
    แบบนี้จะได้ไม่ต้องสแกนไฟล์ระบบทุกครั้งที่ require
    ถ้าแก้ gem โดยตรงก็ต้องแฮชเมทาดาทาใหม่ แต่เอาเข้าจริงก็ไม่ได้แนะนำให้แก้ด้วยมือตั้งแต่แรกอยู่แล้ว

    • ผมเคยเขียนโค้ดที่คล้าย ๆ กันมาก่อน ถึงจะไม่มีดิสก์แคช แต่แค่ สร้างแฮชแบบ on the fly ก็เร็วขึ้นมากแล้ว
      ตอนนี้มันคงเก่าไปแล้ว แต่ก็ยังเป็นมินิโปรเจกต์ที่ผมผูกพันอยู่
      โค้ด: fastup
    • การ optimize “bundle install” เป็นแนวทางที่ผิดทิศทาง
      ปัญหาจริงคือ $LOAD_PATH เพิ่ม gem ทุกตัวเข้าไปจนเกิด combinatorial explosion ในเชิงโครงสร้าง
      การที่มีหลายโปรเจกต์ทำแคชขึ้นมาก็เป็นหลักฐานว่านี่คือปัญหาจริง
      เมื่อก่อนแอปใช้เวลาสตาร์ตเป็นนาที แต่ผมเคยลดลงได้ เป็นระดับนาที ด้วยการจัดการ load path
    • เคยพยายามจัดการเรื่องนี้ตอนรันไทม์ แต่ใน Ruby ขาด data structure ที่มีประสิทธิภาพเลยทำให้ทำได้ยาก
    • จริง ๆ แล้วนี่คือสิ่งที่ bootsnap ทำอยู่แล้ว
      ผมเคยเสนอให้รวม bootsnap เข้ากับ bundler แต่ถูกปฏิเสธ
  • คำอธิบายโครงสร้างของ RubyGems น่าสนใจดี
    gem เป็นไฟล์ tar และภายในมี YAML GemSpec ที่ประกาศ dependency
    RubyGems.org ให้ข้อมูลนี้ผ่าน API อยู่แล้ว จึงตรวจสอบ dependency ได้โดยไม่ต้อง eval
    แต่ YAML เป็น ฟอร์แมตที่ parse ได้ไม่มีประสิทธิภาพนัก ดังนั้นทางเลือกอย่าง JSON หรือ protobuf อาจดีกว่า
    ถึงอย่างนั้น ถ้า gemserver ส่งคืนข้อมูล dependency อยู่แล้ว ก็คงไม่ใช่ปัญหาใหญ่มาก

    • YAML อาจไม่ค่อยดีนัก แต่สำหรับขนาด gemspec ทั่วไป ผลกระทบด้านประสิทธิภาพ น่าจะน้อยมาก
    • ถ้าเป็น lockfile ที่มีไว้ให้ตรวจทานอย่างเดียว ไม่ได้แก้ด้วยมือ ก็อาจทำ parser แบบเรียบง่าย ที่ตัดฟีเจอร์ซับซ้อนของ YAML ออกได้
      เช่น โครงสร้างที่มีแค่เวอร์ชัน, dependency และแฮช
    • จริง ๆ แล้วเมทาดาทาแบบนี้ RubyGems หรือ PyPI ก็ parse ล่วงหน้าแล้วเก็บไว้ในฐานข้อมูล
      นี่ก็เป็นเหตุผลที่ uv เร็ว — คำนวณ dependency ได้โดยไม่ต้องดาวน์โหลดแพ็กเกจ
  • เมื่อก่อนผมเคยทำ วิดีโอต้นแบบ ของวิธีติดตั้ง gem ที่น่าจะดีกว่านี้
    how_gems_should_be.mov

  • fibers ของ Ruby (หรือไลบรารี Async) มักถูกประเมินค่าสูงเกินไป
    เช่นเดียวกับ thread ปัญหาการประสานงานระดับสูงอย่าง connection pool ก็ยังมีอยู่
    ถึงอย่างนั้น ถ้าทำงานติดตั้งที่ติดคอขวดด้าน IO แบบ asynchronous ก็ยังเห็น การเพิ่มประสิทธิภาพที่มีนัยสำคัญ ได้

    • ถ้าจะเค้นจาก Ruby ล้วน ๆ เพิ่มอีก ก็น่าจะทำแบบนี้
      1. ใช้ฟอร์แมตดัชนีที่ parse ได้เร็ว (gist ที่เกี่ยวข้อง)
      2. ให้การดาวน์โหลดช่วงแรกทำด้วย thread
      3. แยกการแตกไฟล์และ post-install ไปทำด้วย fork
        น่าจะประมาณนี้
  • ตอนนี้กำลังพิจารณาไอเดีย “global cache ที่แชร์กันระหว่าง bundler ทุกอินสแตนซ์
    ระยะยาวน่าจะได้ประโยชน์มาก แต่ยังประเมินอยู่ว่ามีความซับซ้อนแฝงหรือไม่
    issue ที่เกี่ยวข้อง: rubygems #7249

    • มันไม่ถึงกับง่ายทั้งหมด แต่ถ้าดู กรณีศึกษาที่ทำมาก่อนใน ecosystem อื่น ก็ถือว่าทำได้แน่นอน
      Ruby ไม่ใช่รายแรกที่แก้ปัญหานี้ เพราะงั้นตอนนี้ถึงเวลาที่จะได้รับประโยชน์จากมันแล้ว
  • หลักพื้นฐานของการ optimize นั้นง่ายมาก — ไม่ทำอะไรเลยคือเร็วที่สุด

    • ต้องเลิกหลงคิดว่า “โค้ดที่ฉลาดย่อมเร็ว”
      การไม่ทำสิ่งที่ไม่จำเป็นตั้งแต่แรก ต่างหากคือ optimization ที่แท้จริง