3 điểm bởi GN⁺ 2026-01-03 | 2 bình luận | Chia sẻ qua WhatsApp
  • Phân tích giới hạn hiệu năng của Bundler, đồng thời so sánh lý do trình quản lý gói uv của Python lại nhanh
  • Tốc độ của uv không đến từ Rust, mà nhờ tải xuống song song, bộ nhớ đệm toàn cục, xử lý phụ thuộc dựa trên metadata và các lựa chọn thiết kế mang tính cấu trúc
  • Bundler gộp quá trình tải xuống và cài đặt, nên bị hạn chế trong xử lý song song; nếu tách hai phần này ra thì có thể cải thiện đáng kể
  • Có thể giảm trùng lặp giữa RubyGems và Bundler bằng tích hợp bộ nhớ đệm toàn cục, cài đặt bằng hardlink, tích hợp bộ giải PubGrub
  • Ngay cả khi không viết lại bằng ngôn ngữ khác, phần lớn cải thiện hiệu năng vẫn có thể đạt được trong mã Ruby, đủ để tiệm cận tốc độ của uv

So sánh hiệu năng giữa Bundler và uv

  • Từ câu hỏi được đặt ra tại RailsWorld: “Vì sao Bundler không nhanh bằng uv?”, tác giả đã điều tra các điểm nghẽn hiệu năng của Bundler
  • Tác giả khẳng định Bundler có thể đạt tốc độ ngang ngửa uv, và nhấn mạnh rằng khác biệt về hiệu năng là vấn đề thiết kế chứ không phải ngôn ngữ
  • Trích dẫn bài viết “How uv got so fast” của Andrew Nesbitt, rồi phân tích liệu các kỹ thuật tối ưu cốt lõi của uv có thể áp dụng cho Bundler hay không

Có cần viết lại bằng Rust không?

  • Đúng là uv được viết bằng Rust, nhưng nguyên nhân cốt lõi của tốc độ không nằm ở bản thân Rust
  • Nếu loại bỏ được các điểm nghẽn của Bundler để rồi “viết lại bằng Rust” trở thành phương án cải thiện duy nhất còn lại, thì đó đã là một thành công
  • Việc viết lại bằng Rust mang lại sự tự do để thử nghiệm các thiết kế mới mà không bị ràng buộc bởi tương thích cũ, nhưng không phải điều kiện bắt buộc

Các điểm nghẽn cấu trúc của Bundler

  • Bundler gộp việc tải gem và cài đặt vào cùng một phương thức, khiến việc tải song song trở nên bất khả thi
    • Trong ví dụ mã, phương thức install thực thi liên tiếp fetch_gem_if_not_cachedinstall
    • Vì vậy, các gem có quan hệ phụ thuộc (a -> b -> c) chỉ có thể được cài đặt tuần tự
  • Kết quả thử nghiệm cho thấy, khi có phụ thuộc thì mất hơn 9 giây, còn các gem độc lập (d, e, f) thì hoàn tất trong vòng chưa đến 4 giây nhờ tải song song
  • Nếu tách riêng tải xuống và cài đặt, vẫn có thể giữ nguyên quy tắc phụ thuộc mà đồng thời cho phép xử lý song song
    • Đề xuất tách thành bốn bước (tải xuống → giải nén → biên dịch → cài đặt)
    • Với gem Ruby thuần, có thể nới lỏng thứ tự cài đặt phụ thuộc để tăng tốc thêm

Tối ưu bộ nhớ đệm và cài đặt

  • Cách làm bộ nhớ đệm toàn cục và cài đặt bằng hardlink của uv cũng có thể áp dụng cho Bundler
    • Hiện tại Bundler và RubyGems dùng bộ nhớ đệm riêng theo từng phiên bản Ruby
    • Cần hợp nhất thành bộ nhớ đệm dùng chung dựa trên $XDG_CACHE_HOME
    • Sau khi hợp nhất bộ nhớ đệm, có thể áp dụng cài đặt bằng hardlink
  • Bundler đã dùng bộ giải phụ thuộc PubGrub, nhưng RubyGems vẫn còn dùng molinillo
    • Hợp nhất bộ giải của hai hệ thống là chìa khóa để xử lý nợ kỹ thuật

Khả năng áp dụng các yếu tố tối ưu liên quan đến Rust

  • Giải tuần tự hóa zero-copy có thể áp dụng phần nào ở giai đoạn phân tích YAML của RubyGems
  • GVL (Global VM Lock) của Ruby không phải là rào cản lớn với các tác vụ thiên về IO
    • Xử lý IO và ZLIB đều nhả GVL, nên có thể chạy song song
    • Tuy nhiên, khi ghi nhiều tệp nhỏ, overhead quản lý GVL lại trở thành yếu tố làm giảm hiệu năng
    • Hiện đang có các nỗ lực cải thiện điểm này bên trong Ruby
  • Tối ưu so sánh phiên bản: uv mã hóa phiên bản thành số nguyên u64 để tăng tốc so sánh
    • Trong Ruby cũng có thể chuyển Gem::Version sang dạng số nguyên để cải thiện hiệu năng bộ giải
    • Đã từng có nỗ lực refactor liên quan, nhưng bị hoãn do vấn đề tương thích ngược

Kết luận và kế hoạch tiếp theo

  • Tốc độ của uv đến từ thiết kế loại bỏ các công việc không cần thiết, hơn là từ ngôn ngữ lập trình; Bundler cũng có thể cải thiện theo cùng hướng
  • RubyGems và Bundler vốn đã có cấu trúc quản lý gói hiện đại, nên việc đạt tốc độ cỡ uv là mục tiêu thực tế
  • Thách thức lớn nhất là duy trì tương thích với mã legacy
  • Ngay cả khi không viết lại bằng Rust, 99% mức cải thiện hiệu năng vẫn có thể đạt được trong mã Ruby, còn 1% còn lại là không đáng kể
  • Trong bài viết tiếp theo, tác giả dự định sẽ bàn về profiling thực tế và các nguyên nhân cụ thể gây nghẽn của Bundler và RubyGems

2 bình luận

 
iolothebard 2026-01-06

