3 điểm bởi GN⁺ 2025-03-11 | 1 bình luận | Chia sẻ qua WhatsApp
  • Dự án CPython gần đây đã áp dụng một chiến lược triển khai mới cho trình thông dịch bytecode. Kết quả ban đầu cho thấy hiệu năng trung bình tăng 10-15% trên nhiều nền tảng
  • Tuy nhiên, mức tăng hiệu năng này chủ yếu là kết quả của việc né tránh một vấn đề hồi quy trong LLVM 19. Khi so với các mốc chuẩn tốt hơn (ví dụ: GCC, clang-18, LLVM 19 với các cờ tinh chỉnh cụ thể), mức cải thiện giảm xuống còn 1-5%

Kết quả hiệu năng

  • Đã benchmark nhiều bản build của trình thông dịch CPython bằng nhiều trình biên dịch và tùy chọn cấu hình khác nhau. Việc thử nghiệm được thực hiện trên máy chủ Intel và Apple M1 Macbook Air.
  • Tất cả các bản build đều sử dụng LTO và PGO. Lấy clang18 làm mốc chuẩn và dùng giá trị trung bình được báo cáo bởi pypeformance/pyperf compare_to.
  • So sánh hiệu năng trình biên dịch
    • Apple M1 Macbook Air :
      • clang18: mốc chuẩn
      • clang19: chậm hơn 1.12 lần
      • clang19.taildup: chậm hơn 1.02 lần
      • clang19.tc: chậm hơn 1.00 lần
      • gcc: N/A
  • Trình thông dịch tail-call vẫn cho thấy cải thiện tốc độ so với clang-18, nhưng khi chuyển sang clang-19 thì mức sụt giảm tốc độ còn rõ rệt hơn.

Hồi quy của LLVM

Bối cảnh ngắn gọn

  • Trình thông dịch bytecode truyền thống được cấu thành từ câu lệnh switch bên trong vòng lặp while. Hầu hết trình biên dịch sẽ biên dịch switch thành bảng nhảy.
  • Các trình biên dịch C hiện đại hỗ trợ mẫu lấy địa chỉ của nhãn và dùng nó như một "computed goto". CPython đã dùng mẫu này cho tới trước khi có công việc về tail-call.

Hồi quy trong LLVM 19

  • LLVM 19 đã đặt giới hạn cho pass tail-duplication, khiến việc sao chép dừng lại nếu kích thước IR vượt quá một ngưỡng nhất định. Vì thế trong CPython, mọi cú nhảy dispatch đều bị gộp lại, hoàn toàn làm mất mục đích của cách triển khai dựa trên goto tính toán.

Các hiện tượng bất thường khác

  • Có thể chắc chắn rằng thay đổi trong logic nhân bản tail-call đã gây ra hồi quy, nhưng vẫn chưa thể giải thích đầy đủ mức độ lớn của hồi quy này.
  • Trên các bộ xử lý hiện đại, mức cải thiện tốc độ 2-4% là phổ biến hơn.

Có cần computed goto không?

  • Benchmark clang19.nocg được cho là nhanh hơn clang19. Điều này cho thấy trình biên dịch có thể thực hiện cùng kiểu tối ưu hóa đó với trình thông dịch dựa trên switch.

Bản sửa lỗi

  • Pull request LLVM 114990 đã sửa lỗi hồi quy này. Bản sửa khôi phục hiệu năng như kỳ vọng.

Suy ngẫm

Về benchmark

  • Khi tối ưu hóa hệ thống, người ta xây dựng benchmark và phương pháp benchmark để đánh giá các thay đổi được đề xuất.
  • Benchmark đòi hỏi nhiều giả định và niềm tin hơn để có thể khái quát hóa từ các điểm dữ liệu cụ thể.

Mốc chuẩn

  • Khi đề xuất một giải pháp hay phương pháp mới, thông lệ là so sánh với "cách tiếp cận tốt nhất đang được biết đến hiện tại".

Về kỹ thuật phần mềm

  • Các hệ thống phần mềm rất phức tạp, liên kết chặt chẽ với nhau và thay đổi nhanh chóng.
  • Trình biên dịch tối ưu hóa luôn ở trong thế căng giữa việc phải tôn trọng ý đồ của lập trình viên và việc tối ưu mã.

Trình biên dịch tối ưu hóa

  • Thuộc tính musttail đại diện cho một loại tính năng trình biên dịch mới liên quan đến tối ưu hóa. Nó có thể mang lại một phong cách mạnh hơn để viết mã nhạy cảm với hiệu năng.

