Tối ưu hóa biên dịch
Ngôn ngữ thường dùng trong OI là C++. Đã dùng C++ thì phải làm việc với compiler và chuẩn. C++ rất “hỗn loạn và nguy hiểm”; bài này đưa kiến thức compiler thực dụng, đủ cho thi.
Giới thiệu tối ưu hóa compiler
Tối ưu hóa là gì (Optimization)
Theo as-if rule, trong khi giữ nguyên ngữ nghĩa, tối ưu tốc độ chạy và/hoặc kích thước file thực thi.
Các tối ưu phổ biến
Gập hằng (Constant Folding)
Gập hằng, hay truyền bá hằng (Constant Propagation): nếu một biểu thức có thể xác định là hằng, thì trước định nghĩa tiếp theo, có thể thay bằng hằng.
1 2 3 4 5 | |
Compiler có thể biến thành:
1 2 3 4 5 | |
Ví dụ: https://godbolt.org/z/oEfY35TTd
Loại bỏ mã chết (Deadcode Elimination)
Mã không dùng sẽ bị bỏ.
1 2 3 4 5 6 | |
Có thể thành:
1 | |
Ở đây trước tiên gập hằng, rồi a, b là biến không hoạt động nên bị xóa.
Xoay vòng lặp (Loop Rotate)
Chuyển vòng for thành do-while, thêm điều kiện trước. Mục đích là chuẩn bị cho tối ưu khác.
1 2 3 4 | |
Thành:
1 2 3 4 5 6 7 | |
Đưa bất biến ra ngoài (Loop Invariant Code Motion)
Dựa trên phân tích alias, đưa đoạn bất biến ra ngoài vòng để giảm code trong vòng.
1 2 3 4 | |
Trực quan có thể thành:
1 2 3 4 | |
Nhưng nếu n <= 0, vòng không chạy; ta lại thực hiện lệnh thừa (có thể có side effect). Vì vậy thường rotate sang do-while rồi thêm “loop guard”:
1 2 3 4 5 6 7 | |
Mở rộng vòng lặp (Loop Unroll)
Vòng lặp có thân và nhánh, CPU cần dự đoán nhánh. Unroll dùng kích thước code đổi lấy tốc độ.
1 2 3 | |
Thành:
1 2 3 | |
Đưa điều kiện ra ngoài (Loop Unswitching)
Đưa điều kiện trong vòng ra ngoài, tạo 2 vòng riêng, tăng khả năng vector hóa/ song song.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
Tối ưu bố cục code (Code Layout Optimizations)
Khi chạy, đường đi có “nóng/lạnh”. Nhảy thường chậm hơn chạy tuần tự (fallthrough). Code hay chạy là nóng; ít chạy là lạnh. Trong OI, kiểm tra biên hoặc xử lý ngoại lệ thường là lạnh.
Basic block là khối điều khiển cơ bản; thủ tục là đồ thị các block. Khi tạo executable, compiler sắp xếp layout; đây là trọng tâm tối ưu.
Nguyên tắc: ghép nóng gần nhau, lạnh tách ra, giúp cache tốt hơn.
1 2 3 4 5 6 | |
Sắp xếp basic block
Dùng label mô tả “mã giả”. Ví dụ:
Layout 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Layout tốt hơn:
Layout 2
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Để báo nhánh dễ xảy ra, dùng C++20 [[likely]], [[unlikely]]: https://en.cppreference.com/w/cpp/language/attributes/likely
Nếu không có C++20, dùng __builtin_expect (GNU extension):
1 2 3 4 5 6 | |
Tách code nóng/lạnh (Hot Cold Splitting)
Một thủ tục có cả nóng và lạnh; nếu lạnh dài, nên tách thành hàm gọi, thay vì chặn đường nóng. Điều này nhắc ta không nên ép mọi hàm inline: code lạnh nội tuyến làm chậm hơn gọi hàm.
Bố cục không tốt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Bố cục tốt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Tách nóng/lạnh là thao tác ngược của inlining. Inlining không luôn nhanh hơn; nếu inline code lạnh thì chậm hơn. Compiler có mô hình chi phí, tự quyết định inline; ta tự quyết không chắc tốt hơn.
Không có thông tin thêm, compiler giả định nhánh true/false có xác suất như nhau. PGO (Profile Guided Optimization) dùng profiling để lấy xác suất thực, giúp layout tốt hơn.
Inline (Function Inlining)
Gọi hàm cần truyền tham số qua thanh ghi/stack; caller và callee phải lưu trạng thái, gọi là calling convention. Inline là chèn thân hàm vào nơi gọi, tránh overhead.
1 2 3 4 5 6 | |
Có thể thành:
1 2 3 4 | |
always_inline, __force_inline
https://clang.llvm.org/docs/AttributeReference.html#always-inline-force-inline
Một số compiler cho phép ép inline bằng __attribute__((always_inline)). Không chắc nhanh hơn; compiler tin rằng lập trình viên biết rõ hơn.
Tối ưu gọi đuôi (Tail Call Optimization)
Nếu lời gọi hàm ở cuối hàm, gọi là tail call. Với tail call, có thể tối ưu vì không cần giữ stack frame của hàm ngoài.
Dùng jump thay vì call
Gọi hàm thường lưu $pc, lưu registers... Tail call không cần, có thể dịch thành jump. Vì tail recursion không quay lại vị trí cũ.
Ví dụ: https://godbolt.org/z/e7b1safaW
1 2 3 | |
1 2 | |
Tự động chuyển tail recursion
Nếu tail call gọi chính nó, là tail recursion. Có thể tối ưu thành vòng lặp, giảm stack. Khi bật tối ưu, compiler có thể làm thay.
1 2 3 4 | |
Viết lại:
1 2 3 4 | |
Compiler có thể tự nhận dạng và biến đổi.
Loại bỏ tail recursion -Rpass=tailcallelim
Nếu tail recursion, có thể loại bỏ hoàn toàn. Ví dụ:
GCD
1 | |
Fibonacci
1 2 3 4 5 6 | |
Giai thừa
1 2 3 4 5 | |
Các hàm này sau tối ưu có assembly tương tự phiên bản không đệ quy. Với O2, có thể viết đệ quy mà không thua hiệu năng, nếu compiler tối ưu được.
Giảm độ mạnh (Strength Reduction)
Ví dụ x * 2 thành x << 1. Compiler tự tối ưu; khi bật tối ưu, hai cách tương đương.
Biến đổi toán tử vô hướng
Dịch bit thay nhân
1 2 3 | |
Lưu ý khác biệt giữa số có dấu và không dấu; phép dịch có khác biệt. Chia số có dấu không thể luôn tối ưu thành shift.
1 2 3 4 5 6 7 8 | |
1 2 3 4 | |
Giải pháp:
- dùng
unsigned l, r;vì chỉ số nên không dấu - tự viết shift
Nhân thay chia
1 | |
Có thể biến thành x = a * 0x55555556 >> 32, xem Zhihu hoặc paper.
Strength reduction cho biến chỉ số (IndVars)
Compiler nhận dạng biến chỉ số và biến đổi phép toán nặng thành nhẹ.
1 2 3 4 5 | |
Compiler có thể biến a = 3 * i thành a = a + 3. Phân tích tiến hóa biến gọi là SCEV.
SCEV còn tối ưu:
1 2 3 4 5 6 7 | |
Có thể tối ưu thành O(1); xem https://godbolt.org/z/ET8d89vvK. Hiện chỉ LLVM làm, GCC bảo thủ hơn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Tự động vector hóa (Auto-Vectorization)
SIMD giúp xử lý nhiều dữ liệu trong một lệnh. OI không cần chi tiết; Clang thường vector hóa mạnh hơn GCC.
1 2 3 4 5 6 | |
__restrict (GNU, MSVC)
Hai con trỏ có thể trỏ vùng chồng lấn, khiến khó vector hóa. __restrict giả định không chồng lấn.

1 2 3 4 5 | |
__restrict không thuộc chuẩn C++ nhưng compiler hỗ trợ. Hữu ích khi cực kỳ tối ưu.
Lỗi dùng ngôn ngữ liên quan tối ưu
inline
Bật O2 thì compiler tự inline. inline trong struct thường thừa. Nếu không bật O2, inline cũng không chắc inline. Trong C++ hiện đại, inline là về liên kết, không phải tối ưu.
register
Compiler hiện đại bỏ qua register; tự phân bổ tốt hơn. register bị bỏ từ C++11 và xóa ở C++171.
https://en.cppreference.com/w/cpp/keyword/register
UB (Undefined Behavior) và tối ưu
Compiler có thể giả định không có UB, nên khi có UB, tối ưu có thể gây kết quả bất ngờ.
UB thường gặp:
- Tràn số có dấu
- Dùng biến chưa init
- Truy cập vượt giới hạn
- Dereference null
- Vòng lặp vô hạn không side-effect
Tràn số có dấu
1 | |
Compiler có thể tối ưu thành:
1 | |
Ví dụ: https://godbolt.org/z/WKv3W5hvM、https://godbolt.org/z/qqE9nxP1j.
Dùng -fwrapv để tắt giả định này. Ví dụ: https://godbolt.org/z/5x3K5KGnr、https://godbolt.org/z/4r4a4EzMW.
Dùng biến chưa init
1 2 3 4 5 6 | |
Compiler giả định không UB nên a luôn init; có thể tối ưu thành:
1 | |
Ví dụ: https://godbolt.org/z/8WYMYYjdG、https://godbolt.org/z/qvGd1nvv9.
Truy cập vượt giới hạn
1 2 3 4 5 6 7 8 | |
Compiler giả định không UB nên hàm luôn trả true trước khi out-of-bounds:
1 | |
Ví dụ: https://godbolt.org/z/xfePeYsE3.
Dereference null
1 2 3 4 5 6 7 | |
Compiler giả định không UB, nên !p luôn false, tối ưu thành:
1 | |
Ví dụ: https://godbolt.org/z/GY1jvsrb5、https://godbolt.org/z/4ronPsnxf.
Vòng lặp vô hạn không side-effect
Kiểm chứng định lý Fermat lớn
Theo Fermat lớn, phương trình \(a^3=b^3+c^3\) không có nghiệm nguyên dương. Dưới đây thử kiểm chứng trong [1,1000], nếu trả true thì tìm thấy nghiệm (mâu thuẫn).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
Compiler có thể giả định vòng lặp sẽ kết thúc và trả true, nên chương trình có thể in:
1 | |
Ví dụ: https://godbolt.org/z/d834MK7bz、https://godbolt.org/z/Eov9nsKqf.
Sanitizer
“Bảo đảm sự tỉnh táo”: kiểm tra UB, out-of-bounds, null... khi chạy. Debug local nên bật để giảm thời gian. Google phát triển; GCC/Clang đều hỗ trợ. LLVM hỗ trợ tốt hơn, nên dùng Clang khi debug.
Address Sanitizer -fsanitize=address
https://clang.llvm.org/docs/AddressSanitizer.html
GCC/Clang đều hỗ trợ. Kiểm tra:
- Out-of-bounds
- Use-after-free
- Use-after-return
- Double-free
- Memory-leaks
- Use-after-scope
Chạy chậm ~2x.
Undefined Behavior Sanitizer -fsanitize=undefined
https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
UBSan kiểm tra UB, gồm:
- Tràn dịch bit (ví dụ 32-bit dịch trái 72)
- Tràn số có dấu
- Tràn khi convert float->int
Xem trang doc để biết tác động.
Misc
Compiler Explorer
Xem hành vi compiler và assembly tại https://godbolt.org
Đọc thêm
Tài liệu và chú thích
Last updated on this page:, Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:inclyc
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply