簡體   English   中英

C++:a*ab*b 與 (a+b)*(ab) 什么計算速度更快?

[英]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 mlsadd + 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM