正如评论中提到的,您可能需要在性能测试中考虑您的一致性。可能出现的情况是,一种 memcpy 解决方案与另一种解决方案可能会命中我所说的这些提取线。
stm32 cortex-m7 部件。
测试中的代码:
/* r0 count */
/* r1 timer address */
.thumb_func
.globl TEST
TEST:
push {r4,r5}
ldr r4,[r1]
loop:
sub r0,#1
bne loop
ldr r5,[r1]
sub r0,r4,r5
pop {r4,r5}
bx lr
原始对齐方式
08000100 <TEST>:
8000100: b430 push {r4, r5}
8000102: 680c ldr r4, [r1, #0]
08000104 <loop>:
8000104: 3801 subs r0, #1
8000106: d1fd bne.n 8000104 <loop>
8000108: 680d ldr r5, [r1, #0]
800010a: 1b60 subs r0, r4, r5
800010c: bc30 pop {r4, r5}
800010e: 4770 bx lr
使用了 systick 计时器,没有理由使用调试计时器,它不会增加任何价值。
ra=TEST(0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=TEST(0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=TEST(0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=TEST(0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
第一次运行
00001029
00001006
00001006
00001006
这是一个 stm32,因此有一个无法禁用的闪存缓存,因此您可以在第一次运行时看到上面的内容。
循环是这样对齐的
8000104: 3801 subs r0, #1
8000106: d1fd bne.n 8000104 <loop>
添加 nop 将循环移动半个字
08000100 <TEST>:
8000100: 46c0 nop ; (mov r8, r8)
8000102: b430 push {r4, r5}
8000104: 680c ldr r4, [r1, #0]
08000106 <loop>:
8000106: 3801 subs r0, #1
8000108: d1fd bne.n 8000106 <loop>
800010a: 680d ldr r5, [r1, #0]
800010c: 1b60 subs r0, r4, r5
800010e: bc30 pop {r4, r5}
8000110: 4770 bx lr
整个测试从定时器读取到定时器读取都是相同的机器代码。
但性能却截然不同
00002013
00002003
00002003
00002003
执行时间是原来的两倍。
如果如文档所述,提取是 64 位,则每次提取 4 条指令。
如果我在每次测试中添加一个 nop
00001028
00001006
00001006
00001006
00001027
00001006
00001006
00001006
00001026
00001006
00001006
00001006
我又得到了三个返回 0x1000 的结果,然后......
08000100 <TEST>:
8000100: 46c0 nop ; (mov r8, r8)
8000102: 46c0 nop ; (mov r8, r8)
8000104: 46c0 nop ; (mov r8, r8)
8000106: 46c0 nop ; (mov r8, r8)
8000108: 46c0 nop ; (mov r8, r8)
800010a: b430 push {r4, r5}
800010c: 680c ldr r4, [r1, #0]
0800010e <loop>:
800010e: 3801 subs r0, #1
8000110: d1fd bne.n 800010e <loop>
8000112: 680d ldr r5, [r1, #0]
8000114: 1b60 subs r0, r4, r5
8000116: bc30 pop {r4, r5}
8000118: 4770 bx lr
00002010
00002001
00002001
00002001
您可以在 sram 中运行它以避免缓存,并执行其他操作,但我希望您在达到为循环添加额外获取的边界时会看到相同的效果。显然,这是最好的情况,整个循环一次获取,有时两次。使循环更长,它变成 N,然后 N+1 以不太严重的比率进行取指。
我还假设这里的 systick 是手臂时钟除以二,这对于这种性能测试来说非常适合。
因此,很可能由于两个不同函数的对齐,一个函数可能会受到性能影响,而另一个函数则不会受到额外的获取影响。
我倾向于将测试中的代码转换为 asm,将其放在二进制文件前面附近的引导程序中,这样我添加或删除的任何其他代码都不会影响对齐。我还可以将计时器包裹在它周围并以非常受控的方式循环。在定时区域之外添加 nop,以移动循环的对齐方式。如果被测代码中有多个循环,则可以在被测代码中间添加 nops 来控制每个循环的对齐方式。
您还需要调整数据的对齐方式,我不记得 cortex-ms 如何处理未对齐的访问,如果它们支持它,我认为它们会带来性能损失。
我针对 MCU 演示了与上述类似的内容,这也会影响到您。由于 sram(普通 sram 内存或就此而言的高速缓存)不是以字节形式组织的,因此它至少有 32 位宽(如果是 ecc/奇偶校验则更宽)。因此,单字节写入需要读取-修改-写入,半字相同,但对齐字写入不需要读取。通常,这被隐藏在噪音中,因为您没有进行足够的连续写入来从 sram 控制逻辑获得反压。但至少有一个 MCU 确实提到您可以/将会看到这种性能,并且我在某个时候在 SO 上发布了这一点。您还应该在未对齐的字写入中看到这一点,现在您需要两次读取-修改-写入。
显然,四条存储指令比一条字指令花费更多的时间。
我会这么做为什么不呢
/* r0 address */
/* r1 count */
/* r2 timer address */
.thumb_func
.globl swtest
swtest:
push {r4,r5}
ldr r4,[r2]
swloop:
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
str r3,[r0]
sub r1,#1
bne swloop
ldr r5,[r2]
sub r0,r4,r5
pop {r4,r5}
bx lr
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
00012012
0001200A
0001200A
0001200A
0002FFFD
0002FFFD
0002FFFD
0002FFFD
未对齐的执行时间要长两倍多。
不幸的是,您无法控制通用 memcpy 的地址,因此地址可能是 0x1000 和 0x2001,而且速度会很慢。但是,如果这里的练习是因为您有需要经常复制的代码(并且芯片中没有 DMA 机制可以使速度更快,请记住 DMA 不是免费的,有时它只是一种懒惰的方法,使用较少的代码但运行速度较慢,了解架构)但如果您可以控制它是字对齐地址和整个字数至少要复制的数据量,那么请制作您自己的副本,而不是不将其称为memcpy。然后用手调一下。
编辑,从 SRAM 运行
for(rd=0;rd<8;rd++)
{
rb=0x20002000;
for(rc=0;rc<rd;rc++)
{
PUT32(rb,0xb430); rb+=2; //46c0 nop ; (mov r8, r8)
}
PUT32(rb,0xb430); rb+=2; // 800010a: b430 push {r4, r5}
PUT32(rb,0x680c); rb+=2; // 800010c: 680c ldr r4, [r1, #0]
//0800010e <loop>:
PUT32(rb,0x3801); rb+=2; // 800010e: 3801 subs r0, #1
PUT32(rb,0xd1fd); rb+=2; // 8000110: d1fd bne.n 800010e <loop>
PUT32(rb,0x680d); rb+=2; // 8000112: 680d ldr r5, [r1, #0]
PUT32(rb,0x1b60); rb+=2; // 8000114: 1b60 subs r0, r4, r5
PUT32(rb,0xbc30); rb+=2; // 8000116: bc30 pop {r4, r5}
PUT32(rb,0x4770); rb+=2; // 8000118: 4770 bx lr
PUT32(rb,0x46c0); rb+=2;
PUT32(rb,0x46c0); rb+=2;
PUT32(rb,0x46c0); rb+=2;
PUT32(rb,0x46c0); rb+=2;
PUT32(rb,0x46c0); rb+=2;
PUT32(rb,0x46c0); rb+=2;
ra=HOP(0x1000,STK_CVR,0x20002001); hexstrings(rd); hexstring(ra%0x00FFFFFF);
ra=HOP(0x1000,STK_CVR,0x20002001); hexstrings(rd); hexstring(ra%0x00FFFFFF);
ra=HOP(0x1000,STK_CVR,0x20002001); hexstrings(rd); hexstring(ra%0x00FFFFFF);
ra=HOP(0x1000,STK_CVR,0x20002001); hexstrings(rd); hexstring(ra%0x00FFFFFF);
}
00000000 00001011
00000000 00001006
00000000 00001006
00000000 00001006
00000001 00002010
00000001 00002003
00000001 00002003
00000001 00002003
00000002 00001014
00000002 00001006
00000002 00001006
00000002 00001006
00000003 00001014
00000003 00001006
00000003 00001006
00000003 00001006
00000004 00001014
00000004 00001006
00000004 00001006
00000004 00001006
00000005 00002010
00000005 00002001
00000005 00002002
00000005 00002002
00000006 00001012
00000006 00001006
00000006 00001006
00000006 00001006
00000007 00001014
00000007 00001006
00000007 00001006
00000007 00001006
现在仍然看到类似缓存的效果。我确实看到我的 CCR 是 0x00040200 并且我无法禁用它,我相信 m7 说你不能。
好的,正在使用 BTAC,但在 ACTLR 中设置位 13 将其更改为静态分支预测。所以现在时间实际上更有意义了,来自 sram:
00000000 00004003
00000000 00004003
00000000 00004003
00000000 00004003
00000001 00005002
00000001 00005002
00000001 00005002
00000001 00005002
00000002 00004003
00000002 00004003
00000002 00004003
00000002 00004003
00000003 00004003
00000003 00004003
00000003 00004003
00000003 00004003
00000004 00004003
00000004 00004003
00000004 00004003
00000004 00004003
00000005 00005002
00000005 00005002
00000005 00005002
00000005 00005002
00000006 00004003
00000006 00004003
00000006 00004003
00000006 00004003
00000007 00004003
00000007 00004003
00000007 00004003
00000007 00004003
我们确实看到了额外的获取行,但每次运行都与 sram 一致。
即使我知道 st 有缓存功能,Flash 也没有显示出从一个测试到另一个测试的任何变化。
00010FFC
00010FFC
00010FFC
00010FFC
相对于从 sram 运行而言,闪存的这种性能也感觉不错,闪存速度很慢,您对此无能为力,因此上面的数字确实看起来很奇怪。这说明了在性能测试中您可能会陷入多少陷阱,以及为什么所有基准测试都是b......t。
由于我对这个答案非常感兴趣,还要注意,假设 sram 是 32 位宽,则预计未对齐读取也会对未对齐读取造成性能影响,与一个周期相比,读取未对齐需要两个 sram 总线周期对于对齐,如果你击打得足够用力,那应该会产生反压。
禁用 BTAC 时
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002002,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
store word aligned
00019FFE
00019FFE
store word unaligned
00030007
00030007
load word aligned
00020001
00020001
load word unaligned
0002A00C
0002A00C
因此,如果您的 memcpy 是从 0x1000 到 0x2002 或从 0x1001 到 0x2002,即使您预先对齐然后进行基于字的复制,性能仍然会受到影响。这就是为什么我提到你需要尝试不同的对齐方式。
关于你的一个问题,我记得几年前的全尺寸arm memcpy,我认为在newlib中他们有一些性能步骤,例如,如果要复制的数量小于x,他们只会执行一个字节循环,完成。否则,如果其中一个从 0x1001 开始,那么他们至少会尝试对齐其中一个,然后他们会执行一个字节、一个半字,然后是一堆字或多个字,然后根据长度在末尾添加一个额外的半字或字节来完成。但这只有在两个指针以相同方式对齐或未对齐的情况下才有效。
从你的桌子上看,我觉得你没有考虑到所有这些因素。你落入基准测试是b......t,一个基准测试代表一个源代码,即使该核心/芯片/系统可以在不同数量的时钟中运行该代码,有时严格地由于C编译器和连接器,没有其他因素。
然后再次
beg=get_timer();
for(i = 0;i<1000;i++)
{
memcpy(a,b);
}
end=get_timer();
放大您的测量误差。单独调用 memcpy 的 for 循环也受到获取和分支预测的影响。我希望你不要这样测试。