Acheiving this using the arbitrary precision integers in Python
Certain math operations are safe to chain where you can delay computing the modulus until later. Examples of this is +
, -
, *
, <<
(right shift).
((a%c) + (b%c)) % c == (a + b) % c
((a%c) - (b%c)) % c == (a - b) % c
((a%c) * (b%c)) % c == (a * b) % c
((a%c) << (b%c)) % c == (a << b) % c
Others are not, like /
, //
, %
(modulo) and >>
(left shift).
Also note that when our modulo is a power of 2, we can replace the operation with a bitwise AND operation - which essentially zeros out the binary digits you don't want.
x % (2**n) == x & (2**n - 1)
So for 32 bit operations, that becomes:
x % (2**32) == x & 0xFFFFFFFF
That means in order to match the result of u32-operations in C/numpy, you could perform x & 0xFFFFFFFF
for every intermediate result of your computation. You can temporarily skip this for certain operations as highlighted above.
This doesn't require any dependencies, and is less verbose than using
ctypes.c_uint32(...).value
for every intermediate result.
When p == 2**n
, certain bitwise operations also make it safe to delay the modulo operation.
((a%p) & (b%p)) % p == (a & b) % p # BITWISE AND
((a%p) | (b%p)) % p == (a | b) % p # BITWISE OR
((a%p) ^ (b%p)) % p == (a ^ b) % p # BITWISE XOR