精确的机器代码副本运行速度比原始功能慢50%

fsc*_*idl 7 performance assembly arm arduino cortex-m

我一直在尝试从嵌入式系统上的RAM和闪存执行性能。为了快速进行原型制作和测试,我目前使用的是Arduino Due(SAM3X8E ARM Cortex-M3)。据我所知,Arduino运行时和引导程序在这里应该没有任何区别。

问题出在这里:我有一个用ARM Thumb Assembly编写的函数(calc)。calc计算一个数字并将其返回。(对于给定的输入,> 1s运行时)现在,我手动提取了该函数的汇编机器代码,并将其作为原始字节放入另一个函数中。确认这两个功能都驻留在闪存中(地址0x80149和0x8017D紧挨着)。通过反汇编和运行时检查已确认了这一点。

void setup() {
  Serial.begin(115200);
  timeFnc(calc);
  timeFnc(calc2);
}

void timeFnc(int (*functionPtr)(void)) {
  unsigned long time1 = micros();

  int res = (*functionPtr)();

  unsigned long time2 = micros();
  Serial.print("Address: ");
  Serial.print((unsigned int)functionPtr);
  Serial.print(" Res: ");
  Serial.print(res);
  Serial.print(": ");
  Serial.print(time2-time1);
  Serial.println("us");

}

int calc() {
   asm volatile(
      "movs r1, #33 \n\t"
      "push {r1,r4,r5,lr} \n\t"
      "bl .in \n\t"
      "pop {r1,r4,r5,lr} \n\t"
      "bx lr \n\t"

      ".in: \n\t"
      "movs r5,#1 \n\t"
      "subs r1, r1, #1 \n\t"
      "cmp r1, #2 \n\t"
      "blo .lblb \n\t"
      "movs r5,#1 \n\t"

      ".lbla: \n\t"
      "push {r1, r5, lr} \n\t"
      "bl .in \n\t"
      "pop {r1, r5, lr} \n\t"
      "adds r5,r0 \n\t"
      "subs r1,#2 \n\t"
      "cmp r1,#1 \n\t"
      "bhi .lbla \n\t"
      ".lblb: \n\t"
      "movs r0,r5 \n\t"
      "bx lr \n\t"
      ::
   ); //redundant auto generated bx lr, aware of that
}

int calc2() {
  asm volatile(
    ".word  0xB5322121 \n\t"
    ".word  0xF803F000 \n\t"
    ".word  0x4032E8BD \n\t"
    ".word  0x25014770 \n\t"

    ".word  0x29023901 \n\t"
    ".word  0x800BF0C0 \n\t"
    ".word  0xB5222501 \n\t"
    ".word  0xFFF7F7FF \n\t"
    ".word  0x4022E8BD \n\t"
    ".word  0x3902182D \n\t"
    ".word  0xF63F2901 \n\t"
    ".word  0x0028AFF6 \n\t"
    ".word  0x47704770 \n\t"
  );
}

void loop() {

}
Run Code Online (Sandbox Code Playgroud)

上面的程序在Arduino Due目标上的输出为:

Address: 524617 Res: 3524578: 1338254us
Address: 524669 Res: 3524578: 2058819us
Run Code Online (Sandbox Code Playgroud)

因此,我们确认结果是相等的,并且运行时的地址与预期的一样。手动输入的机器代码功能的执行速度降低了50%。

使用arm-none-eabi-objdump进行反汇编可以进一步确认相应的地址,闪存驻留时间以及机器代码的均等性(注意字节序和字节分组!):

00080148 <_Z4calcv>:
   80148:   2121        movs    r1, #33 ; 0x21
   8014a:   b532        push    {r1, r4, r5, lr}
   8014c:   f000 f803   bl  80156 <.in>
   80150:   e8bd 4032   ldmia.w sp!, {r1, r4, r5, lr}
   80154:   4770        bx  lr

00080156 <.in>:
   80156:   2501        movs    r5, #1
   80158:   3901        subs    r1, #1
   8015a:   2902        cmp r1, #2
   8015c:   f0c0 800b   bcc.w   80176 <.lblb>
   80160:   2501        movs    r5, #1

00080162 <.lbla>:
   80162:   b522        push    {r1, r5, lr}
   80164:   f7ff fff7   bl  80156 <.in>
   80168:   e8bd 4022   ldmia.w sp!, {r1, r5, lr}
   8016c:   182d        adds    r5, r5, r0
   8016e:   3902        subs    r1, #2
   80170:   2901        cmp r1, #1
   80172:   f63f aff6   bhi.w   80162 <.lbla>

00080176 <.lblb>:
   80176:   0028        movs    r0, r5
   80178:   4770        bx  lr
}
   8017a:   4770        bx  lr

