Arithmetics

March 12, 2025 ยท View on GitHub

Maximum (unsigned)

Given inputs rx and ry, output rd is set to the greater of the two values (interpreted as unsigned integers).

Compatible ARMv6-M

On ARMv6-M, the comparison result can be turned into a condition, which can then be used in a selection sequence, for a total of 5 cycles:

@ rd <- max_u(rx, ry) (maximum of values, unsigned interpretation)
@ rx is preserved
@ ry is preserved
@ rt is scratch
.macro UMAX_1  rd, rx, ry, rt
        cmp     \rx, \ry
        sbcs    \rt, \rt
        subs    \rd, \rx, \ry
        bics    \rd, \rt
        adds    \rd, \ry
.endm

Specific to ARMv7-M

On ARMv7-M, a combination of comparison and selection yields a 4-cycle method:

@ rd <- max_u(rx, ry) (maximum of values, unsigned interpretation)
@ rx is preserved
@ ry is preserved
@ rt is scratch
@ VARIANT: rd can be the same register as rx
.macro UMAX_2  rd, rx, ry, rt
        subs    \rt, \rx, \ry
        sbcs    \rt, \rt
        bics    \rd, \rx, \rt
        mls     \rd, \ry, \rt, \rd
.endm

However, we can do better, down to 3 cycles, if the output is written in the same register as the input rx:

@ rx <- max_u(rx, ry) (maximum of values, unsigned interpretation)
@ ry is preserved
@ rt is scratch
@ ru is scratch
@ VARIANT: ru can be the same register as ry (consumed)
.macro UMAX_3  rx, ry, rt, ru
        subs    \ru, \ry, \rx
        sbcs    \rt, \rt
        umaal   \rx, \ru, \rt, \ru
.endm

Minimum (unsigned)

Given inputs rx and ry, output rd is set to the lesser of the two values (interpreted as unsigned integers).

Compatible ARMv6-M

On ARMv6-M, the comparison result can be turned into a condition, which can then be used in a selection sequence, for a total of 5 cycles:

@ rd <- min_u(rx, ry) (minimum of values, unsigned interpretation)
@ rx is preserved
@ ry is preserved
@ rt is scratch
.macro UMIN_1  rd, rx, ry, rt
        cmp     \ry, \rx
        sbcs    \rt, \rt
        subs    \rd, \rx, \ry
        bics    \rd, \rt
        adds    \rd, \ry
.endm

Specific to ARMv7-M

On ARMv7-M, a combination of comparison and selection yields a 4-cycle method:

@ rd <- min_u(rx, ry) (minimum of values, unsigned interpretation)
@ rx is preserved
@ ry is preserved
@ rt is scratch
@ VARIANT: rd can be the same register as rx
.macro UMIN_2  rd, rx, ry, rt
        subs    \rt, \ry, \rx
        sbcs    \rt, \rt
        bics    \rd, \rx, \rt
        mls     \rd, \ry, \rt, \rd
.endm

By writing the output into the same register as the first operand (rx), we can get a 3-cycle method:

@ rx <- min_u(rx, ry) (minimum of values, unsigned interpretation)
@ ry is preserved
@ rt is scratch
@ ru is scratch
@ VARIANT: ru can be the same register as ry (consumed)
.macro UMIN_3  rx, ry, rt, ru
        subs    \ru, \ry, \rx
        sbcs    \rt, \rt
        umlal   \ru, \rx, \rt, \ru
.endm

Combined Maximum and Minimum (unsigned)

If we have two values rx and ry, then we can compute the minimum and the maximum of the two (unsigned interpretation), faster than doing the two computations separately, and also faster than doing a comparison then using a conditional swap.

Compatible ARMv6-M

A 5-cycle combined min/max is obtained, with outputs written over the inputs, and two scratch registers:

@ (rx, ry) <- (min_u(rx, ry), max_u(rx, ry))
@ rt is scratch
@ ru is scratch
.macro UMINMAX_1  rx, ry, rt, ru
        subs    \rt, \ry, \rx
        sbcs    \ru, \ru
        ands    \rt, \ru
        subs    \ry, \rt
        adds    \rx, \rt
.endm

