我正在尝试在我的恩智浦LPC11U37H主板( ARM Cortex-M0 )上分析一些算法,因为我想知道执行特定算法需要多少个时钟周期。
我编写了这些简单的宏来进行一些分析:
#define START_COUNT clock_cycles = 0;\
Chip_TIMER_Enable(LPC_TIMER32_1);\
Chip_TIMER_Reset(LPC_TIMER32_1);\
#define STOP_COUNT Chip_TIMER_Disable(LPC_TIMER32_1);\
#define GET_COUNT clock_cycles = Chip_TIMER_ReadCount(LPC_TIMER32_1);\
myprintf("%d\n\r", clock_cycles);\
基本上, START_COUNT 会重置 clock_cycles 变量,并启用并重置计数器,该计数器配置为以与微控制器相同的频率计数(48MHz)。 STOP_COUNT 停止计时器,而 GET_COUNT 读取计时器值并使用UART打印它( myprintf()只是一个通过串口)。
当我想描述一些算法时,我只是这样做:
START_COUNT;
algorithm();
STOP_COUNT;
GET_COUNT;
一切正常,但似乎有些不对劲。确实,我试图描述这段代码:
START_COUNT;
for (volatile int i = 0; i < 1000; i++);
STOP_COUNT;
GET_COUNT;
START_COUNT;
for (volatile int i = 0; i < 1000; i++);
STOP_COUNT;
GET_COUNT;
START_COUNT;
for (volatile int i = 0; i < 1000; i++);
STOP_COUNT;
GET_COUNT;
我得到了以下时钟周期值:
21076
19074
21074
这很奇怪,因为编译器配置为不优化任何东西(GCC -O0,在调试模式下)。因此,我检查了三个代码块的汇编代码,它们完全相同(内存地址等除外)你可以在这里查看:http://pastebin.com/raw/x6tbi3Mr - 如果你看到一些ISB / DSB函数,那是因为我试图解决这个问题,但它没有用。)
此外,我禁用了任何中断。
我想知道问题是什么。有什么我不考虑的吗?
答案 0 :(得分:4)
好的有一些乐趣,为你做了一个简单的例子。首先,每年都会有新的开发人员出现,不知道Michael Abrash是谁,世界变了是,工具更好,硬件,很多人可以调整东西。但汇编语言的禅与IMO非常相关,尤其是这个问题。
https://github.com/jagregory/abrash-zen-of-asm
当这本书出版并且今天对其进行的性能调整甚至不那么重要时,8088就是旧闻。但如果这就是你在本书中看到的所有内容,那么你就错过了。我使用了我在下面学到的东西,每天都在使用逻辑,芯片和电路板......让它们表现出色和/或让它们破碎。这个答案的重点不一定是为了展示如何分析某些东西,尽管它会,因为你也已经在分析某些东西。但它有助于表明它并不像您期望的那样简单,除了您编写的C代码之外还有其他因素。将C代码放置在闪存中,闪存与内存,等待或不等待,如果你有预取,分支预测,如果你有它,都会产生很大的不同。我甚至可能会演示相同的指令序列,并使用不同的对齐方式更改结果。很高兴你没有在cortex-m0上有一个缓存,这需要混乱和正方形......
我在某处有NXP芯片,附近至少有一个cortex-m0 +,但是从st。选择了一个cortex-m0。 STM32F030K6T6因为它已经连线并准备好玩了。有一个内置的8Mhz振荡器和一个pll乘以第一次播放8Mhz然后用48.它没有四种不同的等待状态作为你的芯片,它有两个选择,小于或等于24Mhz或更大(最多48)。但它确实有一个预取,你可能没有。
您可能有一个systick定时器,芯片供应商可以选择是否编译。它们总是在同一个地址(到目前为止,如果存在的皮层中)
#define STK_CSR 0xE000E010
#define STK_RVR 0xE000E014
#define STK_CVR 0xE000E018
#define STK_MASK 0x00FFFFFF
PUT32(STK_CSR,4);
PUT32(STK_RVR,0xFFFFFFFF);
PUT32(STK_CVR,0x00000000);
PUT32(STK_CSR,5);
//count down.
PUT32是一个抽象,长篇故事不会在这里进入
.thumb_func
.globl PUT32
PUT32:
str r1,[r0]
bx lr
现在添加测试功能
.align 8
.thumb_func
.globl TEST
TEST:
ldr r3,[r0]
test_loop:
sub r1,#1
bne test_loop
ldr r2,[r0]
sub r3,r2
mov r0,r3
bx lr
最简单的一个是读取时间,循环传递次数,然后读取时间并减去以获得时间上的增量。并返回。不久将在循环顶部和减去之间添加nops。
使用对齐我正在强制启动函数:
08000100 <TEST>:
8000100: 6803 ldr r3, [r0, #0]
08000102 <test_loop>:
8000102: 3901 subs r1, #1
8000104: d1fd bne.n 8000102 <test_loop>
8000106: 6802 ldr r2, [r0, #0]
8000108: 1a9b subs r3, r3, r2
800010a: 1c18 adds r0, r3, #0
800010c: 4770 bx lr
800010e: 46c0 nop ; (mov r8, r8)
8000110: 46c0 nop ; (mov r8, r8)
8000112: 46c0 nop ; (mov r8, r8)
顺便说一下,谢谢你提出这个问题,我没有意识到这个芯片的示例代码,没有将闪存等待状态设置为48MHz ......
所以在8mhz我可以使用四种组合,快速和慢速闪光设置,无论是否启用预取。
PUT32(FLASH_ACR,0x00);
ra=TEST(STK_CVR,1000);
hexstring(ra);
ra=TEST(STK_CVR,1000);
hexstring(ra);
PUT32(FLASH_ACR,0x10);
ra=TEST(STK_CVR,1000);
hexstring(ra);
ra=TEST(STK_CVR,1000);
hexstring(ra);
PUT32(FLASH_ACR,0x01);
ra=TEST(STK_CVR,1000);
hexstring(ra);
ra=TEST(STK_CVR,1000);
hexstring(ra);
PUT32(FLASH_ACR,0x11);
ra=TEST(STK_CVR,1000);
hexstring(ra);
ra=TEST(STK_CVR,1000);
hexstring(ra);
所以TEST函数如上所述使用8mhz内部没有pll。
00000FA0
00000FA0
00000FA0
00000FA0
00001B56
00001B56
00000FA2
00000FA2
然后在测试循环中添加更多nops
add one nop
00001388
00001388
00001388
00001388
00001F3F
00001F3F
00001389
00001389
two nops
00001770
00001770
00001770
00001770
0000270E
0000270E
00001B57
00001B57
three nops
00001B58
00001B58
00001B58
00001B58
00002AF7
00002AF7
00002133
00002133
eight nops
00002EE0
00002EE0
00002EE0
00002EE0
00004A36
00004A36
000036AE
000036AE
9
000032C8
000032C8
000032C8
000032C8
00004E1F
00004E1F
00003A96
00003A96
10
000036B0
000036B0
000036B0
000036B0
000055EE
000055EE
00003E7E
00003E7E
11
00003A98
00003A98
00003A98
00003A98
000059D7
000059D7
00004266
00004266
12
00003E80
00003E80
00003E80
00003E80
000061A6
000061A6
0000464E
0000464E
16
00004E20
00004E20
00004E20
00004E20
00007916
00007916
000055EE
000055EE
no wait state speeds
0x0FA0 = 4000 0
0x1388 = 5000 1
0x1770 = 6000 2
0x1B58 = 7000 3
0x2EE0 = 12000 8
0x4E20 = 20000 16
slow flash times
0x1B56 = 6998 0
0x1F3F = 7999 1
0x270E = 9998 2
0x2AF7 = 10999 3
0x4A36 = 18998 8
0x4E1F = 19999 9
0x55EE = 21998 10
0x59D7 = 22999 11
0x61A6 = 24998 12
0x7916 = 30998
因此,对于这个芯片,无预取的无等待状态是相同的,并且就我测试而言是线性的。添加一个nop你添加1000个时钟。现在为什么没有nop这是一个减法和一个分支,如果不是每个循环4个指令而不是2.可能是管道或可能是amba / axi总线,早已消失的是cpu总线只是一个地址的日子和一些闪光灯(可以在开放式线上设计叉骨)。你可以从武器网站下载amba / axi的东西,看看那里发生了什么,所以这可能是管道,或者这可能是公交车的副作用,我的猜测就是管道。
现在慢速闪光设置是迄今为止最有趣的。 no nop循环基本上是7000个时钟而不是4000个,所以感觉每个指令中还有3个等待状态。每个nop给我们1000多个时钟,所以没关系。直到我们从9到10开始,这花费我们2000,然后从11到12又是2000.所以与无等待状态版本不同,这是非线性的,是因为指令的预取推动跨越边界?
所以,如果我绕道而行,在TEST标签和时间戳加载到r3之间我添加一个nop,这也应该推动循环后端的对齐。但这并没有改变循环中8个nops的时间。在前面添加第二个nop以推动对齐也不会改变时间。这个理论非常重要。
切换到48MHz。
slow, no prefetch
00001B56
00001B56
slow, with prefetch
00000FA0
00000FA2
9 wait states
00004E1F
00004E1F
00003A96
00003A96
10 wait states
000055EE
000055EE
00003E7E
00003E7E
那里没有真正的惊喜。我不应该使用快速闪存设置运行,因此无论预取还是不预取都会很慢。并且相对于定时器的速度是相同的,该定时器基于整个芯片运行的时钟。我们看到同样有趣的情况,即性能中存在非线性步骤。记住/理解,即使它的时钟周期数相同,在这种情况下,这个时钟的速度要快6倍,因此这个代码运行速度比8MHz快6倍。应该是显而易见的,但不要忘记将其纳入分析。
我想有趣的是,启用预取功能后,我们得到0xFA0数字。了解预取有时会有所帮助,有时甚至会受到伤害,可能不会太难以创建一个基准来证明它有助于而不是帮助或不以线性方式提供帮助。我们不知道这个硬件是如何工作的,但如果预取是4个字,则第一个字处于3个等待状态,但接下来的三个处于一个等待状态。但是,如果我的代码正在做一些跳跃的事情
b one
nop
nop
nop
one:
b two
nop
nop
nop
two:
等等。不知道硬件如何工作每个分支目的地将是6个时钟来获取预取,他们可能只有3个时钟没有,谁知道...像缓存那样,你读取的额外东西的时间惩罚和不#&# 39;使用。缓存是否超重读取和未使用的东西?同样这里预取时间增益是否超过未使用的预取内容?
在离开之前的最后一件事,如果我把代码用零nops,并且有很多方法可以做到这一点,但是如果我只是强行将它强制转换为自我修改代码中的sram(或者如果你的引导加载方式) will)然后分支到它
ra=0x20000800;
PUT16(ra,0x6803); ra+=2;
PUT16(ra,0x3901); ra+=2;
PUT16(ra,0xd1fd); ra+=2;
PUT16(ra,0x6802); ra+=2;
PUT16(ra,0x1a9b); ra+=2;
PUT16(ra,0x1c18); ra+=2;
PUT16(ra,0x4770); ra+=2;
PUT16(ra,0x46c0); ra+=2;
PUT16(ra,0x46c0); ra+=2;
PUT16(ra,0x46c0); ra+=2;
PUT16(ra,0x46c0); ra+=2;
PUT16(ra,0x46c0); ra+=2;
PUT16(ra,0x46c0); ra+=2;
ra=branchto(STK_CVR,1000,0x20000801);
hexstring(ra);
ra=branchto(STK_CVR,1000,0x20000801);
hexstring(ra);
.thumb_func
.globl branchto
branchto:
bx r2
00000FA2
00000FA0
这是48Mhz顺便说一句。我得到了没有等待状态和/或预取的0xFA0数字。在此之后我没有尝试任何更多的实验,但我怀疑从ram运行将不会有任何悬崖性能它将是线性的,像这样的简单测试。这将是你最好的表现。但是通常相对于闪光灯,你没有太多的东西。
当你拥有类似你拥有的芯片时,以及当你使用相对时钟时。在这种情况下,例如在8MHz,我们有一个占用0xFA0或4000个时钟的循环。 500US。在48mhz,我们从146us开始,并达到83us。但是相同的4000个时钟在24MHz没有预取,预计在没有预取的25Mhz 280us时为167us,性能明显更快,因为我们必须添加这些等待状态。当您处于等待状态设置的最高时钟速率时,您的芯片具有四种不同的等待状态设置(或任何具有无法在没有等待状态的情况下无法运行全速范围的闪存的微控制器),然后只是在下一个等待状态设置该设置的最慢时钟有一个性能命中。理想情况下,您希望以目标等待状态设置的最大时钟速度运行性能(而不是关心功耗和其他问题)。
当你说使用皮质-m4,使用i和d缓存,更宽的时钟范围,我认为和其他东西时,这些皮质-m0都很简单。分析变得难以实现,在内存中移动相同的指令,无论哪种方式,您的性能都可以从根本不变为10%或20%。更改高级别的一行代码或在代码中添加单个指令,您可以再次看到性能从小到大的变化。这意味着你不能为此调整,你不能只说这100行代码快速,然后修改它们周围的代码,并假设它们将继续这么快。将它们放在一个函数中并没有帮助,当你在程序的其余部分添加或删除东西时,该函数也会移动,从而改变它的性能。最好你必须做我在这里演示的内容,并且更准确地控制放置代码的位置,以便功能始终存在。这仍然无法在具有缓存的平台上提供可重复的性能,因为每次调用该函数之间发生的事情都会影响缓存中的内容和内容,以及该函数如何执行结果。
这是汇编代码,而不是我测试的编译C。编译器为此添加了另一个皱纹。有些人认为相同的C代码总是生成相同的机器代码。当然不是真的,首先是优化。还要理解一个编译器与另一个编译器不生成相同的代码,或者你不能假设,例如gcc vs llvm / clang。同样的同一编译器的不同版本,gcc 3.x,4.x等,gcc甚至颠覆的情况在性能上有时变化很大,其他一切保持不变(相同的源和相同的构建命令),它是不正确的是,新版本产生更快的代码,gcc没有遵循这一趋势,通用编译器不适用于任何特定平台。他们从一个版本添加到下一个版本的内容并不完全是关于输出的性能。 Gcc作为源代码分发,有很多构建旋钮,你可以使用不同的构建选项制作相同版本的gcc的多个版本,我敢打赌,你可能最终得到报告相同版本的两个编译器构建的东西的不同结果,所有其他事情都是平等的。
根据经验,有时很容易采用相同的代码并在同一硬件上进行性能更改。或者进行微小的修改,你不会认为这会产生影响但是会有所改变。或者,如果您可以访问逻辑,则可以创建程序以执行具有显着不同的性能时间的任务。这一切都始于一本像集会或其他人的书,以睁开你的眼睛看这些简单的事情,快速前进20年与几十个硬件性能小玩意儿,每一个有时帮助和伤害他人。正如Abrash用很多话说的那样,有时候你必须尝试一些疯狂的事情并且只是为了看时间,你最终可能会得到更好的表现。
所以我不知道你对这个微控制器的目标是什么,但你需要继续重新编写你的代码,不要假设第一次是最终的答案。每次从任何源代码行更改任何内容到编译器选项或版本时,性能都会发生变化,性能会发生显着变化。允许您的设计有很大的余地,或者测试和调整每个版本。
你所看到的并不一定是一个惊喜。再次使用Abrash,它也可能就是您使用该计时器的方式......了解您的工具并确保您的计时器以您期望的方式工作。或者它可能是别的东西。