除了 ARMv7 之前的拇指互通之外,有什么理由在 MOV pc、R 上使用 BX R?

Tim*_*win 8 assembly arm cpu-architecture branch-prediction micro-architecture

Linux 定义了一个BX在支持它的 CPU上使用的汇编宏,这让我怀疑有一些性能原因。

这个答案Cortex-A7 MPCore 技术参考手册也指出它有助于分支预测。

然而,我的基准测试工作未能发现与 ARM1176、Cortex-A17、Cortex-A72 和 Neoverse-N1 cpu 的性能差异。

有没有因此任何理由,更喜欢BXMOV pc,上了MMU的CPU,并实现了32位ARM指令集,比Thumb代码交互等?

编辑添加基准代码,全部对齐到 64 字节:

执行无用的计算lr并使用返回BX

div_bx
        mov  r9, #2
        mul  lr, r9, lr
        udiv lr, lr, r9
        mul  lr, r9, lr
        udiv lr, lr, r9
        bx   lr
Run Code Online (Sandbox Code Playgroud)

在另一个寄存器上执行无用的计算并使用BX以下方法返回:

div_bx2
        mov  r9, #2
        mul  r3, r9, lr
        udiv r3, r3, r9
        mul  r3, r9, r3
        udiv r3, r3, r9
        bx   lr
Run Code Online (Sandbox Code Playgroud)

执行无用的计算lr并使用返回MOV

div_mov
        mov  r9, #2
        mul  lr, r9, lr
        udiv lr, lr, r9
        mul  lr, r9, lr
        udiv lr, lr, r9
        mov  pc, lr
Run Code Online (Sandbox Code Playgroud)

使用经典函数指针序列调用:

movmov
        push {lr}
loop    mov  lr, pc
        mov  pc, r1
        mov  lr, pc
        mov  pc, r1
        mov  lr, pc
        mov  pc, r1
        mov  lr, pc
        mov  pc, r1
        subs r0, r0, #1
        bne  loop
        pop  {pc}
Run Code Online (Sandbox Code Playgroud)

调用使用BLX

blx
        push {lr}
loop    nop
        blx  r1
        nop
        blx  r1
        nop
        blx  r1
        nop
        blx  r1
        subs r0, r0, #1
        bne  loop
        pop  {pc}
Run Code Online (Sandbox Code Playgroud)

删除nops make 的速度较慢。

结果为每 100000000 次循环的秒数:

Neoverse-N1 r3p1 (AWS c6g.medium)
           mov+mov   blx 
div_bx        5.73  1.70 
div_mov       5.89  1.71 
div_bx2       2.81  1.69 

Cortex-A72 r0p3 (AWS a1.medium)
           mov+mov   blx 
div_bx        5.32  1.63 
div_mov       5.39  1.58 
div_bx2       2.79  1.63 

Cortex-A17 r0p1 (ASUS C100P)
           mov+mov   blx 
div_bx       12.52  5.69 
div_mov      12.52  5.75 
div_bx2       5.51  5.56 
Run Code Online (Sandbox Code Playgroud)

我测试的 3 个 ARMv7 处理器似乎同时识别mov pc, lrbx lr作为返回指令。然而,带有 ARM1176Raspberry Pi 1被记录为具有仅将BX lr某些负载识别为返回指令的返回预测,但我没有发现返回预测的证据。

header: .string "       Calle      BL       B  Difference"
format: .string "%12s %7i %7i %11i\n"
        .align

        .global main
main:   push    {r3-r5, lr}
        adr     r0, header
        bl      puts

        @ Warm up
        bl      clock
        mov     r0, #0x40000000
1:      subs    r0, r0, #1
        bne     1b
        bl      clock

        .macro  run_test test
2:      bl      1f
        nop
        bl      clock
        mov     r4, r0
        ldr     r0, =10000000
        .balign 64
3:      mov     lr, pc
        bl      1f
        nop
        mov     lr, pc
        bl      1f
        nop
        mov     lr, pc
        bl      1f
        nop
        subs    r0, r0, #1
        bne     3b
        bl      clock
        mov     r5, r0
        ldr     r0, =10000000

        .balign 64
5:      mov     lr, pc
        b       1f
        nop
        mov     lr, pc
        b       1f
        nop
        mov     lr, pc
        b       1f
        nop
        subs    r0, r0, #1
        bne     5b
        bl      clock
        sub     r2, r5, r4
        sub     r3, r0, r5
        sub     r0, r3, r2
        str     r0, [sp]
        adr     r1, 4f
        ldr     r0, =format
        bl      printf
        b       2f
        .ltorg
4:      .string "\test"
        .balign 64
