鱼塘

好多好多好多鱼

0%

0-a 与 -a 的区别

今天逛 V2EX 发现了一个很有意思的问题:用 0-a 替换 -a 能提升性能。原帖链接,楼主和评论区大神都做了实验,并讲解了原理(一个是减法 一个直接取反)。现在记录下这个奇技淫巧。

1. 问题

@nyanyh:

认识的一位技术人员告诉我用 0-a 替换-a,可以提升性能,我将信将疑,写了小代码测试一下

1
2
3
4
5
6
7
8
int main() {
int a = 100;

auto a1 = -a;
auto a2 = 0-a;

return 0;
}

然后看看 LLVM 的中间码

1
2
3
4
5
%6:gr32 = SUB32rm %0:gr32(tied-def 0), %stack.1, 1, $noreg, 0, $noreg, implicit-def $eflags, debug-location !19 :: (load 4 from %ir.2); ./test.cpp:5:15
MOV32mr %stack.2, 1, $noreg, 0, $noreg, killed %6:gr32, debug-location !17 :: (store 4 into %ir.3); ./test.cpp:5:10

%3:gr32 = SUB32rm %0:gr32(tied-def 0), %stack.1, 1, $noreg, 0, $noreg, implicit-def $eflags, debug-location !23 :: (load 4 from %ir.2); ./test.cpp:6:16
MOV32mr %stack.3, 1, $noreg, 0, $noreg, killed %3:gr32, debug-location !21 :: (store 4 into %ir.4); ./test.cpp:6:10

是完全一样的

再试试如果 a 是 double 呢?

1
2
3
4
5
6
7
8
int main() {
double a = 100;

auto a1 = -a;
auto a2 = 0-a;

return 0;
}

出现了明显不同:

1
2
3
4
5
6
7
8
9
10
11
%11:fr64 = MOVSDrm_alt %stack.1, 1, $noreg, 0, $noreg, debug-location !19 :: (load 8 from %ir.2); ./test.cpp:5:16
%7:gr64 = MOVSDto64rr killed %11:fr64, debug-location !20; ./test.cpp:5:15
%8:gr64 = MOV64ri -9223372036854775808, debug-location !20; ./test.cpp:5:15
%9:gr64 = XOR64rr killed %7:gr64(tied-def 0), %8:gr64, implicit-def $eflags, debug-location !20; ./test.cpp:5:15
%10:fr64 = MOV64toSDrr killed %9:gr64, debug-location !20; ./test.cpp:5:15
MOVSDmr %stack.2, 1, $noreg, 0, $noreg, killed %10:fr64, debug-location !18 :: (store 8 into %ir.3); ./test.cpp:5:10

%2:fr64 = FsFLD0SD debug-location !24; ./test.cpp:6:16
%4:fr64 = SUBSDrm %2:fr64(tied-def 0), %stack.1, 1, $noreg, 0, $noreg, debug-location !24 :: (load 8 from %ir.2); ./test.cpp:6:16
MOVSDmr %stack.3, 1, $noreg, 0, $noreg, killed %4:fr64, debug-location !22 :: (store 8 into %ir.4); ./test.cpp:6:10
$eax = COPY %0:gr32, debug-location !25; ./test.cpp:8:5

上面的代码都是用-O0测试的,打开优化的话就测试不出了,两行代码直接被优化掉了

所以在某些情况下确实 0-a 比-a 少几条指令,所以会快一些?

2. 评论

@xcstream:

一个是减法 一个直接取反

@msg7086:

测优化后代码的话可以在后面接一个 printf 打印出来。
大部分情况应该都是能优化掉的,浮点数因为涉及到 rounding error 所以少数操作可能不会给你优化。
(比如浮点下 a+b+c+d 和(a+b)+(c+d)的结果是不同的。而且理论上后者比前者快。)

@msg7086:

https://godbolt.org/z/RYVwp7, 意料之中,浮点数减法调用的是 sub,而浮点数取反则是和 -0.0 取异或。

3. 原理

@msg7086 依据大佬的实验,利用 godbolt (一个可以根据 C++ 代码生产汇编的网站)得到了汇编代码,结果如下:

1
2
3
4
5
6
7
// 源码
#include <stdio.h>
void func(double a) {
volatile auto a1 = -a;
volatile auto a2 = 0-a;
printf("%lf %lf", a1, a2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 汇编代码
.LCPI0_0:
.quad -9223372036854775808 # double -0
.quad -9223372036854775808 # double -0
func(double): # @func(double)
; volatile auto a1 = -a;
vxorpd xmm1, xmm0, xmmword ptr [rip + .LCPI0_0]
vmovlpd qword ptr [rsp - 8], xmm1
vxorpd xmm1, xmm1, xmm1
; volatile auto a2 = 0-a;
vsubsd xmm0, xmm1, xmm0
vmovsd qword ptr [rsp - 16], xmm0
vmovsd xmm0, qword ptr [rsp - 8] # xmm0 = mem[0],zero
vmovsd xmm1, qword ptr [rsp - 16] # xmm1 = mem[0],zero
mov edi, offset .L.str
mov al, 2
jmp printf # TAILCALL
.L.str:
.asciz "%lf %lf"

可以看到,浮点数 a 取反是和 -0.0 取异或;而浮点数减法 0-a 调用的是 sub。

4. 链接

https://www.v2ex.com/t/667397