簡體   English   中英

Cortex M3 的 Keil ARMCC int64 比較

[英]Keil ARMCC int64 comparison for Cortex M3

我注意到 armcc 生成這種代碼來比較兩個 int64 值:

0x080001B0 EA840006 EOR  r0,r4,r6
0x080001B4 EA850107 EOR  r1,r5,r7
0x080001B8 4308     ORRS r0,r0,r1
0x080001BA D101     BNE  0x080001C0

大致可以翻譯為:

r0 = lower_word_1 ^ lower_word_2
r1 = higher_word_1 ^ higher_word_2
r0 = r1 | r0
jump if r0 is not zero

和類似的東西,當比較 int64 (int r0,r1) 與整數常數(即 int,在 r3 中)

0x08000674 4058  EORS  r0,r0,r3
0x08000676 4308  ORRS  r0,r0,r1
0x08000678 D116  BNE   0x080006A8

出於同樣的想法,只是完全跳過比較更高的單詞,因為它只需要為零。

但我很感興趣——為什么這么復雜?

通過比較較低和較高的單詞並在兩者之后制作 BNE,這兩種情況都可以非常直接地完成:

對於兩個 int64,假設寄存器相同

CMP lower words
BNE
CMP higher words
BNE

對於具有整數常量的 int64:

CMP lower words
BNE
CBNZ if higher word is non-zero

這將采用相同數量的指令,每個指令的長度可能(或可能不,取決於所使用的寄存器)為 2 個字節。

arm-none-eabi-gcc 做了一些不同的事情,但也沒有玩轉 EORS

那么為什么 armcc 會這樣做呢? 我看不到任何真正的好處; 兩個版本都需要相同數量的命令(每個命令都可以是寬的或短的,所以那里沒有真正的利潤)。

我能看到的唯一一點好處是更少的分支,這對閃存預取緩沖區有些好處。 但是由於沒有緩存或分支預測,我並沒有真正購買它。

所以我的推理是這種模式只是遺留下來的,來自 ARM7 架構,其中不存在 CBZ/CBNZ 並且混合 ARM 和 Thumb 指令不是很容易。 我錯過了什么嗎?

PS Armcc 在每個優化級別都這樣做,所以我認為它是某種“硬編碼”的部分

UPD:當然,有一個執行管道會隨着每個分支被刷新,但是每個解決方案都需要至少一個條件分支,該分支會或不會被采用(取決於比較的整數),因此無論如何都會刷新管道等概率。
所以我真的看不出最小化條件分支的意義。

此外,如果將顯式比較較低和較高的單詞並且整數不相等,則會更快地進行分支。

IT-block 可以完全避免分支指令,但在 Cortex-M3 上,它最多只能有 4 條指令,所以為了一般性,我將忽略這一點。

生成代碼的效率不計入機器代碼指令的數量。 您還需要了解目標機器的內部結構(不僅是時鍾/指令),還需要了解獲取/解碼/執行過程的工作原理。

Cortex M3 設備中的每個分支指令都會刷新流水線。 管道必須再次饋送。 如果您從 FLASH 內存運行(它很慢),等待狀態也會顯着減慢此過程。 編譯器會盡量避免分支。

可以使用其他說明按照您的方式完成:

int foo(int64_t x, int64_t y)
{
    return x == y;
}

        cmp     r1, r3
        itte    eq
        cmpeq   r0, r2
        moveq   r0, #1
        movne   r0, #0
        bx      lr

相信你的編譯器。 寫他們的人知道他們的行業:)。 在您了解有關 ARM Cortex 的更多信息之前,您不能像現在這樣簡單地判斷編譯器。

您示例中的代碼經過很好的優化和簡單。 凱爾做得很好。

正如所指出的,區別在於分支與不分支。 如果你可以避免分支,你就想避免分支。

