Đừng chế giễu bộ dự đoán nhánh vui vẻ
- Gần đây đang viết khá nhiều assembly AArch64
- Một ý tưởng "thông minh" nhằm loại bỏ một lệnh nhảy trong vòng lặp lại làm hiệu năng giảm đi
- Giải thích sai lầm này để người khác không lặp lại cùng một lỗi
Ví dụ mã
float run(const float* data, size_t n) {
float g = 0.0;
while (n) {
n--;
const float f = *data++;
foo(f, &g);
}
return g;
}
static void foo(float f, float* g) {
// g를 수정하는 작업
}
Dịch sang assembly AArch64
// x0: const float* data
// x1: size_t n
// s0: float trả về
stp x29, x30, [sp, #-16]!
mov s0, #0.0
loop:
cmp x1, #0
b.eq exit
sub x1, x1, #1
ldr s1, [x0], #4
bl foo
b loop
foo:
// đọc từ s1 và cộng dồn vào s0
// ...
ret
exit:
ldp x29, x30, [sp], #16
ret
Thử tối ưu hóa
- Muốn cải thiện hiệu năng bằng cách giảm lệnh
bl
- Nhưng hiệu năng lại giảm đi
So sánh hiệu năng
- Mã gốc: 969 ns
- Mã tối ưu hóa: 3.85 µs
Phân tích nguyên nhân
- Bộ dự đoán nhánh bị rối vì cặp
bl và ret không khớp nhau
- Theo tài liệu ARM, lệnh
ret giúp dự đoán việc trả về hàm
Cách giải quyết
- Dùng
br x30 thay cho ret
- Hiệu năng phục hồi: 913 ns
Tối ưu hóa bổ sung
- Inline
foo để cải thiện hiệu năng
- Unroll vòng lặp và dùng lệnh SIMD
Hiệu năng cuối cùng
- SIMD + unroll vòng lặp thủ công: 94 ns
Kết luận
- Đừng làm bộ dự đoán nhánh bị rối
- Mã SIMD nhanh hơn, nhưng vì phép cộng số thực dấu chấm động không tuân theo tính kết hợp nên kết quả có thể khác
Ý kiến của GN⁺
- Bài viết này cho thấy rõ tầm quan trọng của tối ưu hóa assembly AArch64
- Hiểu nguyên lý hoạt động của bộ dự đoán nhánh là điều thiết yếu để tối ưu hiệu năng
- Tối ưu hóa bằng lệnh SIMD rất hiệu quả, nhưng cần cân nhắc vấn đề độ chính xác
- Nếu dùng ngôn ngữ bậc cao như Rust, có thể dễ dàng cải thiện hiệu năng nhờ tối ưu hóa của compiler
- Một dự án có chức năng tương tự là hướng dẫn tối ưu hóa assembly của Agner Fog
1 bình luận
Ý kiến trên Hacker News
Đã tóm tắt bài viết cùng với những người bạn từ thời Apple II
Raymond Chen đã bàn về cùng chủ đề này từ gần 20 năm trước
foomà không cần nhánhMã SIMD có thể thực hiện phép cộng theo thứ tự khác vì phép cộng dấu phẩy động không tuân theo tính kết hợp
Từ Rust 1.78 trở đi, trình biên dịch dùng loop unrolling quyết liệt hơn và một chút SIMD
Có người thấy khó hiểu về cách
x0tăng lên trong assembly ARM/ARM64ldr s1, [x0], #4sẽ load rồi tăngx0thêm 4Ngạc nhiên là tác giả không thử cách ít phức tạp hơn để tối ưu mã assembly
foovà bỏ lệnhRETCó ý kiến mong tác giả đừng liên tục đổi đơn vị