簡體   English   中英

gcc -O0在2的冪(矩陣換位)矩陣大小上表現優於-O3

[英]gcc -O0 outperforming -O3 on matrix sizes that are powers of 2 (matrix transpositions)

(出於測試目的)我編寫了一個簡單的方法來計算nxn矩陣的轉置

void transpose(const size_t _n, double* _A) {
    for(uint i=0; i < _n; ++i) {
        for(uint j=i+1; j < _n; ++j) {
            double tmp  = _A[i*_n+j];
            _A[i*_n+j] = _A[j*_n+i];
            _A[j*_n+i] = tmp;
        }
    }
}

當使用優化級別O3或Ofast時,我期望編譯器展開一些循環,這將導致更高的性能,尤其是當矩陣大小是2的倍數(即,每次迭代可以執行雙循環體)或類似時。 相反,我測量的恰恰相反。 2的權力實際上表明執行時間顯着增加。

這些尖峰也是64的固定間隔,間隔128更明顯,依此類推。 每個尖峰延伸到相鄰的矩陣大小,如下表所示

size n  time(us)
1020    2649
1021    2815
1022    3100
1023    5428
1024    15791
1025    6778
1026    3106
1027    2847
1028    2660
1029    3038
1030    2613

我使用gcc版本4.8.2編譯但是同樣的事情發生在clang 3.5上,所以這可能是一些通用的東西?

所以我的問題基本上是:為什么執行時間周期性增加? 是否有一些通用的東西與任何優化選項一起出現(就像clang和gcc一樣)? 如果是這樣的優化選項導致了這個?

這怎么可能如此重要,即使O0版本的程序在512的倍數時優於03版本?

執行時間與O0和O3的矩陣大小


編輯:注意此(對數)圖中峰值的大小。 轉換具有優化的1024x1024矩陣實際上花費的時間與在沒有優化的情況下轉置1300x1300矩陣一樣多。 如果這是一個緩存故障/頁面錯誤問題,那么有人需要向我解釋為什么內存布局對於程序的優化版本來說是如此顯着不同,它失敗了2的權限,只是為了恢復高性能稍大的矩陣。 緩存故障是否應該創建更多類似步驟的模式? 為什么執行時間會再次下降? (為什么優化會創建以前不存在的緩存錯誤?)


編輯:以下應該是gcc生成的匯編代碼

沒有優化(O0):

_Z9transposemRPd:
.LFB0:
    .cfi_startproc
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    mov rbp, rsp
    .cfi_def_cfa_register 6
    mov QWORD PTR [rbp-24], rdi
    mov QWORD PTR [rbp-32], rsi
    mov DWORD PTR [rbp-4], 0
    jmp .L2
.L5:
    mov eax, DWORD PTR [rbp-4]
    add eax, 1
    mov DWORD PTR [rbp-8], eax
    jmp .L3
.L4:
    mov rax, QWORD PTR [rbp-32]
    mov rdx, QWORD PTR [rax]
    mov eax, DWORD PTR [rbp-4]
    imul    rax, QWORD PTR [rbp-24]
    mov rcx, rax
    mov eax, DWORD PTR [rbp-8]
    add rax, rcx
    sal rax, 3
    add rax, rdx
    mov rax, QWORD PTR [rax]
    mov QWORD PTR [rbp-16], rax
    mov rax, QWORD PTR [rbp-32]
    mov rdx, QWORD PTR [rax]
    mov eax, DWORD PTR [rbp-4]
    imul    rax, QWORD PTR [rbp-24]
    mov rcx, rax
    mov eax, DWORD PTR [rbp-8]
    add rax, rcx
    sal rax, 3
    add rdx, rax
    mov rax, QWORD PTR [rbp-32]
    mov rcx, QWORD PTR [rax]
    mov eax, DWORD PTR [rbp-8]
    imul    rax, QWORD PTR [rbp-24]
    mov rsi, rax
    mov eax, DWORD PTR [rbp-4]
    add rax, rsi
    sal rax, 3
    add rax, rcx
    mov rax, QWORD PTR [rax]
    mov QWORD PTR [rdx], rax
    mov rax, QWORD PTR [rbp-32]
    mov rdx, QWORD PTR [rax]
    mov eax, DWORD PTR [rbp-8]
    imul    rax, QWORD PTR [rbp-24]
    mov rcx, rax
    mov eax, DWORD PTR [rbp-4]
    add rax, rcx
    sal rax, 3
    add rdx, rax
    mov rax, QWORD PTR [rbp-16]
    mov QWORD PTR [rdx], rax
    add DWORD PTR [rbp-8], 1