Thêm một điều về nix

  • nix đã rất hữu ích trong dự án này. Nó hỗ trợ rất nhiều cho việc quản lý và build nhiều phiên bản trình thông dịch Python.

1 bình luận

 
GN⁺ 2025-03-11
Ý kiến trên Hacker News
  • Xin chào. Tôi là tác giả của PR đưa trình thông dịch tail-calling vào CPython

    • Trước hết, tôi muốn gửi lời cảm ơn tới Nelson, người đã dành gần một tháng để tìm ra căn nguyên của vấn đề này
    • Tôi cũng cảm thấy vô cùng xấu hổ và xin lỗi vì đã mắc phải một sai lầm lớn như vậy
    • Ngay cả đội ngũ CPython cũng không ngờ rằng trình biên dịch mà chúng tôi dùng lại có lỗi như thế này
    • Tôi đã đăng bài viết xin lỗi tại đây: liên kết
  • Benchmark thực sự là một công việc cực kỳ khó làm cho đúng

    • Gần đây tôi đã tìm ra cách làm một thuật toán nhanh hơn khoảng 15%
    • Nhưng trong lúc kiểm thử, đoạn mã gốc lại nhanh hơn 15% mà thậm chí không cần gọi đến phiên bản hàm nhanh hơn
    • Đây là vấn đề về bố trí mã và bộ nhớ, vì nó căn chỉnh tốt hơn với CPU cache
    • Casey Muratori đang thực hiện một series khá thú vị về chủ đề này
  • Xin dành lời khen cho tác giả vì đã lần ra sự thật của vấn đề này

    • Trình thông dịch tail-call của Python 3.14 vẫn là một cải tiến tốt
    • Sự việc này đã cho thấy tầm quan trọng của benchmark nghiêm ngặt và kiểm thử trong nhiều môi trường khác nhau
    • Ngoài ra, giờ đây chúng ta còn phát hiện ra một lỗi trình biên dịch có thể mang lại lợi ích cho mọi người
    • Tôi tự hỏi có bao nhiêu kết quả kiểu "nhanh hơn X%" thực ra là do artifact của benchmark hoặc do regression chưa được biết đến
  • Đây là một ví dụ hay cho thấy C không phải là ngôn ngữ "gần với máy"

    • clang-19 biên dịch trình thông dịch computed goto một cách "đúng", nhưng lại tạo ra đầu ra hoàn toàn khác với ý đồ tối ưu hóa
    • Các phiên bản trình biên dịch khác cũng áp dụng tối ưu hóa cho trình thông dịch dựa trên switch() theo kiểu "ngây thơ"
  • Do cách trình biên dịch điều chỉnh việc tổ chức vòng lặp, trình thông dịch tail-call không hiệu quả như mức đã được công bố

    • Kiến trúc CPU và phiên bản cụ thể là những yếu tố cực kỳ quan trọng
    • Máy trừu tượng C không đủ thấp tầng để diễn đạt đúng ý đồ
    • Một số cách triển khai trình thông dịch cực kỳ thận trọng quay lại với việc tự viết assembly trực tiếp
    • luajit triển khai một hệ thống macro để giúp cách cài đặt vòng lặp assembly hiệu quả có thể được перенос giữa các kiến trúc
  • Việc đánh giá hiệu năng của một bản build Python là rất khó

    • Gần đây đội ngũ astral đã cho thấy bản build conda-forge nhanh hơn phần lớn các bản build khác
    • Tôi tò mò không biết trình thông dịch tail-call hoạt động ra sao khi kết hợp với các tối ưu hóa build khác
  • Thảo luận liên quan:

  • Bài viết rất hay

    • Trong một trong các bài được dẫn chiếu, có nhắc rằng 3.14.0a5 nhanh hơn 3.13 khoảng 1,12 lần
    • Tôi hơi bối rối không biết benchmark có được chạy trong tình trạng bị quá tải bởi các tiến trình khác hay không
    • Benchmark nên được thực hiện trong một môi trường được kiểm soát chặt chẽ để loại bỏ các biến số bên ngoài
  • Gần đây tôi đã benchmark từ Python 3.9 đến 3.13

    • Cho đến 3.11 thì hiệu năng có cải thiện, nhưng 3.12 và 3.13 lại chậm hơn 3.11 khoảng 10%
    • Tôi từng nghĩ benchmark nội bộ của mình là chưa đủ, nhưng khi triển khai lên dịch vụ cốt lõi thì tôi vẫn quan sát thấy cùng một thay đổi
  • Tôi thắc mắc tối ưu hóa này liên quan thế nào đến tail-call optimization

    • Cách triển khai jump table của trình thông dịch lẽ ra không nên ảnh hưởng đến việc tạo stack frame