雖然 ARM 文檔可能很有趣,但對於 x86 和全尺寸 ARM 以及系統在此發揮作用的許多其他地方。 像 ARM 那樣的高性能內核對系統實現很敏感。 這些 cortex-m 內核用於對成本非常敏感的微控制器中,因此雖然它們將 PIC 或 AVR 或 msp430 用於 mips 到 mhz 和每美元的 mips,但它們仍然對成本敏感。 使用更新的技術或可能更高的成本,您開始看到在整個范圍內以處理器速度的閃存(不必在有效時鍾速度范圍內的不同位置添加等待狀態),但是很長一段時間當您在最慢的核心速度下以核心速度的一半看到閃光時。 然后隨着您選擇更高的核心速度而變得更糟。 但sram往往與核心相匹配。 無論哪種方式,閃存都是零件成本的主要部分,它的數量和速度在一定程度上推動了零件價格。

根據內核(來自 ARM 的任何東西),獲取大小和結果對齊方式會有所不同,因此可以根據循環樣式測試的對齊方式和需要多少次獲取(用許多皮質來演示)基准測試可以傾斜/操縱多發性硬化症)。 cortex-ms 通常是半字或全字提取,有些是芯片供應商的編譯時間選項(因此您可能有兩個具有相同內核但性能不同的芯片)。 這也可以演示......只是不在這里......除非被推動,我現在在這個站點上做了太多次這個演示。 但是我們可以在這個測試中管理它。

我手邊沒有 cortex-m3,如果需要,我必須挖出一個並將其連接起來,但不應該需要手邊的 cortex-m4,它也是 armv7-m。 一個 NUCLEO-F411RE

測試治具

.thumb_func
.globl HOP
HOP:
    bx r2

.balign 0x20

.thumb_func
.globl TEST0
TEST0:
    push {r4,r5}

    mov r4,#0
    mov r5,#0

    ldr r2,[r0]
t0:
    cmp r4,r5
    beq skip
skip:   
    subs r1,r1,#1
    bne t0
    
    ldr r3,[r0]
    subs r0,r2,r3

    pop {r4,r5}
    bx lr

systick 計時器通常適用於這些類型的測試,無需弄亂調試器計時器,它通常只顯示更多工作的相同內容。 這里綽綽有余。

像這樣調用,結果以十六進制打印

hexstring(TEST0(STK_CVR,0x10000));
hexstring(TEST0(STK_CVR,0x10000));

將閃存代碼復制到 ram 並在那里執行

hexstring(HOP(STK_CVR,0x10000,0x20000001));
hexstring(HOP(STK_CVR,0x10000,0x20000001));

現在 stm32 在閃存前面有這個緩存東西,它會影響這些基於循環的基准測試以及針對這些部分的其他基准測試,有時你無法超越它,最終會得到一個虛假的基准測試。 但在這種情況下不是。

為了演示提取效果,您希望系統延遲提取,如果提取速度太快,您可能看不到提取效果。

0800002c <t0>:
 800002c:   42ac        cmp r4, r5
 800002e:   d1ff        bne.n   8000030 <skip>

08000030 <skip>:

00050001 <-- flash time
00050001 <-- flash time
00060004 <-- sram time
00060004 <-- sram time

0800002c <t0>:
 800002c:   42ac        cmp r4, r5
 800002e:   d0ff        beq.n   8000030 <skip>

08000030 <skip>:

00060001
00060001
00080000
00080000

0800002c <t0>:
 800002c:   42ac        cmp r4, r5
 800002e:   bf00        nop

08000030 <skip>:

00050001
00050001
00060000
00060000

所以我們可以看到,如果分支沒有被采用,它與 nop 是一樣的。 就這個基於循環的測試而言。 所以也許有一個分支預測器(通常是一個小的緩存,它記住最后 N 個分支及其目的地,並且可以提前一兩個時鍾開始預取)。 我還沒有深入研究它,也沒有真正需要,因為我們已經可以看到由於必須采用分支而導致性能成本(盡管指令數量相同,但您建議的代碼並不相等,這是相同數量的指令但不相同的性能)。