.L3:
    mov eax, DWORD PTR [rbp-8]
    cmp rax, QWORD PTR [rbp-24]
    jb  .L4
    add DWORD PTR [rbp-4], 1
.L2:
    mov eax, DWORD PTR [rbp-4]
    cmp rax, QWORD PTR [rbp-24]
    jb  .L5
    pop rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z9transposemRPd, .-_Z9transposemRPd
    .ident  "GCC: (Debian 4.8.2-15) 4.8.2"
    .section    .note.GNU-stack,"",@progbits

優化(O3)

_Z9transposemRPd:
.LFB0:
    .cfi_startproc
    push    rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    xor r11d, r11d
    xor ebx, ebx
.L2:
    cmp r11, rdi
    mov r9, r11
    jae .L10
    .p2align 4,,10
    .p2align 3
.L7:
    add ebx, 1
    mov r11d, ebx
    cmp rdi, r11
    mov rax, r11
    jbe .L2
    mov r10, r9
    mov r8, QWORD PTR [rsi]
    mov edx, ebx
    imul    r10, rdi
    .p2align 4,,10
    .p2align 3
.L6:
    lea rcx, [rax+r10]
    add edx, 1
    imul    rax, rdi
    lea rcx, [r8+rcx*8]
    movsd   xmm0, QWORD PTR [rcx]
    add rax, r9
    lea rax, [r8+rax*8]
    movsd   xmm1, QWORD PTR [rax]
    movsd   QWORD PTR [rcx], xmm1
    movsd   QWORD PTR [rax], xmm0
    mov eax, edx
    cmp rdi, rax
    ja  .L6
    cmp r11, rdi
    mov r9, r11
    jb  .L7
.L10:
    pop rbx
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z9transposemRPd, .-_Z9transposemRPd
    .ident  "GCC: (Debian 4.8.2-15) 4.8.2"
    .section    .note.GNU-stack,"",@progbits

執行時間的周期性增加必須歸因於緩存僅是N路關聯而不是完全關聯。 您正在目睹與緩存行選擇算法相關的哈希沖突。

最快的L1緩存具有比下一級L2更少的緩存行數。 在每個級別中,每個高速緩存行只能從有限的一組源中填充。

高速緩存行選擇算法的典型硬件實現將僅使用來自存儲器地址的少量位來確定應在哪個高速緩存槽中寫入數據 - 在HW位移位中是空閑的。

這導致存儲器范圍之間的競爭,例如在地址0x300010和0x341010之間。 在完全順序算法中,這無關緊要 - N對於幾乎所有形式的算法足夠大:

 for (i=0;i<1000;i++) a[i] += b[i] * c[i] + d[i];

但是當輸入(或輸出)的數量變大時,這在算法被優化時在內部發生,在高速緩存中具有一個輸入迫使另一輸入離開高速緩存。

 // one possible method of optimization with 2 outputs and 6 inputs
 // with two unrelated execution paths -- should be faster, but maybe it isn't
 for (i=0;i<500;i++) { 
       a[i]     += b[i]     * c[i]     + d[i];
       a[i+500] += b[i+500] * c[i+500] + d[i+500];
 }

示例5中的圖表:高速緩存關聯性說明矩陣行之間的512字節偏移是特定系統的全局最壞情況維度。 當這已知時,工作緩解是將矩陣水平過度分配到某個其他維度char matrix[512][512 + 64]

性能的提高可能與CPU / RAM緩存有關。

