【问题标题】:Something weird with arm cortex-m4 cycle countarm cortex-m4 循环计数有些奇怪
【发布时间】:2023-04-07 13:34:01
【问题描述】:

我最近用一块板子 (LPCXpresso 5411x) 来做一些计算,我们尽量减少周期,以节省运行时间以满足我们的特定需求,所以我需要做一些研究 cortex-m4 指令如何花费周期。而且我发现了很多奇怪的东西(无法用我从互联网上找到的东西来解释)

我使用 DWT->CYCCNT 来计算我要测试的函数消耗的周期数。

int start_cycle, end_cycle;

__asm volatile (
  "LDR %[s1], [%[a]], #0\n\t"
  :[s1] "=&r"(start_cycle): [a] "r"(&(DWT->CYCCNT)):);

AddrSumTest();
__asm volatile (
  "LDR %[s1], [%[a]], #0\n\t"
  :[s1] "=&r"(end_cycle): [a] "r"(&(DWT->CYCCNT)):);

printf("inside the func() cycles: %d\n",end_cycle - start_cycle);

这是我的函数的定义方式:

__attribute__( ( always_inline )) static inline void AddrSumTest(){
    uint32_t x, y, i, q;

    __asm volatile (
        "nop\n\t"
        :[x] "=r" (x), [y] "=r" (y), [i] "=r" (i), [q] "=r" (q):);
    }
}
  • 根据Arm Infocenter,指令MOV应该花费一个周期,但我发现

以下指令需要 8 个周期(不是 3 个,因为从 DWT->CYCCNT 读取需要额外的周期)

  "nop\n\t"
  "MOV %[x], #2\n\t"
  "nop\n\t"

添加一条 MOV 指令后,接下来的周期需要 10 个周期(为什么不是 9 个周期)

  "nop\n\t"
  "MOV %[x], #2\n\t"
  "MOV %[y], #3\n\t"
  "nop\n\t"

后一种情况的汇编代码是

4000578:    f853 4b00   ldr.w   r4, [r3], #0
400057c:    bf00        nop
400057e:    f04f 0502   mov.w   r5, #2
4000582:    f04f 0603   mov.w   r6, #3
4000586:    bf00        nop
4000588:    f853 1b00   ldr.w   r1, [r3], #0
400058c:    4805        ldr r0, [pc, #20]   ;(40005a4<test_AddrSum+0x30>)
400058e:    1b09        subs    r1, r1, r4
4000590:    f000 f80e   bl  40005b0 <__printf_veneer>

这两个 ldr 正在从 DWT->CYCCNT 读取,此外,这也很奇怪为什么这会花费 10 个周期,而我估计是 2(from ldr) + 4 = 6

顺便说一下,板子没有缓存,我把代码存储在sramx中,堆栈在sram2中。

我错过了什么吗?有什么方法可以弄清楚每个周期是如何消耗的?此外,我也对 cortex-m4 的数据依赖性感到困惑。

【问题讨论】:

  • 如果没有任何缓存,您可能必须为指令提取支付额外的周期成本。另请注意,您计算的周期时间将仅包括读取DWT-&gt;CYCCNT 的两条 LDR 指令之一,而不是两者。据推测,他们在两条指令的执行中(例如在开始时)的相同相对点读取循环计数。要包含两者,循环计数必须在第一条 LDR 指令的开头和第二条 LDR 指令的结尾。
  • 我同意你的观点,但你的意思是说每条指令提取都会产生额外的周期成本
  • 每次取指令,但不知道Cortex-M4是怎么取指令的。例如,它可能会在每次提取时提取 32 位字,因此并非每条指令都会支付。

标签: assembly arm cortex-m lpc


【解决方案1】:

采用变体,我没有那个芯片,但有其他芯片。在这种情况下使用 ti cortex-m4。 st 部分在闪存前面有这个缓存,我认为你不能关闭它并且(按设计)会影响性能。

00000082 <test>:
  82:   f3bf 8f4f   dsb sy
  86:   f3bf 8f6f   isb sy
  8a:   6802        ldr r2, [r0, #0]
  8c:   46c0        nop         ; (mov r8, r8)
  8e:   46c0        nop         ; (mov r8, r8)
  90:   46c0        nop         ; (mov r8, r8)
  92:   46c0        nop         ; (mov r8, r8)
  94:   46c0        nop         ; (mov r8, r8)
  96:   46c0        nop         ; (mov r8, r8)
  98:   f240 0102   movw    r1, #2
  9c:   f240 0103   movw    r1, #3
  a0:   46c0        nop         ; (mov r8, r8)
  a2:   46c0        nop         ; (mov r8, r8)
  a4:   46c0        nop         ; (mov r8, r8)
  a6:   46c0        nop         ; (mov r8, r8)
  a8:   46c0        nop         ; (mov r8, r8)
  aa:   46c0        nop         ; (mov r8, r8)
  ac:   46c0        nop         ; (mov r8, r8)
  ae:   6803        ldr r3, [r0, #0]
  b0:   1ad0        subs    r0, r2, r3
  b2:   4770        bx  lr

因此,如果没有第二个 movw,它在闪存中需要 0x11 个时钟,在 ram 中需要 0x10 和 0x11 之间,具体取决于对齐方式。当 thumb2 指令在字边界上对齐时,它比未对齐时需要更长的时钟。

使用拇指指令0x2102

00000000 20001016 00000010 
00000002 20001018 00000010 
00000004 2000101A 00000010 
00000006 2000101C 00000010 

使用 thumb2 扩展 0xf240, 0x0102

00000000 20001016 00000010 
00000002 20001018 00000011 
00000004 2000101A 00000010 
00000006 2000101C 00000011 

使用 thumb2 扩展 0xf240、0x0102、0xf240、0x0103

00000000 20001016 00000012 
00000002 20001018 00000013 
00000004 2000101A 00000012 
00000006 2000101C 00000013 

这并不奇怪,可能与获取有关。这些微控制器比全尺寸手臂简单得多。完整大小的每次取指将取 8 条指令,并且取决于取指线中的内容可能会影响性能,此外还有循环和分支在取指线中的位置(不管缓存是打开还是关闭) .分支也有分支预测器,您可以打开和关闭,并且可以在设计上有所不同。

这个特定的芯片说,在 40Mhz 以上,它启用了一个预取来获取一个字,这意味着它在它以下获取一个半字(总线可能是一个字宽,所以读取相同的地址两次以获得两条指令......为什么?)

其他芯片(cortex-ms 以及其他)您必须控制闪存上的等待状态,有时闪存的速度是 ram 的一半,并且相同的代码,相同的机器代码,即使在 ram 上运行得更快当您增加时钟并增加闪存上的等待状态数以控制其速度时,速度会变得更慢。

特别是 ST 系列有一些营销术语,用于表示他们放入的预取缓存,您无法禁用。您可以在被测代码之前执行 dsb/isb,例如查看等待状态对单次通过的影响,但如果执行测试循环

test_loop: sub r3,#1
bne test_loop

并且运行它很多次,开始时的那几个时钟被反射但很小,就像使用缓存一样,但是如果处理器允许你看到这些,你仍然应该看到对缓存的提取线效果。

某些芯片具有可以启用或禁用的闪存预取,尤其是在循环中,如果您将事物对齐恰到好处,从而使预取器在循环结束后继续读取,则可能会损害性能而不是帮助。

ARM ip 停止在内核边缘的 arm 总线(AXI、AMBA、AHB、APB 等),通常您可能有用于 L2 缓存的 ARM ip(不在这些微控制器中),您可能购买一些 arm ip 来帮助您处理他们的总线,但最终芯片中包含芯片特定的东西,arm 与芯片供应商无关,并且芯片供应商之间不一致,特别是闪存和 sram 接口。

首先没有理由期望流水线处理器的结果可预测,如上所示,并且很容易用两条指令循环显示,相同的机器代码可能会因仅对齐而在性能上有很大差异,但也有一些因素您可以直接或间接控制闪存等待状态、时钟与闪存的相对速度。如果我们设备上 N 和 N+1 个等待状态之间的边界是 24Mhz,那么 N 个等待状态下的 24Mhz 比 N+1 个等待状态下的 24Mhz 快得多。 28Mhz(N+1 个等待状态)在 N+1 个等待状态下比 24Mhz 快,但最终 cpu 时钟可能会克服等待状态,您可以找到一个优于 24Mhz n+1 个等待状态的 cpu 速度,就整个墙而言时钟计时性能,而不是计算 CPU 时钟,如果受闪存等待状态影响,计算的 CPU 时钟应该始终受到闪存等待状态的影响。

sram 往往没有等待状态,运行速度与 CPU 一样快,但可能有例外。毫无疑问,外设是有限制的,许多供应商都有关于外设时钟的规定,这个不能超过32mhz,即使部分达到48,这样的事情,所以访问外设的基准测试将占用不同数量的cpu不同 CPU/系统速度设置的时钟。

处理器中还有可配置的选项,基本上是编译时选项。 cortex-m4 不宣传这一点,但 cortex-m0+ 确实可以配置为 16 或 32 位指令获取宽度。我看不到那个源代码,所以它可能是编译时的东西,或者如果你选择你可以设置一个控制寄存器并让它运行时可配置,或者可能有逻辑表明 pll 设置是否是然后强制一种方式,否则另一种方式,依此类推。因此,即使您有两个来自不同供应商的芯片具有相同的 rev 和型号 cpu 内核,这并不意味着它们的行为相同。更何况芯片厂商有源码,可以修改。

因此,在您无法查看的系统中尝试预测流水线处理器上的循环计数是不会发生的。您将有时添加一个额外的 nop 并且它变得更快,有时您添加一个并且它会像预期的那样变慢,有时它不会改变。如果一个 nop 可以做到这一点,那么任何其他指令也可以。

更不用说弄乱管道本身了,这些 cortex-ms 真的很短,所以我们被告知,强制执行具有很多依赖关系的指令序列与没有类似序列的指令序列不会产生太大影响。

测试相同的机器代码,在不同供应商的几个 cortex-m4(甚至是 cortex-m3 和 cortex-m7s)、闪存和 ram 上以不同的设置运行它,如果cpu ticks 中的执行时间各不相同。

【讨论】:

  • 如果您使用内联汇编来获得一些手动汇编性能,那是另一个未知数,它会影响您的性能,或者会因编译而异(在此处添加一行,在程序的其他地方删除一行)。
  • 非常感谢,我也注意到指令对齐会影响性能。稍后我会仔细阅读您所说的内容。
  • 你在正确的理解轨道上。或者视情况不理解。您应该能够像您一样进行实验,并看到相同的机器代码可以执行不同的操作。为什么这样做,我们可以推测并非常接近,但无法访问逻辑并且无法确定模拟。
猜你喜欢
  • 2019-12-14
  • 1970-01-01
  • 2012-07-16
  • 2018-09-29
  • 2017-12-03
  • 1970-01-01
  • 1970-01-01
  • 2018-12-04
  • 2017-06-23
相关资源
最近更新 更多