具有零操作数的 Cortex M7 浮点算术指令持续时间

Posted

技术标签:

【中文标题】具有零操作数的 Cortex M7 浮点算术指令持续时间【英文标题】:Cortex M7 floating arithmetic instruction duration with zero operand 【发布时间】:2019-08-07 14:40:53 【问题描述】:

我想知道在 Cortex M7 FPU 上,当操作数为零时,像 VMUL 这样的浮点指令的持续时间是否明显更短。

原因是我正在分析一个软件,它处理来自模拟源的许多变量,更准确地说是这些变量随时间的演变。但是现在“前端”(即模拟源)不可用,所以我使用的是模拟变量,但由于它们没有随着时间的推移而演变,代码中的许多变量都是零。

【问题讨论】:

我能看到你得到一个有意义的答案的唯一现实方法就是自己分析它。 @ThomasJager:或者可以从 ARM 下载处理器手册并检查时序信息。 @EricPostpischil 据我所知,ARM 不发布 M7 循环表。 如果0.f*x 的延迟比其他操作数更快,我会非常感到惊讶。即使是较小的 cortex-m 也有单周期 32 位整数乘法器,如果您的 23 位尾数可以在一个周期内相乘,float-multiplication 几乎可以肯定在 2-3 个周期内相当容易完成。为了基本上没有收益、增加复杂性和更糟糕的确定性而使指令复杂化是没有任何意义的。唯一的例外可能是乘以归一化数字的异常结果,但我希望微控制器刷新为零而不是采用可变延迟 即使有一些没有意义的表格也会有。您仍然希望/需要在该芯片/系统上对其进行分析。定向裸机测试、缓存关闭、从 ram 运行、控制内存上的等待状态(如果有)、更快的时钟速度通常不利于此类工作和问题,如果您需要内存上的最小等待状态追求核心性能。与 sram 相比,闪存具有您无法控制的缓存以及每隔一个时钟从一条指令开始的情况并不少见。 【参考方案1】:

除了像div 这样的非常慢的操作外,流水线 CPU 通常对所有事情都有固定的延迟(不依赖于数据)。否则,如果您在“慢”指令后一两个周期启动“快”指令,则必须处理回写冲突。

您可以通过在受延迟限制的循环中运行vmul 来自己测试它(例如,在展开的循环中将寄存器自身乘以 3 或 4 次)。尝试使用像0.0 这样的“简单”值,然后使用像1.0000000001 这样的非简单值(它有很多有效数字)。运行足够多的循环迭代以隐藏测量开销,但足够少以至于您在溢出到 +Inf 之前停止。

【讨论】:

【参考方案2】:

所以我已经克服了我的懒惰并做了一个分析自己:)

这是我用来在 STM32H753 上使用 GCC 执行双精度 vmul 循环的函数代码(选项 FPv5-D16、-mfloat-abi=hard、-Ofast):

void __attribute__((noinline))
asmMulDsimple(double a, double b) 
  asm volatile( "vmul.f64 d2, d0, d1 \n"
                "vmul.f64 d2, d0, d1 \n"
                "vmul.f64 d2, d0, d1 \n"
                ...
                ( 100 times )
                ...
                "vmul.f64 d2, d0, d1 \n"
               : [a] "+&r"(a), [b] "+&r"(b)
               :
               : "cc", "memory", "r12");

而main中的调用(Reset_Cycle_CounterGet_Cycle_Counter是使用DWT_CYCCNT循环计数器的基本函数):

    Reset_Cycle_Counter();
    
        asmMulDsimple(1.00000001, 2.0000000004);

        printf("Duration with 100 vmul, complex operands: %lu cycles\r\n", Get_Cycle_Counter());
    


    Reset_Cycle_Counter();
    
        asmMulDsimple(1, 2);

        printf("Duration with 100 vmul, simple operands: %lu cycles\r\n", Get_Cycle_Counter());
    

    Reset_Cycle_Counter();
    
        asmMulDsimple(0, 2.0000000004);

        printf("Duration with 100 vmul, 0 operands: %lu cycles\r\n", Get_Cycle_Counter());
    

还有输出,I 和 D 都启用了缓存:

Duration with 100 vmul, complex operands: 502 cycles
Duration with 100 vmul, simple operands: 499 cycles
Duration with 100 vmul, 0 operands: 406 cycles

如您所见,当操作数为 0 时存在显着差异,大约为 -20%。

【讨论】:

vmul.f64 d2, d0, d1 衡量的是吞吐量,而不是延迟。一个新的vmul 可以在前一个仍在运行时启动并读取d0, d1,然后它会产生d2 结果。 另外,我看不出您的 "r" 约束如何确保输入实际上位于 d0d1 中。 "r" 也是整数寄存器约束,而不是 FP。实际上,它使编译器执行vmov r2, r3, d0 以将位模式转换为整数reg:这就是为什么您必须使用noinline 来使损坏的asm 约束碰巧起作用的原因。 godbolt.org/z/toVy68。对d 寄存器使用"+w" 约束:gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html。但是,是的,使用 noinline 意味着您确实使用您想要的输入测量了吞吐量。 然而,延迟仍然是固定的,而吞吐量变化是合理的。或者可能 FP 乘法器没有流水线化,整个 CPU 流水线停止等待结果。 (所以吞吐量=延迟)。在开始新的乘法之前等待结果可以很容易地避免写回冲突。我不会对 Cortex-M7 感到惊讶,它是一个非常简单的管道。 我没有考虑吞吐量和延迟之间的区别。但是,我不明白:“延迟仍然是固定的,而吞吐量变化是合理的”。如果延迟是固定的,吞吐量如何变化?特别是使用固定操作数且无法访问内存? 你可以想象一个部分流水线的 FPU 有足够的阶段来应对最坏情况 mul 的延迟(除了异常/次正常的辅助),它通过修复操作的延迟来避免回写冲突。但它迟早会准备好开始一个新的独立操作,这取决于一条指令占用第一个流水线阶段的周期数。因此,简单的操作可能具有更好的吞吐量,但与更复杂的操作具有相同的延迟。仅当执行单元的某些实际流水线部分证明扩展简单延迟是合理的时,这才有意义

以上是关于具有零操作数的 Cortex M7 浮点算术指令持续时间的主要内容,如果未能解决你的问题,请参考以下文章

Cortex-M0指令集——ASR

ARM

Jvm(44),指令集----运算指令

SSE3指令有啥功能?

TI AM3517 Cortex-A8 上的浮点设置/错误

如何为 ARM Cortex M7 编译和调试 ASM 代码?