1:
        .endm

        run_test mov
        mov     lr, lr
        mov     pc, lr

        run_test bx
        mov     lr, lr
        bx      lr

        run_test mov_mov
        mov     r2, lr
        mov     pc, r2

        run_test mov_bx
        mov     r2, lr
        bx      r2

        run_test pp_mov_mov
        push    {r1-r11, lr}
        pop     {r1-r11, lr}
        mov     r12, lr
        mov     pc, r12

        run_test pp_mov_bx
        push    {r1-r11, lr}
        pop     {r1-r11, lr}
        mov     r12, lr
        bx      r12

        run_test pp_mov_mov_f
        push    {r0-r11}
        pop     {r0-r11}
        mov     r12, lr
        mov     pc, r12

        run_test pp_mov_bx_f
        push    {r0-r11}
        pop     {r0-r11}
        mov     r12, lr
        bx      r12

        run_test pp_mov
        push    {r1-r11, lr}
        pop     {r1-r11, lr}
        mov     r12, lr
        mov     pc, lr

        run_test pp_bx
        push    {r1-r11, lr}
        pop     {r1-r11, lr}
        mov     r12, lr
        bx      lr

        run_test pp_mov_f
        push    {r0-r11}
        pop     {r0-r11}
        mov     r12, lr
        bx      lr

        run_test pp_bx_f
        push    {r0-r11}
        pop     {r0-r11}
        mov     r12, lr
        bx      lr

        run_test add_mov
        nop
        add     r2, lr, #4
        mov     pc, r2

        run_test add_bx
        nop
        add     r2, lr, #4
        bx      r2

2:      pop     {r3-r5, pc}
Run Code Online (Sandbox Code Playgroud)

Cortex-A17 的结果符合预期:

       Calle      BL       B  Difference
         mov   94492  255882      161390
          bx   94673  255752      161079
     mov_mov  255872  255806         -66
      mov_bx  255902  255796        -106
  pp_mov_mov  506079  506132          53
   pp_mov_bx  506108  506262         154
pp_mov_mov_f  439339  439436          97
 pp_mov_bx_f  439437  439776         339
      pp_mov  247941  495527      247586
       pp_bx  247891  494873      246982
    pp_mov_f  230846  422626      191780
     pp_bx_f  230850  422772      191922
     add_mov  255997  255896        -101
      add_bx  255900  256288         388
Run Code Online (Sandbox Code Playgroud)

然而,在我的 Raspberry Pi1 上,ARM1176 运行来自 Raspbery Pi OS 的 Linux 5.4.51+,没有显示可预测指令的优势:

       Calle      BL       B  Difference
         mov  464367  464372           5
          bx  464343  465104         761
     mov_mov  464346  464417          71
      mov_bx  464280  464577         297
  pp_mov_mov 1073684 1074169         485
   pp_mov_bx 1074009 1073832        -177
pp_mov_mov_f  769160  768757        -403
 pp_mov_bx_f  769354  769368          14
      pp_mov  885585 1030520      144935
       pp_bx  885222 1032396      147174
    pp_mov_f  682139  726129       43990
     pp_bx_f  682431  725210       42779
     add_mov  494061  493306        -755
      add_bx  494080  493093        -987
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 4

如果您正在测试总是跳转到相同返回地址的简单情况mov pc, ...,则常规间接分支预测可能会很好。

我猜想bx lr可能会使用返回地址预测器,该预测器假设匹配 call/ret ( blx/ bx lr) 来正确预测到各个调用站点的返回,并且不会浪费正常间接分支预测器中的空间。

根据 Timothy 对 Cortex-A17、Cortex-A72 和 Neoverse-N1 的测试,在这些 CPU 上mov pc, lr被识别为可以与blx. 所以这个答案中的猜测对于那些 CPU 来说似乎是错误的。


为了检验这个假设,尝试类似的事情

testfunc:
   bx lr         @ or mov pc,lr

caller:
 mov  r0, #100000000
.p2align 4
 .loop:
  blx   testfunc
  blx   testfunc     # different return address than the previous blx
  blx   testfunc
  blx   testfunc
  subs   r0, #1
  bne   .loop
Run Code Online (Sandbox Code Playgroud)

如果我的假设是正确的,我预测这mov pc, lr会比 慢bx lr

(可能需要更复杂的目标地址模式(在本例中为调用点)来混淆某些 CPU 上的间接分支预测。某些 CPU 具有只能记住 1 个目标地址的间接分支预测器,但更复杂的预测器可以处理简单的目标地址。 4 个地址的重复模式。)


(这是一个猜测,我对这些芯片没有任何经验,但返回地址预测器的通用 cpu 架构技术是众所周知的,并且我读到它在多个 ISA 上实际使用。我确信 x86 使用它: http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/ 不匹配的调用/ret 肯定是一个问题。)