c++: a*a-b*b vs (a+b)*(a-b)哪个计算更快?



c++中计算平方差的方式,a*a-b*b(a+b)*(a-b)哪一种更快?第一个表达式使用两次乘法和一次加法,而第二个表达式需要两个加法和一个乘法。因此,第二种方法似乎更快。另一方面,在第一种方法中,向寄存器加载数据的数量更少,这可能会补偿一次乘法和加法。

如果运行以下代码

#include <iostream>
int main()
{
int a = 6, b = 7;
int c1 = a*a-b*b;
int c2 = (a-b)*(a+b);
return 0;
}

在这里说,如果没有优化标志-O,那么汇编指令的数量将相同:

行:int c1 = a*a-b*b;:

mov    eax,DWORD PTR [rbp-0x4]
imul   eax,eax
mov    edx,eax
mov    eax,DWORD PTR [rbp-0x8]
imul   eax,eax
sub    edx,eax
mov    DWORD PTR [rbp-0xc],edx

:int c2 = (a-b)*(a+b);:

mov    eax,DWORD PTR [rbp-0x4]
sub    eax,DWORD PTR [rbp-0x8]
mov    ecx,DWORD PTR [rbp-0x4]
mov    edx,DWORD PTR [rbp-0x8]
add    edx,ecx
imul   eax,edx
mov    DWORD PTR [rbp-0x10],eax

另一方面,第一个指令集包含4个只在寄存器之间产生的操作,而第二个指令集只包含2个在寄存器之间产生的操作,其他指令集使用内存和寄存器。

所以问题是是否有可能估计出哪个指令集合更快?


回答后添加

谢谢你的回复,我找到答案了。请看下面的代码:

#include <iostream>
int dsq1(int a, int b) 
{
return a*a-b*b;
};

int dsq2(int a, int b) 
{
return (a+b)*(a-b);
};
int main()
{
int a,b;
// just to be sure that the compiler does not know
// precise values of a and b and will not optimize them
std::cin >> a; 
std::cin >> b; 
volatile int c1 = dsq1(a,b);
volatile int c2 = dsq2(a,b);
return 0;
}

现在a*a-b*b的第一个函数接受以下5条汇编指令,并进行两次乘法:

mov    esi,eax
mov    ecx,edx
imul   esi,eax
imul   ecx,edx
sub    ecx,esi

(a-b)*(a+b)只需要4条指令和一次乘法:

mov    ecx,edx
sub    ecx,eax
add    eax,edx
imul   eax,ecx

看来(a-b)*(a+b)应该比a*a-b*b快。

这取决于编译器和体系结构。让我们看看这两个函数:

int f1(int a, int b) {
return a*a-b*b;
}
int f2(int a, int b) {
return (a-b)*(a+b);
}

让我们看看x86_64上的结果:

MSVC

a$ = 8
b$ = 16
int f1(int,int) PROC                                 ; f1, COMDAT
imul    ecx, ecx
imul    edx, edx
sub     ecx, edx
mov     eax, ecx
ret     0
int f1(int,int) ENDP                                 ; f1
a$ = 8
b$ = 16
int f2(int,int) PROC                                 ; f2, COMDAT
mov     eax, ecx
add     ecx, edx
sub     eax, edx
imul    eax, ecx
ret     0
int f2(int,int) ENDP                                 ; f2

gcc 12.1

f1(int, int):
imul    edi, edi
imul    esi, esi
mov     eax, edi
sub     eax, esi
ret
f2(int, int):
mov     eax, edi
add     edi, esi
sub     eax, esi
imul    eax, edi
ret

叮当声14.0

f1(int, int):                                # @f1(int, int)
mov     eax, edi
imul    eax, edi
imul    esi, esi
sub     eax, esi
ret
f2(int, int):                                # @f2(int, int)
lea     eax, [rsi + rdi]
mov     ecx, edi
sub     ecx, esi
imul    eax, ecx
ret

都是相同的4个操作码的排列。你正在用imul交换add。这可能更快,或者更确切地说有更多的执行单元并行运行。

clangf2我觉得最有趣的是因为它使用地址计算单元而不是算术加法器。所以这四个操作码使用不同的执行单元。

现在与ARM/ARM64进行对比:

手臂MSVC

|int f1(int,int)| PROC                           ; f1
mul         r2,r0,r0
mul         r3,r1,r1
subs        r0,r2,r3
|$M4|
bx          lr
ENDP  ; |int f1(int,int)|, f1
|int f2(int,int)| PROC                           ; f2
subs        r2,r0,r1
adds        r3,r0,r1
mul         r0,r2,r3
|$M4|
bx          lr
ENDP  ; |int f2(int,int)|, f2

ARM64 msvc

|int f1(int,int)| PROC                           ; f1
mul         w8,w0,w0
msub        w0,w1,w1,w8
ret
ENDP  ; |int f1(int,int)|, f1
|int f2(int,int)| PROC                           ; f2
sub         w9,w0,w1
add         w8,w0,w1
mul         w0,w9,w8
ret
ENDP  ; |int f2(int,int)|, f2

ARM gcc 12.1

f1(int, int):
mul     r0, r0, r0
mls     r0, r1, r1, r0
bx      lr
f2(int, int):
subs    r3, r0, r1
add     r0, r0, r1
mul     r0, r3, r0
bx      lr

ARM64 gcc 12.1

f1(int, int):
mul     w0, w0, w0
msub    w0, w1, w1, w0
ret
f2(int, int):
sub     w2, w0, w1
add     w0, w0, w1
mul     w0, w2, w0
ret

ARM clang 11.0.1

f1(int, int):
mul     r2, r1, r1
mul     r1, r0, r0
sub     r0, r1, r2
bx      lr
f2(int, int):
add     r2, r1, r0
sub     r1, r0, r1
mul     r0, r1, r2
bx      lr

ARM64 clang 11.0.1

f1(int, int):                                // @f1(int, int)
mul     w8, w1, w1
neg     w8, w8
madd    w0, w0, w0, w8
ret
f2(int, int):                                // @f2(int, int)
sub     w8, w0, w1
add     w9, w1, w0
mul     w0, w8, w9
ret

所有的编译器都取消了mov指令,因为有更多的选择输入和输出寄存器使用。但是生成的代码有很大的不同。并不是所有的编译器都知道ARM/ARM64有一个乘法和减法操作码。Clang似乎知道乘法和加法。

现在问题变成了:mlsadd+sub快还是慢?与gccf1似乎更好,与msvc只适用于arm64和clang我认为是未定的。

现在是完全不同的:

AVR gcc 11.1.0
f1(int, int):
mov r19,r22
mov r18,r23
mov r22,r24
mov r23,r25
rcall __mulhi3
mov r31,r25
mov r30,r24
mov r24,r19
mov r25,r18
mov r22,r19
mov r23,r18
rcall __mulhi3
mov r19,r31
mov r18,r30
sub r18,r24
sbc r19,r25
mov r25,r19
mov r24,r18
ret
f2(int, int):
mov r18,r22
mov r19,r23
mov r23,r25
mov r22,r24
add r22,r18
adc r23,r19
sub r24,r18
sbc r25,r19
rcall __mulhi3
ret

我认为f2更好是毫无争议的。

PS:注意这两个函数是不等价的。它们的行为与溢出不同。或者当它们溢出时。

最新更新