所以刪除循環和避免 stm32 緩存的最快方法是在 ram 中做這樣的事情

push {r4,r5}

mov r4,#0
mov r5,#0
cmp r4,r5

ldr r2,[r0]

instruction under test repeated many times

ldr r3,[r0]
subs r0,r2,r3

pop {r4,r5}
bx lr

被測指令為 bne to next、beq to next 或 nop

// 800002e: d1ff        bne.n   8000030 <skip>
00002001
// 800002e: d0ff        beq.n   8000030 <skip>
00004000
// 800002e: bf00        nop
00001001

我沒有空間容納 0x10000 指令,所以我使用了 0x1000,我們可以看到這兩種分支類型都受到了影響,而分支類型的成本更高。

請注意,基於循環的基准測試沒有表現出這種差異,在做基准測試或判斷結果時必須小心。 即使是我在這里展示的那些。

我可以花更多的時間來調整核心設置或系統設置,但根據經驗,我認為這已經表明不想讓 cmp、bne、cbnz 替換 eor、orr、bne。 現在公平地說,你的另一個是 eor.w(thumb2 擴展),它比thumb2 指令消耗更多的時鍾,所以還有另一件事需要考慮(我也測量了它)。

請記住,對於這些高性能內核,您需要對獲取和獲取對齊非常敏感,這很容易導致糟糕的基准測試。 並不是說 x86 不是高性能,而是為了讓低效的內核運行得更順暢,它周圍有大量的東西來嘗試保持內核的供給,類似於運行半卡車與跑車,卡車可以高效一旦在高速公路上加速但在城市駕駛,即使保持速度限制,Yugo 也將比半卡車更快地穿過城鎮(如果它沒有發生故障)。 在 x86 中很難看到獲取效果、未對齊的傳輸等,但在 ARM 中比較容易,因此為了獲得最佳性能,您要避免容易的循環吞噬者。

編輯

請注意,我對 GCC 產生的結果過早下結論。 必須更多地嘗試制作等效的比較。 我開始了

unsigned long long fun2 ( unsigned long long a)
{
    if(a==0) return(1);
    return(0);
}
unsigned long long fun3 ( unsigned long long a)
{
    if(a!=0) return(1);
    return(0);
}
00000028 <fun2>:
  28:   460b        mov r3, r1
  2a:   2100        movs    r1, #0
  2c:   4303        orrs    r3, r0
  2e:   bf0c        ite eq
  30:   2001        moveq   r0, #1
  32:   4608        movne   r0, r1
  34:   4770        bx  lr
  36:   bf00        nop

00000038 <fun3>:
  38:   460b        mov r3, r1
  3a:   2100        movs    r1, #0
  3c:   4303        orrs    r3, r0
  3e:   bf14        ite ne
  40:   2001        movne   r0, #1
  42:   4608        moveq   r0, r1
  44:   4770        bx  lr
  46:   bf00        nop

其中使用了 it 指令,這是一個自然的解決方案,因為 if-then-else 情況可以是單個指令。 有趣的是,他們選擇使用 r1 而不是立即數 #0 我想知道這是否是一種通用優化,因為固定長度指令集上的立即數很復雜,或者立即數在某些體系結構上占用的空間更少。 誰知道。

 800002e:   bf0c        ite eq
 8000030:   bf00        nopeq
 8000032:   bf00        nopne
00003002 
00003002 

 800002e:   bf14        ite ne
 8000030:   bf00        nopne
 8000032:   bf00        nopeq
00003002 
00003002 

線性使用 sram 0x1000 組三個指令,因此 0x3002 意味着平均每條指令 1 個時鍾。

將 mov 放入 it 塊不會改變性能

ite eq
moveq   r0, #1
movne   r0, r1

它仍然是一個時鍾。

