4 ms·
I've had to implement BigNum on an embedded system that had very little RAM and where the initial optimized version of ModExp took many minutes to complete. Aft
by worstspotgain 2y ago
I've had to implement BigNum on an embedded system that had very little RAM and where the initial optimized version of ModExp took many minutes to complete. After much hair-pulling, the final version took 40 seconds.
First, you should work with unsigned numbers, and use a power of 2 as your word size. The fastest choice for a word size is very operation- and CPU-dependent.
A key trick is to lay out your bignums in the same order as the endianity of each word in memory, e.g. least-significant word first on little-endian systems. This will allow you to choose your word size dynamically for each operation: in memory, a number with M words of N bits each is identical to a number with M / 2 words of N * 2 bits each.
For multiplication, identify the CPU instruction with the widest result, then use half that size as your word size. Each step through the arrays generates a word result in the low half and a word carry in the top half. The carry gets added to the result of the next step, possibly overflowing.
For addition, use the widest result as your word size. This can also overflow.
How you deal with overflows is very CPU-dependent. You can use adc/addc as someone else mentioned, which will be faster on embedded and may be faster on fatter chips. Alternatively, you can halve the word size and use the top half as the carry.
If addc is not available, you can test for overflows as follows:
uint32_t a = ..., b = ...;
uint32_t res = a + b;
uint32_t carry = res < a;
On overflow, res must necessarily be less than both a and b, so no need to check b.
If SIMD instructions are available, that will almost always be the fastest choice by far. While it doesn't change the above guidelines in principle, there are often e.g. optimized overflow mechanisms.
- less_less 2y ago""" uint32_t a = ..., b = ...; uint32_t res = a + b; uint32_t carry = res < a; """ But note well: this trick doesn't work when there is also a carry-in. That is, uint32_t res = a + b + carry; carry = res < a; won't work when b = 0xFFFFFFFF and carry = 1, so you have to check for carries twice, or set `carry = res < a || (res == a && carry);` or similar. So if you are implementing bignums on an architecture that doesn't have ADC (such as RISC-V), or on a vector architecture where the performance would be bad, then you often need an alternative strategy such as using less than 32/64 bits per limb (whether that's half-size, or 30 bits or 51 bits or whatever).
- deleted 2y ago[deleted]
- worstspotgain 2y agoYep. Those are two additions, and naturally every full-word addition needs a separate overflow check. You can avoid the short-circuiting branches by just adding the carries: uint32_t res1 = a + carry; carry = res1 < a; uint32_t res2 = b + res1; carry += res2 < b;
- jonathrg 2y agoJust curious, what kind of embedded system were you working on that required arbitrary precision numbers, and where it was acceptable for the system to stall for 40 seconds during a calculation?
- worstspotgain 2y agoThe ModExp was used for cryptographic authentication, and only ran once per session or once when pairing. IIRC it was a battery BLE device.