Specific to ARMv7-M

On ARMv7-M, a 4-cycle method is the following (with two scratch registers):

@ (rx, ry) <- (min_u(rx, ry), max_u(rx, ry))
@ rt is scratch
@ ru is scratch
.macro UMINMAX_2  rx, ry, rt, ru
        subs    \rt, \ry, \rx
        sbcs    \ru, \ru
        umlal   \ry, \rx, \ru, \rt
        subs    \rx, \ru
.endm

Generalized Adder

The adcs opcode is a generalized adder, accepting a carry on input, and producing a carry on output. The carry uses the C flag. On ARMv7-M, a one-cycle full adder can also be implemented with the umaal opcode without using or impacting the C flag; this can be used if two big integer addition chains must be interleaved:

@ rm = 1
@ rc \in { 0, 1 }
@ rx:rc <- rx + ry + rc  (addition with carry)
@ rm is preserved
.macro FULL_ADDER  rx, ry, rc
        umaal   rx, rc, rm, ry
.endm

This operation generalizes to adding m*y to x by setting rm to the multipler m (the carry value in rc then extends to the range [0,m]). In particular, by setting m to 2^n - 1 and using the same register for rx and ry, then we obtain a method to left-shift a big integer by n bits with an amortized cost of 1 cycle per word:

        @ Example: left-shift of r0:r1:r2:r3:r4:r5 by 7 bits; extra top
        @ bits go to r6.
        movs    r6, #0
        movs    r7, #127         @ 127 == $2^{7}$ - 1
        umaal   r0, r6, r7, r0
        umaal   r1, r6, r7, r1
        umaal   r2, r6, r7, r2
        umaal   r3, r6, r7, r3
        umaal   r4, r6, r7, r4
        umaal   r5, r6, r7, r5

32x32->64 Multiplication (unsigned)

Given two 32-bit integers (considered unsigned), compute their product over 64 bits.

Compatible ARMv6-M

A 17-cycle multiplication routine can be obtained, with some usage constraints: low word of the output overwrites the first operand, high word of the output goes to a distinct register, second operand is consumed, two scratch registers are needed:

@ rx:rh <- rx * ry  (32x32->64 unsigned multiplication)
@ ry is consumed
@ rt is scratch
@ ru is scratch
.macro UMUL32x32_64_1  rx, rh, ry, rt, ru
        lsrs    \rt, \ry, #16      @ rt <- hi(y)
        lsrs    \rh, \rx, #16
        muls    \rh, \rt           @ rh <- hi(x)*hi(y)

        uxth    \ry, \ry           @ ry <- lo(y)
        lsrs    \ru, \rx, #16      @ ru <- hi(x)
        uxth    \rx, \rx           @ rx <- lo(x)
        muls    \rt, \rx           @ rt <- lo(x)*hi(y)
        muls    \ru, \ry           @ ru <- hi(x)*lo(y)
        muls    \rx, \ry           @ rx <- lo(x)*lo(y)

        @ rx:rh <- rx:rh + rt*$2^{16}$; we use ry as scratch
        lsrs    \ry, \rt, #16
        lsls    \rt, \rt, #16
        adds    \rx, \rt
        adcs    \rh, \ry

        @ rx:rh <- rx:rh + ru*$2^{16}$; we use ry as scratch
        lsrs    \ry, \ru, #16
        lsls    \ru, \ru, #16
        adds    \rx, \ru
        adcs    \rh, \ry
.endm

Specific to ARMv7-M

On ARMv7-M, the 32x32->64 unsigned multiplication is simply the umull opcode.

64x64->64 Multiplication

Given two 64-bit integers, compute their product (64 low bits only). Since we only keep the low 64 bits, the output is the same regardless of whether we consider the inputs signed or unsigned.

Compatible ARMv6-M

Given rxl:rxh and ryl:ryh, the product (64-bit) is written into rxl:rxh; ryl:ryh is consumed, and an extra scratch register is needed. The sequence cost is 21 cycles:

