Hiệu năng của trình thông dịch tail call trong Python 3.14
(blog.nelhage.com)- Trình thông dịch tail call mới của CPython ban đầu trông như đem lại mức cải thiện trung bình 10–15% trên pyperformance, nhưng khi điều chỉnh lại baseline, mức cải thiện thực tế thu hẹp xuống khoảng 1–5% tùy cấu hình
- Mức cải thiện lớn không hẳn đến từ riêng cách triển khai mới, mà gần hơn với kết quả của việc né được hồi quy LLVM 19; biến số cốt lõi là Clang 19 không sao chép đúng cơ chế dispatch computed goto hiện có
- Trên Intel Raptor Lake i5-13500, bản build clang19 chậm hơn clang18 1,09 lần còn clang19.tc nhanh hơn 1,03 lần; nhưng trên Apple M1 MacBook Air, clang19 chậm hơn 1,12 lần và clang19.tc cũng chỉ ở mức chậm hơn 1,00 lần
- Do giới hạn tail duplication của LLVM, số indirect jump giảm từ 332 ở clang18 xuống còn 3 ở clang19, khiến cấu trúc mà trình thông dịch dựa trên computed goto hướng tới gần như biến mất
- Cách tiếp cận tail call vẫn là một cải thiện có ý nghĩa, nhưng với mã nhạy cảm hiệu năng, cách yêu cầu rõ ràng việc tối ưu hóa của trình biên dịch như
musttailcó thể vững chắc hơn
Hiệu ứng baseline trông giống như cải thiện hiệu năng
- Dự án CPython đã hợp nhất chiến lược triển khai mới cho trình thông dịch bytecode khoảng một tháng trước
- Kết quả ban đầu cho thấy hiệu năng tăng 10–15% trung bình trên nhiều nền tảng và benchmark pyperformance
- Phân tích sau đó cho thấy mức tăng lớn này chủ yếu là kết quả của việc vô tình né được hồi quy LLVM 19
- Khi so với GCC, clang-18, hoặc LLVM 19 có áp dụng các cờ tinh chỉnh cụ thể, mức cải thiện giảm xuống khoảng 1–5%
- Bản thân trình thông dịch tail call thật sự tạo ra tăng tốc, nhưng mức cải thiện nhẹ hơn so với những gì các con số ban đầu gợi ý
- Nếu build bằng clang-19 hoặc phiên bản sau đó, có khả năng đường đi cũ thực tế đã chậm hơn 10–15%
- Simon Willison đã tái hiện mức tăng tốc 10% khi so sánh bản build
python-build-standalonevới Python 3.13
- Simon Willison đã tái hiện mức tăng tốc 10% khi so sánh bản build
Cấu hình benchmark và các con số chính
- Nhiều bản build CPython được so sánh trên máy chủ Intel và Apple M1 MacBook Air
- Máy chủ Intel là Raptor Lake i5-13500 vận hành tại Hetzner
- Tất cả bản build đều dùng LTO và PGO
- Việc tái lập build dùng cấu hình nix
- Các đối tượng so sánh như sau
clang18: Clang 18.1.8, computed gotogcc: GCC 14.2.1, computed goto, chỉ áp dụng với Intelclang19: Clang 19.1.7, computed gotoclang19.tc: Clang 19.1.7, trình thông dịch tail call mớiclang19.taildup: Clang 19.1.7, computed goto cùng cờ tinh chỉnh-mllvmđể né hồi quy
- Kết quả trung bình pyperformance lấy
clang18làm baseline như sau- Raptor Lake i5-13500:
clang19: chậm hơn 1,09 lầnclang19.taildup: nhanh hơn 1,01 lầnclang19.tc: nhanh hơn 1,03 lầngcc: nhanh hơn 1,02 lần
- Apple M1 MacBook Air:
clang19: chậm hơn 1,12 lầnclang19.taildup: chậm hơn 1,02 lầnclang19.tc: chậm hơn 1,00 lần
- Raptor Lake i5-13500:
- Trình thông dịch tail call cho thấy một số cải thiện tốc độ so với clang-18, nhưng nhỏ hơn mức suy giảm hiệu năng phát sinh khi chuyển sang clang-19
clang18.tckhông đo được- Vì trình thông dịch tail call phụ thuộc vào tính năng trình biên dịch mới được đưa vào Clang 19
- Do ràng buộc này, cần thêm nhiều tổ hợp benchmark hơn để hiểu tình hình
Hồi quy LLVM 19 làm sụp đổ dispatch
- Trình thông dịch bytecode truyền thống xử lý opcode bằng câu lệnh
switchtrong vòng lặpwhile- Trình biên dịch thường biên dịch
switchthành jump table và indirect jump
- Trình biên dịch thường biên dịch
- Từ lâu, việc sao chép logic dispatch vào thân từng opcode đã có thể làm các trình thông dịch như vậy nhanh hơn
- Thay vì quay lại đầu vòng lặp ở cuối opcode, logic giải mã lệnh tiếp theo và lập chỉ mục jump table được đặt riêng trong từng opcode
- Trình biên dịch C cung cấp tính năng lấy địa chỉ label để dùng làm computed goto, và CPython cũng dùng vòng lặp thông dịch theo cách này trước công việc tail call
- Clang/LLVM, vì lý do hiệu năng trình biên dịch, hợp nhất nhiều
gotocủa computed goto bên trong thành một lệnh indirectbr LLVM- Sau đó ở giai đoạn sinh mã, nó thực hiện tail duplication để sao chép lại logic rẽ nhánh vào từng vị trí
- Luồng này được ghi lại ở mức khái quát trong bài blog cũ của LLVM
- LLVM 19 đã đưa vào giới hạn cho pass tail duplication nhằm tránh tăng mạnh thời gian biên dịch hoặc mức dùng bộ nhớ trong một số trường hợp
- Trong CPython, giới hạn này khiến Clang để nguyên các jump dispatch ở trạng thái đã hợp nhất
- Kết quả là mục đích của cách triển khai dựa trên computed goto gần như bị vô hiệu hóa
- Vấn đề này đã được nhận diện trước ở một triển khai ngôn ngữ khác có vòng lặp thông dịch tương tự, nhưng việc nó ảnh hưởng đến CPython thì chưa được biết đến
- Khi disassemble mã đối tượng và đếm số indirect jump, khác biệt hiện ra trực tiếp
_PyEval_EvalFrameDefaulttrong bản build clang18: 332 lệnhjmp *_PyEval_EvalFrameDefaulttrong bản build clang19: 3 lệnhjmp *
Vị thế mập mờ của computed goto
- Việc thay đổi logic tail duplication gây hồi quy được xác nhận qua chuyện sau khi sửa thì hiệu năng phục hồi về mức clang-18
- Tuy vậy, độ lớn của hồi quy chưa được giải thích hoàn toàn
- Trong lịch sử, việc sao chép dispatch opcode từng được dẫn là có thể làm trình thông dịch nhanh hơn từ 20% đến 100%
- Trên các bộ xử lý hiện đại với bộ dự đoán rẽ nhánh tốt hơn, nghiên cứu gần đây hơn cho thấy mức cải thiện nhỏ hơn, khoảng 2–4%
- Python cũng hỗ trợ trình thông dịch kiểu cũ dùng một câu lệnh
switchduy nhất thông qua tùy chọn cấu hìnhclang18.nocg: nhanh hơn clang18 1,01 lầnclang19.nocg: chậm hơn clang18 1,02 lầnclang19: chậm hơn clang18 1,09 lần
- Việc
clang19.nocgnhanh hơnclang19là một tình tiết đảo chiều khác- Clang 18 hoặc Clang 19 với cờ phù hợp cũng sao chép logic dispatch vào thân từng opcode ngay cả với trình thông dịch dựa trên
switch
- Clang 18 hoặc Clang 19 với cờ phù hợp cũng sao chép logic dispatch vào thân từng opcode ngay cả với trình thông dịch dựa trên
- So sánh số indirect jump cũng cho thấy khác biệt này
clang18: 332clang18.nocg: 306clang19.nocg: 3clang19: 3
- Trên Clang hiện đại, toàn bộ trình thông dịch computed goto có thể là độ phức tạp không cần thiết
- Vì trình biên dịch có thể thực hiện cùng phép biến đổi đó cả với mã dựa trên
switch - Ngược lại, bản thân computed goto cũng không đủ để bảo đảm phép biến đổi
- Vì trình biên dịch có thể thực hiện cùng phép biến đổi đó cả với mã dựa trên
- GCC 14.2.1 không sao chép
switch, nhưng khi dùng computed goto thì triển khai hành vi mong muốn
Cách sửa và phương án né tránh
- LLVM pull request 114990 đã được hợp nhất ngay sau khi bài viết được đăng, qua đó sửa hồi quy
- Các benchmark trước khi hợp nhất cũng xác nhận bản sửa đó khôi phục hiệu năng như kỳ vọng
- Với các bản phát hành trước khi sửa, có thể điều chỉnh ngưỡng dừng tail duplication bằng tùy chọn tinh chỉnh được PR gây hồi quy thêm vào
- Nếu đặt giới hạn đó thành giá trị rất lớn trong clang-19, có thể khôi phục hành vi tương tự
- Trong build LTO, việc truyền tùy chọn này khá phức tạp
- Tail duplication diễn ra trong lúc sinh mã, còn sinh mã của build LTO diễn ra tại thời điểm liên kết, không phải thời điểm biên dịch
- Vì vậy cần truyền cờ không chỉ cho trình biên dịch mà cả cho
lld
- Ví dụ cấu hình đã dùng là truyền
-mllvm -tail-dup-pred-size=5000vàoOPTvàLDFLAGSở bước./configure
Vấn đề baseline mà benchmarking phơi bày
- Benchmark có thể đo chính xác chênh lệch hiệu năng giữa các bản build cụ thể, nhưng để mở rộng kết quả đó thành “cải thiện hiệu năng nói chung” thì cần thêm các giả định
- Benchmark của trình thông dịch tail call cho kết quả nhanh hơn 10–15% so với trình thông dịch computed goto hiện có, nhưng baseline quá phức tạp để khái quát hóa thành kết luận rộng hơn
- Trong công việc tối ưu hiệu năng, so sánh với baseline nào luôn là một vấn đề khó lặp đi lặp lại
- Ngay cả khi hiểu về mặt lý thuyết cách tiếp cận tốt nhất hiện biết, việc điều chỉnh đúng hệ điều hành, tùy chọn trình biên dịch và cờ trong thực tế là một vấn đề riêng
- Benchmark công khai trên phần cứng cũ hoặc ở quy mô khó tái lập có thể không phù hợp để so sánh trực tiếp
- Trong các bài báo machine learning, khi tuyên bố cải thiện thuật toán, câu hỏi “so với baseline nào” thường quan trọng trước cả “đã làm gì”
- Nếu so với một baseline được tinh chỉnh sai, rất dễ tạo ra kết quả ấn tượng
Trình biên dịch tối ưu hóa và musttail
- Trường hợp computed goto cho thấy các kỳ vọng với trình biên dịch tối ưu hóa có thể xung đột với nhau
- Trình biên dịch phải tôn trọng ý định của lập trình viên và giữ nguyên cùng hành vi
- Đồng thời, nó cũng phải thực hiện những phép biến đổi phức tạp, không trực quan để làm mã chạy nhanh hơn
- clang-19 đã biên dịch trình thông dịch computed goto một cách đúng xét theo hành vi chương trình, nhưng tạo ra đầu ra hoàn toàn khác với ý định tối ưu hóa
- Các phiên bản Clang khác áp dụng kiểu tối ưu hóa như mong muốn ngay cả cho trình thông dịch đơn giản dựa trên
switch() - computed goto ở mức mã nguồn và sao chép dispatch ở mức mã máy gần như hiện ra như hai khái niệm trực giao
- Vì kết quả thực thi giống nhau, các công cụ hiện tại khó biểu diễn nhất quán khác biệt này
- Trình thông dịch tail call dựa trên thuộc tính musttail
musttailkhông thay đổi hành vi chương trình có thể quan sát theo nghĩa truyền thống, mà gần với một cuộc đối thoại với trình tối ưu hóa- Trình biên dịch phải có khả năng thực hiện một tối ưu hóa cụ thể, và nếu tối ưu hóa đó không xảy ra thì yêu cầu biên dịch thất bại
- Cách này có thể trở thành một phong cách viết mã nhạy cảm hiệu năng vững chắc hơn ngay cả khi trình biên dịch tiếp tục tiến hóa
- Cũng đáng xem xét liệu một thuộc tính giả định như
[[clang::musttailduplicate]]có thể thay thế computed goto trong vòng lặpwhilecủa trình thông dịch hay không
Khả năng tái lập và giới hạn mà nix đem lại
nixgiúp ích rất nhiều trong việc quản lý nhiều bản build trình thông dịch Python- Trong quá trình thử nghiệm, hàng chục trình thông dịch Python đã được build và benchmark với bốn trình biên dịch
gcc,clang-18,clang-19,clang-20cùng nhiều tổ hợp cờ khác nhau
- Trong quá trình thử nghiệm, hàng chục trình thông dịch Python đã được build và benchmark với bốn trình biên dịch
- Dùng
nixcho phép duy trì các phiên bản song song theo cách tái lập được và cô lập- Có thể chắc chắn một bản build cụ thể đến từ trình biên dịch và cờ nào
- Định nghĩa ma trận build cũng có thể được quản lý bằng một lớp trừu tượng ngắn
- Việc build LLVM tùy chỉnh đã áp dụng bản vá sửa lỗi rồi dùng trình biên dịch đó để build Python cũng chỉ cần khoảng 10 dòng mã
- Vẫn còn một số nhược điểm
nixcó những điểm khác với cách dùng phần mềm thông thường, nên khó loại trừ hoàn toàn khả năng các khác biệt này đã ảnh hưởng tới benchmark hoặc kết luận- Ví dụ, mặc định nix build dự án với một số cờ hardening nhất định, và từ sớm đã phát hiện các cờ này ảnh hưởng không cân xứng đến trình thông dịch tail call
- Nix có khả năng mở rộng và tùy biến mạnh, nhưng để tìm ra cách tùy biến cụ thể cần nhiều thử-sai và dò tìm trong mã nguồn
nixpkgs
1 bình luận
Ý kiến trên Hacker News
Tôi là tác giả PR đưa trình thông dịch tail-call vào CPython
Trước hết, cảm ơn Nelson vì đã dành gần một tháng để tìm ra nguyên nhân của vấn đề này
Tôi, và có lẽ cả đội CPython, đều không ngờ rằng trình biên dịch dùng làm baseline lại có lỗi như vậy; tôi đã mắc một sai lầm lớn nên rất xấu hổ và xin lỗi
Tôi cũng đã đăng bài xin lỗi: https://fidget-spinner.github.io/posts/apology-tail-call.htm...
Đã công bố là tăng hiệu năng 10~15%, nhưng trên trình biên dịch không có lỗi thì gần với 1~5% hơn; con số đó cũng không hoàn toàn sai, chỉ là chỉ đúng trong một số điều kiện nhất định
Bạn đã tạo ra cải tiến, đo đạc nó, và PR cũng đã được review, nên coi như đã làm những việc cần làm. Chỉ là đúng lúc phiên bản clang dùng để đo có vấn đề khiến các con số gây hiểu nhầm; đây có vẻ là một sai lầm hợp lý mà ai cũng có thể mắc phải
Dù vậy, nó vẫn đem lại cải thiện hiệu năng có ý nghĩa và còn phát hiện ra một hồi quy của trình biên dịch, nên con số sai có vẻ là chuyện nhỏ so với những điều đó. Tôi cũng không rõ thực sự có ai bị thiệt hại vì việc này không, và có vẻ chưa đến mức phải xin lỗi
Nếu mức 3~5% được duy trì trong một hệ thống lâu đời như trình thông dịch Python, riêng điều đó đã là thành tựu lớn và hoàn toàn đáng tự hào
Sau khoảng 30 năm, tôi thường nghi ngờ trước những cải thiện hiệu năng có ý nghĩa trong các hệ thống tồn tại lâu năm, đặc biệt là những cải thiện vượt quá 1%
Cũng có những cải thiện thật, nhưng không phổ biến; thường thì chỉ là chuyển thời gian sang chỗ khác nên benchmark không bắt được. Hơn nữa, benchmark được chạy trong môi trường có kiểm soát để tách riêng hiệu ứng, còn phần mềm thực tế thì chạy trên VM hoặc desktop cùng đủ thứ khác
Tôi đã thấy nhiều trường hợp một cải tiến trông rõ ràng là lớn trong môi trường tách biệt nhưng khi vào môi trường vận hành thì biến mất hoặc trở thành âm
CPython còn khó hơn vì phải nhắm tới nhiều môi trường, và cũng không có một mục tiêu vận hành đơn lẻ nào để nói rằng “nếu không nhanh hơn trong production thì không phải là nhanh hơn thật”. Cố cải thiện hiệu năng trong thế giới như vậy thực sự rất khó
Rốt cuộc, tinh chỉnh và đo lường hiệu năng là việc rất khó; điều duy nhất đáng xin lỗi có lẽ chỉ là đã học được sự thật đó
Mong bạn đừng sợ sai. Dù sao thì ai cũng sai. Cứ như bây giờ, nói rằng “có vẻ chúng ta đã làm hỏng chuyện này”, rồi tìm cách xử lý và cách tránh trong tương lai là được
[1] Điều này không chỉ thường gặp trong hiệu năng mà cả trong quy trình của con người. Ví dụ, nếu đội công cụ code review nói “chúng tôi đã giảm thời gian review code 15% và làm workflow của mọi người nhanh hơn”, thì thực tế có thể họ đã tạo thêm việc ở phần khác của hệ thống, luồng tổng thể không nhanh hơn, và chỉ chuyển 15% đó sang nơi không được đo
Về lý thuyết, với đồ thị luồng điều khiển và profile như thế này, trình biên dịch đáng lẽ có đủ thông tin để tạo mã tối ưu cho trình thông dịch truyền thống dựa trên
switch(). Nhưng trên thực tế, khi hàm lớn và liên kết chằng chịt như vậy, bạn sẽ phải vật lộn với trình biên dịchNó spill các biến quan trọng mà bạn muốn giữ trong thanh ghi, kéo lên các thao tác stack frame mà bạn muốn thu gọn quanh lời gọi hàm fallback, và gộp những đường mã giống nhau mà bạn muốn tách ra vì dự đoán nhánh. Cảm giác có thể giống như chơi piano khi đang đeo găng tay
Ở đây cũng đã xảy ra đúng việc “gộp các đường mã giống nhau” đó, và trình biên dịch “có lỗi” đã gộp các đường giống nhau khiến hiệu năng xấu đi
Trình biên dịch “đã sửa” không còn làm vậy nữa, nhưng bản sửa đó rốt cuộc cũng gần như là điều chỉnh heuristic nội bộ của trình biên dịch. Không có gì bảo đảm rằng trình biên dịch này hay trình biên dịch khác trong tương lai sẽ tiếp tục giữ các heuristic theo hướng có lợi cho chúng ta
Ngược lại, trình thông dịch tail-call cho phép biểu diễn mẫu mã máy mong muốn ngay trong chính trình thông dịch. Khi dùng cùng các thuộc tính
musttail,noinline,preserve_none, ta có thể ràng buộc vấn đề để ít bị các heuristic của optimizer chi phối hơn nhiềuVì vậy, lợi ích của trình thông dịch tail-call không chỉ là cải thiện hiệu năng 3~5%, mà trên một số trình biên dịch còn có thể là mức cải thiện hiệu năng đáng tin cậy lớn hơn thế
Có thể cải thiện benchmark để ngăn những việc tương tự không?
[0] https://github.com/faster-cpython/benchmarking-public
Benchmarking thật sự khó đến phát điên để làm cho đúng. Có quá nhiều yếu tố đánh lừa con người
Gần đây tôi tưởng mình đã tìm ra cách làm một thuật toán nhanh hơn khoảng 15%. Ít nhất thì mọi benchmark đều nói như vậy
Nhưng khi tôi sao chép hàm nhanh hơn vào test harness mà thực tế không gọi nó, chỉ gọi phiên bản chậm ban đầu, thì nó vẫn nhanh hơn 15%. Tức là đoạn code thậm chí không được thực thi lại làm code gốc nhanh hơn
Dĩ nhiên đó là vấn đề về code và bố cục bộ nhớ; thứ gì đó đã dịch chuyển và tình cờ khớp với CPU cache tốt hơn
Rất khó để biết phần tăng tốc đạt được là do code thực sự “tốt hơn”, hay chỉ vì may mắn có được căn chỉnh tốt hơn ở đâu đó
Casey Muratori đang viết một loạt bài rất thú vị về chủ đề này trên Substack
Xin khen tác giả bài viết vì đã đào sâu và làm rõ tình hình thực tế. Trình thông dịch tail-call của Python 3.14 vẫn là một cải tiến tốt, và vài phần trăm cải thiện trong runtime của ngôn ngữ là thành quả khó có được
Chỉ là nó không phải bữa trưa miễn phí 15% như phép màu
Quan trọng hơn, chuyện này cho thấy rõ tầm quan trọng của tính nghiêm ngặt trong benchmarking và việc kiểm thử trên nhiều môi trường. Nó cũng làm lộ ra một bug compiler có thể mang lại lợi ích cho mọi người
Đây là kiểu phân tích chuyên sâu khiến ta phải kiểm chứng lại tuyên bố lớn tiếp theo về cải thiện hiệu năng. Câu hỏi đáng suy nghĩ thêm là: trong vô số kết quả “nhanh hơn X%” hiện nay, có bao nhiêu thực ra chỉ là artifact của benchmark hoặc một regression chưa được biết đến?
Về sau làm sao để tránh những cái bẫy này tốt hơn?
Người ta không benchmark chính compiler à? Hay các benchmark hiện có phía compiler hoặc phía Python đã không dùng compiler đó?
Đây là một ví dụ hay cho thấy những câu như C “gần với máy” hay “assembly có tính portable” không đúng đến mức nào. Các optimizer hiện đại sẵn sàng thay đổi logic mạnh tay nếu không có hiệu ứng quan sát được
Bài viết cũng nói rằng “clang-19 biên dịch trình thông dịch computed-goto một cách ‘đúng’ theo nghĩa binary kết quả tạo ra mọi giá trị mong đợi, nhưng đồng thời output đó hoàn toàn đi ngược ý đồ tối ưu hóa. Hơn nữa, các phiên bản compiler khác lại áp dụng tối ưu hóa cho trình thông dịch dựa trên
switch()‘ngây thơ’, hiện thực đúng cùng kiểu tối ưu mà chúng ta ‘định’ làm khi viết lại source code”Với
a += 1trong C, bạn có thể tin rằng nó tăng giá trị số; nhưng cùng biểu thức đó trong C++ có thể cấp phát bộ nhớ, tháo stack gọi hàm, hoặc làm những việc không thể biết. Tương tự,a = "a"trong C chỉ là gán con trỏ đơn giản, còn trong C++ có thể xảy ra cấp phát bộ nhớ, v.v.Cụm “C là assembly có tính portable” không có nghĩa là từng câu lệnh được biên dịch trực tiếp thành mã máy tương đương
Không ngạc nhiên khi compiler đụng vào cấu trúc vòng lặp khiến toàn bộ trình thông dịch tail-call không hiệu quả như công bố
Các triển khai trình thông dịch theo kiểu hoang tưởng đôi khi lại quay về viết assembly trực tiếp. LuaJIT nổi tiếng vì đã triển khai một hệ thống macro để làm cho implementation vòng lặp assembly rất hiệu quả có thể portable giữa các kiến trúc. Vì vậy đụng vào những thứ này cũng khá thú vị
Vài năm trước tôi cũng đã viết bài và làm test về các cách triển khai vòng lặp trình thông dịch phổ biến:
https://github.com/vkazanov/bytecode-interpreters-post
Branch predictor hiện đại, nếu đoạn chạy đủ dài và hành vi của chính code được diễn dịch ổn định, có thể dự đoán gần như chính xác cả một indirect jump duy nhất
Có một bài báo nghiên cứu điều này trên cả phần cứng thực tế lẫn một branch predictor mô phỏng cụ thể: https://inria.hal.science/hal-01100647/document
Các thí nghiệm tôi làm trong dự án này, về mặt giai thoại, cũng ủng hộ kết luận đó. Tôi không đưa vào bài, nhưng đã xem vài trình thông dịch bằng hardware CPU counters và
perf stat, và branch misprediction không hiện ra như yếu tố chi phốiĐánh giá hiệu năng build Python cực kỳ khó. Vì có quá nhiều kỹ thuật build có thể cải thiện hiệu năng
Gần đây phía astral cũng gặp vấn đề này khi cho thấy bản build conda-forge nhanh hơn đáng kể so với phần lớn bản build khác:
https://github.com/astral-sh/python-build-standalone/pull/54...
Tôi tò mò trình thông dịch tail-call hoạt động ra sao khi dùng cùng các tối ưu hóa build khác hiện có
Tác giả đã dùng thuật toán di truyền để thử nhiều tổ hợp compiler và cờ tối ưu hóa
Thảo luận liên quan:
https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-... --> https://news.ycombinator.com/item?id=42999672 (66 điểm | 25 ngày trước | 22 bình luận)
https://blog.reverberate.org/2025/02/10/tail-call-updates.ht... --> https://news.ycombinator.com/item?id=43076088 (124 điểm | 18 ngày trước | 92 bình luận)
Bài viết hay. Có một chi tiết khiến tôi chú ý
Trong một bài được tham chiếu, https://simonwillison.net/2025/Feb/13/python-3140a5/, có viết rằng “3.14.0a5 nhanh hơn 3.13 1,12 lần trong benchmark, trên chiếc M2 MacBook Pro cực kỳ quá tải của tôi”
Chỗ này khá khó hiểu. Ý là đã chạy benchmark khi máy tính đang bị các tiến trình khác làm quá tải sao? Nếu vậy thì kết quả chẳng phải hoàn toàn không đáng tin sao?
Tôi cứ nghĩ các benchmark kiểu này được thực hiện trong môi trường được kiểm soát rất chặt để loại bỏ biến ngoại lai
Về phía CPython thì có thể xem https://github.com/faster-cpython/benchmarking-public
Ở đây có người nói 10% là “lớn”, 1% là “bình thường”, nhưng các tối ưu như inline một phần cho Fibonacci đệ quy kép có thể giảm khối lượng công việc và thời gian theo hàm mũ
Với đối số hai chữ số, có thể hơn 10 lần, tức hàng nghìn phần trăm. Nói chính xác thì nó tăng theo hàm mũ theo chênh lệch độ sâu đệ quy, không phải kích thước bài toán [1]
Trình biên dịch C cũng có thể phản ứng rất nhạy với các chỉ số inline mã, nên việc mức tăng tốc khổng lồ đó có thực sự xuất hiện hay không có thể rất phụ thuộc vào hình dạng mã
Vì vậy một phần vấn đề là CPU đã trở nên rất tinh vi và phức tạp, nhưng ở một khía cạnh khác, các trình biên dịch vượt ngoài
-O0hay-O1cũng đã trở nên tinh vi và phức tạpBài viết này hay và đáng đọc, nhưng cũng là một trong rất nhiều ví dụ cho thấy khi hai thứ phức tạp tương tác với nhau thì có thể cho ra kết quả rất bất ngờ. Điều này cũng đúng cả ngoài lĩnh vực điện toán
Dù bài học này được lặp lại bao nhiêu lần, con người vẫn có xu hướng đơn giản hóa quá mức
Ngoài ra, bài viết dùng ít nhất hai CPU là Intel và Apple M1, cùng hai trình biên dịch gcc và clang, nhưng môi trường triển khai thực tế có thể có rất nhiều thế hệ và triển khai Intel, AMD, ARM khác nhau, cũng như các trình biên dịch khác. Tức là chỉ mới lấy mẫu một phần rất nhỏ của toàn bộ độ phức tạp
Để làm khoa học hơn, đặc biệt với các chênh lệch như “1,01 lần”, cần phải có thanh sai số dưới một hình thức nào đó cho phép đo thời gian. Có thể là độ lệch chuẩn của trung bình, hoặc trong trường hợp này độ lệch chuẩn của giá trị nhỏ nhất có lẽ còn tốt hơn [2]
Để giảm sai số đo, nhiều khả năng cũng cần cơ chế lập lịch cố định CPU core trong OS
[1] https://stackoverflow.com/questions/360748/computational-com...
[2] https://github.com/c-blake/bu/blob/main/doc/tim.md
Gần đây tôi đã benchmark từ Python 3.9 đến 3.13, và hiệu năng cứ tiếp tục tốt hơn cho đến 3.11
Nhưng Python 3.12 và 3.13 chậm hơn 3.11 khoảng 10%
Tôi nghĩ benchmark tự làm của mình chưa đủ tốt, nhưng vẫn thử triển khai lên dịch vụ lõi, và các chỉ số thu thập được cũng cho thấy cùng một thay đổi
Có ai gặp vấn đề tương tự không?
[0]: https://github.com/python/cpython/issues/123540