gpt4 book ai didi

c - 相同(重复)代码的时钟周期值不同

转载 作者:行者123 更新时间:2023-12-04 15:23:46 34 4
gpt4 key购买 nike

我想在我的NXP 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指令,那是因为我试图解决此问题,但没有成功)。

此外,我禁用了任何中断。

我想知道是什么问题。有没有我不考虑的东西吗?

最佳答案

好的,很开心,为您举了一个简单的例子。首先,每年都会过去,不知道迈克尔·阿布拉什(Michael Abrash)是谁的新开发人员来了,世界已经改变了,工具更好,硬件更好,很多人都可以进行调整。但是禅宗的汇编语言与IMO非常相关,尤其是这个问题。

https://github.com/jagregory/abrash-zen-of-asm

当这本书问世时,8088是个老新闻,而今天对其进行性能调整的意义甚至不大。但是,如果这是您在本书中看到的全部内容,那么您将丢失。我用在下面学到的东西,每天都在逻辑,芯片和板上跳动……使它们发挥作用和/或使它们断裂。

答案的重点不一定是显示如何概要分析某些内容,尽管它可以,因为您已经在概要分析某些内容。但这有助于表明它并不像您期望的那样简单,除了您编写的C代码之外,还有其他因素。 C代码在flash中的放置,flash与ram,是否等待状态,是否预取(如果有),分支预测(如果有)都将产生很大的不同。我什至可以演示相同的指令序列,但对齐方式会有所不同,从而改变结果。高兴的是,您在cortex-m0上没有缓存,这会使混乱并平方...

我在某处有NXP芯片,并且附近至少有一个cortex-m0 +,但是从st.com选择了一个cortex-m0。 STM32F030K6T6,因为它已经连接好并可以使用了。有一个内置的8Mhz振荡器和一个pll乘以,因此首先使用8Mhz然后再使用48。它没有四个不同的等待状态作为您的芯片,它有两种选择,小于或等于24Mhz或大于(最多48个)。但是它确实有预取功能,您可能没有。

您可能有一个systick计时器,芯片供应商可以选择是否编译。它们始终位于同一地址(到目前为止,在cortex-ms中)

#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


最简单的方法是读取时间,对传入的次数进行循环,然后读取时间并减去以得到时间增量。并返回。不久将在循环顶部和减法之间添加点。

与对齐我强迫功能的开始:

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)


顺便说一句,感谢您提出这个问题,我没有意识到我的示例代码,没有将Flash等待状态设置为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);


因此,使用8mhz内部no pll可以如上编写测试功能。

00000FA0
00000FA0
00000FA0
00000FA0
00001B56
00001B56
00000FA2
00000FA2


然后在测试循环中添加更多点

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


因此,对于该芯片,有无预取的无等待状态是相同的,据我测试,它是线性的。添加一个点,您将添加1000个时钟。现在为什么没有nop是一个减法和一个分支,如果每个循环不等于4条指令而不是2条指令。那可能是管道,也可能是amba / axi总线,而cpu总线只是一个地址的日子早已过去和一些频闪灯(好在opencore上的叉骨设计)。您可以从武器网站下载amba / axi资料,以查看发生了什么,因此这可能是管道,也可能是总线的副作用,我想是管道。

现在,慢速闪光设置是迄今为止最有趣的设置。 no nop循环基本上是7000个时钟而不是4000个时钟,因此感觉每条指令那里还有3个等待状态。每个小点都会给我们提供1000个时钟,这样就可以了。直到我们从9点减少到10点,这要花费我们2000,然后从11点减少到12点才是2000。所以与无等待状态版本不同,这是非线性的,是因为指令的预取越过边界吗?

因此,如果我绕道而行,并且在TEST标签和载入r3的时间戳之间添加了一个nop,那也应该推动循环后端的对齐。但这并不会改变循环中8点的时间。向前添加第二个点按以推动对齐方式也不会更改时间。对于该理论而言,如此之多。

切换到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个等待状态,而接下来的3个处于一个等待状态。但是如果我的代码正在做一些跳跃的事情怎么办

b one
nop
nop
nop
one:
b two
nop
nop
nop
two:


等等。不知道硬件的工作方式,每个分支目标都需要6个时钟来进行预取,而如果没有预取,它们可能只有3个时钟,谁知道...像高速缓存一样,读取和删除多余的东西会浪费时间。使用。缓存命中超过了已读取但未使用的内容吗?同样,预取时序增益是否超过未使用的预取内容?

