2 điểm bởi GN⁺ 2024-08-05 | 1 bình luận | Chia sẻ qua WhatsApp
  • Tính chất thời gian hằng (constant-time) quan trọng trong mã mật mã có thể bị phá vỡ chỉ bởi tối ưu hóa của trình biên dịch, nên đã có thử nghiệm đưa bản vá cảnh báo vào nội bộ LLVM để tìm các mẫu nguy hiểm
  • “Tối ưu hóa” của trình biên dịch có thể làm một số benchmark nhanh hơn, nhưng các đường dẫn cốt lõi thực tế thường dựa vào intrinsics và assembly, còn chi phí lỗi phát sinh do tối ưu hóa thì tích lũy riêng
  • Tháng 6/2024, Antoon Purnal xác nhận mã tham chiếu Kyber ở một số tùy chọn tối ưu hóa của Clang 15 trở lên có thể bị biến thành nhánh điều kiện dựa trên giá trị bí mật, cho phép tấn công timing
  • TIMECOP 2 kiểm tra kết quả biên dịch được khai báo là thời gian hằng trong SUPERCOP, nhưng có giới hạn ở các lệnh Valgrind hỗ trợ và các luồng dữ liệu lộ ra khi chạy kiểm thử thực tế
  • Cách ứng phó trong thực tế là dùng các hàm như crypto_{int,uint}{8,16,32,64}.h để khiến trình biên dịch không nhìn kết quả 1 bit như bool, hoặc chuyển sang assembly đã được kiểm chứng, ngôn ngữ hướng bảo mật, hay trình biên dịch chuyên dụng

Khoảng trống trách nhiệm do “tối ưu hóa” trình biên dịch tạo ra

  • Trong lịch sử thay đổi của LLVMGCC mới nhất liên tục xuất hiện “tối ưu hóa”, kiểm thử “tối ưu hóa”, sửa kiểm thử và sửa lỗi “tối ưu hóa”
  • Ngay cả khi mã hoạt động tốt trước khi biên dịch nhưng thay đổi sau một thay đổi của trình biên dịch, trong nhiều trường hợp trách nhiệm vẫn bị quy về lập trình viên đã đụng phải “undefined behavior”
  • Các “language standards” như vậy do những người viết trình biên dịch tạo ra; kết quả là cấu trúc trong đó mã của hàng triệu lập trình viên phải gánh trách nhiệm lớn hơn so với thay đổi của một nhóm nhỏ người viết trình biên dịch
  • Lấy mã mật mã làm ví dụ, trên nhiều benchmark CPU, bản triển khai avx2 của kyber768 nhanh hơn khoảng 4 lần so với mã portable được biên dịch bằng trình biên dịch “tối ưu hóa”

Giới hạn của việc đo hiệu năng tối ưu hóa

  • Năm 2000, Todd A. Proebsting trong Proebsting's Law diễn đạt rằng “tiến bộ trình biên dịch làm sức mạnh tính toán tăng gấp đôi sau mỗi 18 năm”, và kết luận đóng góp của tối ưu hóa trình biên dịch chỉ mang tính bên lề
  • Trong benchmark năm 2022, Arseny Kapoulkine tổng kết rằng LLVM 11 mất thời gian biên dịch tối ưu hóa lâu gấp 2 lần LLVM 2.7, còn mã chạy nhìn chung nhanh hơn 10–20%
  • Cả hai thảo luận đều bỏ sót việc đo hiệu năng mà người dùng thực sự cảm nhận
    • Các hotspot nơi hiệu năng tập trung thường chứa nhiều intrinsics và assembly
    • FFmpeg160.000 dòng assembly tính theo các tệp .asm.S
    • Khi máy tính và mạng xử lý nhiều dữ liệu hơn, thời gian CPU thực tế càng dồn nhiều hơn vào các hotspot này
  • Chi phí bảo mật cũng tăng riêng trong thảo luận về tối ưu hóa
    • Deloitte báo cáo rằng ngân sách bảo mật IT năm 2023 bằng 0,5% doanh thu doanh nghiệp
    • Kết hợp với số liệu tổng doanh thu doanh nghiệp toàn cầu năm 2022 là hơn 48 nghìn tỷ USD, quy mô tổng thể có thể ở mức hàng trăm tỷ USD
    • Tuy nhiên, có lưu ý rằng mức 0,5% của Deloitte có thể là trung bình đơn giản theo từng doanh nghiệp, và không phải mọi doanh nghiệp đều trả lời khảo sát

