Điều này sẽ phụ thuộc khá nhiều vào mô hình thưa thớt chính xác của ma trận và nền tảng đang được sử dụng. Tôi đã thử nghiệm một vài thứ với gcc 8.3.0
và cờ trình biên dịch -O3 -march=native
( -march=skylake
trên CPU của tôi) trên tam giác dưới của ma trận có kích thước 3006 này với 19554 mục nhập khác. Hy vọng rằng điều này hơi gần với thiết lập của bạn, nhưng trong mọi trường hợp tôi hy vọng những điều này có thể cho bạn ý tưởng về nơi bắt đầu.
Để tính thời gian tôi đã sử dụng google / điểm chuẩn với tệp nguồn này . Nó xác định benchBacksolveBaseline
điểm chuẩn nào cho việc thực hiện được đưa ra trong câu hỏi và benchBacksolveOptimized
điểm chuẩn nào cho việc triển khai "tối ưu hóa" được đề xuất. Ngoài ra còn benchFillRhs
có điểm chuẩn riêng biệt chức năng được sử dụng trong cả hai để tạo ra một số giá trị không hoàn toàn tầm thường cho phía bên tay phải. Để có được thời gian của backsolves "tinh khiết", thời gian benchFillRhs
cần phải được trừ đi.
1. Lặp lại hoàn toàn ngược
Vòng lặp bên ngoài trong triển khai của bạn lặp qua các cột ngược, trong khi vòng lặp bên trong lặp lại qua cột hiện tại về phía trước. Có vẻ như nó sẽ phù hợp hơn để lặp qua từng cột ngược lại:
for (int i=n-1; i>=0; --i) {
for (int j=Lp[i+1]-1; j>=Lp[i]; --j) {
x[i] -= Lx[j] * x[Li[j]];
}
}
Điều này hầu như không thay đổi lắp ráp ( https://godbolt.org/z/CBZAT5 ), nhưng thời gian chuẩn cho thấy một sự cải thiện có thể đo lường được:
------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
benchFillRhs 2737 ns 2734 ns 5120000
benchBacksolveBaseline 17412 ns 17421 ns 829630
benchBacksolveOptimized 16046 ns 16040 ns 853333
Tôi cho rằng điều này là do truy cập bộ đệm có thể dự đoán được bằng cách nào đó, nhưng tôi đã không xem xét thêm về nó.
2. Tải ít hơn / lưu trữ trong vòng lặp bên trong
Như A là tam giác thấp hơn, chúng ta có i < Li[j]
. Do đó, chúng tôi biết rằng x[Li[j]]
sẽ không thay đổi do những thay đổi x[i]
trong vòng lặp bên trong. Chúng ta có thể đưa kiến thức này vào việc thực hiện bằng cách sử dụng một biến tạm thời:
for (int i=n-1; i>=0; --i) {
double xi_temp = x[i];
for (int j=Lp[i+1]-1; j>=Lp[i]; --j) {
xi_temp -= Lx[j] * x[Li[j]];
}
x[i] = xi_temp;
}
Điều này làm cho việc gcc 8.3.0
chuyển cửa hàng vào bộ nhớ từ bên trong vòng lặp bên trong đến trực tiếp sau khi kết thúc ( https://godbolt.org/z/vM4gPD ). Điểm chuẩn cho ma trận thử nghiệm trên hệ thống của tôi cho thấy một sự cải thiện nhỏ:
------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
benchFillRhs 2737 ns 2740 ns 5120000
benchBacksolveBaseline 17410 ns 17418 ns 814545
benchBacksolveOptimized 15155 ns 15147 ns 887129
3. Bỏ vòng lặp
Mặc dù clang
đã bắt đầu hủy đăng ký vòng lặp sau khi thay đổi mã được đề xuất đầu tiên, gcc 8.3.0
nhưng vẫn không có. Vì vậy, hãy thử xem bằng cách vượt qua -funroll-loops
.
------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
benchFillRhs 2733 ns 2734 ns 5120000
benchBacksolveBaseline 15079 ns 15081 ns 953191
benchBacksolveOptimized 14392 ns 14385 ns 963441
Lưu ý rằng đường cơ sở cũng được cải thiện, vì vòng lặp trong quá trình thực hiện đó cũng không được kiểm soát. Phiên bản tối ưu hóa của chúng tôi cũng có lợi một chút từ việc không kiểm soát vòng lặp, nhưng có thể không nhiều như chúng tôi có thể thích. Nhìn vào hội đồng được tạo ra ( https://godbolt.org/z/_LJC5f ), có vẻ như gcc
có thể đã đi hơi xa với 8 lần hủy đăng ký. Đối với thiết lập của tôi, trên thực tế tôi có thể làm tốt hơn một chút chỉ với một lần hủy thủ công đơn giản. Vì vậy, thả cờ -funroll-loops
một lần nữa và thực hiện việc hủy đăng ký với một cái gì đó như thế này:
for (int i=n-1; i>=0; --i) {
const int col_begin = Lp[i];
const int col_end = Lp[i+1];
const bool is_col_nnz_odd = (col_end - col_begin) & 1;
double xi_temp = x[i];
int j = col_end - 1;
if (is_col_nnz_odd) {
xi_temp -= Lx[j] * x[Li[j]];
--j;
}
for (; j >= col_begin; j -= 2) {
xi_temp -= Lx[j - 0] * x[Li[j - 0]] +
Lx[j - 1] * x[Li[j - 1]];
}
x[i] = xi_temp;
}
Với điều đó tôi đo lường:
------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
benchFillRhs 2728 ns 2729 ns 5090909
benchBacksolveBaseline 17451 ns 17449 ns 822018
benchBacksolveOptimized 13440 ns 13443 ns 1018182
Các thuật toán khác
Tất cả các phiên bản này vẫn sử dụng cùng một cách thực hiện đơn giản cho việc giải quyết ngược trên cấu trúc ma trận thưa thớt. Do đó, hoạt động trên các cấu trúc ma trận thưa thớt như thế này có thể có vấn đề đáng kể với lưu lượng bộ nhớ. Ít nhất là đối với các yếu tố ma trận, có các phương thức tinh vi hơn, hoạt động trên các mô hình con dày đặc được lắp ráp từ cấu trúc thưa thớt. Ví dụ là phương pháp siêu nhiên và đa phương. Tôi hơi mơ hồ về điều này, nhưng tôi nghĩ rằng các phương pháp như vậy cũng sẽ áp dụng ý tưởng này để bố trí và sử dụng các phép toán ma trận dày đặc cho các giải pháp ngược hình tam giác thấp hơn (ví dụ cho các yếu tố kiểu Cholesky). Vì vậy, có thể đáng để xem xét các loại phương thức đó, nếu bạn không bị buộc phải tuân theo phương pháp đơn giản hoạt động trực tiếp trên cấu trúc thưa thớt. Xem ví dụ khảo sát này bởi Davis.
i >= Li[j]
cho tất cảj
trong vòng lặp bên trong?