當數據不是2的冪時,高速緩存行負載(如16,32或64個字)的轉移比捆綁總線所需的數據傳輸更多 - 結果是無用的。 對於功率為2的數據集,使用所有預取數據。

我打賭如果你要禁用L1和L2緩存,性能將是完全平滑和可預測的。 但它會慢得多。 緩存真的有助於提高性能!

用代碼注釋:在-O3情況下,用

#include <cstdlib>

extern void transpose(const size_t n, double* a)
{
    for (size_t i = 0; i < n; ++i) {
        for (size_t j = i + 1; j < n; ++j) {
            std::swap(a[i * n + j], a[j * n + i]); // or your expanded version.
        }
    }
}

用...編譯

$ g++ --version
g++ (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
...
$ g++ -g1 -std=c++11 -Wall -o test.S -S test.cpp -O3

我明白了

_Z9transposemPd:
.LFB68:
    .cfi_startproc
.LBB2:
    testq   %rdi, %rdi
    je  .L1
    leaq    8(,%rdi,8), %r10
    xorl    %r8d, %r8d
.LBB3:
    addq    $1, %r8
    leaq    -8(%r10), %rcx
    cmpq    %rdi, %r8
    leaq    (%rsi,%rcx), %r9
    je  .L1
    .p2align 4,,10
    .p2align 3
.L10:
    movq    %r9, %rdx
    movq    %r8, %rax
    .p2align 4,,10
    .p2align 3
.L5:
.LBB4:
    movsd   (%rdx), %xmm1
    movsd   (%rsi,%rax,8), %xmm0
    movsd   %xmm1, (%rsi,%rax,8)
.LBE4:
    addq    $1, %rax
.LBB5:
    movsd   %xmm0, (%rdx)
    addq    %rcx, %rdx
.LBE5:
    cmpq    %rdi, %rax
    jne .L5
    addq    $1, %r8
    addq    %r10, %r9
    addq    %rcx, %rsi
    cmpq    %rdi, %r8
    jne .L10
.L1:
    rep ret
.LBE3:
.LBE2:
    .cfi_endproc

如果我添加-m32,那就完全不同了。

(注意:無論我使用std :: swap還是你的變體,它對程序集沒有任何影響)

但是,為了理解導致峰值的原因,您可能希望可視化正在進行的內存操作。

要添加到其他人: g++ -std=c++11 -march=core2 -O3 -c -S -gcc version 4.8.2(MacPorts gcc48 4.8.2_0) - x86_64-apple-darwin13.0.0:

__Z9transposemPd:
LFB0:
        testq   %rdi, %rdi
        je      L1
        leaq    8(,%rdi,8), %r10
        xorl    %r8d, %r8d
        leaq    -8(%r10), %rcx
        addq    $1, %r8
        leaq    (%rsi,%rcx), %r9
        cmpq    %rdi, %r8
        je      L1
        .align 4,0x90
L10:
        movq    %r9, %rdx
        movq    %r8, %rax
        .align 4,0x90
L5:
        movsd   (%rdx), %xmm0
        movsd   (%rsi,%rax,8), %xmm1
        movsd   %xmm0, (%rsi,%rax,8)
        addq    $1, %rax
        movsd   %xmm1, (%rdx)
        addq    %rcx, %rdx
        cmpq    %rdi, %rax
        jne     L5
        addq    $1, %r8
        addq    %r10, %r9
        addq    %rcx, %rsi
        cmpq    %rdi, %r8
        jne     L10
L1:
        rep; ret

與@ ksfone的代碼基本相同,用於:

#include <cstddef>

void transpose(const size_t _n, double* _A) {
    for(size_t i=0; i < _n; ++i) {
        for(size_t j=i+1; j < _n; ++j) {
            double tmp  = _A[i*_n+j];
            _A[i*_n+j] = _A[j*_n+i];
            _A[j*_n+i] = tmp;
        }
    }
}

除了Mach-O'作為'差異(額外的下划線,對齊和DWARF位置),它也是一樣的。 但與OP的裝配輸出有很大不同。 一個更“緊密”的內循環。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM