【发布时间】:2020-03-21 07:22:44
【问题描述】:
我有一个简单的向量-向量加法算法 (c = a + b * lambda),使用 AVX 指令用 intel 汇编语言编写。 这是我的代码:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Dense to dense
;; Uses cache
;; AVX
;; Without tolerances
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
global _denseToDenseAddAVX_cache_64_linux
_denseToDenseAddAVX_cache_64_linux:
push rbp
mov rbp, rsp
; rdi: address1
; rsi: address2
; rdx: address3
; rcx: count
; xmm0: lambda
mov rax, rcx
shr rcx, 3
and rax, 0x07
vzeroupper
vmovupd ymm5, [abs_mask]
sub rsp, 8
vmovlpd [rbp - 8], xmm0
vbroadcastsd ymm7, [rbp - 8]
vmovapd ymm6, ymm7
cmp rcx, 0
je after_loop_denseToDenseAddAVX_cache_64_linux
start_denseToDenseAddAVX_cache_64_linux:
vmovapd ymm0, [rdi] ; a
vmovapd ymm1, ymm7
vmulpd ymm1, [rsi] ; b
vaddpd ymm0, ymm1 ; ymm0 = c = a + b * lambda
vmovapd [rdx], ymm0
vmovapd ymm2, [rdi + 32] ; a
vmovapd ymm3, ymm6
vmulpd ymm3, [rsi + 32] ; b
vaddpd ymm2, ymm3 ; ymm2 = c = a + b * lambda
vmovapd [rdx + 32], ymm2
add rdi, 64
add rsi, 64
add rdx, 64
loop start_denseToDenseAddAVX_cache_64_linux
after_loop_denseToDenseAddAVX_cache_64_linux:
cmp rax, 0
je end_denseToDenseAddAVX_cache_64_linux
mov rcx, rax
last_loop_denseToDenseAddAVX_cache_64_linux:
vmovlpd xmm0, [rdi] ; a
vmovapd xmm1, xmm7
vmulsd xmm1, [rsi] ; b
vaddsd xmm0, xmm1 ; xmm0 = c = a + b * lambda
vmovlpd [rdx], xmm0
add rdi, 8
add rsi, 8
add rdx, 8
loop last_loop_denseToDenseAddAVX_cache_64_linux
end_denseToDenseAddAVX_cache_64_linux:
mov rsp, rbp
pop rbp
ret
人们经常建议我使用 intel 内部函数,因为它更好、更安全。现在我已经实现了这个算法:
void denseToDenseAddAVX_cache(const double * __restrict__ a,
const double * __restrict__ b,
double * __restrict__ c,
size_t count, double lambda) {
const size_t firstCount = count / 8;
const size_t rem1 = count % 8;
int i;
__m256d mul = _mm256_broadcast_sd(&lambda);
for (i = 0; i < firstCount; i++) {
// c = a + b * lambda
__m256d dataA1 = _mm256_load_pd(&a[i * 8]);
__m256d dataC1 = _mm256_add_pd(dataA1, _mm256_mul_pd(_mm256_load_pd(&b[i * 8]), mul ));
_mm256_store_pd(&c[i * 8], dataC1);
__m256d dataA2 = _mm256_load_pd(&a[i * 8 + 4]);
__m256d dataC2 = _mm256_add_pd(dataA2, _mm256_mul_pd(_mm256_load_pd(&b[i * 8 + 4]), mul ));
_mm256_store_pd(&c[i * 8 + 4], dataC2);
}
const size_t secondCount = rem1 / 4;
const size_t rem2 = rem1 % 4;
if (secondCount) {
__m256d dataA = _mm256_load_pd(&a[i * 8]);
__m256d dataC = _mm256_add_pd(dataA, _mm256_mul_pd(_mm256_load_pd(&b[i * 8]), mul ));
_mm256_store_pd(&c[i * 8], dataC);
i += 4;
}
for (; i < count; i++) {
c[i] = a[i] + b[i] * lambda;
}
}
我的问题是汇编版本比第二个快两倍。 c++版本有什么问题?
【问题讨论】:
-
至于任何与性能相关的问题:您是否正在优化 (
-O3) 您的 C++ 代码? -
您在测试哪个编译器、什么选项以及什么硬件?我假设与同一个来电者?如果两个调用者都在同一个测试程序中,您是否首先进行了不定时的预热运行以消除页面错误,并使 CPU 达到最大 turbo?
-
如果您的编译器无法击败这个 asm,您可能忘记启用优化或测试错误。不需要
vmovapd ymm1, ymm7,使用像vmulpd ymm1, ymm7, [rsi]这样的三操作数AVX 指令。另外,您使用慢速英特尔loop指令,每 7 个时钟周期 1 次迭代(2 个向量)使该循环成为瓶颈。 agner.org/optimize。我认为即使编译器没有展开,并使用索引寻址模式来击败微融合和英特尔上的端口 7 存储 AGU,它仍然至少和这个一样好。像 GCC 一样:godbolt.org/z/MHgtfa -
顺便说一句,您可以通过使用未对齐的向量加载可能与您已经加载的数据重叠的最后最多 8 个元素来处理不均匀的计数。 (在数组末尾结束)。至少如果您的输入大小已知为 >=4 个元素。
-
另外,不要使用
vmovlpd作为负载,除非您想要合并到现有向量的低元素中。您希望vmovsd避免错误的依赖和额外的 ALU uop。
标签: c++ performance compiler-optimization intrinsics avx