Rò rỉ timing và trường hợp Kyber

  • Vấn đề bảo mật do trình biên dịch “tối ưu hóa” tạo ra không chỉ gồm lỗi truyền thống, mà còn gồm rò rỉ timing, tức thông tin bí mật lộ qua thời gian thực thi
  • Bài báo EuroS&P 2018 của Laurent Simon, David Chisnall và Ross Anderson cảnh báo rằng nâng cấp trình biên dịch có thể âm thầm mở kênh timing trong mã trước đó vốn an toàn
  • Ví dụ được nhấn mạnh trong bài báo năm 2018 là mã chọn một trong hai giá trị bằng bool, và bool khiến trình biên dịch sinh lệnh nhảy có điều kiện
    • Trong triển khai mật mã, để tránh điều này, có thực hành loại bỏ bool khỏi mã quan trọng và tạo riêng các hàm so sánh thời gian hằng
    • OpenSSL được trích dẫn là khai báo 37 hàm cho mục đích này
  • Trường hợp curve25519-donna và MSVC 2015 năm 2015 được bài viết kết luận là một hiểu nhầm
    • Thực tế, khi biên dịch cho x86 32-bit, phép toán int64 được chuyển thành lời gọi đến thư viện int64 32-bit của Microsoft là llmul.asm
    • Rò rỉ timing được tạo ra từ nhánh phụ thuộc dữ liệu trong llmul.asm, và thư viện này cũng nên được xem là nằm trong khái niệm mã nguồn hợp lý
  • Tháng 6/2024, Antoon Purnal xác nhận mã tham chiếu Kyber có thể cho phép tấn công timing ở một số tùy chọn tối ưu hóa của Clang 15 trở lên
    • Dạng vấn đề là (-((x>>j)&1))&y, phép tính tạo ra y nếu bit thứ j của x được đặt, ngược lại tạo ra 0
    • Clang chuyển bit đó thành bool bằng lệnh bit test, rồi sinh nhánh điều kiện dựa trên bool đó
    • Bên trong LLVM, combineShiftAnd1ToBitTest trong lib/CodeGen/SelectionDAG/DAGCombiner.cpp xử lý “tối ưu hóa” này
    • Hàm này được Sanjay Patel thêm vào tháng 9/2019 và sau đó được nhiều người sửa đổi
  • GCC cũng có trường hợp xâm phạm ranh giới tương tự
    • Bản vá GCC tháng 11/2021 của ARM đổi (-x)>>31 thành -(x>0)
    • Tháng 4/2024 đã có cảnh báo về việc này

TIMECOP và kiểm tra thời gian hằng

  • TIMECOP 2 được tích hợp trong framework kiểm thử mật mã SUPERCOP, tự động kiểm tra nhánh điều kiện phát sinh từ giá trị bí mật trong mã biên dịch được khai báo là thời gian hằng
  • Đối tượng kiểm tra ngoài nhánh điều kiện còn gồm chỉ số mảng phát sinh từ giá trị bí mật
    • Bài báo KyberSlash cũng mô tả bản vá kiểm tra phép chia phát sinh từ giá trị bí mật
  • TIMECOP 1 là công cụ do Moritz Neikes tạo bằng cách sửa SUPERCOP, tự động hóa cách tiếp cận ctgrind của Adam Langley
  • TIMECOP 2 mở rộng một số điểm so với cách cũ
    • Tự động đánh dấu đầu ra RNG là giá trị bí mật
    • Hỗ trợ “declassification”
    • Hỗ trợ chỉ định “public inputs”
    • Chạy trên nhiều lõi
  • TIMECOP có các giới hạn rõ ràng
    • Chỉ xử lý được các lệnh Valgrind hỗ trợ, nên sẽ dừng ở các lệnh như AMD XOP
    • Chỉ kiểm tra các luồng dữ liệu thấy được khi chạy kiểm thử thực tế
  • Công việc về công cụ kiểm tra hành vi thời gian hằng vẫn tiếp tục, và danh sách công cụ liên quan có tại ct-tools
  • Kiểm tra tương ứng với TIMECOP đã được đưa vào bộ kiểm thử libmceliece và có thể lan sang các thư viện khác

