编辑:有关使用 SSE 内在函数的版本,请参阅上面的 Adam 的答案。比我这里的好...
为了使它更有用,让我们在这里看看编译器生成的代码。我正在使用 gcc 4.8.0,是的,值得检查您的特定编译器(版本),因为 gcc 4.4、4.8、clang 3.2 或 Intel 的 icc 的输出存在很大差异。
您的原始使用g++ -O8 -msse4.2 ...
转换为以下循环:
.L2:
cvtsi2sd (%rcx,%rax,4), %xmm0
mulsd %xmm1, %xmm0
addl $1, %edx
movsd %xmm0, (%rsi,%rax,8)
movslq %edx, %rax
cmpq %rdi, %rax
jbe .L2
where%xmm1
成立1.0/32768.0
,因此编译器会自动将除法转换为反向乘法。
另一方面,使用g++ -msse4.2 -O8 -funroll-loops ...
,为循环创建的代码会发生显着变化:
[ ... ]
leaq -1(%rax), %rdi
movq %rdi, %r8
andl $7, %r8d
je .L3
[ ... insert a duff's device here, up to 6 * 2 conversions ... ]
jmp .L3
.p2align 4,,10
.p2align 3
.L39:
leaq 2(%rsi), %r11
cvtsi2sd (%rdx,%r10,4), %xmm9
mulsd %xmm0, %xmm9
leaq 5(%rsi), %r9
leaq 3(%rsi), %rax
leaq 4(%rsi), %r8
cvtsi2sd (%rdx,%r11,4), %xmm10
mulsd %xmm0, %xmm10
cvtsi2sd (%rdx,%rax,4), %xmm11
cvtsi2sd (%rdx,%r8,4), %xmm12
cvtsi2sd (%rdx,%r9,4), %xmm13
movsd %xmm9, (%rcx,%r10,8)
leaq 6(%rsi), %r10
mulsd %xmm0, %xmm11
mulsd %xmm0, %xmm12
movsd %xmm10, (%rcx,%r11,8)
leaq 7(%rsi), %r11
mulsd %xmm0, %xmm13
cvtsi2sd (%rdx,%r10,4), %xmm14
mulsd %xmm0, %xmm14
cvtsi2sd (%rdx,%r11,4), %xmm15
mulsd %xmm0, %xmm15
movsd %xmm11, (%rcx,%rax,8)
movsd %xmm12, (%rcx,%r8,8)
movsd %xmm13, (%rcx,%r9,8)
leaq 8(%rsi), %r9
movsd %xmm14, (%rcx,%r10,8)
movsd %xmm15, (%rcx,%r11,8)
movq %r9, %rsi
.L3:
cvtsi2sd (%rdx,%r9,4), %xmm8
mulsd %xmm0, %xmm8
leaq 1(%rsi), %r10
cmpq %rdi, %r10
movsd %xmm8, (%rcx,%r9,8)
jbe .L39
[ ... out ... ]
所以它阻止了操作,但仍然一次转换一个值。
如果您更改原始循环以在每次迭代中对几个元素进行操作:
size_t i;
for (i = 0; i < uIntegers.size() - 3; i += 4)
{
uDoubles[i] = uIntegers[i] / 32768.0;
uDoubles[i+1] = uIntegers[i+1] / 32768.0;
uDoubles[i+2] = uIntegers[i+2] / 32768.0;
uDoubles[i+3] = uIntegers[i+3] / 32768.0;
}
for (; i < uIntegers.size(); i++)
uDoubles[i] = uIntegers[i] / 32768.0;
编译器gcc -msse4.2 -O8 ...
(即即使没有请求展开)识别使用CVTDQ2PD
/的可能性,MULPD
并且循环的核心变为:
.p2align 4,,10
.p2align 3
.L4:
movdqu (%rcx), %xmm0
addq $16, %rcx
cvtdq2pd %xmm0, %xmm1
pshufd $238, %xmm0, %xmm0
mulpd %xmm2, %xmm1
cvtdq2pd %xmm0, %xmm0
mulpd %xmm2, %xmm0
movlpd %xmm1, (%rdx,%rax,8)
movhpd %xmm1, 8(%rdx,%rax,8)
movlpd %xmm0, 16(%rdx,%rax,8)
movhpd %xmm0, 24(%rdx,%rax,8)
addq $4, %rax
cmpq %r8, %rax
jb .L4
cmpq %rdi, %rax
jae .L29
[ ... duff's device style for the "tail" ... ]
.L29:
rep ret
即现在编译器认识到有机会在double
每个 SSE 寄存器中放置两个,并进行并行乘法/转换。这与 Adam 的 SSE 内在函数版本将生成的代码非常接近。
总代码(我只展示了其中的 1/6)比“直接”内在函数复杂得多,因为如前所述,编译器尝试预先添加/附加未对齐/非块-循环的多个“头”和“尾”。这在很大程度上取决于向量的平均/预期大小,这是否有益;对于“通用”情况(向量是“最内层”循环处理的块大小的两倍以上),它会有所帮助。
这个练习的结果主要是......如果你强制(通过编译器选项/优化)或提示(通过稍微重新排列代码)你的编译器做正确的事情,那么对于这种特定类型的复制/转换循环,它提供的代码不会落后于手写的内在函数。
最后的实验......制作代码:
static double c(int x) { return x / 32768.0; }
void Convert(const std::vector<int>& uIntegers, std::vector<double>& uDoubles)
{
std::transform(uIntegers.begin(), uIntegers.end(), uDoubles.begin(), c);
}
并且(对于最好读的程序集输出,这次使用 gcc 4.4 with gcc -O8 -msse4.2 ...
)生成的程序集核心循环(同样,有一个前/后位)变为:
.p2align 4,,10
.p2align 3
.L8:
movdqu (%r9,%rax), %xmm0
addq $1, %rcx
cvtdq2pd %xmm0, %xmm1
pshufd $238, %xmm0, %xmm0
mulpd %xmm2, %xmm1
cvtdq2pd %xmm0, %xmm0
mulpd %xmm2, %xmm0
movapd %xmm1, (%rsi,%rax,2)
movapd %xmm0, 16(%rsi,%rax,2)
addq $16, %rax
cmpq %rcx, %rdi
ja .L8
cmpq %rbx, %rbp
leaq (%r11,%rbx,4), %r11
leaq (%rdx,%rbx,8), %rdx
je .L10
[ ... ]
.L10:
[ ... ]
ret
有了这个,我们学到了什么?如果您想使用 C++,请真正使用C++ ;-)