![](/img/trans.png)
[英]In C++, is A+=B preferable to A=A+B in the same way ++A is to A++?
[英]C++: a*a-b*b vs (a+b)*(a-b) what is faster to compute?
在 C++ 中計算平方差的哪種方法更快: a*ab*b
或(a+b)*(ab)
? 第一個表達式使用兩次乘法和一次加法,而第二個表達式需要兩次加法和一次乘法。 所以第二種方法似乎更快。 另一方面,在第一種方法中加載到寄存器的數據數量較少,這可能會補償一個乘法與加法。
如果您運行此代碼
#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*ab*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 = (ab)*(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*ab*b
的第一個函數采用以下 5 條匯編指令和兩次乘法:
mov esi,eax
mov ecx,edx
imul esi,eax
imul ecx,edx
sub ecx,esi
而(ab)*(a+b)
只需要 4 條指令和一次乘法:
mov ecx,edx
sub ecx,eax
add eax,edx
imul eax,ecx
似乎(ab)*(a+b)
應該比a*ab*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
。 這可能會更快,或者更確切地說有更多的執行單元並行運行。
clang f2
我覺得最有趣,因為它使用地址計算單元而不是算術加法器。 所以所有 4 個操作碼都使用不同的執行單元。
現在將其與 ARM/ARM64 進行對比:
ARM 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 鏗鏘聲 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 鏗鏘聲 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 似乎知道乘法和加法。
現在問題變成了: a mls
比add
+ sub
快還是慢。 使用 gcc f1
似乎更好,使用 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:請注意,這兩個功能是不等價的。 它們的行為因溢出而異。 或者更確切地說,當它們溢出時。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.