Cách viết lại theo thời gian hằng

  • Sau khi tìm thấy đoạn mã thời gian biến thiên, cần có cách viết lại thành thời gian hằng mà không tạo lỗi
  • Bài trình bày tháng 7/2024 giới thiệu một số hàm thời gian hằng do libmceliece và SUPERCOP cung cấp
    • Tên tệp là crypto_{int,uint}{8,16,32,64}.h
    • Các tệp này có thể được sao chép sang dự án khác để sử dụng
  • Hàm ví dụ crypto_uint32_bitmod_mask(x,j) có hiệu quả như -((x>>(j&31))&1), nhưng khiến trình biên dịch không nhìn thấy kết quả 1 bit
  • Một ví dụ phức tạp hơn là crypto_uint32_max(x,y)
  • Bài báo năm 2018 bàn về một tweak thêm hàm thời gian hằng __builtin_ct_choose(bool cond, x, y) vào Clang/LLVM
    • Bài báo đó đã đề xuất sai rằng chỉ một hàm này là đủ
    • Hàm này có thể một ngày nào đó được đưa vào trình biên dịch, nhưng có thể mất nhiều thời gian trước khi các dự án có thể phụ thuộc vào nó
    • Cách triển khai được đánh giá là có vẻ mong manh hơn crypto_{int,uint}{8,16,32,64}.h

Cách tránh vấn đề từ trước

  • Nếu kiểm thử trước khi phát hành thư viện đã biên dịch bắt được rò rỉ timing do trình biên dịch đưa vào, có thể dùng phiên bản trình biên dịch cũ để phát hành trong lúc viết lại mã
    • Cách này là biện pháp tạm thời để tiếp tục giữ an toàn cho người dùng
  • Một giải pháp là phát hành thư viện dưới dạng assembly
    • Bài trình bày RWC 2024 Adoption of high-assurance and highly performant cryptographic algorithms at AWS giới thiệu phần mềm X25519 nhanh, được chứng minh là tính đúng X25519 trên mọi đầu vào
    • Triển khai được viết bằng assembly với 2 phiên bản cho CPU Intel/AMD 64-bit và 2 phiên bản cho CPU ARM 64-bit
    • Mệnh đề đúng đắn là định lý về mã máy mà người dùng thực sự chạy, và chứng minh được kiểm chứng bằng bộ chứng minh định lý HOL Light
  • Tuy vậy, với phần mềm mật mã chưa đạt đến mức này, vấn đề độ khó khi audit assembly vẫn còn đó
  • Với mã viết bằng C, C++..., cũng đang có hướng tìm cách nhanh chóng đưa “vaccine” chống rò rỉ timing vào

Thử nghiệm bản vá clang-vs-clang

  • Điểm chung của x&1x>>31 là kết quả chỉ có hai khả năng
    • x&10 hoặc 1
    • x>>31 của uint320 hoặc 1
    • x>>31 của int320 hoặc -1
  • Những dạng này dễ bị người viết “tối ưu hóa” trình biên dịch đưa kết quả 1 bit vào bool
  • Có khuyến nghị luôn biên dịch với -fwrapv để GCC và Clang giả định số học bù hai
  • Chỉ cần quét nguồn tìm &1, 1&, >>31... cũng cho ra nhiều ví dụ, nhưng tác giả đã quét theo cách khác bằng cách đưa bản vá trực tiếp vào “optimizer” của LLVM
  • Bản vá bắt đầu từ LLVM commit 68df06a0b2998765cb0a41353fcf0919bbf57ddb, tìm &1>>31 rồi phát cảnh báo sau
    • please take this away before clang does something bad
  • Lệnh biên dịch ví dụ là clang -Rpass-analysis=clang-vs-clang -O -c x.c
  • Hàm kiểm thử như sau