@ rxl:rxh <- rxl:rxh * ryl:ryh mod $2^{64}$  (unsigned 64-bit mul, 64-bit output)
@ ryl is consumed
@ ryh is consumed
@ rt is scratch
.macro UMUL64x64_64_1  rxl, rxh, ryl, ryh, rt
        muls    \ryh, \rxl
        muls    \rxh, \ryl
        adds    \ryh, \rxh

        lsrs    \rt, \ryl, #16      @ rt <- hi(yl)
        lsrs    \rxh, \rxl, #16
        muls    \rxh, \rt           @ rxh <- hi(xl)*hi(yl)
        adds    \rxh, \ryh

        uxth    \ryl, \ryl          @ ryl <- lo(yl)
        lsrs    \ryh, \rxl, #16     @ ryh <- hi(xl)
        uxth    \rxl, \rxl          @ rxl <- lo(xl)
        muls    \rt, \rxl           @ rt <- lo(xl)*hi(yl)
        muls    \ryh, \ryl          @ ryh <- hi(xl)*lo(yl)
        muls    \rxl, \ryl          @ rxl <- lo(xl)*lo(yl)

        @ rxl:rxh <- rxl:rxh + rt*$2^{16}$; we use ryl as scratch
        lsrs    \ryl, \rt, #16
        lsls    \rt, \rt, #16
        adds    \rxl, \rt
        adcs    \rxh, \ryl

        @ rxl:rxh <- rxl:rxh + ryh*$2^{16}$; we use ryl as scratch
        lsrs    \ryl, \ryh, #16
        lsls    \ryh, \ryh, #16
        adds    \rxl, \ryh
        adcs    \rxh, \ryl
.endm

C compilers typically provide this routine as a stand-alone function, to implement multiplication on 64-bit types (e.g. uint64_t). That function can be implemented as follows, using the macro defined above:

        .align  2
        .global llmul
        .thumb
        .thumb_func
        .type   llmul, %function
llmul:
        mov     r12, r4
        UMUL64x64_64_1  r0, r1, r2, r3, r4
        mov     r4, r12
        bx      lr
        .size   llmul, .-llmul

Note that we use register r12, which is caller-saved, as temporary storage for r4, which we need as scratch register (r12 is a "high" register and cannot be used for computations on ARMv6-M, but it is still there and can be used for storage; using r12 is faster than saving r4 on the stack). The overall function cost is 25 cycles (including the final bx lr, but exclusing the bl call that is used to invoke this function).

Specific to ARMv7-M

A full 64-bit multiply can be done in 4 cycles on ARMv7-M:

@ rxl:rxh <- rxl:rxh * ryl:ryh mod $2^{64}$  (64-bit mul with 64-bit output)
@ ryl is consumed
@ ryh is consumed
.macro UMUL64x64_64_2  rxl, rxh, ryl, ryh
        mul     \ryh, \rxl, \ryh
        mla     \rxh, \ryl, \rxh, \ryh
        umull   \rxl, \ryl, \rxl, \ryl
        add     \rxh, \rxh, \ryl
.endm

This sequence can be reduced to 3 cycles if we accept to obtain the result in different registers:

@ rdl:rdh <- rxl:rxh * ryl:ryh mod $2^{64}$  (64-bit mul with 64-bit output)
@ rxl is preserved
@ rxh is preserved
@ ryl is preserved
@ ryh is preserved
.macro UMUL64x64_64_3  rdl, rdh, rxl, rxh, ryl, ryh
        umull   \rdl, \rdh, \rxl, \ryl
        mla     \rdh, \rxl, \ryh, \rdh
        mla     \rdh, \rxh, \ryl, \rdh
.endm

64x64->128 Multiplication

On ARMv7-M, a full 64-bit multiplication with 128-bit output is obtained in only 4 cycles:

@ rd0:rd1:rd2:rd3 <- rxl:rxh * ryl:ryh  (64x64->128 multiplication, unsigned)
@ VARIANT: rxl can be the same register as either rd0 or rd2
.macro UMUL64x64_128_1  rd0, rd1, rd2, rd3, rxl, rxh, ryl, ryh
        umull   \rd1, \rd3, \rxl, \ryh
        umull   \rd0, \rd2, \rxl, \ryl
        umaal   \rd1, \rd2, \rxh, \ryl
        umaal   \rd2, \rd3, \rxh, \ryh
.endm