Bit Twiddling
March 12, 2025 ยท View on GitHub
Some Tricks with Multiplications
On ARMv7-M, the umull, umlal and umaal opcodes can be used to
efficiently perform several non-trivial operations; in particular, these
opcodes, contrary to most other 1-cycle instructions, can write to two
different registers.
If register ra is known to be zero, then two other registers rb and
rc can be cleared in a single cycle:
@ ra == 0
@ (rb, rc) <- (0, 0)
@ ra is preserved
.macro CLEAR_TWO ra, rb, rc
umull \rb, \rc, \ra, \ra
.endm
Register rx can be decremented with saturation at zero, i.e. if the
register is already zero then it is not decremented. This requires another
register loaded with the constant -1 (preserved) and an extra scratch:
@ rm == -1
@ rx <- rx - 1 if rx != 0; rx is preserved if rx == 0
@ rm is preserved
@ rt is scratch
.macro DECREMENT_SATZERO rx, rm
umull rt, rx, rx, rm
.endm
Split Even/Odd Bits
Given input x, we want to separate its value into even-indexed bits
and odd-indexed bits, i.e. move bits around such that bits 0, 2, 4, 6...
go to positions 0 to 15 in the output, while bits 1, 3, 5, 7... go
to positions 16 to 31. The macro BIT_SPLIT_32 performs this operation
in 14 cycles, provided that the ASPR.GE flags have been set to the
appropriate value beforehand:
@ On input, ASPR.GE must be 0110.
@ Split bits of rx into rd.
@ rx is consumed
@ ASPR.GE is preserved
.macro BIT_SPLIT_32 rd, rx
eor \rd, \rx, \rx, lsr #1
and \rd, \rd, #0x22222222
eor \rx, \rx, \rd
eor \rx, \rx, \rd, lsl #1
eor \rd, \rx, \rx, lsr #2
and \rd, \rd, #0x0C0C0C0C
eor \rx, \rx, \rd
eor \rx, \rx, \rd, lsl #2
eor \rd, \rx, \rx, lsr #4
and \rd, \rd, #0x00F000F0
eor \rx, \rx, \rd
eor \rx, \rx, \rd, lsl #4
rev \rd, \rx
sel \rd, \rd, \rx
.endm
The ASPR.GE flags are used for the final step (last two instructions
in BIT_SPLIT_32), which swap the two "middle bytes" of the value in
rx. Setting ASPR.GE takes three cycles:
movw r0, #0xFF00
movt r0, #0x00FF
uadd8 r0, r0, r0
If only a single bit split operation needs to be done, then it is
more efficient to do the final byte movement without the ASPR.GE
flags, with these four instructions instead of rev + sel:
eor \rd, \rx, \rx, lsr #8
and \rd, \rd, #0x0000FF00
eor \rx, \rx, \rd
eor \rx, \rx, \rd, lsl #8
This brings the cost of BIT_SPLIT_32 to 16 cycles (instead of 14)
but does not need the 3-cycle initialization of ASPR.GE. If multiple
BIT_SPLIT_32 must be performed, though, then the method with rev and
sel is faster since the ASPR.GE flags need to be set only once.
BIT_SPLIT_64 extends BIT_SPLIT_32 to a 64-bit input:
@ On input, ASPR.GE must be 0110.
@ Split bits of ra:rb into ra:rb (ra receives the even-indexed bits)
@ rt is scratch
@ ASPR.GE is preserved
.macro BIT_SPLIT_64 ra, rb, rt
BIT_SPLIT_32 \rt, \ra
BIT_SPLIT_32 \ra, \rb
pkhtb \rb, \ra, \rt, asr #16
pkhbt \ra, \rt, \ra, lsl #16
.endm
This is typically used in an implementation of SHA-3/SHAKE; for
SHAKE256, 17 BIT_SPLIT_64 operations must be performed.
The reverse operations (re-interleaving bits) can be done similarly:
@ On input, ASPR.GE must be 0110.
@ Interleave bits of rx into rd.
@ rx is consumed
@ ASPR.GE is preserved
.macro BIT_MERGE_32 rd, rx
rev \rd, \rx
sel \rx, \rd, \rx
eor \rd, \rx, \rx, lsr #4
and \rd, \rd, #0x00F000F0
eor \rx, \rx, \rd
eor \rx, \rx, \rd, lsl #4
eor \rd, \rx, \rx, lsr #2
and \rd, \rd, #0x0C0C0C0C
eor \rx, \rx, \rd
eor \rx, \rx, \rd, lsl #2
eor \rd, \rx, \rx, lsr #1
and \rd, \rd, #0x22222222
eor \rx, \rx, \rd
eor \rd, \rx, \rd, lsl #1
.endm
@ On input, ASPR.GE must be 0110.
@ Interleave bits of ra:rb into ra:rb (ra provides the even-indexed bits)
@ rt is scratch
@ ASPR.GE is preserved
.macro BIT_MERGE_64 ra, rb, rt
pkhtb \rt, \rb, \ra, asr #16
pkhbt \rb, \ra, \rb, lsl #16
BIT_MERGE_32 \ra, \rb
BIT_MERGE_32 \rb, \rt
.endm