int sra31(int x)
    {
      x >>= 31;
      return x;
    }
  • Việc cùng một cảnh báo lặp lại không có gì đáng ngạc nhiên
    • Trình biên dịch tiếp tục thử áp dụng “tối ưu hóa” cho đến khi không còn tiến triển nữa
  • Đầu ra clang-vs-clang phân biệt signedunsigned trong phép shift
    • Khác biệt này quan trọng đối với việc viết lại thủ công hoặc tự động dựa trên crypto_{int,uint}{8,16,32,64}.h
    • Một cách tự động hóa chuyển đổi nguồn là dùng clang-tidy
  • Mã bị loại bởi #ifdef hoặc đã bị xóa trước bước “tối ưu hóa” này sẽ không tạo cảnh báo clang-vs-clang

Kết quả chạy SUPERCOP và các trường hợp phát hiện

  • SUPERCOP 20240716 được chạy bằng ./data-do-biglittle trên dual EPYC 7742
    • Ép xung bị tắt
    • Danh sách trình biên dịch SUPERCOP được điều chỉnh để dùng clang-vs-clang bằng cách thêm -Rpass-analysis=clang-vs-clang vào dòng clang trong okcompilers/{c,cpp}
  • Kết quả sẵn sàng sau 3 giờ
    • Đầu ra Clang tổng cộng 675.752 dòng
    • Kích thước gốc 210.786.494 byte
    • Kết quả nén là 20240803-fromclang.txt.gz dung lượng 3.595.199 byte
  • Trong đầu ra có nhiều nhiễu do các nhánh nguồn dựa trên public data tạo ra &1 bên trong Clang
  • Ví dụ rõ ràng đáng thay đổi trước là như sau
a0 += (a0>>15)&106;
  • Ví dụ cần nỗ lực phân tích C để tìm bằng quét nguồn đơn giản như sau
    • Macro ONE8 được định nghĩa là ((uint8_t)1)
*pk2^=(((* pk_cp)>>ir)&ONE8)<<jr;
  • Ví dụ khó tìm hơn xuất hiện từ macro dựa trên AVX2 intrinsic
    • signmask_x16(x) được định nghĩa là _mm256_srai_epi16((x),15)
    • Nó shift phải 15 bit từng mảnh signed 16-bit bên trong vector 256-bit
mask = signmask_x16(sub_x16(x,const_x16((q+1)/2)));
  • Trường hợp AVX2 này không có ưu tiên cao
    • Để phép toán vector bị đổi thành nhánh điều kiện, phải biên dịch bằng AVX-512 và trình biên dịch phải đưa ra quyết định kỳ lạ là đổi bool đã vector hóa thành nhánh điều kiện bool tuần tự
    • TIMECOP dùng Valgrind, và Valgrind không hỗ trợ AVX-512
    • Hiện tại không khuyến nghị biên dịch AVX-512

int128 và hướng ứng phó rộng hơn

  • Phát hiện thú vị nhất là trường hợp shift phải 64 bit của int128 gây cảnh báo >>
  • Triển khai int128 có thể dùng shift phải 63 bit trong nội bộ để xác định dấu của word 64 bit cao
  • Nếu Clang bổ sung hỗ trợ như GCC, biến shift phải 63 bit thành bool rồi thành nhánh điều kiện, nhiều mã int128 có thể đột ngột trở thành thời gian biến thiên
    • Khi đó tình huống sẽ giống điều mà tiêu đề bài báo năm 2015 đã tuyên bố, nhưng lần này thực sự xảy ra dù nguồn không có bool
  • Cách bảo vệ dễ nhất ở cấp nguồn là tránh triển khai int128 hiện có của trình biên dịch và dùng các hàm crypto_int128
    • Khác với int128 của GCC và Clang, crypto_int128 có thể hoạt động cả trên nền tảng 32-bit nhỏ
  • Ý tưởng thêm kiểu dữ liệu bí mật vào GCC và Clang có vẻ tốt, nhưng với cấu trúc của hai trình biên dịch này, khó thấy cách làm cho nó vững chắc
  • Có nhiều kỳ vọng hơn vào các trình biên dịch được thiết kế ngay từ đầu cho bảo mật
    • Các trình biên dịch hướng bảo mật yêu cầu ngôn ngữ đầu vào mới gồm FaCTJasmin đang được phát triển tích cực
    • Có lo ngại về thời gian viết lại mã, nhưng nhìn vào cách các trình biên dịch hiện tại xử lý mã hiện có, vẫn cần hành động dưới hình thức nào đó