如果让我零点走代码,还有很多方法可以做到这一点,但是如果我只是以一种自我修改的代码方式(或者如果愿意的话,可以采用引导加载程序的方式)将其强行插入sram,然后分支到它

    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 btw。我得到了没有等待状态和/或启用了预取时看到的0xFA0数字。在此之后,我没有再尝试任何实验,但是我怀疑从ram跑下来不会对性能产生任何影响,对于像这样的简单测试,它将是线性的。这将是您最好的表现。但您通常没有很多相对于Flash的东西。

当您拥有类似的筹码时,以及使用相对时钟时。在这种情况下,例如在8MHz下,我们有一个循环采用0xFA0或4000个时钟。 500us。在48MHz时,我们从146us开始,直到83us。但是如果在不进行预取的情况下,在24MHz不进行预取的情况下,相同的4000个时钟在不进行预取的情况下在25Mhz 280us时为167us,则更快的时钟会明显降低性能,因为我们必须添加这些等待状态。当您处于等待状态设置的最高时钟频率时,您的芯片具有四个不同的等待状态设置(或其中任何带有闪存的微控制器中的任何一个都无法在没有等待状态的情况下以全速运行),然后刚好处于下一个等待状态设置最慢的时钟会影响性能。理想的情况是要提高性能(而不关心功耗和其他问题),而要以目标等待状态设置的最大时钟速度运行。

当您说使用带有i和d缓存的cortex-m4,更宽的时钟范围,我认为的最小mmu等功能时,这些cortex-m0几乎就变得简单了。分析变得困难甚至不可能,在内存中四处移动相同的指令,您的性能可能会从根本不改变为百分之十或百分之二十。在高级别上更改一行代码或在代码内添加一条指令,您就可以再次看到性能的小到大变化。这意味着您不能为此进行调整,您不能仅说这100行代码如此之快,然后修改它们周围的代码并假定它们将继续如此之快。将它们放在函数中并没有帮助,当您在程序的其余部分添加或删除内容时,该函数也会移动,从而改变其性能。最好的情况下,您必须执行我在此处演示的内容,并更好地控制代码的确切位置,以使函数始终存在。而且,在具有缓存的平台上,这仍然不能为您提供可重复的性能,因为每次调用该函数之间发生的情况都会影响缓存中的内容和内容以及该功能的执行方式。

这是汇编代码,而不是我测试过的编译后的C语言。编译器为此增加了另一条皱纹。有些人认为相同的C代码总是产生相同的机器代码。当然不正确,首先要进行优化。还应了解,一个编译器与另一个编译器不会生成相同的代码,或者您不能假设,例如gcc vs llvm / clang。同样,同一编译器的不同版本(gcc 3.x,4.x等),对于gcc,即使是子版本,其性能有时也会有很大差异,而其他所有内容都保持不变(相同的源代码和相同的构建命令),这是较新的版本会生成更快的代码是不正确的,gcc并未遵循这种趋势,通用编译器在任何特定平台上均无法很好地工作。他们从一个版本添加到下一个版本的事情并不仅仅是输出的性能。 Gcc作为源分发有很多构建旋钮,您可以使用不同的构建选项对同一个版本的gcc进行多个构建,我敢打赌,在报告这两个版本的两个编译器构建的结果中,您最终可能会获得不同的结果,所有其他条件保持不变。

凭经验,有时使用相同的代码并在相同的硬件上更改其性能变得非常容易。或进行一些微小的修改,但您认为不会有所作为,但确实可以。或者,如果您有权访问逻辑,则可以创建程序来执行具有明显不同的性能时间的任务。一切始于诸如禅宗的汇编书或其他一些书,这些书使您对这些简单的事物睁开眼睛,在数十年的硬件性能小发明中飞速向前迈进了20年,其中每一个有时都会有所帮助并伤害他人。正如Abrash所说的那样,有时您不得不尝试一些疯狂的事情并花些时间看一下,您最终可能会获得性能更好的东西。

因此,我不知道使用该微控制器的目标是什么,但是您将需要在进行过程中继续重新配置代码,不要以为第一次是最终答案。每次从源代码行到编译器选项或版本进行任何更改时,性能都会发生显着变化。在设计中留出很大的余地,或者测试和调整每个版本。

您所看到的不一定是一个惊喜。再次使用Abrash,这也可能只是您使用该计时器的方式...了解您的工具,并确保您的计时器以您期望的方式工作。或者可能是其他。

关于c - 相同(重复)代码的时钟周期值不同,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36627456/

34 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com