正如所指出的,区别在于分支与不分支。如果您可以避免分支,则希望避免分支。
虽然 ARM 文档可能很有趣,但对于 x86 和全尺寸 ARM 以及系统在此处发挥作用的许多其他地方,可能会很有趣。 ARM 等高性能内核对系统实现很敏感。这些 cortex-m 内核用于对成本非常敏感的微控制器,因此虽然它们将 PIC 或 AVR 或 msp430 的 mips 提高到 mhz 和每美元 mips,但它们仍然对成本敏感。使用更新的技术或更高的成本,您开始看到在整个范围内以处理器速度运行的闪存(不必在有效时钟速度范围内的各个位置添加等待状态),但持续时间很长您以最慢的核心速度以核心速度的一半看到闪存的时间。然后随着您选择更高的核心速度而变得更糟。但是sram经常匹配核心。无论哪种方式,闪存都是部件成本的主要部分,它的数量和速度在一定程度上推动了部件价格。
取决于内核(来自 ARM 的任何东西),提取大小和结果对齐方式会有所不同,因此可以根据循环样式测试的对齐方式和需要多少次提取来扭曲/操纵基准测试(用许多皮质-ms)。 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));
将 flash 代码复制到 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 到下一个,一个 beq 到下一个或一个 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 个时钟。
在 it 块中放置一个 mov 不会改变性能
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 对齐和未对齐,它并没有改变性能。还有两个时钟。