1 bình luận

 
GN⁺ 2024-08-05
Các ý kiến trên Hacker News
  • Không đúng khi gọi đó là lỗi trình biên dịch chỉ vì mã có hành vi không xác định không hoạt động như mong muốn.
    Điều này giống như chạy dd với đối số sai, làm mất dữ liệu rồi nói dd bị lỗi.

    • Có vẻ ở đây tác giả đã nhầm lẫn giữa hành vi do triển khai định nghĩa và hành vi không xác định. Phần lớn các ví dụ trong bài là mã hợp lệ, và vấn đề thực sự nằm ở việc tối ưu hóa của trình biên dịch biến số học thao tác bit thành rẽ nhánh, từ đó khiến mã mật mã có thể bị tấn công timing.
      Khó có thể xem mã nguồn hay trình biên dịch là có lỗi; đúng hơn là chuẩn C được đặc tả quá thiếu theo tiêu chí của tác giả, nên tạo ra lỗi bảo mật trên một số mục tiêu.
      Rốt cuộc, những người viết chuẩn C không thể định nghĩa cả cách phần cứng hoạt động mà chỉ có thể định nghĩa ngữ nghĩa của ngôn ngữ, nên phía mật mã buộc phải khổ sở vì các lỗi do phần cứng gây ra.
    • Vấn đề là hành vi không xác định trong C và C++ nhiều đến phi lý, và cực kỳ khó tránh hết.
      Một trong những ưu điểm của Rust là giới hạn các hành vi không xác định tiềm tàng vào trong các khối unsafe. Dù Rust đã định nghĩa nhiều phần vốn là hành vi không xác định trong C, nhưng khi bước vào mã unsafe, vẫn rất dễ vô tình vấp phải các hành vi không xác định tinh vi.
    • Với người dùng trình biên dịch, chỉ có hai mô hình hành vi không xác định hữu ích: hoặc từ chối biên dịch nếu đó là ý tưởng tồi, hoặc làm một việc hợp lý và ổn định.
      Mô hình thứ ba, âm thầm thất bại và sinh ra mã không thể dự đoán, chỉ hữu ích cho người viết trình biên dịch. Nấp sau đặc tả không mang lại lợi ích cho người dùng thực tế.
    • Bài viết C and C++ Prioritize Performance over Correctness của Russ Cox bàn rất tốt về chủ đề này: https://research.swtch.com/ub
    • Lời phản bác đó gần như là đánh vào người rơm. Điểm cốt lõi là những người viết trình biên dịch tự quyết định cái gì là hành vi không xác định, rồi định nghĩa chuẩn sao cho họ có thêm nhiều dư địa tối ưu hóa hơn.
      Các tối ưu hóa đó phá vỡ những đoạn mã trước đây vẫn chạy tốt. Những người viết trình biên dịch có thể ưu tiên tương thích ngược, nhưng họ không làm vậy.
      Hơn nữa, các tối ưu hóa kiểu này cũng không cải thiện đáng kể hiệu năng mã thực tế, nên cần phản bác lập luận rằng đánh đổi bằng việc phá vỡ mã là không đáng.
  • Tôi thích Bernstein, nhưng đôi khi ông ấy đi sai hướng và trở nên quá khích; bài này là một ví dụ rõ. Ở cuối bài, chính ông ấy cũng gần như thừa nhận một nửa điều đó.
    Một phần lớn bài viết xoay quanh điểm phụ là lợi ích tối ưu hóa tốt đến mức nào, và dù có dữ liệu thì đánh giá vẫn phụ thuộc vào từng trường hợp sử dụng.
    Phàn nàn cốt lõi là trình biên dịch C không xét đến các ngữ nghĩa không thể biểu đạt bằng ngôn ngữ, điều này cũng không có gì đáng ngạc nhiên.
    Ở cuối, ông ấy nói “hãy dùng ngôn ngữ có thể biểu đạt những ngữ nghĩa cần thiết”, và cả bài viết lẽ ra có thể được thay bằng đúng một câu đó.

    • Điểm quan trọng là phía định nghĩa ngữ nghĩa C và C++ ném quá nhiều hành vi vào rổ “hành vi không xác định”.
      Trong số đó, khá nhiều điều có cơ sở đáng ngờ và khiến việc viết chương trình đúng trở nên khó hơn.
    • Phần nói rằng lợi ích tối ưu hóa phụ thuộc vào trường hợp sử dụng là một ngữ cảnh hữu ích và khá mở mang.
    • Ở đây DJB không thuyết phục lắm. Bài thể hiện khá nhiều quan điểm tôn giáo kiểu tinh hoa chủ nghĩa thiếu căn cứ.
  • C và C++ không phù hợp để viết các thuật toán có bảo đảm thời gian hằng.
    Chuẩn gần như không có khái niệm thời gian thực, và trình biên dịch cũng không cung cấp thêm bảo đảm nào dưới dạng phần mở rộng.
    Nhưng đổ lỗi chuyện này cho các nhà phát triển trình biên dịch là sai hướng.

    • Nếu muốn tạo mã máy luôn thực hiện phép toán trong thời gian hằng bất kể có rẽ nhánh hay không, bạn phải dùng ngôn ngữ có thể biểu đạt điều đó. C không hỗ trợ việc này.
    • Tôi tò mò ngôn ngữ nào phù hợp để viết thuật toán có bảo đảm thời gian hằng.
  • Trên CPU Intel, dù là clang hay thứ gì khác cũng không thể sinh mã đúng ở chế độ người dùng, vì ngay từ đầu mã đúng không tồn tại.
    https://www.intel.com/content/www/us/en/developer/articles/t...
    Nhìn vào DOITM trong tài liệu, đơn giản là thư viện mật mã ở không gian người dùng không thể đặt bit cần thiết.

    • Mã chế độ người dùng vẫn có thể chạy trong chế độ đúng. Chỉ là nó không thể tự bật/tắt công tắc cho chế độ đó.
      Một khi đã bật, nó hoạt động ổn trong không gian người dùng, nên chẳng hạn có thể biến nó thành một cờ theo từng tiến trình được kích hoạt qua lệnh gọi hệ thống prctl, rồi để scheduler điều chỉnh MSR khi chuyển tác vụ.
    • Không thể gọi hệ thống vào kernel để đặt cờ, rồi quay lại chế độ người dùng trong trạng thái đó sao?
  • Chỉ riêng câu “bất cứ khi nào có thể, những người viết trình biên dịch từ chối chịu trách nhiệm về các lỗi do chính họ tạo ra” đã cho thấy hiếm có bài blog nào đánh mất tính chuyên môn nhanh đến vậy
    Nếu lần theo cả liên kết, thì đó chỉ là kiến thức C rất cơ bản rằng hành vi không xác định không có nghĩa là tạo ra “giá trị tùy ý”

    • Có vẻ hai bên đang nhìn vào những đối tượng khác nhau rồi gọi là “bug”. Một bên nói về bug trong mã nguồn, bên kia nói về bug trong chương trình được tạo ra
      Ngay cả khi có hành vi không xác định, mã nguồn vẫn là có bug, nhưng chương trình được tạo ra thường vẫn đúng. Về sau, khi người viết trình biên dịch thêm một tối ưu hóa mới và dựa vào hành vi không xác định đó để tạo ra một chương trình có bug, cuộc tranh cãi trách nhiệm bắt đầu
      Phần mà người ta không muốn thừa nhận là trách nhiệm đối với người dùng được chia sẻ cho mọi phía. Nếu pin bốc cháy chỉ vì một ứng dụng CRUD dereference NULL, người bình thường sẽ không chỉ trách người viết ứng dụng đã quên kiểm tra NULL
      Trình biên dịch, hệ điều hành và nhà sản xuất phần cứng cũng phải chịu trách nhiệm về những sản phẩm được thiết kế vô trách nhiệm, chứ không thể kết thúc chỉ bằng cụm từ “hành vi không xác định” theo tiêu chuẩn ISO. Mọi thành viên trong chuỗi cung ứng cùng chia sẻ trách nhiệm dự đoán sản phẩm có thể bị dùng sai như thế nào và xử lý một cách hợp lý
    • Tôi nghĩ tác giả hiểu rõ hành vi không xác định là gì. Chỉ là họ đang nhìn toàn bộ hệ thống theo cách phê phán
      Hành vi không xác định tồn tại để mang lại giá trị. Có thể tạo ra ngôn ngữ mà không cần những thứ như vậy, nhưng lý do nó vẫn được đưa vào là vì tính khả chuyển và sự linh hoạt dành cho người viết trình biên dịch
      Điểm cốt lõi của bài viết là liệu sự linh hoạt đó có xứng đáng khi so với độ khó của việc viết chương trình không có hành vi không xác định hay không
      Tác giả cho rằng số tiền mất vì bug có vẻ lớn hơn số tiền tiết kiệm được nhờ bytecode nhanh hơn, và vì người viết trình biên dịch có ảnh hưởng lớn khi quyết định nội dung đưa vào tiêu chuẩn ngôn ngữ, nên động lực sửa vấn đề này là yếu
  • Nhân tiện, clang có thuộc tính clang::optnone để tắt mọi tối ưu hóa theo từng hàm, còn GCC có thuộc tính gnu::optimize rất hay, cho phép thêm/bỏ tối ưu hóa theo tên hoặc đặt mức tối ưu hóa bất kể các cờ trình biên dịch
    gnu::optimize(0) tương tự cờ clang đó. clang còn có clang::no_builtins để tắt riêng tối ưu hóa memcpymemset

  • Tôi phần nào đồng cảm với mục tiêu mà giới mật mã mong muốn, chẳng hạn đánh giá thời gian hằng định và che giấu giá trị bí mật
    Nhưng trình biên dịch đa dụng hầu hết thời gian không nghĩ đến những thứ đó, nên có vẻ khó vượt quá mức một hack thường hoạt động được
    Nếu muốn làm nghiêm túc, có lẽ cần một trình biên dịch chuyên dụng riêng, hoặc tiếp tục đi bằng assembly

    • Tác giả đã viết một trình biên dịch như vậy rồi: https://cr.yp.to/qhasm.html ít nhất cũng là một prototype như thế
  • Có lẽ một ngày nào đó chúng ta sẽ nhìn lại hiện tại như “thời xưa tồi tệ”, và đã rời bỏ C để dùng những ngôn ngữ có ít hành vi không xác định hơn nhiều
    Trong C, quá dễ để viết những biểu thức vẫn biên dịch được nhưng trình biên dịch hoàn toàn không thể biết ý định là gì
    Ví dụ trong Python có thể viết mã như result = [something(value) for value in set_object]. Vì đối tượng set không có thứ tự, nên rõ ràng thứ tự xử lý các phần tử và thứ tự kết quả là không quan trọng; điều này mở ra nhiều tối ưu hóa ở cấp ngôn ngữ mà không buộc trình biên dịch phải đoán ý định của tác giả
    Mã tương tự trong các ngôn ngữ khác có dữ liệu bất biến còn tiến thêm một bước: vì something(value1) không thể ảnh hưởng đến something(value2), nên có thể chạy song song, dù bằng luồng hay tiến trình
    Một phần lớn tối ưu hóa của trình biên dịch C là nhìn vào mẫu mã và tìm cách làm nhanh hơn điều mà tác giả có lẽ đã định làm. So với các ngôn ngữ hiện đại, C thiếu khả năng biểu đạt ý định nên trình biên dịch có nhiều tự do để suy đoán, nhưng để đạt hiệu năng decent thì vẫn phải suy luận như vậy
    Dù vậy, cũng có thể đây là điều may trong rủi, giống chuyện kính viễn vọng Hubble cần “đeo kính”. Để vượt qua giới hạn, người ta đã tạo ra các kỹ thuật xuất sắc, và sau khi sửa vấn đề, những kỹ thuật đó đem lại hiệu năng cao hơn nhiều so với dự kiến ban đầu. Nếu áp dụng các tối ưu hóa trình biên dịch C cho những ngôn ngữ không phải C, chúng có thể hoạt động như siêu năng lực

    • Nhược điểm của ví dụ Python là dù thứ tự không được đặc tả, người ta vẫn có thể phụ thuộc vào một số thuộc tính nào đó, và nếu bộ tối ưu hóa đổi thứ tự thì mã có thể hỏng
      Về cơ bản nó giống hành vi không xác định, nhưng không biểu hiện ngay thành vấn đề an toàn mà có thể thành kết quả sai. Tất nhiên kết quả sai về sau có thể dẫn đến vấn đề an toàn
      Khác với hành vi không xác định, việc tạo một “sanitizer” để kiểm tra mã có hoạt động với mọi thứ tự set có thể có gần như là bất khả thi
      gccclang có nhiều gợi ý cấp thấp mà các ngôn ngữ khác thường không có. Có __builtin_expect/__builtin_unpredictable, __builtin_unreachable/__builtin_assume, #pragma clang loop vectorize(assume_safety)/#pragma GCC ivdep, các pragma để tắt unroll vòng lặp hay vector hóa, hoặc chọn giá trị cụ thể, v.v.
      Thứ thiếu lớn nhất, theo tôi, là hàng rào tối ưu hóa để chặn rõ ràng việc trình biên dịch suy luận dựa trên nguồn gốc của giá trị. __asm__ phần nào làm được, nhưng có tác dụng phụ không mong muốn và cần tên loại thanh ghi theo từng nền tảng
      Tiềm năng của tối ưu hóa cấp cao dựa trên ý định cũng rõ ràng. Chẳng hạn đặt trước dung lượng cho array list trước khi push n lần trong vòng lặp, gộp các lần tra cứu hashmap kiểu contains→get→put với cùng một khóa, hoặc suy luận cục bộ hành vi cấp phát toàn cục để loại bỏ đối tượng và phép cấp phát
    • Về lý thuyết thì nghe hợp lý, nhưng chưa có gì chứng minh được là nhanh hơn C trong thực tế
      C đủ gần với phần cứng thực để lập trình viên có thể đơn giản nói cần làm gì, nên trình biên dịch không cần đoán ý định của lập trình viên
    • Đúng là có dư địa cho tối ưu hóa dựa trên ngữ nghĩa, nhưng theo quan sát thì những tối ưu hóa như vậy chủ yếu nằm quanh cấp phát bộ nhớ
      Các ngôn ngữ triển khai kiểu tối ưu hóa bộ nhớ đó thường thuộc họ Java, và ngay từ đầu đã có những bi quan hóa chủ động khá mạnh, nên mới có động lực để làm các tối ưu hóa ấy. Nhưng ngay cả các tối ưu hóa đó cũng không bù lại được tổn thất
      Ý chính là C cũng chẳng tốt đẹp gì, nhưng phía còn lại còn tệ hơn
  • Nếu không thích ngữ nghĩa của C thì đừng nổi giận với kỹ sư trình biên dịch, hãy dùng ngôn ngữ lập trình khác

    • Thành thật mà nói, tôi không biết djb có chịu nổi thứ gì ngoài qhasm của chính ông ấy hay không. Kể cả Zig cũng vậy. Nhận xét lần này từ ông ấy không có gì quá bất ngờ
  • Đây là một bài viết mới mẻ, đưa ra góc nhìn hiếm khi được nghe. Nên đọc cùng: https://gavinhoward.com/2023/08/the-scourge-of-00ub/