这个问题基本上是你自己回答的。你会在ARM和MIPS等高性能内核上看到这种情况,而在x86等高开销内核上却看不到(并不是说没有发生)。
现在公平地说,您可能会看到其他影响,并在具有其他开销的RTOS中运行。但我可以很容易地证明这一点,而不需要任何这些东西,裸露的金属,没有中断,等等。只是核心一次做一件事。
我们都可以想象缓存会发生什么,第一次可能会出现缓存未命中,这可能会导致第一次的巨大延迟(取决于系统,不一定是mcu的巨大延迟),然后是第二次,假设它在缓存中,速度会更快。同样,如果将循环对齐到缓存行的末尾附近,以便循环本身或下一个预取进入第二个缓存行。让第一圈势均力敌
比较长的我所说的提取线也是如此,但更糟的是,因为就其本身而言,它们第二次不会变快。现在,一些分支预测因子会有所帮助。
分支预测不一定是看起来超级智能的逻辑,它试图解码指令并提前查看可能导致该分支发生的指令/结果,然后作为结果启动提取。相反,当它执行地址时,更有可能存在一个微小的地址缓存
第一次和该地址导致或可能导致回迁时,他们会将其添加到这个短列表中,当你接近该地址时(即使你自己修改代码),它也会抛出一个回迁。现在,如果你不得不这样做,那可能会很痛苦。但事实是,分支预测只是比它本来要早几个时钟开始预取(这很好,但既不是魔术,也不是复杂的逻辑)。
我乘坐NUCLEO-F767ZI,因为这是我手边的东西。它将是你芯片中的同一个cortex-m7。我们不能保证整个芯片是一样的(st使芯片不记得)。但是,随着多年来从一端到另一端使用的STM32芯片数量之多,cortex-m7基础设施将与其说不同,不如说相似。有了cortex-m7 st,您将看到,虽然它们仍然支持其经典的0x08000000地址,但ITCM地址是0x00200000,这是您应该用于链接这些部分的地址。显然,你可以也应该在自己的芯片上尝试一下。你应该看到类似的结果。
我没有使用来自st的任何代码或任何其他代码,我的所有代码都是从ARM和st文档中编写的。切换到车载水晶以获得
更可靠的uart参考时钟。设置uart以打印结果。更新的零件更好,但在很长一段时间内,对于许多供应商的零件,零等待状态意味着闪存以系统速率的一半运行。当你提高系统的速度时,你过去和现在可能都需要添加等待状态。最大化时钟并不能让你的系统运行得更快,你仍然受到闪存速度限制的约束(我们已经很长时间没有受到处理器的约束了)核心可以快速地运行指令,但必须等待更多的时钟来等待指令。或者外围设备(如果外围设备有另一个)。我没有查你的芯片文档,这一个肯定有一个电源到系统时钟等待状态表。欢迎您打卡并正确等待状态,并重复这些实验。它应该与仅以8MHz的频率运行并添加这些等待状态相同或相当。基本上,欢迎您使用最大时钟,但您仍然会看到我正在演示的问题,您可能有其他问题,但演示这个问题是微不足道的。
我认为这是一种聚会技巧,在实际的聚会上可能不起作用,但你可以让你的同事感到困惑或娱乐。请注意,当你看到更原始的皮层ms带有半单词或单词大小的提取时,你必须有一条提取线,你可能看不到。不过m7。
预取部件
预取单元(PFU)提供:
64位指令获取带宽。
4x64位预取队列,用于将指令预取与DPU管道操作解耦。
分支目标地址缓存(BTAC),用于分支预测器状态和目标地址的单周期翻转。
未指定BTAC时的静态分支预测器。
在解码器和处理器流水线的第一执行阶段中转发用于早期解析直接分支的标志。
测试中的第一个代码是
.balign 0x100
/* 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
nop
nop
nop
nop
只是一个简单的计数循环(不是统一的语法)。对齐良好。
我正在使用systick计时器,我可以通过演示DWT计时器给出相同的结果。的确,一些芯片供应商在系统上加了一个除数。好吧,好吧,st文档显示除以8,但我认为这是一个长期存在的拼写错误。。。
从systick开始,检查dwt,如果dwt不是8倍,则返回systick。
从0x08000000开始
08000100 <TEST>:
8000100: b430 push {r4, r5}
8000102: 680c ldr r4, [r1, #0]
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芯片上,你有这个无法关闭的缓存闪存。在这个部分和其他stm32 cortex-m7上,你可以。事实上,文档说(FLASH_ACR寄存器)ART和预取都被禁用。有趣的是,这些数字看起来很可疑,如果缓存关闭,第一个循环会有什么不同?是心脏收缩吗?
载重吨位
00001029
00001006
00001006
00001006
arm文档谈到了分支预测等,而IMO写得不好(如果你试图搜索)。看起来BTAC是默认启用的,我们可以在ACTLR寄存器中关闭它(分支目标地址缓存,缓存一些地址及其预取目的地)。
0000400F
0000400F
0000400F
0000400F
好多了。
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
这个芯片没有看到两个地址之间的性能差异,这很奇怪。还有另一个实验要搞清楚。
因此,64位获取是4个16位的东西或2个32位的东西。假设总线是32或64位宽。因此,对于上面的情况,我们假设在0x100处有一个提取,它开始通过管道运行这些提取,然后再提取下一行,使其排队。
.balign 0x100
nop
/* r0 count */
/* r1 timer address */
在这里放一个nop或其他东西来改变我们简单测试的对齐方式。
00200102 <TEST>:
200102: b430 push {r4, r5}
200104: 680c ldr r4, [r1, #0]
00005002
00005002
00005002
00005002
就这样。完全相同的机器代码,相同的芯片,相同的系统,相同的一切,除了完全相同的计算机代码在不同的对齐方式上。
请注意,启用BTAC后,您仍然可以获得不同的执行时间。
00002010
00002004
00002004
00002004
如果我们添加另一个nop,并将这两条指令放在获取行的中间会怎么样。
00200104 <TEST>:
200104: b430 push {r4, r5}
200106: 680c ldr r4, [r1, #0]
00004003
00004003
00004003
00004003
hmmm
00200106 <TEST>:
200106: b430 push {r4, r5}
200108: 680c ldr r4, [r1, #0]
00004003
00004003
00004003
00004003
有趣的我将切换到sram,在许多MCU上,即使在“零等待状态”下,闪存也比sram慢。使用sram可以让我做一些自我修改的代码,每次运行不止一次测试。
第一个数字是测试代码前面的nop数量,用于控制对齐。
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
因此从0x20002000到0x20002002以及从4x2=8 0x2008到0x200A。8字节是64位。因此,对于同一个机器代码,打乱提取行显然会给我们带来两个结果。我错了上面两条指令是4个字节,这是提取行的四分之一?你可能会认为,如果在64位对齐上,一个超过它会导致它变慢,那么你几乎可以预期其他对齐错误,即64个字节中的4个字节,也会变慢。我将停止尝试分析它,我无法访问sim核心,如果我访问了,我无论如何都无法谈论它。
我们看到,至少在这个芯片上,闪存和缓存在闪存上处于零等待状态时是相同的,而不是更慢。mcus的闪光越来越好。
哈哈,是的是的。。。
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
00200104 <loop>:
200104: 3801 subs r0, #1
200106: d1fd bne.n 200104 <loop>
循环不在0x100,而是在0x104开始,然后我们将其移动到0x106,使bne在下一个提取行中处于0x108。我已经看到核心提取
第二行后面马上有个分支,这个可能在等,不知道,我没有权限。
无论如何
00200100 <TEST>:
200100: b430 push {r4, r5}
200102: 680c ldr r4, [r1, #0]
00200104 <loop>:
200104: 46c0 nop ; (mov r8, r8)
200106: 3801 subs r0, #1
200108: d1fc bne.n 200104 <loop>
如果我坚持不
00005002
00005002
00005002
00005002
这是有道理的。
将nop放在它们之间得到相同的结果。
如果我们使用sram和不同数量的排列。
00000000 00005002
00000000 00005002
00000000 00005002
00000001 00005002
00000001 00005002
00000001 00005002
00000001 00005002
00000002 00005002
00000002 00005002
00000002 00005002
00000002 00005002
00000003 00005002
00000003 00005002
00000003 00005002
00000003 00005002
00000004 00005002
00000004 00005002
00000004 00005002
00000004 00005002
00000005 00005002
00000005 00005002
00000005 00005002
00000005 00005002
00000006 00005002
00000006 00005002
00000006 00005002
00000006 00005002
00000007 00005002
00000007 00005002
00000007 00005002
00000007 00005002
并不是每个循环都有这个问题。
循环中有两个nop
00000000 00005003
00000000 00005003
00000000 00005003
00000000 00005003
00000001 00006002
00000001 00006002
00000001 00006002
00000001 00006002
00000002 00005003
00000002 00005003
00000002 00005003
00000002 00005003
00000003 00005003
00000003 00005003
00000003 00005003
00000003 00005003
00000004 00005003
00000004 00005003
00000004 00005003
00000004 00005003
00000005 00006002
00000005 00006002
00000005 00006002
00000005 00006002
00000006 00005003
00000006 00005003
00000006 00005003
00000006 00005003
00000007 00005003
00000007 00005003
00000007 00005003
00000007 00005003
(如果处理器如此敏感或可能如此敏感,请考虑基准测试的价值)。
您可以在实际应用程序中看到这些性能差异的变化。在一个完全无关的函数中添加或删除代码可以在整个二进制文件中产生级联效应。有些循环将对位置敏感,有些则不敏感。循环包裹循环,内部有多个循环的循环可能会相互抵消或放大问题。
0x2004/0x1006=199.8%。超过了我在某个地方评论的两位数。
00200100<测试>:
200100:b430推{r4,r5}
200102:680c ldr r4,[r1,#0]
00200104<循环>:
200104:3801潜艇r0,#1
200106:d1fd bne.n 200104<循环>
启用BTAC,我们以前看到过。
00001011
00001006
00001006
00001006
FLASH_ACR寄存器中的PRFTEN没有变化。
ART加速器打开,没有变化。
所以我一直在玩FLASH_ACR寄存器。看起来我们被处理器限制了。或者某个地方正在进行缓存。
现在,以RTOS为例。不一定是在这个核心中,但添加指令缓存会有所帮助,但请记住,无论有没有缓存,您仍然在获取相同的行,并且存在相同的获取行边界问题。备份内存有时可能会更快,但对齐问题仍然存在(在您可以首先检测到的系统上)。添加了一个我们在cortex-m上没有的MMU,他们定义了内存区域,这样你就不必使用缓存来告诉系统什么是非缓存外设什么是指令内存什么是数据内存。mmu增加了自己的性能命中率,通常有不止一种方法可以将虚拟地址空间映射到物理地址空间,但如何映射可能会出现性能问题等。你可能会遇到更多的rto问题,但如果你严格地使用相同的机器代码和不同的对齐方式获得两个特定的数字,您可能会陷入一个简单的问题,即每个循环的获取次数。
简短回答:
你基本上已经完成了所有的工作并找到了答案。
cortex-m7有一个64位的提取。当它确定需要向后分支并执行提取操作时,您在提取行和管道中的位置现在会影响每个循环的总提取次数。如果您保留相同的机器代码,并将其在地址空间中移动,则某些循环每个循环可能会有额外的获取。零等待状态并不意味着零时钟,回迁不是空闲的,也不是所有的系统内存都参与了回迁。获取到达的时间相对于管道准备将其用于分支的时间也会影响循环性能。循环的代码大小决定了额外的获取惩罚,每个循环三次获取有时是四次,十次获取有时为11次,这将具有不同的相对命中率。20%、10%等。。。
它与rom/flash中的对齐无关,但总的来说,如果你在sram中运行该代码,你也应该能够找到两个执行时间。
请注意,不要假设任何其他核心与皮层-m7完全相同,其他皮层ms也没有足够的提取能力来看到这一点。不同的管道等等。有些可能需要更多的系统时钟来做同样的事情。旧的内核与旧的闪存技术相匹配,例如,这些系统设计可能表明,从闪存运行和从sram运行,相同的代码,具有不同的性能。
在广泛的STM32系列中,还有一些芯片上无法禁用的闪存缓存和预取,这使得像这样的性能分析变得更加困难。虽然其他品牌可以购买相同的内核,但请注意,内核有编译时选项,ARM内核只是芯片的一部分,其余的逻辑是其他人的,没有两家公司拥有相同的IP,当然,其中一些IP是芯片公司的,而不是购买的。例如,您应该在所有基于cortex-m7的芯片上看到这一点,但确切的差异可能会有所不同。
在具有RTOS影响的RTOS上运行也可能导致性能问题。对于这个特定的ARM内核,可以为同一代码生成两个不同的执行时间,我相信你已经发现了这一点。除此之外,您可能还会发现其他性能问题。
在您的情况下,单循环时间会根据对齐情况而变化,所以您测量的不是X循环,而是Y时间中的循环数量,同样,每个循环的时钟越多,每秒的计数就越少。你不像我那样严格地测量时间(迈克尔·阿布拉什:汇编语言的禅宗),所以你的中断延迟和开销可能会在这里产生影响。就我个人而言,我认为从你的结果和这个核心的工作方式等来看,至少在每次编译中,这两个任务的任务切换时间应该相等。我想我只是在发布一个免责声明,说你可能看到了除上述内容之外的其他内容(或者你可能没有看到上述内容,如果是,请告诉我,我会删除这个答案)。
关于我们能做些什么的问题。这从过早优化讨论的答案开始。一旦您出于某种原因决定要进行一些优化,并且隔离了该代码,那么你该怎么办呢?有时你可以更改C代码来帮助编译器做一些更简单或更快的事情(记住,指令少并不意味着更快……更快意味着更快,更快是相对于系统的,所以一个系统上的相同代码在另一个系统中可能会更慢,反之亦然)。
如果这不起作用,一个常见的解决方案是让编译器先完成工作,然后手动优化。因此,使用gcc,我们知道工具链步骤通过汇编程序,C代码被编译
转换为汇编语言,然后调用汇编程序将其转换为对象,然后调用链接器使其成为二进制(gcc是二进制
它本身不是一个编译器,它只是启动一些其他程序
实际完成工作的)。我觉得这种输出很痛苦,我宁愿从YMMV的拆卸中工作。无论如何,现在你的职能
或者任何汇编语言,你可以用这种方式管理手动调优。您也可以在中从头开始编写关键代码
真正的组装。在这种情况下,这是可行的。您可以看到维护是什么样子的,以及过早优化人员的来源。
对于这个特定的平台/代码,我们可以做一些技巧,尽管知道我们所知道的,它仍然是非常手动的,可能需要大量的维护。
volatile unsigned int ctr1,ctr2;
void thread1(void)
{
while(1)
{
ctr1++;
}
}
void thread2(void)
{
while(1)
{
ctr2++;
}
}
不公正地批评
Disassembly of section .text:
00200000 <reset-0x8>:
200000: 20001000 andcs r1, r0, r0
200004: 00200009 eoreq r0, r0, r9
00200008 <reset>:
200008: e7fe b.n 200008 <reset>
...
0020000c <thread1>:
20000c: 4a02 ldr r2, [pc, #8] ; (200018 <thread1+0xc>)
20000e: 6813 ldr r3, [r2, #0]
200010: 3301 adds r3, #1
200012: 6013 str r3, [r2, #0]
200014: e7fb b.n 20000e <thread1+0x2>
200016: bf00 nop
200018: 00200030 eoreq r0, r0, r0, lsr r0
0020001c <thread2>:
20001c: 4a02 ldr r2, [pc, #8] ; (200028 <thread2+0xc>)
20001e: 6813 ldr r3, [r2, #0]
200020: 3301 adds r3, #1
200022: 6013 str r3, [r2, #0]
200024: e7fb b.n 20001e <thread2+0x2>
200026: bf00 nop
200028: 0020002c eoreq r0, r0, ip, lsr #32
Disassembly of section .bss:
0020002c <ctr2>:
20002c: 00000000 andeq r0, r0, r0
00200030 <ctr1>:
200030: 00000000 andeq r0, r0, r0
这与一些虚假代码有关,以证明某些东西。如果分解对象,它从00000000开始,循环从那里开始对齐。相对于彼此,它们应该保持静止。它们的整体对齐受到之前的代码的影响。使用-save-temps,我们可以看到编译器的输出。
.cpu cortex-m7
...
.text
.align 1
.p2align 2,,3
.global thread1
.arch armv7e-m
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type thread1, %function
thread1:
@ Volatile: function does not return.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
ldr r2, .L4
.L2:
ldr r3, [r2]
adds r3, r3, #1
str r3, [r2]
b .L2
.L5:
.align 2
.L4:
.word ctr1
.size thread1, .-thread1
.align 1
.p2align 2,,3
.global thread2
.syntax unified
它有一些.p2align,你可以去查找并玩它们(在它们上撒上一些.bytes,看看它们如何影响下一件事,看看它们是否用nops填充或用什么填充)。
00200000 <reset-0x8>:
200000: 20001000 andcs r1, r0, r0
200004: 00200009 eoreq r0, r0, r9
00200008 <reset>:
200008: e7fe b.n 200008 <reset>
...
0020000c <thread1>:
这个很可能是一个nop,用于填充以使thread1在一个单词边界上对齐。让我们试试吧。
.thumb
.word 0x20001000
.word reset
.thumb_func
reset: b reset
nop
给予
00200000 <reset-0x8>:
200000: 20001000 andcs r1, r0, r0
200004: 00200009 eoreq r0, r0, r9
00200008 <reset>:
200008: e7fe b.n 200008 <reset>
20000a: 46c0 nop ; (mov r8, r8)
0020000c <thread1>:
相同的对齐方式。
20000e: 6813 ldr r3, [r2, #0]
200010: 3301 adds r3, #1
200012: 6013 str r3, [r2, #0]
200014: e7fb b.n 20000e <thread1+0x2>
20001e: 6813 ldr r3, [r2, #0]
200020: 3301 adds r3, #1
200022: 6013 str r3, [r2, #0]
200024: e7fb b.n 20001e <thread2+0x2>
这两个环路可能具有相同的对准,这不是故意的。
也许我们想换一个词。
volatile unsigned int ctr1,ctr2;
asm ("nop");
void thread1(void)
{
这是多么令人敬畏啊,一定比例的人现在退缩了,另一个比例的人是“你可以那样做!哇!”
20000a: 46c0 nop ; (mov r8, r8)
20000c: bf00 nop
20000e: bf00 nop
00200010 <thread1>:
200010: 4a02 ldr r2, [pc, #8] ; (20001c <thread1+0xc>)
200012: 6813 ldr r3, [r2, #0]
200014: 3301 adds r3, #1
200016: 6013 str r3, [r2, #0]
200018: e7fb b.n 200012 <thread1+0x2>
20001a: bf00 nop
20001c: 00200034 eoreq r0, r0, r4, lsr r0
00200020 <thread2>:
200020: 4a02 ldr r2, [pc, #8] ; (20002c <thread2+0xc>)
200022: 6813 ldr r3, [r2, #0]
200024: 3301 adds r3, #1
200026: 6013 str r3, [r2, #0]
200028: e7fb b.n 200022 <thread2+0x2>
20002a: bf00 nop
20002c: 00200030 eoreq r0, r0, r0, lsr r0
虽然起了作用,对他们施加了一些压力,但比我想要的要多。
void thread1(void)
{
asm ("nop");
while(1)
{
ctr1++;
}
}
void thread2(void)
{
while(1)
{
ctr2++;
}
}
奏效了。
0020000c <thread1>:
20000c: bf00 nop
20000e: 4a02 ldr r2, [pc, #8] ; (200018 <thread1+0xc>)
200010: 6813 ldr r3, [r2, #0]
200012: 3301 adds r3, #1
200014: 6013 str r3, [r2, #0]
200016: e7fb b.n 200010 <thread1+0x4>
200018: 00200030 eoreq r0, r0, r0, lsr r0
0020001c <thread2>:
20001c: 4a02 ldr r2, [pc, #8] ; (200028 <thread2+0xc>)
20001e: 6813 ldr r3, [r2, #0]
200020: 3301 adds r3, #1
200022: 6013 str r3, [r2, #0]
200024: e7fb b.n 20001e <thread2+0x2>
200026: bf00 nop
200028: 0020002c eoreq r0, r0, ip, lsr #32
把它从0x20000e推到0x200010,这可能是我想要的对齐方式,至少我可以移动一点。它确实在执行路径中添加了一条额外的指令,但是。。。对齐一个循环。
现在就对另一个做同样的事
200010: 6813 ldr r3, [r2, #0]
200012: 3301 adds r3, #1
200014: 6013 str r3, [r2, #0]
200016: e7fb b.n 200010 <thread1+0x4>
200020: 6813 ldr r3, [r2, #0]
200022: 3301 adds r3, #1
200024: 6013 str r3, [r2, #0]
200026: e7fb b.n 200020 <thread2+0x4>
我改变了他们的路线。丑陋的是的!。没有从头开始写讽刺或转换为讽刺那么丑陋?可辩论。但是您可以看到,任何链接到这些函数前面的代码更改都需要重新调优。这会非常痛苦。
volatile unsigned int ctr1,ctr2;
asm (".balign 0x10; .word 0,0,0");
void thread1(void)
{
asm ("nop");
while(1)
{
ctr1++;
}
}
void thread2(void)
{
asm ("nop");
while(1)
{
ctr2++;
}
}
不过,只要我们不更改这些函数,也不更改编译器或编译器选项,这种方法就应该有效。基本上,如果编译器不断生成相同的机器代码,那么这种破解将使它们保持一致。是的,非常令人尴尬,但如果它有效,那是愚蠢的吗?
0020001c <thread1>:
20001c: bf00 nop
20001e: 4a02 ldr r2, [pc, #8] ; (200028 <thread1+0xc>)
200020: 6813 ldr r3, [r2, #0]
200022: 3301 adds r3, #1
200024: 6013 str r3, [r2, #0]
200026: e7fb b.n 200020 <thread1+0x4>
200028: 00200040 eoreq r0, r0, r0, asr #32
0020002c <thread2>:
20002c: bf00 nop
20002e: 4a02 ldr r2, [pc, #8] ; (200038 <thread2+0xc>)
200030: 6813 ldr r3, [r2, #0]
200032: 3301 adds r3, #1
200034: 6013 str r3, [r2, #0]
200036: e7fb b.n 200030 <thread2+0x4>
200038: 0020003c eoreq r0, r0, ip, lsr r0
哈哈,我超过了答案的最大大小?如果你已经做到了,请举手。这不是我第一次。为我和其他所有经历过这一切的人感到难过。。。。
我删除的1000行/字符中有什么。基本上,您可以根据特定平台的编译器或手动调整汇编语言,并为其提供一些保护,以免工具破坏您的对齐。例如,如果你正在尝试计算空闲时间,那么你会想要手动调整asm。。。。或者实际上,“它有一个管道,愚蠢”的人会突然出来,告诉你用计时器代替。他们是对的。
尝试手动调谐的一刀切。。。只会失败。
好的,这很合适,我没有超出答案的大小。