【问题标题】:Number of instructions used ARMv7使用的指令数 ARMv7
【发布时间】:2017-05-27 09:12:24
【问题描述】:

我正在尝试计算执行延迟函数将使用多少 CPU 周期

delay:
 subs r0, #1
 bmi end_delay
 b delay
 end_delay:
 bx lr

我直觉上觉得每条指令应该使用1个CPU周期,所以如果我们从r0 =4开始,那么完成下面的代码需要11个CPU周期,对吗?

【问题讨论】:

  • 不确定,也因平台和实现而异。
  • 嗯,抱歉,您出于某种原因说的是 cortex-m4。如果这只是一般的armv7,意味着它可能是armv7-a armv7-r armv7-m,那么在下面回答我的答案并在两个方向上更快和更慢地延长执行时间,我将不得不挖出树莓派(不适合这个) 或 beaglebone 或 NTC C.H.I.P 或其他带有 armv7 的板,理想情况下是一些片上 sram。 (allwinner 是的,不要认为 Broadcom 会,也许 ti,不记得了)。然后开始测试。 caches 和 DRAM 只会让它变慢。

标签: arm cpu-architecture bare-metal cpu-speed


【解决方案1】:

cortex-m 与微芯片 pic 芯片(或 z80 和其他一些芯片)不同,您无法使用此指令集以这种方式创建可预测的延迟。您可以确保它会在某个时间量(时钟)处于 OR SLOWER 但不正确。

0000009c <hello>:
  9c:   3801        subs    r0, #1
  9e:   d1fd        bne.n   9c <hello>

你的循环在那里有一个分支决策,更多的指令和更多的路径,所以执行时间变化的机会变得更糟。

00000090 <delay>:
  90:   3801        subs    r0, #1
  92:   d400        bmi.n   96 <end_delay>
  94:   e7fc        b.n 90 <delay>

00000096 <end_delay>:

所以如果我们专注于这三个指令。

一些 cortex-ms 有一个构建(逻辑的)时间选项,可以按指令或按字获取,cortex-m4 文档说:

所有提取都是字宽的。

所以我们希望半字对齐不会影响性能。有了这些说明,我们不一定希望看到差异。使用全尺寸手臂,提取是多个单词,因此您肯定会看到提取线(大小)的影响。

执行很大程度上取决于实现。 cortex-m 只是 arm 核心,芯片的其余部分来自芯片供应商、购买的 IP 或内置或组合(很可能是后者)。 ARM 不生产芯片(可能用于验证除外),他们生产自己销售的 IP。

芯片供应商确定闪存(和 ram)的实现方式,通常使用这些类型的芯片,闪存速度等于或低于 cpu 速度,这意味着它可能需要两个时钟来获取一条指令,这意味着您永远不会馈送 cpu尽可能快。有些像 ST 有一个缓存,你不能(据我所知)关闭,所以很难看到这种效果(但仍然可能),我为此使用的特定芯片说:

8.2.3.1 预取缓冲区 Flash 存储器控制器有一个预取缓冲区,当 CPU 频率较高时会自动使用该缓冲区 大于 40 兆赫。在这种模式下,闪存运行在一半 系统时钟。预取缓冲器每个时钟提取两个 32 位字 允许在没有等待状态的情况下获取指令,同时代码 线性执行。提取缓冲区包括分支推测 识别分支并避免额外等待状态的机制 阅读下一个单词对。此外,短循环分支经常留在 缓冲区。结果,一些分支可以在没有等待的情况下执行 状态。其他分支会产生一个等待状态。

当然,像 ST 他们并没有真正告诉你整个故事。所以我们就进去试试这个。如果需要,您可以使用调试计时器,但 systick 运行相同的时钟并为您提供相同的结果

00000086 <test>:
  86:   f3bf 8f4f   dsb sy
  8a:   f3bf 8f6f   isb sy
  8e:   680a        ldr r2, [r1, #0]

00000090 <delay>:
  90:   3801        subs    r0, #1
  92:   d400        bmi.n   96 <end_delay>
  94:   e7fc        b.n 90 <delay>

00000096 <end_delay>:
  96:   680b        ldr r3, [r1, #0]
  98:   1ad0        subs    r0, r2, r3
  9a:   4770        bx  lr

所以我读取了 CCR 和 CPUID

00000200 CCR
410FC241 CPUID

只是因为。然后将被测代码运行 3 次

00000015
00000015
00000015

这些数字是十六进制的,所以是 21 条指令。每次执行时间相同,因此没有缓存或分支预测缓存效果。我没有看到任何与 cortex-m4 上的分支预测相关的东西,其他 cortex-ms 确实有分支预测(可能只有 m7)。我关闭了 I 和 D 缓存,它们当然会随着对齐方式极大地影响执行时间(并且该时间可以/会随着您的应用程序运行而变化)。

我更改了对齐方式(在此代码前面添加或删除 nop)

0000008a <delay>:
  8a:   3801        subs    r0, #1
  8c:   d400        bmi.n   90 <end_delay>
  8e:   e7fc        b.n 8a <delay>

并且不影响执行时间。

AFAIK 使用此处理器,我们无法直接更改闪存等待状态设置,它是基于时钟设置的自动设置,因此以不同的时钟速度运行,我得到的 40Mhz 标记以上

0000001E                                                                                         
0000001E                                                                                         
0000001E 

对于相同的机器代码,相同的对齐现在 30 个时钟而不是 21 个。

通常 ram 更快并且没有等待状态(理解这些总线每个事务需要几个时钟,所以它不像过去那样,但仍然可以检测到延迟),所以在 ram 中运行这些指令应该会告诉我们的东西

for(rb=0;rb<0x20;rb+=2)
{

    hexstrings(rb);
    ra=0x20001000+rb;
    PUT16(ra,0x680a); ra+=2;
    hexstrings(ra);
    PUT16(ra,0x3801); ra+=2;
    PUT16(ra,0xd400); ra+=2;
    PUT16(ra,0xe7fc); ra+=2;
    PUT16(ra,0x680b); ra+=2;
    PUT16(ra,0x1ad0); 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;
    hexstring(BRANCHTO(4,STCURRENT,0x20001001+rb)&STMASK);
}

这肯定会很有趣......

00000000 20001002 00000026                                                                       
00000002 20001004 00000020                                                                       
00000004 20001006 00000026                                                                       
00000006 20001008 00000020                                                                       
00000008 2000100A 00000026                                                                       
0000000A 2000100C 00000020                                                                       
0000000C 2000100E 00000026                                                                       
0000000E 20001010 00000020                                                                       
00000010 20001012 00000026                                                                       
00000012 20001014 00000020                                                                       
00000014 20001016 00000026                                                                       
00000016 20001018 00000020                                                                       
00000018 2000101A 00000026                                                                       
0000001A 2000101C 00000020                                                                       
0000001C 2000101E 00000026                                                                       
0000001E 20001020 00000020 

首先是 32 或 38 个时钟,其次是对齐效果

armv7-m CCR 显示一个分支预测位,但 trm 和供应商文档没有显示它,因此它可能是并非所有内核都支持的通用事物。

因此,对于特定的 cortex-m4 芯片,执行循环的时间在 21 到 38 个时钟之间,如果我愿意,我可能会使其变慢。我不认为我可以在这个芯片上把它降到 11。

例如,如果您正在做 i2c 位敲击,您可以使用类似的东西来延迟,这将正常工作,不会是最佳的,但会正常工作。如果您在一个时间窗口内需要更精确的东西,至少这个但不大于然后使用计时器(并且了解轮询或中断您的准确性会有一些错误)如果计时器外围设备或其他可以产生您想要的信号,您可以然后得到一个时钟准确的波形(如果这是你的延迟)。

另一个 cortex-m4 预计会有不同的结果,我希望 stm32 的 sram 与 flash 相同或更快,而不是在这种情况下更慢。如果您依赖其他人来设置您的芯片,您可能会弄乱您的初始化代码,这可能/将会影响执行时间。

编辑

我不知道我从哪里得到的想法是用于 cortex-m4,它是 armv7-m,所以我没有手边的树莓派 2,但有一个 pi3,并在 aarch32 模式下运行,32 位指令。我不知道要让计时器运行然后启用缓存需要做多少工作。 pi 用完了 DRAM,即使使用裸机也非常不一致。所以我想我会启用 l1 缓存,第一次运行后它应该都在缓存中并且一致。现在我想起来有四个核心,每个都在运行,不知道如何禁用它们,其他三个在循环中旋转,等待邮箱寄存器告诉他们运行什么代码。也许我需要让它们在某个地方分支并用完 l1 缓存......不确定 l1 是每个核心还是共享的,我想我曾经看过。

无论如何代码都在测试中

000080c8 <COUNTER>:
    80c8:   ee192f1d    mrc 15, 0, r2, cr9, cr13, {0}

000080cc <delay>:
    80cc:   e2500001    subs    r0, r0, #1
    80d0:   4a000000    bmi 80d8 <end_delay>
    80d4:   eafffffc    b   80cc <delay>

000080d8 <end_delay>:
    80d8:   ee193f1d    mrc 15, 0, r3, cr9, cr13, {0}
    80dc:   e0430002    sub r0, r3, r2
    80e0:   e12fff1e    bx  lr

并且妙语是该对齐方式的第一列是通过的 r0,接下来的三列是三个运行,最后一列如果存在从先前运行到当前运行的增量(r0 中额外计数值的成本)

00000000 0000000A 0000000A 0000000A 
00000001 00000014 00000014 00000014 0000000A 
00000002 0000001E 0000001E 0000001E 0000000A 
00000003 00000028 00000028 00000028 0000000A 
00000004 00000032 00000032 00000032 0000000A 
00000005 0000003C 0000003C 0000003C 0000000A 
00000006 00000046 00000046 00000046 0000000A 
00000007 00000050 00000050 00000050 0000000A 
00000008 0000005A 0000005A 0000005A 0000000A 
00000009 00000064 00000064 00000064 0000000A 
0000000A 0000006E 0000006E 0000006E 0000000A 
0000000B 00000078 00000078 00000078 0000000A 
0000000C 00000082 00000082 00000082 0000000A 
0000000D 0000008C 0000008C 0000008C 0000000A 
0000000E 00000096 00000096 00000096 0000000A 
0000000F 000000A0 000000A0 000000A0 0000000A 
00000010 000000AA 000000AA 000000AA 0000000A 
00000011 000000B4 000000B4 000000B4 0000000A 
00000012 000000BE 000000BE 000000BE 0000000A 
00000013 000000C8 000000C8 000000C8 0000000A 

然后使对齐检查更容易,我最终不需要做 让它为上述代码尝试不同的对齐方式(第一列中的地址)以及 r0 为 4 的结果。

00010000 00000032 00010004 0000002D 00010008 00000032 0001000C 0000002D

这会重复到地址 0x101FC

如果我在编译的测试中更改对齐方式

000080cc <COUNTER>:
    80cc:   ee192f1d    mrc 15, 0, r2, cr9, cr13, {0}

000080d0 <delay>:
    80d0:   e2500001    subs    r0, r0, #1
    80d4:   4a000000    bmi 80dc <end_delay>
    80d8:   eafffffc    b   80d0 <delay>

000080dc <end_delay>:
    80dc:   ee193f1d    mrc 15, 0, r3, cr9, cr13, {0}
    80e0:   e0430002    sub r0, r3, r2
    80e4:   e12fff1e    bx  lr

然后它会快一点。

00000000 00000009 00000009 00000009 
00000001 00000012 00000012 00000012 00000009 
00000002 0000001B 0000001B 0000001B 00000009 
00000003 00000024 00000024 00000024 00000009 
00000004 0000002D 0000002D 0000002D 00000009 
00000005 00000036 00000036 00000036 00000009 
00000006 0000003F 0000003F 0000003F 00000009 
00000007 00000048 00000048 00000048 00000009 
00000008 00000051 00000051 00000051 00000009 
00000009 0000005A 0000005A 0000005A 00000009 
0000000A 00000063 00000063 00000063 00000009 
0000000B 0000006C 0000006C 0000006C 00000009 
0000000C 00000075 00000075 00000075 00000009 
0000000D 0000007E 0000007E 0000007E 00000009 
0000000E 00000087 00000087 00000087 00000009 
0000000F 00000090 00000090 00000090 00000009 
00000010 00000099 00000099 00000099 00000009 
00000011 000000A2 000000A2 000000A2 00000009 
00000012 000000AB 000000AB 000000AB 00000009 
00000013 000000B4 000000B4 000000B4 00000009 

如果我把它改成函数调用

000080cc <COUNTER>:
    80cc:   e92d4001    push    {r0, lr}
    80d0:   ee192f1d    mrc 15, 0, r2, cr9, cr13, {0}
    80d4:   eb000003    bl  80e8 <delay>
    80d8:   ee193f1d    mrc 15, 0, r3, cr9, cr13, {0}
    80dc:   e8bd4001    pop {r0, lr}
    80e0:   e0430002    sub r0, r3, r2
    80e4:   e12fff1e    bx  lr

000080e8 <delay>:
    80e8:   e2500001    subs    r0, r0, #1
    80ec:   4a000000    bmi 80f4 <end_delay>
    80f0:   eafffffc    b   80e8 <delay>

000080f4 <end_delay>:
    80f4:   e12fff1e    bx  lr

00000000 0000001A 0000001A 0000001A 
00000001 00000023 00000023 00000023 00000009 
00000002 0000002C 0000002C 0000002C 00000009 
00000003 00000035 00000035 00000035 00000009 
00000004 0000003E 0000003E 0000003E 00000009 
00000005 00000047 00000047 00000047 00000009 
00000006 00000050 00000050 00000050 00000009 
00000007 00000059 00000059 00000059 00000009 
00000008 00000062 00000062 00000062 00000009 
00000009 0000006B 0000006B 0000006B 00000009 
0000000A 00000074 00000074 00000074 00000009 
0000000B 0000007D 0000007D 0000007D 00000009 
0000000C 00000086 00000086 00000086 00000009 
0000000D 0000008F 0000008F 0000008F 00000009 
0000000E 00000098 00000098 00000098 00000009 
0000000F 000000A1 000000A1 000000A1 00000009 
00000010 000000AA 000000AA 000000AA 00000009 
00000011 000000B3 000000B3 000000B3 00000009 
00000012 000000BC 000000BC 000000BC 00000009 
00000013 000000C5 000000C5 000000C5 00000009 

每次计数的成本相同,但调用开销更昂贵

这让我可以使用拇指模式只是为了好玩,以避免链接器添加的模式更改我让它更快(并且一致)。

000080cc <COUNTER>:
    80cc:   e92d4001    push    {r0, lr}
    80d0:   e59f103c    ldr r1, [pc, #60]   ; 8114 <edel+0x2>
    80d4:   e59fe03c    ldr lr, [pc, #60]   ; 8118 <edel+0x6>
    80d8:   ee192f1d    mrc 15, 0, r2, cr9, cr13, {0}
    80dc:   e12fff11    bx  r1

000080e0 <here>:
    80e0:   ee193f1d    mrc 15, 0, r3, cr9, cr13, {0}
    80e4:   e8bd4001    pop {r0, lr}
    80e8:   e0430002    sub r0, r3, r2
    80ec:   e12fff1e    bx  lr

000080f0 <delay>:
    80f0:   e2500001    subs    r0, r0, #1
    80f4:   4a000000    bmi 80fc <end_delay>
    80f8:   eafffffc    b   80f0 <delay>

000080fc <end_delay>:
    80fc:   e12fff1e    bx  lr
    8100:   e1a00000    nop         ; (mov r0, r0)
    8104:   e1a00000    nop         ; (mov r0, r0)
    8108:   e1a00000    nop         ; (mov r0, r0)

0000810c <del>:
    810c:   3801        subs    r0, #1
    810e:   d400        bmi.n   8112 <edel>
    8110:   e7fc        b.n 810c <del>

00008112 <edel>:
    8112:   4770        bx  lr

00000000 000000F4 0000001B 0000001B 
00000001 00000024 00000024 00000024 00000009 
00000002 0000002D 0000002D 0000002D 00000009 
00000003 00000036 00000036 00000036 00000009 
00000004 0000003F 0000003F 0000003F 00000009 
00000005 00000048 00000048 00000048 00000009 
00000006 00000051 00000051 00000051 00000009 
00000007 0000005A 0000005A 0000005A 00000009 
00000008 00000063 00000063 00000063 00000009 
00000009 0000006C 0000006C 0000006C 00000009 
0000000A 00000075 00000075 00000075 00000009 
0000000B 0000007E 0000007E 0000007E 00000009 
0000000C 00000087 00000087 00000087 00000009 
0000000D 00000090 00000090 00000090 00000009 
0000000E 00000099 00000099 00000099 00000009 
0000000F 000000A2 000000A2 000000A2 00000009 
00000010 000000AB 000000AB 000000AB 00000009 
00000011 000000B4 000000B4 000000B4 00000009 
00000012 000000BD 000000BD 000000BD 00000009 
00000013 000000C6 000000C6 000000C6 00000009

这种对齐方式

0000810e <del>:
    810e:   3801        subs    r0, #1
    8110:   d400        bmi.n   8114 <edel>
    8112:   e7fc        b.n 810e <del>

00008114 <edel>:
    8114:   4770        bx  lr


00000000 0000007E 0000001C 0000001C 
00000001 00000026 00000026 00000026 0000000A 
00000002 00000030 00000030 00000030 0000000A 
00000003 0000003A 0000003A 0000003A 0000000A 
00000004 00000044 00000044 00000044 0000000A 
00000005 0000004E 0000004E 0000004E 0000000A 
00000006 00000058 00000058 00000058 0000000A 
00000007 00000062 00000062 00000062 0000000A 
00000008 0000006C 0000006C 0000006C 0000000A 
00000009 00000076 00000076 00000076 0000000A 
0000000A 00000080 00000080 00000080 0000000A 
0000000B 0000008A 0000008A 0000008A 0000000A 
0000000C 00000094 00000094 00000094 0000000A 
0000000D 0000009E 0000009E 0000009E 0000000A 
0000000E 000000A8 000000A8 000000A8 0000000A 
0000000F 000000B2 000000B2 000000B2 0000000A 
00000010 000000BC 000000BC 000000BC 0000000A 
00000011 000000C6 000000C6 000000C6 0000000A 
00000012 000000D0 000000D0 000000D0 0000000A 
00000013 000000DA 000000DA 000000DA 0000000A 

所以在这个处理器上的某个理想世界中,假设缓存命中延迟代码

00000004 00000032 00000032 00000032 0000000A 
00000004 0000002D 0000002D 0000002D 00000009 
00000004 0000003E 0000003E 0000003E 00000009 
00000004 0000003F 0000003F 0000003F 00000009 
00000004 00000044 00000044 00000044 0000000A 

在 0x2D 和 0x44 时钟之间以 r0 = 4 运行该循环

实际上在这个平台上没有启用缓存和/或如果您遇到缓存未命中可能会看到什么。

00000000 0000030B 000002B7 000002ED 
00000001 0000035B 00000389 000003E9 
00000002 000003FB 00000439 0000041B 
00000003 0000058F 000004E7 0000055B 
00000004 000005FF 0000069D 000006D1 
00000005 00000745 00000733 000006F7 
00000006 00000883 00000817 00000801 
00000007 00000873 00000853 0000089B 
00000008 00000923 00000B05 0000092F 
00000009 00000A3F 000009A9 00000B4D 
0000000A 00000B79 00000BA9 00000C57 
0000000B 00000C21 00000D13 00000B51 
0000000C 00000C0B 00000E91 00000DE9 
0000000D 00000D97 00000E0D 00000E81 
0000000E 00000E5B 0000100B 00000F25 
0000000F 00001097 00001095 00000F37 
00000010 000010DB 000010FD 0000118B 
00000011 00001071 0000114D 0000123F 
00000012 000012CF 0000126D 000011DB 
00000013 0000140D 0000143D 0000141B 
000002B7 0000143D 

r0=4 行

00000004 000005FF 0000069D 000006D1 

cpu 数量太多了...

希望我已经把这个话题放到了床上。虽然尝试假设代码运行的速度或计数的数量等很有趣……但在这些类型的处理器、管道、缓存、分支预测、复杂的系统总线上,使用通用内核并不那么简单芯片供应商管理与处理器 IP 供应商代码分开的内存/闪存的各种芯片实现。

我在第二个实验中没有弄乱分支预测,如果我这样做了,那么对齐不会那么一致,这取决于分支预测的实现方式,它可以根据分支相对于提取线的位置来改变其有用性当分支预测器确定它不需要执行该提取和/或启动分支提取时,下一个提取是否已经开始或以某种方式通过,在这种情况下,分支提前两个,因此您可能看不到它与此代码,您可能希望在其间添加一些 nop,以便 bmi 目标位于单独的提取行中(以便查看差异)。

这是很容易操作的东西,使用相同的机器代码序列,并根据我们所看到的来查看执行时间的不同。在 0x3F 和 0x6D1 之间,对于相同的机器代码,最快和最慢之间的差异超过 27 倍。通过一条指令更改代码的对齐方式(在不相关代码中的其他地方,之前构建的指令多一条或少一条)是 5 个计数差异。

公平地说,测试结束时的 mrc 可能是部分时间

000080c8 <COUNTER>:
    80c8:   ee192f1d    mrc 15, 0, r2, cr9, cr13, {0}
    80cc:   ee193f1d    mrc 15, 0, r3, cr9, cr13, {0}
    80d0:   e0430002    sub r0, r3, r2
    80d4:   e12fff1e    bx  lr

导致任一对齐的计数为 1。所以不能保证它只是测量中的一个错误计数,但可能不是一打。

无论如何,我希望这有助于您的理解。

【讨论】:

  • 谢谢!我花了几个小时弄清楚如何运行它,让性能计数器运行,因为 armv8 没有简单的 systick 计数器,启用缓存和清除它们已经改变。 cortex-m4 更像是五分钟来做那个测试。已经做过很多次了,有代码在周围测试其他机器代码。所以我自己在这方面学到了很多。
【解决方案2】:

我直觉上觉得每条指令应该使用1个CPU周期,所以如果我们从r0 =4开始,那么完成下面的代码需要11个CPU周期,对吗?

鉴于大多数 ARM CPU 有 3-8 个流水线阶段,很难说大部分指令需要 1 个 CPU 周期才能完成。理想情况下,在流水线 CPU 中,每个时钟周期都应该有一条指令退出,但由于上面的代码有分支语句,因此很难判断每条指令何时退出。原因是我们不知道如何处理分支,因为这取决于处理器设计中存在的分支预测器算法。因此,如果预测正确,则管道中不会插入任何气泡,但如果预测不正确,则取决于内部管道结构将插入多少气泡。对于理想的 5 级管道,每个错误预测都会插入 2 个气泡。但这又取决于内部微架构的实现。 因此,很难准确预测上述代码需要多少个周期。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-02-13
    • 1970-01-01
    • 2016-11-04
    • 1970-01-01
    • 1970-01-01
    • 2015-11-14
    • 2020-10-31
    • 2013-04-13
    相关资源
    最近更新 更多