3 điểm bởi GN⁺ 2025-03-11 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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ư musttail có 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-standalone với Python 3.13

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 LTOPGO
    • 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 goto
    • gcc: GCC 14.2.1, computed goto, chỉ áp dụng với Intel
    • clang19: Clang 19.1.7, computed goto
    • clang19.tc: Clang 19.1.7, trình thông dịch tail call mới
    • clang19.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 clang18 làm baseline như sau
    • Raptor Lake i5-13500:
      • clang19: chậm hơn 1,09 lần
      • clang19.taildup: nhanh hơn 1,01 lần
      • clang19.tc: nhanh hơn 1,03 lần
      • gcc: nhanh hơn 1,02 lần
    • Apple M1 MacBook Air:
      • 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
  • 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.tc khô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 switch trong vòng lặp while
    • Trình biên dịch thường biên dịch switch thành jump table và indirect jump
  • 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 goto củ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_EvalFrameDefault trong bản build clang18: 332 lệnh jmp *
    • _PyEval_EvalFrameDefault trong bản build clang19: 3 lệnh jmp *

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 switch duy nhất thông qua tùy chọn cấu hình
    • clang18.nocg: nhanh hơn clang18 1,01 lần
    • clang19.nocg: chậm hơn clang18 1,02 lần
    • clang19: chậm hơn clang18 1,09 lần
  • Việc clang19.nocg nhanh hơn clang19 là 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
  • So sánh số indirect jump cũng cho thấy khác biệt này
    • clang18: 332
    • clang18.nocg: 306
    • clang19.nocg: 3
    • clang19: 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
  • 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=5000 vào OPTLDFLAGS ở 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
    • musttail khô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ặp while củ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

  • nix giú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-20 cùng nhiều tổ hợp cờ khác nhau
  • Dùng nix cho 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
    • nix có 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

 
GN⁺ 2025-03-11
Ý 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...

    • Thấy câu “đó là một sai lầm lớn nên tôi rất xấu hổ và xin lỗi”, tôi tưởng là đã làm hỏng hiệu năng của CPython, nhưng thực tế hoàn toàn không phải vậy
      Đã 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
    • Nhân tiện, sau khi bài blog đó được viết thì bản sửa đã được merge rồ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
    • Tôi cho rằng một trong những động lực chính của thiết kế trình thông dịch tail-call là bớt dễ bị ảnh hưởng bởi tính thất thường của optimizer. Bài viết gốc về kỹ thuật này (https://blog.reverberate.org/2021/04/21/musttail-efficient-i...) cũng giải thích như vậy
      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ịch
      Nó 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ều
      Vì 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ế
    • Tôi tôn trọng thái độ có thể nói “xin lỗi, tôi đã sai”. Tôi thực sự ghét văn hóa giả vờ chống chế rồi làm như đã thành công có vẻ đang trở thành chuẩn mực ngày nay
    • Tôi tò mò vì sao hồi quy hiệu năng baseline không xuất hiện trên trang benchmark faster-cpython [0], hoặc liệu nó có xuất hiện không
      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

    • Thật đáng ngạc nhiên khi kiểu xổ số linker đó có thể tạo ra mức cải thiện tới 15%. Tôi tò mò trong trường hợp nào mức tăng lớn như vậy xuất hiện, nó có hiếm không, và cuối cùng họ đã đánh giá ra sao
    • Tôi mơ hồ nhớ từng có một dự án benchmarking cố ý ngẫu nhiên hóa một phần các quyết định của compiler, để ước lượng ổn định hơn code thực sự chạy tốt đến mức nào và ít bị chi phối bởi việc thắng hay thua xổ số linker
    • Aleksey Shipilёv, người đã lâu năm hoạt động như một “kỹ sư hiệu năng” Java, đã để lại nhiều bài viết và bài nói về độ khó của benchmarking. Rất khuyến nghị các bài blog hoặc bài thuyết trình của ông ấy
  • 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?

    • Câu hỏi lớn hơn là vì sao hiệu năng Python giảm 10% khi một tính năng compiler lỗi được đưa vào lại không bị phát hiệ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”

    • Nhìn từ góc độ các ngôn ngữ lập trình hệ thống khác của thập niên 80–90, C vẫn khá gần với assembly có tính portable
      Với a += 1 trong 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 có hiệu ứng quan sát được” đã bị kéo dài thành một bài blog 10.000 từ
  • 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ố

    1. Kiến trúc CPU và phiên bản cực kỳ quan trọng. 95% vấn đề là bố trí code dispatch lệnh sao cho branch predictor hoạt động tối ưu, mà C vốn không phải ngôn ngữ được tạo ra để hỗ trợ việc này
    2. Máy trừu tượng C cũng không đủ low-level để biểu đạt đúng ý đồ. Bất kỳ cách triển khai nào cũng trở nên quá nhạy với đặc tính của một compiler cụ thể và một phiên bản cụ thể
      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
    • Với tư cách tác giả, khi viết bài này tôi đã học được rằng câu “95% vấn đề là bố trí code dispatch lệnh sao cho branch predictor hoạt động tối ưu” giờ không còn đúng nữa
      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ó

  • 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

    • Simon Willison là một người rất giỏi, nhưng không phải core developer của Python, và benchmark tạm thời của ông ấy không phải thứ nhóm core CPython dùng
      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 -O0 hay -O1 cũng đã trở nên tinh vi và phức tạp
    Bà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?

    • Đúng vậy. Tôi đã tìm thấy hồi quy hiệu năng vòng lặp trong 3.12 và 3.13 [0]
      [0]: https://github.com/python/cpython/issues/123540
    • Ứng dụng FastAPI cũng khá chậm trên 3.12 và 3.13 nên chúng tôi vẫn đang dùng 3.11