ARM Cortex-M4循环计数有些奇怪

4
我最近使用了一块板子(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读取)
注:本文中的HTML标签已被保留。
  "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(来自ldr)+4 = 6。顺便说一下,该板没有任何缓存,我将代码存储在sramx中,堆栈在sram2中。我是否遗漏了什么,并且有没有办法找出每个周期如何消耗?此外,我也对cortex-m4的数据依赖性感到困惑。

没有缓存的话,你可能需要为指令获取支付额外的周期成本。还要注意,你计算的周期时间只包括读取DWT->CYCCNT的两个LDR指令中的一个,而不是两个都包括。假设它们在执行这两个指令的相对点上同时读取循环计数(例如,在开始时)。如果要包括两个指令,则循环计数必须在第一个LDR指令的开头和第二个LDR指令的结尾处。 - Ross Ridge
我同意你的观点,但是你是指每个指令获取都需要额外的周期成本吗? - Stephen Yuan
每个指令都会被获取,但我不知道Cortex-M4如何进行指令获取。例如,它可能在每次获取时获取32位字,因此并非每个指令都需要付出代价。 - Ross Ridge
1个回答

4

我使用的是TI Cortex-M4芯片,但是需要进行修改,而现在我手头没有这种芯片。与此同时,ST部分的芯片在flash前面有cache,这个cache按照设计无法关闭,会对性能产生影响。

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指令,它在flash中需要0x11个时钟周期,在ram中根据对齐方式需要在0x10和0x11之间。当Thumb2指令在一个字边界上对齐时,它比未对齐时需要多1个时钟周期。

使用Thumb指令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-M)需要控制闪存的等待状态,有时闪存的速度只有RAM的一半,相同的代码、机器码在低速时在RAM上运行得更快,在增加时钟速度并增加闪存的等待状态数以保持其速度的情况下更糟。
ST系列特别是有一个营销术语,用于描述他们放置的预取缓存,您无法禁用。您可以在测试代码之前执行dsb/isb,并查看单个传递等待状态的效果,但如果进行测试循环,则...
test_loop: sub r3,#1
bne test_loop

通过多次运行,就像使用缓存一样,最初的几个时钟周期是反映在其中的,但是很小。但是,如果处理器让您看到它们,您仍应该看到针对缓存的获取行效果。

有些芯片具有可以启用或禁用的Flash预取,特别是与循环一起使用时,如果将事情调整得恰当,使预取器读取超出循环末尾,则可能会损害性能而不是帮助性能。

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时钟可能会克服等待状态,并且您可以找到一种CPU速度,以超过24Mhz n + 1等待状态的总体墙钟定时性能,无论计数的CPU时钟如何受到闪存等待状态的影响。

SRAM通常不需要等待状态并且与CPU一样快,但可能有例外。毫无疑问,外设有限制,许多供应商有关于外设时钟的规则,即使该部件可达到48,此部件也不能超过32mhz等等,因此访问外设的基准测试将在不同的CPU / 系统速度设置下占用不同数量的CPU时钟。

处理器还具有可配置选项,基本上是编译时间选项。Cortex-m4没有宣传这一点,但Cortex-m0+可以配置为16位或32位指令提取宽度。我无法看到那个源代码,因此它可能是必须编译时间的东西,或者如果您选择,可以设置一个控制寄存器并使其运行时可配置,或者可能有逻辑说如果pll设置是这样的,则强制一种方式,否则是另一种方式等等。因此,即使您具有来自不同供应商的具有相同修订版和型号CPU核心的两个芯片,这也并不意味着它们将表现相同。更不用说芯片供应商拥有源代码并且可以进行修改。

尝试在一个你无法看到的系统中预测流水线处理器的周期计数是不可能的。有时添加额外的nop会使处理速度更快,有时添加一个则会使其变慢,这是可以预料的,而有时则没有变化。如果一个nop可以做到这一点,那么任何其他指令也可以做到。
更不用说干扰管道本身了,据说这些cortex-m的管道非常短,所以强制执行具有许多依赖关系的指令序列与类似的没有依赖关系的序列相比,影响不会太大。
将相同的机器代码运行在来自不同供应商(甚至是cortex-m3和cortex-m7)的几个cortex-m4上,使用不同的设置,flash和ram,在cpu时钟周期中执行时间有所变化是很正常的,不应感到惊讶。

1
如果您正在使用内联汇编来获得一些手工汇编性能,那么这将是另一个未知因素,会影响您的性能,或者会因为每次编译而有所不同(在程序的其他地方添加一行,在某个地方删除一行)。 - old_timer
非常感谢,我也注意到指令对齐会影响性能。稍后我会认真阅读你所说的内容。 - Stephen Yuan
1
你正在正确的道路上理解问题。或者说,也有可能理解得不够深入。你应该能够像现在这样进行实验,并且看到相同的机器码可以表现出不同的结果。关于为什么会发生这样的情况,我们可以进行推测并且接近真相,但是如果没有逻辑和模拟的数据支持,我们就无法确切地知道原因。 - old_timer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接