【发布时间】:2014-03-17 12:50:58
【问题描述】:
(出于测试目的)我写了一个简单的方法来计算 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 版本?
编辑:注意此(对数)图中尖峰的幅度。转置带优化的 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
【问题讨论】:
-
该函数的大小是标称的,我非常好奇在每种情况下生成的 asm 代码是什么样的。如果您可以提供 as-was-tested 和 production 的具体 asm您的结果,它将为您的问题提供很好的补充。
-
@WhozCraig 添加了汇编代码。它们是从一个新的单独文件生成的,但具有相同的编译器调用,因此它们应该与我用于绘图的代码相同。
-
如果用
#pragma GCC ivdep限定循环会发生什么?与其说是 SIMD 优化,不如说是告诉编译器每个元素转置都是独立的。 -
您已将其标记为“c++”,那么为什么不将循环的内部部分替换为“std::swap”呢? 2 的幂可能是缓存或页面错误。有关矩阵转置的更优化方法,请参阅stackoverflow.com/a/16743203/257645
-
二次方的超级对齐减速是众所周知的:stackoverflow.com/q/12264970/922184, stackoverflow.com/q/8547778/922184 我不确定它对这里有什么影响。也许没有优化的人正在以一种远离超级对齐的方式进行改进。
标签: c++ gcc linear-algebra compiler-optimization execution-time