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似乎知道乘法和加法。
现在问题变成了:mls
比add
+sub
快还是慢?与gccf1
似乎更好,与msvc只适用于arm64和clang我认为是未定的。
现在是完全不同的:
AVR gcc 11.1.0f1(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:注意这两个函数是不等价的。它们的行为与溢出不同。或者当它们溢出时。