avr-gcc 使用重复加法代替 MULU 指令

电器工程 avr C++ avr-gcc 乘法
2022-02-01 06:41:24

我最近在 Atmel Studio 中为ATmega1284P编译了一些 C++ 代码,并正在使用我的示波器分析一些例程的时序。令我惊讶的是,我认为我已经优化的循环花费的时间比预期的要长。

看了一眼汇编代码后,我注意到编译器编译了以下乘法:

word *= 10;

重复添加:

add    r28, r28
adc    r29, r29
add    r18, r18
adc    r19, r19
add    r18, r18
adc    r19, r19
add    r18, r18
adc    r19, r19
add    r18, r28
adc    r19, r29

现在虽然我很欣赏这种重复加法的优化,但当芯片有可用的硬件乘法指令时,没有理由这样做,比如mul.

支持1284P硬件乘法

为什么 gcc 会以这种方式运行,我如何告诉它使用硬件乘法指令?请注意,我已经设置了乘法,所以溢出不是问题。word是一个 uint16_t。

2个回答

假设您要优化速度:

除非你真的知道使用 需要多少个周期,否则你mul无法比较。

所以让我们试试:

  • 如果使用mul指令,则需要其中两个(操作数为 16 位)。所以这已经花费了4个周期。
  • 您必须将常量 10 加载到一个寄存器中:1 个周期。
  • 然后你必须添加两个 16 位结果。这需要另外2个周期。
  • 然后您必须考虑,两个muls 的结果总是进入寄存器对 r1:r0,但是为了将两个结果相加,您必须将其中一个移动到其他地方:这需要多花费2个周期。

所以它已经是 4 + 1 + 2 + 2 = 9个周期,只比重复加法少一个周期,但是你使用了更多的寄存器。仅此一项就可以证明重复添加是合理的(取决于还做了什么,多使用一个寄存器可能会花费您超过一个周期)。

编辑:
上面的计算是粗略的计算;进一步分析(参见 2012rcampion 的回答)表明移动寄存器对可以在 1 个周期内完成(有一条特殊指令),并且对于将两个乘法结果相加,一个加法就足够了(因为我们对完整的 3 字节结果不感兴趣,但是想要做一个 16 位宽度的环绕);另一方面需要额外的措施:例如清除零寄存器(我猜是r0)。
另请注意,变体mul使用更多的寄存器:7 而不是只有 4;价格高,因为它可能会在其他地方导致更多的周期(和代码大小)。

我不能告诉你为什么 GCC 会以这种方式进行乘法运算,但我可以告诉你它并不比使用mul. 我使用数据表(第 33 节)来计算以下例程所需的周期数(AVR-GCC 9.2.0,编译器资源管理器):

#include <stdint.h>

uint16_t times_ten(uint16_t word) {
    return word * 10;
}

对于-O1通过-O3(优化速度):

movw r18,r24 ; 1 cycle 
lsl r18      ; 1
rol r19      ; 1
lsl r18      ; 1
rol r19      ; 1
add r24,r18  ; 1
adc r25,r19  ; 1
lsl r24      ; 1
rol r25      ; 1
             ; 9 total

请注意,将寄存器对乘以 2的组合lsl与将寄存器对添加到自身的速度相同如问题所示(2 个周期和 2 条指令)。roladdadc

对于-Os(针对代码大小进行优化):

ldi r18,lo8(10)  ; 1
movw r20,r24     ; 1
mul r18,r20      ; 2
movw r24,r0      ; 1
mul r18,r21      ; 2
add r25,r0       ; 1
clr __zero_reg__ ; 1
                 ; 9 total

有趣的是,这两种方法都采用相同数量的周期,并且使用mul更少的指令。事实上,作为一个更大函数的一部分,可以多次完成,其中 10 可以保存在寄存器中,并且可以将 r1 归零直到结束,使用mul会快两个周期!