机器代码的精确副本运行速度比原始函数慢 50%
Exact copy of machine code runs 50% slower than the original function
我一直在尝试在嵌入式系统上从 RAM 和闪存执行。对于快速原型制作和测试,我目前使用的是 Arduino Due (SAM3X8E ARM Cortex-M3)。据我所知,Arduino 运行时和引导加载程序在这里应该没有区别。
这里是问题所在:我有一个用 ARM Thumb 程序集编写的函数 (calc)。 calc 计算一个数字并 returns 它。 (给定输入的运行时间>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() {
}
上述程序在 Arduino Due 目标上的输出是:
Address: 524617 Res: 3524578: 1338254us
Address: 524669 Res: 3524578: 2058819us
所以我们确认结果相等,运行时的地址也符合预期。手动输入机器代码函数的执行速度慢了 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
...
我们可以进一步确认类似使用的调用约定:
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
我可以排除这是由于某种推测性提取(Cortex-M3 似乎有!)或中断造成的。 (编辑:不,我不能。可能是某种预取)更改执行顺序或在其间添加函数调用不会更改结果。罪魁祸首是什么?
编辑:
更改机器代码函数的对齐方式(插入 nops 作为序言)后,我得到以下结果:
+16bit for calc2:
Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1846968us
+32 位用于 calc2:
Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1535424us
+48bit for calc2:
Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1413180us
+64 位用于 calc2:
Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1346606us
+80bit for calc2:
Address: 524617 Res: 3524578: 1102145us
Address: 524669 Res: 3524578: 1180105us
编辑2:
只有 运行 计算:
Address: 524617 Res: 3524578: 1102155us
只有 运行 calc2:
Address: 524617 Res: 3524578: 1102257us
更改顺序:
Address: 524669 Res: 3524578: 1554160us
Address: 524617 Res: 3524578: 1102211us
编辑3:
在标签 .in
前添加 .p2align 4
仅用于计算,单独执行:
Address: 524625 Res: 3524578: 1413185us
两者都与原始基准相同:
Address: 524625 Res: 3524578: 1413185us
Address: 524689 Res: 3524578: 1535424us
EDIT4:反转 flash 中的位置完全改变了结果。 -> 线性预取?
从闪存执行代码的速度取决于每个分支目标的等待周期数和代码对齐。在这个和类似的处理器中,比如STM32F103,当核心运行s处于最高频率时,闪存需要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。
我一直在尝试在嵌入式系统上从 RAM 和闪存执行。对于快速原型制作和测试,我目前使用的是 Arduino Due (SAM3X8E ARM Cortex-M3)。据我所知,Arduino 运行时和引导加载程序在这里应该没有区别。
这里是问题所在:我有一个用 ARM Thumb 程序集编写的函数 (calc)。 calc 计算一个数字并 returns 它。 (给定输入的运行时间>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() {
}
上述程序在 Arduino Due 目标上的输出是:
Address: 524617 Res: 3524578: 1338254us
Address: 524669 Res: 3524578: 2058819us
所以我们确认结果相等,运行时的地址也符合预期。手动输入机器代码函数的执行速度慢了 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
...
我们可以进一步确认类似使用的调用约定:
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
我可以排除这是由于某种推测性提取(Cortex-M3 似乎有!)或中断造成的。 (编辑:不,我不能。可能是某种预取)更改执行顺序或在其间添加函数调用不会更改结果。罪魁祸首是什么?
编辑: 更改机器代码函数的对齐方式(插入 nops 作为序言)后,我得到以下结果:
+16bit for calc2:
Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1846968us
+32 位用于 calc2:
Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1535424us
+48bit for calc2:
Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1413180us
+64 位用于 calc2:
Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1346606us
+80bit for calc2:
Address: 524617 Res: 3524578: 1102145us
Address: 524669 Res: 3524578: 1180105us
编辑2: 只有 运行 计算:
Address: 524617 Res: 3524578: 1102155us
只有 运行 calc2:
Address: 524617 Res: 3524578: 1102257us
更改顺序:
Address: 524669 Res: 3524578: 1554160us
Address: 524617 Res: 3524578: 1102211us
编辑3:
在标签 .in
前添加 .p2align 4
仅用于计算,单独执行:
Address: 524625 Res: 3524578: 1413185us
两者都与原始基准相同:
Address: 524625 Res: 3524578: 1413185us
Address: 524689 Res: 3524578: 1535424us
EDIT4:反转 flash 中的位置完全改变了结果。 -> 线性预取?
从闪存执行代码的速度取决于每个分支目标的等待周期数和代码对齐。在这个和类似的处理器中,比如STM32F103,当核心运行s处于最高频率时,闪存需要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。