void more_fun ( unsigned int );
unsigned long long fun4 ( unsigned long long a)
{
    for(;a!=0;a--)
    {
        more_fun(5);
    }
    return(0);
}
  48:   b538        push    {r3, r4, r5, lr}
  4a:   ea50 0301   orrs.w  r3, r0, r1
  4e:   d00a        beq.n   66 <fun4+0x1e>
  50:   4604        mov r4, r0
  52:   460d        mov r5, r1
  54:   2005        movs    r0, #5
  56:   f7ff fffe   bl  0 <more_fun>
  5a:   3c01        subs    r4, #1
  5c:   f165 0500   sbc.w   r5, r5, #0
  60:   ea54 0305   orrs.w  r3, r4, r5
  64:   d1f6        bne.n   54 <fun4+0xc>
  66:   2000        movs    r0, #0
  68:   2100        movs    r1, #0
  6a:   bd38        pop {r3, r4, r5, pc}

這基本上是與零的比較

  60:   ea54 0305   orrs.w  r3, r4, r5
  64:   d1f6        bne.n   54 <fun4+0xc>

反對另一個

void more_fun ( unsigned int );
unsigned long long fun4 ( unsigned long long a, unsigned long long b)
{
    for(;a!=b;a--)
    {
        more_fun(5);
    }
    return(0);
}

00000048 <fun4>:
  48:   4299        cmp r1, r3
  4a:   bf08        it  eq
  4c:   4290        cmpeq   r0, r2
  4e:   d011        beq.n   74 <fun4+0x2c>
  50:   b5f8        push    {r3, r4, r5, r6, r7, lr}
  52:   4604        mov r4, r0
  54:   460d        mov r5, r1
  56:   4617        mov r7, r2
  58:   461e        mov r6, r3
  5a:   2005        movs    r0, #5
  5c:   f7ff fffe   bl  0 <more_fun>
  60:   3c01        subs    r4, #1
  62:   f165 0500   sbc.w   r5, r5, #0
  66:   42ae        cmp r6, r5
  68:   bf08        it  eq
  6a:   42a7        cmpeq   r7, r4
  6c:   d1f5        bne.n   5a <fun4+0x12>
  6e:   2000        movs    r0, #0
  70:   2100        movs    r1, #0
  72:   bdf8        pop {r3, r4, r5, r6, r7, pc}
  74:   2000        movs    r0, #0
  76:   2100        movs    r1, #0
  78:   4770        bx  lr
  7a:   bf00        nop

他們選擇在這里使用 it 塊。

  66:   42ae        cmp r6, r5
  68:   bf08        it  eq
  6a:   42a7        cmpeq   r7, r4
  6c:   d1f5        bne.n   5a <fun4+0x12>

它的指令數量與此相當。

0x080001B0 EA840006 EOR  r0,r4,r6
0x080001B4 EA850107 EOR  r1,r5,r7
0x080001B8 4308     ORRS r0,r0,r1
0x080001BA D101     BNE  0x080001C0

但是那些thumb2 指令將執行更長的時間。 所以總的來說,我認為 GCC 似乎已經做了一個更好的序列,但是當然你想從相同的 C 代碼開始逐個檢查,看看每個產生了什么。 gcc 比 eor/orr 更容易閱讀,可以少考慮它在做什么。

 8000040:   406c        eors    r4, r5
00001002
 8000042:   ea94 0305   eors.w  r3, r4, r5
00002001

0x1000 指令 一個是兩個半字 (thumb2) 一個是一個半字 (thumb)。 花了兩個時鍾並不感到驚訝。

0x080001B0 EA840006 EOR  r0,r4,r6
0x080001B4 EA850107 EOR  r1,r5,r7
0x080001B8 4308     ORRS r0,r0,r1
0x080001BA D101     BNE  0x080001C0

在添加任何其他懲罰之前,我看到那里有六個時鍾,而不是四個(在這個 cortex-m4 上)。

注意我使 eors.w 對齊和未對齊,它沒有改變性能。 還是兩個鍾。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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