Bạn là một nạn nhân của dự đoán chi nhánh thất bại.
Dự đoán chi nhánh là gì?
Hãy xem xét một ngã ba đường sắt:
Hình ảnh của Mecanismo, qua Wikimedia Commons. Được sử dụng theo giấy phép CC-By-SA 3.0 .
Bây giờ để tranh luận, giả sử điều này trở lại vào những năm 1800 - trước khi liên lạc đường dài hoặc vô tuyến.
Bạn là người điều hành một ngã ba và bạn nghe thấy một chuyến tàu đang đến. Bạn không biết nên đi theo hướng nào. Bạn dừng tàu để hỏi tài xế họ muốn đi theo hướng nào. Và sau đó bạn thiết lập công tắc một cách thích hợp.
Xe lửa nặng và có nhiều quán tính. Vì vậy, họ mất mãi mãi để bắt đầu và chậm lại.
Có cách nào tốt hơn? Bạn đoán hướng tàu sẽ đi!
- Nếu bạn đoán đúng, nó tiếp tục.
- Nếu bạn đoán sai, thuyền trưởng sẽ dừng lại, sao lưu và la mắng bạn để lật công tắc. Sau đó, nó có thể khởi động lại con đường khác.
Nếu bạn đoán đúng mỗi lần , tàu sẽ không bao giờ phải dừng lại.
Nếu bạn đoán sai quá thường xuyên , tàu sẽ mất rất nhiều thời gian để dừng lại, sao lưu và khởi động lại.
Xem xét một câu lệnh if: Ở cấp độ bộ xử lý, đó là một lệnh rẽ nhánh:
Bạn là một bộ xử lý và bạn thấy một chi nhánh. Bạn không biết nó sẽ đi theo hướng nào. Bạn làm nghề gì? Bạn dừng thực thi và đợi cho đến khi các hướng dẫn trước hoàn thành. Sau đó, bạn tiếp tục xuống con đường chính xác.
Bộ xử lý hiện đại rất phức tạp và có đường ống dài. Vì vậy, họ mất mãi mãi để "làm nóng" và "chậm lại".
Có cách nào tốt hơn? Bạn đoán hướng đi của chi nhánh!
- Nếu bạn đoán đúng, bạn tiếp tục thực hiện.
- Nếu bạn đoán sai, bạn cần phải xả đường ống và quay trở lại nhánh. Sau đó, bạn có thể khởi động lại con đường khác.
Nếu bạn đoán đúng mọi lúc , việc thực thi sẽ không bao giờ phải dừng lại.
Nếu bạn đoán sai quá thường xuyên , bạn dành nhiều thời gian để trì hoãn, quay trở lại và khởi động lại.
Đây là dự đoán chi nhánh. Tôi thừa nhận đó không phải là sự tương tự tốt nhất vì tàu chỉ có thể báo hiệu hướng đi bằng cờ. Nhưng trong máy tính, bộ xử lý không biết một nhánh sẽ đi theo hướng nào cho đến giây cuối cùng.
Vì vậy, làm thế nào bạn sẽ đoán chiến lược để giảm thiểu số lần tàu phải sao lưu và đi xuống con đường khác? Bạn nhìn vào lịch sử đã qua! Nếu tàu đi trái 99% thời gian, thì bạn đoán trái. Nếu nó thay thế, sau đó bạn thay thế dự đoán của bạn. Nếu nó đi một chiều cứ sau ba lần, bạn đoán giống nhau ...
Nói cách khác, bạn cố gắng xác định một mô hình và làm theo nó. Đây là ít nhiều làm thế nào các dự đoán chi nhánh hoạt động.
Hầu hết các ứng dụng có các nhánh hoạt động tốt. Vì vậy, các dự đoán chi nhánh hiện đại thường sẽ đạt tỷ lệ trúng> 90%. Nhưng khi phải đối mặt với các nhánh không thể đoán trước mà không có mô hình dễ nhận biết, các dự đoán nhánh hầu như vô dụng.
Đọc thêm: bài viết "Dự đoán chi nhánh" trên Wikipedia .
Như được gợi ý từ phía trên, thủ phạm chính là câu lệnh if này:
if (data[c] >= 128)
sum += data[c];
Lưu ý rằng dữ liệu được phân phối đồng đều trong khoảng từ 0 đến 255. Khi dữ liệu được sắp xếp, khoảng nửa đầu của các lần lặp sẽ không nhập câu lệnh if. Sau đó, tất cả chúng sẽ nhập câu lệnh if.
Điều này rất thân thiện với người dự đoán chi nhánh vì chi nhánh liên tiếp đi cùng một hướng nhiều lần. Ngay cả một bộ đếm bão hòa đơn giản cũng sẽ dự đoán chính xác nhánh ngoại trừ một vài lần lặp sau khi nó chuyển hướng.
Hình dung nhanh:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
Tuy nhiên, khi dữ liệu hoàn toàn ngẫu nhiên, bộ dự báo nhánh sẽ vô dụng, vì nó không thể dự đoán dữ liệu ngẫu nhiên. Do đó, có thể sẽ có khoảng 50% hiểu sai (không tốt hơn so với đoán ngẫu nhiên).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
Vậy thì cái gì có thể làm được?
Nếu trình biên dịch không thể tối ưu hóa nhánh thành một động thái có điều kiện, bạn có thể thử một số hack nếu bạn sẵn sàng hy sinh khả năng đọc để thực hiện.
Thay thế:
if (data[c] >= 128)
sum += data[c];
với:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
Điều này loại bỏ nhánh và thay thế nó bằng một số hoạt động bitwise.
(Lưu ý rằng bản hack này không hoàn toàn tương đương với câu lệnh if gốc. Nhưng trong trường hợp này, nó hợp lệ cho tất cả các giá trị đầu vào của data[]
.)
Điểm chuẩn: Core i7 920 @ 3.5 GHz
C ++ - Visual Studio 2010 - Phát hành x64
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java - NetBeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
Quan sát:
- Với chi nhánh: Có một sự khác biệt rất lớn giữa dữ liệu được sắp xếp và chưa được sắp xếp.
- Với Hack: Không có sự khác biệt giữa dữ liệu được sắp xếp và chưa được sắp xếp.
- Trong trường hợp C ++, hack thực sự chậm hơn một chút so với nhánh khi dữ liệu được sắp xếp.
Một nguyên tắc chung là tránh phân nhánh phụ thuộc dữ liệu vào các vòng quan trọng (như trong ví dụ này).
Cập nhật:
GCC 4.6.1 có -O3
hoặc -ftree-vectorize
trên x64 có thể tạo ra một động thái có điều kiện. Vì vậy, không có sự khác biệt giữa dữ liệu được sắp xếp và chưa được sắp xếp - cả hai đều nhanh.
(Hoặc hơi nhanh: đối với trường hợp đã được sắp xếp, cmov
có thể chậm hơn, đặc biệt là nếu GCC đưa nó vào đường dẫn quan trọng thay vì chỉ add
, đặc biệt là trên Intel trước Broadwell, nơi cmov
có độ trễ 2 chu kỳ: cờ tối ưu hóa gcc -O3 làm cho mã chậm hơn -O2 )
VC ++ 2010 không thể tạo ra các động thái có điều kiện cho nhánh này ngay cả dưới /Ox
.
Trình biên dịch Intel C ++ (ICC) 11 làm một điều kỳ diệu. Nó trao đổi hai vòng lặp , do đó nâng các nhánh không thể đoán trước vào vòng lặp bên ngoài. Vì vậy, nó không chỉ miễn dịch với các dự đoán sai, mà còn nhanh gấp đôi so với bất kỳ thứ gì mà VC ++ và GCC có thể tạo ra! Nói cách khác, ICC đã tận dụng vòng kiểm tra để đánh bại điểm chuẩn ...
Nếu bạn cung cấp cho trình biên dịch Intel mã không phân nhánh, nó sẽ hoàn toàn hợp lý hóa nó ... và cũng nhanh như với nhánh (với trao đổi vòng lặp).
Điều này cho thấy rằng ngay cả các trình biên dịch hiện đại trưởng thành cũng có thể thay đổi mạnh mẽ về khả năng tối ưu hóa mã ...