最快的轮询循环 - 如何修剪 1 个 CPU 周期?

fgr*_*ieu 8 assembly cortex-m3 micro-optimization

在 ARM Cortex M3(类似于 STM32F101)上的实时应用程序¹中,我需要在尽可能紧密的循环中轮询一些内部外设寄存器直到它为零。我使用位带来访问适当的位。(工作)C 代码是

while (*(volatile uint32_t*)kMyBit != 0);
Run Code Online (Sandbox Code Playgroud)

该代码被复制到片上可执行 RAM 中。经过一些手动优化²,轮询循环下降到以下,我将³计时为 6 个周期:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 D1FC      BNE      0x00600200
Run Code Online (Sandbox Code Playgroud)

如何降低投票的不确定性?5 个周期的循环符合我的目标:在它变为零后尽可能接近 15.5 个周期对同一位进行采样。

我的规范要求可靠地检测至少 6.5 个 CPU 时钟周期的低脉冲;如果持续时间少于 12.5 个周期,则可靠地将其归类为短;如果它持续超过 18.5 个周期,就可以可靠地对其进行分类。脉冲与 CPU 时钟没有确定的相位关系,这是我唯一准确的时序参考。这需要最多 5 个时钟的轮询循环。实际上,我正在模拟在几十年前的 8 位 CPU 上运行的代码,该 CPU 可以以 5 个时钟周期进行轮询,而这已成为规范。


我试图通过在循环之前插入 NOP 来抵消代码对齐,在我尝试过的许多变体中,但从未观察到任何变化。

我试图反转 CMP 和 LDR,但仍然得到 6 个周期:

0x00600200 681A      LDR      r2,[r3,#0x00]
; we loop here
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FC      BNE      0x00600202
Run Code Online (Sandbox Code Playgroud)

这个是8个周期

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 681A      LDR      r2,[r3,#0x00]
0x00600204 2A00      CMP      r2,#0x00
0x00600206 D1FB      BNE      0x00600200
Run Code Online (Sandbox Code Playgroud)

但这是9个周期:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FB      BNE      0x00600200
Run Code Online (Sandbox Code Playgroud)

¹ 在没有中断发生的情况下测量该位处于低电平的时间。

² 最初的编译器生成的代码使用 r12 作为目标寄存器,并在循环中添加了 4 个代码字节,花费了 1 个周期。

³ 给出的数字是通过一个所谓的周期精确实时STIce 仿真器及其在寄存器地址读取时的仿真器触发功能获得的。以前我在循环中尝试了带有断点的“States”计数器,但结果取决于断点的位置。单步更糟:它总是为 LDR 提供 4 个周期,但有时至少会降到 3 个。

ale*_*_mv 8

如果我正确理解了这个问题,那么需要减少的不一定是循环周期,而是后续样本(即 LDR 指令)之间的周期数。但是每次迭代可以有多个 LDR。你可以尝试这样的事情:

    ldrb    r1, [r0]

loop:
    cbz     r1, out
    ldrb    r2, [r0]
    cbz     r2, out
    ldrb    r1, [r0]
    b       loop

out:
Run Code Online (Sandbox Code Playgroud)

两条 LDRB 指令之间的间距不同,因此样本的间距不均匀。

这可能会稍微延迟退出循环,但从问题描述我不能说它是否重要。

我碰巧可以使用周期精确的 M7 模型,当过程稳定时,您的原始循环在每次迭代 3 个周期内在 M7 上运行(意味着 LDR 每 3 个周期),而上面建议的循环在 4 个周期内运行,但现在有那里有两个 LDR(所以 LDR 每 2 个周期)。采样率肯定会提高。

值得称赞的是,@ Peter Cordes 在评论中提议将 CBZ 作为休息时间展开。

诚然,M3 会更慢,但如果它是您所追求的采样率,它仍然值得一试。

此外,您还可以检查 LDRB 而不是 LDR(如上面的代码所示)是否会改变任何内容,尽管我不希望它会改变。

UPD:我有另一个 2-LDR 循环版本,它在 M7 上在 3 个周期内完成,您可以尝试感兴趣(CBZ 中断也允许在循环后轻松平衡路径):

    ldr     r1, [r0]

loop:
    ldr     r2, [r0]
    cbz     r1, out_slow
    cbz     r2, out_fast
    ldr     r1, [r0]
    b       loop

out_fast:
    /* NOPs as required */

out_slow:
Run Code Online (Sandbox Code Playgroud)

  • 我确认您的第二个轮询循环在我的 Cortex-M3 emu 上以两个等间隔的样本运行 10 个周期。它在简单性上击败了我的答案(现已删除),并且允许测试更短的脉冲。`loop:` 之前的 2 个 `nop` 和 `out_fast:` 之后的 4 个 `nop` 使得 `out_slow:` 之后的 `ldr` 在第一次看到零的样本之后采样 10 个周期,以三个周期中的任意一个为准曾是。我的规格(如问题中的措辞)需要 13,调整起来很简单。问题100%解决了!非常感谢 Peter Cordes 的评论,以及 B Degan 的第一笔赏金。 (3认同)
  • 很高兴它有效:) 在确认基本想法对您有效之前不想均衡样本。而且很难预测我的 M7 运行情况如何转化为 M3。我给了你那个循环版本,因为它在 M7 上产生统一的采样率(每 2 个周期的 LDR)。但我还有另一个版本,实际上在 M7 上运行得更快(每个循环 3 个周期,仍然有 2 个 LDR),您可以出于兴趣尝试一下。我会更新我的答案。 (2认同)