Tối ưu hóa giải quyết ngược cho hệ thống tuyến tính tam giác thấp hơn thưa thớt


8

Tôi có biểu diễn cột thưa (csc) được nén của ma trận tam giác dưới nxn A với các số 0 trên đường chéo chính và muốn giải quyết cho b trong

(A + I)' * x = b

Đây là thói quen tôi có để tính toán này:

void backsolve(const int*__restrict__ Lp,
               const int*__restrict__ Li,
               const double*__restrict__ Lx,
               const int n,
               double*__restrict__ x) {
  for (int i=n-1; i>=0; --i) {
      for (int j=Lp[i]; j<Lp[i+1]; ++j) {
          x[i] -= Lx[j] * x[Li[j]];
      }
  }
}

Do đó, bđược truyền vào thông qua đối số xvà được ghi đè bằng giải pháp. Lp, Li, LxLần lượt là hàng, chỉ số, và con trỏ dữ liệu trong các đại diện csc tiêu chuẩn của ma trận thưa thớt. Chức năng này là điểm nóng hàng đầu trong chương trình, với dòng

x[i] -= Lx[j] * x[Li[j]];

là phần lớn thời gian dành cho. Biên dịch với gcc-8.3 -O3 -mfma -mavx -mavx512fcho

backsolve(int const*, int const*, double const*, int, double*):
        lea     eax, [rcx-1]
        movsx   r11, eax
        lea     r9, [r8+r11*8]
        test    eax, eax
        js      .L9
.L5:
        movsx   rax, DWORD PTR [rdi+r11*4]
        mov     r10d, DWORD PTR [rdi+4+r11*4]
        cmp     eax, r10d
        jge     .L6
        vmovsd  xmm0, QWORD PTR [r9]
.L7:
        movsx   rcx, DWORD PTR [rsi+rax*4]
        vmovsd  xmm1, QWORD PTR [rdx+rax*8]
        add     rax, 1
        vfnmadd231sd    xmm0, xmm1, QWORD PTR [r8+rcx*8]
        vmovsd  QWORD PTR [r9], xmm0
        cmp     r10d, eax
        jg      .L7
.L6:
        sub     r11, 1
        sub     r9, 8
        test    r11d, r11d
        jns     .L5
        ret
.L9:
        ret

Theo vtune,

vmovsd  QWORD PTR [r9], xmm0

là phần chậm nhất. Tôi gần như không có kinh nghiệm về lắp ráp, và tôi không biết làm cách nào để chẩn đoán thêm hoặc tối ưu hóa thao tác này. Tôi đã thử biên dịch với các cờ khác nhau để bật / tắt SSE, FMA, v.v., nhưng không có gì hoạt động.

Bộ xử lý: Xeon Skylake

Câu hỏi Tôi có thể làm gì để tối ưu hóa chức năng này?


Bạn có thể đưa ra giả định rằng i >= Li[j]cho tất cả jtrong vòng lặp bên trong?
chqrlie

AVX512 bao gồm các hướng dẫn phân tán / thu thập và hướng dẫn phát hiện xung đột. Bạn có thể làm như sau: tập hợp các vectơ tải, giả sử tất cả Li[j]đều rời rạc i, kiểm tra giả định với các hướng dẫn phát hiện xung đột, kiểm tra tất cả các is là rời rạc, tính toán, lưu trữ kết quả. Nếu bất kỳ xung đột nào được phát hiện, hãy quay lại thực hiện vô hướng.
EOF

@chqrlie Thật không may. Nhưng chúng ta có i < Li[j] < n. Đã cập nhật câu hỏi để đề cập đến tính chất tam giác dưới của A.
user2476408

Làm thế nào thưa thớt là ma trận? Nó có thể phản tác dụng khi sử dụng thêm chỉ định.
chqrlie

Các yếu tố khác 0%
dùng2476408

Câu trả lời:


2

Đ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.0và cờ trình biên dịch -O3 -march=native( -march=skylaketrê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 benchFillRhscó đ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 benchFillRhscầ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.0chuyể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.0như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ư gcccó 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-loopsmộ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.


Lặp lại lạc hậu: nếu điều này dẫn đến một mẫu truy cập tuần tự hơn, thì việc tìm nạp trước phần cứng có thể trở nên hữu ích. CPU hiện đại có băng thông bộ nhớ khá tốt (đặc biệt đối với mã đơn luồng trên máy tính để bàn / máy tính xách tay), độ trễ bộ nhớ khá khủng . Vì vậy, CT tải trước vào L2 là rất lớn, như độ trễ 12 chu kỳ so với hàng trăm cho DRAM. Hầu hết các trình tải trước CTNH của Intel đều hoạt động tiến hoặc lùi nhưng ít nhất một chỉ hoạt động về phía trước, do đó, trong vòng lặp chung sẽ chuyển tiếp qua bộ nhớ nếu một trong hai lựa chọn là bằng nhau. Nếu không, lạc hậu là tốt.
Peter Cordes

Unrolling: sự khác biệt khác giữa GCC và unang loop unrolling là (với -ffast-math) clang sẽ sử dụng nhiều bộ tích lũy. GCC sẽ hủy đăng ký nhưng không bận tâm tạo ra nhiều chuỗi phụ thuộc để che giấu độ trễ ALU, đánh bại hầu hết các mục đích để giảm các vòng lặp như thế nào xi_temp -=. Mặc dù nếu cải tiến 2. đã biên dịch theo cách chúng ta mong đợi, việc đưa cửa hàng / tải lại độ trễ chuyển tiếp cửa hàng ra khỏi đường dẫn quan trọng, nhưng tăng tốc ít hơn nhiều so với hệ số 2, có vẻ như độ trễ của FP không phải là một nút cổ chai lớn (thay vào đó bộ nhớ / bộ nhớ cache bị mất) hoặc chuỗi dep đủ ngắn để thực thi OoO ẩn.
Peter Cordes

1

Bạn có thể cạo một vài chu kỳ bằng cách sử dụng unsignedthay vì intcho các loại chỉ mục, >= 0dù sao cũng phải :

void backsolve(const unsigned * __restrict__ Lp,
               const unsigned * __restrict__ Li,
               const double * __restrict__ Lx,
               const unsigned n,
               double * __restrict__ x) {
    for (unsigned i = n; i-- > 0; ) {
        for (unsigned j = Lp[i]; j < Lp[i + 1]; ++j) {
            x[i] -= Lx[j] * x[Li[j]];
        }
    }
}

Biên dịch với trình thám hiểm trình biên dịch của Godbolt cho thấy mã hơi khác nhau đối với lớp trong, có khả năng sử dụng đường ống CPU tốt hơn. Tôi không thể kiểm tra, nhưng bạn có thể thử.

Đây là mã được tạo cho vòng lặp bên trong:

.L8:
        mov     rax, rcx
.L5:
        mov     ecx, DWORD PTR [r10+rax*4]
        vmovsd  xmm1, QWORD PTR [r11+rax*8]
        vfnmadd231sd    xmm0, xmm1, QWORD PTR [r8+rcx*8]
        lea     rcx, [rax+1]
        vmovsd  QWORD PTR [r9], xmm0
        cmp     rdi, rax
        jne     .L8

1
Bạn có thể giải thích tại sao điều này sẽ nhanh hơn? Đối với tôi, gcc-9.2.1 tạo ra lắp ráp gần như có hiệu quả tương đương, ngoại trừ trao đổi tải mở rộng ký hiệu với tải chiều rộng đăng ký. Tác động thời gian duy nhất tôi thấy trước là tác động bộ nhớ cache tồi tệ hơn.
EOF

1
@EOF: Tôi đi đến kết luận tương tự. Sử dụng unsignedthay vì size_tvẫn tránh phần mở rộng dấu hiệu mà không có tác động bộ đệm và mã hơi khác nhau, có khả năng cho phép sử dụng đường ống tốt hơn.
chqrlie

Tôi cũng đã thử unsigned, nhưng tôi không thấy bất cứ thứ gì trông giống như đường ống tốt hơn với nó. Đối với tôi, nó trông hơi tệ hơn so với inthoặc size_tmã. Dù sao, nó cũng dường như gccđang cố gắng để lãng phí bộ nhớ bằng cách sử dụng incq %raxvới gcc-9.2.1 -march=skylake-avx512cho intunsignedtrường hợp, nơi incl %raxsẽ tiết kiệm một rex-byte. Hôm nay tôi không ấn tượng với gcc.
EOF

1
@ user2476408: cả icc-19 và clang-9.00 đều hủy vòng lặp, xử lý 2 mục trên mỗi lần lặp.
chqrlie

1
@ user2476408 lắp ráp icc vẫn hoàn toàn vô hướng. Tôi không thấy bất cứ điều gì thú vị ở đây.
EOF
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.