Nói thì rẻ. Đưa tôi xem code!

 
GN⁺ 2026-01-03
Ý kiến trên Hacker News
  • Tôi không rành cấu trúc nội bộ của Bundler, nhưng tôi nghĩ cải tiến lớn nhất sẽ là áp dụng thiết kế cache của uv
    Một phần cốt lõi khiến uv nhanh nằm ở cấu trúc cache, và điều này có thể được sao chép sang các ngôn ngữ hay hệ sinh thái khác
    Tuy nhiên, việc bỏ qua giới hạn trên của requires-python không phải vì hiệu năng mà là để giải quyết phụ thuộc tốt hơn
    Ví dụ, một dự án yêu cầu Python 3.8 trở lên nhưng có phụ thuộc nào đó đặt ràng buộc <4, thì trên Python 4 sẽ không cài được
    uv giải quyết cho mọi phiên bản được hỗ trợ nên việc bỏ qua giới hạn trên gần như không tiết kiệm thêm thời gian
    Có thể xem thảo luận liên quan trên diễn đàn Python Discuss

  • Sau PEP 658, Simple Repository API của Python cung cấp trực tiếp metadata, và RubyGems.org cũng đã cung cấp thông tin tương tự
    Nhưng phải giải nén gem ra mới biết được nó có native extension hay không
    Vì vậy có đề xuất rằng nếu thêm trực tiếp thông tin này vào metadata của RubyGems.org thì có thể song song hóa hoàn toàn cây cài đặt phụ thuộc

    • Tôi cũng đã nghĩ như vậy, nhưng có khả năng thông tin trong gemspec và metadata của RubyGems.org khác nhau
      Khi còn làm ở RubyGems.org, tôi nhớ metadata được trích xuất theo từng phiên bản
      Cần xử lý lại gemspec của các phiên bản cũ, mà điều này có thể trở thành một thay đổi metadata rủi ro
      Vì thế có lẽ khó áp dụng cho các phiên bản cũ, nhưng về sau thì có thể cải thiện để biết thứ tự cài đặt mà không cần unpack
  • Tôi thích việc Aaron tập trung vào cải tiến thuật toán thực chất thay vì viết lại Bundler bằng Rust

    • Tăng tốc cũng tốt, nhưng tôi cần hơn là tính năng quản lý luôn cả việc cài Ruby
      Môi trường lộn xộn với đủ loại công cụ quản lý phiên bản và các bản Ruby trộn lẫn với nhau thực sự rất khó chịu
    • Có lẽ vì Aaron thuộc Shopify nên không nhắc tới dự án gem.coop, điều này khiến tôi có cảm xúc khá phức tạp
      Tôi nghĩ vấn đề không chỉ là tốc độ mà còn là quyền kiểm soát và định hướng của hệ sinh thái
      Ruby đã tập trung vào tốc độ suốt 10 năm qua, nhưng chất lượng tài liệu và việc quản lý cộng đồng mới là thứ quan trọng hơn
      Đã đến lúc phải nghiêm túc suy nghĩ vì sao ngôn ngữ này suy giảm và thúc đẩy nhiều ý tưởng đa dạng hơn
  • Có bài liên quan gần đây là How uv got so fast (tháng 12 năm 2025, 457 bình luận)

  • Muốn làm RubyGems nhanh hơn thì mấu chốt là đưa danh sách file của từng gem vào registry/cơ sở dữ liệu
    Làm vậy sẽ không cần quét file system mỗi lần require
    Nếu chỉnh sửa gem trực tiếp thì phải hash lại metadata, nhưng dù sao việc sửa thủ công cũng vốn không được khuyến khích

    • Trước đây tôi từng viết đoạn mã khá giống như vậy, không có cache trên đĩa nhưng chỉ cần tạo hash tại chỗ cũng đã tăng tốc đáng kể
      Giờ thì nó có thể đã lỗi thời, nhưng vẫn là một mini project mà tôi rất quý
      Mã nguồn: fastup
    • Tối ưu hóa “bundle install” là đi sai hướng
      Vấn đề thực sự là $LOAD_PATH thêm mọi gem vào và gây ra bùng nổ tổ hợp trong cấu trúc
      Việc có nhiều dự án cache tồn tại chính là bằng chứng đây là vấn đề có thật
      Trước đây ứng dụng từng mất vài phút để khởi động, nhưng tôi đã thao tác load path để rút xuống còn tính bằng phút
    • Tôi đã cố xử lý việc này ở runtime, nhưng Ruby thiếu các cấu trúc dữ liệu hiệu quả nên khó triển khai
    • Thực ra đây chính là việc bootsnap đang làm
      Trước đây tôi từng đề xuất tích hợp bootsnap vào bundler nhưng đã bị từ chối
  • Phần giải thích về cấu trúc của RubyGems khá thú vị
    gem là một file tar, bên trong có YAML GemSpec khai báo các phụ thuộc
    RubyGems.org cung cấp thông tin này qua API nên có thể kiểm tra phụ thuộc mà không cần eval
    Tuy nhiên YAML là một định dạng có hiệu quả phân tích không cao, nên các lựa chọn như JSON hoặc protobuf có thể tốt hơn
    Dù vậy, nếu gemserver đã trả về thông tin phụ thuộc thì có lẽ đây không phải vấn đề lớn

    • YAML không hay lắm, nhưng với kích thước gemspec thông thường thì tác động hiệu năng có lẽ rất nhỏ
    • Nếu lockfile chỉ để xem xét chứ không phải để con người chỉnh sửa, thì có thể làm một parser đơn giản bỏ hết các tính năng phức tạp của YAML
      Ví dụ: một cấu trúc chỉ gồm phiên bản, phụ thuộc và hash
    • Thực ra loại metadata này đã được RubyGems hay PyPI phân tích trước và lưu vào cơ sở dữ liệu
      Đây cũng là lý do uv nhanh — có thể tính toán phụ thuộc mà không cần tải package xuống
  • Trước đây tôi từng làm một video prototype về cách cài gem nên hoạt động như thế nào
    how_gems_should_be.mov

  • fibers của Ruby (hoặc thư viện Async) thường bị đánh giá quá cao
    Cũng như thread, các vấn đề điều phối ở cấp cao hơn như connection pool vẫn còn nguyên
    Nhưng nếu xử lý bất đồng bộ các tác vụ cài đặt thiên về IO thì vẫn có thể thấy cải thiện hiệu năng đáng kể

    • Nếu muốn vắt thêm từ Ruby thuần, có lẽ tôi sẽ tiếp cận theo kiểu sau:
      1. dùng định dạng index có tốc độ parse nhanh (gist liên quan)
      2. xử lý tải xuống ban đầu bằng thread
      3. tách giải nén và post-install sang fork
  • Hiện đang xem xét ý tưởng “cache toàn cục được mọi instance bundler cùng chia sẻ”
    Về lâu dài có vẻ sẽ mang lại lợi ích lớn, nhưng vẫn đang cân nhắc xem có độ phức tạp ẩn nào không
    Issue liên quan: rubygems #7249

    • Không hẳn là hoàn toàn đơn giản, nhưng nếu tham khảo các tiền lệ sẵn có ở hệ sinh thái khác thì hoàn toàn khả thi
      Ruby không phải bên đầu tiên giải bài toán này, nên giờ là lúc tận dụng thành quả đó
  • Nguyên tắc cơ bản của tối ưu hóa rất đơn giản — không làm gì cả là nhanh nhất

    • Cần bỏ đi ảo tưởng rằng “code thông minh thì sẽ nhanh”
      Không làm những việc không cần thiết ngay từ đầu mới là tối ưu hóa thật sự