0008017c <_Z5calc2v>:
   8017c:   b5322121    .word   0xb5322121
   80180:   f803f000    .word   0xf803f000
   80184:   4032e8bd    .word   0x4032e8bd
   80188:   25014770    .word   0x25014770
   8018c:   29023901    .word   0x29023901
   80190:   800bf0c0    .word   0x800bf0c0
   80194:   b5222501    .word   0xb5222501
   80198:   fff7f7ff    .word   0xfff7f7ff
   8019c:   4022e8bd    .word   0x4022e8bd
   801a0:   3902182d    .word   0x3902182d
   801a4:   f63f2901    .word   0xf63f2901
   801a8:   0028aff6    .word   0x0028aff6
   801ac:   47704770    .word   0x47704770
}
   801b0:   4770        bx  lr
    ...
Run Code Online (Sandbox Code Playgroud)

我们可以进一步确认类似使用的调用约定:

00080234 <setup>:
void setup() {
   80234:   b508        push    {r3, lr}
  Serial.begin(115200);
   80236:   4806        ldr r0, [pc, #24]   ; (80250 <setup+0x1c>)
   80238:   f44f 31e1   mov.w   r1, #115200 ; 0x1c200
   8023c:   f000 fcb4   bl  80ba8 <_ZN9UARTClass5beginEm>
  timeFnc(calc);
   80240:   4804        ldr r0, [pc, #16]   ; (80254 <setup+0x20>)
   80242:   f7ff ffb7   bl  801b4 <_Z7timeFncPFivE>
}
   80246:   e8bd 4008   ldmia.w sp!, {r3, lr}
  timeFnc(calc2);
   8024a:   4803        ldr r0, [pc, #12]   ; (80258 <setup+0x24>)
   8024c:   f7ff bfb2   b.w 801b4 <_Z7timeFncPFivE>
   80250:   200705cc    .word   0x200705cc
   80254:   00080149    .word   0x00080149
   80258:   0008017d    .word   0x0008017d
Run Code Online (Sandbox Code Playgroud)

我可以排除这是由于某种推测性获取(Cortex-M3似乎具有这种功能!)或中断引起的。(编辑:NOPE,我不能。可能是某种预取)更改执行顺序或在两者之间添加函数调用不会更改结果。这里的罪魁祸首是什么?


编辑:更改机器代码功能的对齐方式(插入nops作为序言)后,我得到以下结果:

calc2 +16位:

Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1846968us
Run Code Online (Sandbox Code Playgroud)

calc2 +32位:

Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1535424us
Run Code Online (Sandbox Code Playgroud)

calc2 +48位:

Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1413180us
Run Code Online (Sandbox Code Playgroud)

calc2 +64位:

Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1346606us
Run Code Online (Sandbox Code Playgroud)

calc2 +80位:

Address: 524617 Res: 3524578: 1102145us
Address: 524669 Res: 3524578: 1180105us
Run Code Online (Sandbox Code Playgroud)

EDIT2:仅运行calc:

Address: 524617 Res: 3524578: 1102155us
Run Code Online (Sandbox Code Playgroud)

仅运行calc2:

Address: 524617 Res: 3524578: 1102257us
Run Code Online (Sandbox Code Playgroud)

更改顺序:

Address: 524669 Res: 3524578: 1554160us
Address: 524617 Res: 3524578: 1102211us
Run Code Online (Sandbox Code Playgroud)

EDIT3:仅在calc .p2align 4之前添加标签.in,单独执行:

Address: 524625 Res: 3524578: 1413185us
Run Code Online (Sandbox Code Playgroud)

两者均与原始基准相同:

Address: 524625 Res: 3524578: 1413185us
Address: 524689 Res: 3524578: 1535424us
Run Code Online (Sandbox Code Playgroud)

EDIT4:反转Flash位置会完全改变结果。->线性预取?

A.K*_*.K. 5

从闪存执行代码的速度取决于每个分支目标的等待周期数和代码对齐。在这个和类似的处理器中,如STM32F103,当内核以最高频率运行时,闪存需要3个等待周期。这意味着每个采取的分支可能需要 2 到 5 个周期,这可能会影响总运行时间。

为了补偿 FLASH 的缓慢,这些处理器有一个宽的 FLASH 总线和一个获取缓冲区。SAM3X 有一对 128 位指令缓冲区,它们似乎以预取模式填充 [1]。

要优化紧密循环,请尝试放入 32 字节的代码块并将其对齐到 16 字节的边界(或更好的 32,以防万一)。此外,检查 FLASH 参数是否设置正确也是一个好主意,即在此 MCU 中启用预取并将总线宽度设置为 128 位。将代码复制到 RAM 可能是一种选择,但与正常工作的获取缓冲区相比,这很痛苦并且实际上会减慢速度。

[1] http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11057-32-bit-Cortex-M3-Microcontroller-SAM3X-SAM3A_Datasheet.pdf,第 294 页,图 18-2、18-3 .