Monday, July 7, 2008

Cpp bignum arithmetic: part I

Adding two C preprocessor bignums is an easy task, we have to merely reproduce the basic algorithm taught at elementary school; if the bignums to add are bn1 and bn2 and length(bn1) ≥ length(bn2) (otherwise we just swap them), we can fold over reverse(bn1), similarly as how we did for bignum comparison, using the following recursive state definition:

statei = (carryi,seqi,resi);
state
0 = (0, reverse(bn2),empty sequence),
carryi+1 = addmsd(reverse(bn1)i,head(seqi),carryi),
seqi+1 = tail(seqi),
resi+1 = append(resi, sublsd(reverse(bn1)i,head(seqi),carryi)),

where addmsd and addlsd (whose suffixes stand for "most significant digit" and "least significant digit", respectively) are defined as:

addmsd(x,y,carry) = floor((x + y + carry) /10),
addlsd(x,y,carry) = (x + y + carry) mod 10.

The bignum corresponding to bn1 + bn2 is obtained from the last resi, to which the last carryi must be appended if it is not null. The whole algorithm is implemented with Boost.Preprocessor as follows:

‎#define BN_ADD(bn1,bn2) \
‎BOOST_PP_IF( \
‎ BOOST_PP_GREATER_EQUAL( \
‎ BOOST_PP_SEQ_SIZE(bn1), \
‎ BOOST_PP_SEQ_SIZE(bn2)), \
‎ BN_ADD_IMPL(bn1,bn2), \
‎ BN_ADD_IMPL(bn2,bn1))

‎#define BN_ADD_IMPL(bn1,bn2) \
‎BN_ADD_POSTPROCESS( \
‎ BOOST_PP_SEQ_FOLD_LEFT( \
‎ BN_ADD_OP, \
‎ (0)(BOOST_PP_SEQ_REVERSE(bn2))((~)), \
‎ BOOST_PP_SEQ_REVERSE(bn1)))

‎#define BN_ADD_POSTPROCESS(state) \
‎BOOST_PP_IF( \
‎ BOOST_PP_NOT_EQUAL(BN_ADD_STATE_CARRY(state),0), \
‎ BOOST_PP_IDENTITY((BN_ADD_STATE_CARRY(state))), \
‎ BOOST_PP_EMPTY)() \
‎BOOST_PP_SEQ_POP_BACK(BN_ADD_STATE_RESULT(state))

‎#define BN_ADD_OP(s,state,x) \
‎BN_ADD_OP_IMPL( \
‎ x, \
‎ BN_ADD_STATE_Y(state), \
‎ BN_ADD_STATE_CARRY(state), \
‎ BN_ADD_STATE_RBN2(state), \
‎ BN_ADD_STATE_RESULT(state))

‎#define BN_ADD_OP_IMPL(x,y,carry,rbn2,result) \
‎(BN_ADD_MSD(x,y,carry)) \
‎(BOOST_PP_SEQ_TAIL(rbn2(0))) \
‎((BN_ADD_LSD(x,y,carry))result)

‎#define BN_ADD_STATE_CARRY(state) BOOST_PP_SEQ_ELEM(0,state)
‎#define BN_ADD_STATE_RBN2(state) BOOST_PP_SEQ_ELEM(1,state)
‎#define BN_ADD_STATE_Y(state) \
‎BOOST_PP_SEQ_HEAD(BN_ADD_STATE_RBN2(state))
‎#define BN_ADD_STATE_RESULT(state) BOOST_PP_SEQ_ELEM(2,state)

‎#define BN_ADD_MSD(x,y,carry) \
‎BOOST_PP_DIV(BOOST_PP_ADD(BOOST_PP_ADD(x,y),carry),10)

‎#define BN_ADD_LSD(x,y,carry) \
‎BOOST_PP_MOD(BOOST_PP_ADD(BOOST_PP_ADD(x,y),carry),10)

Subtraction is similar. As our bignums only represent positive integers, we define BN_SUB(bn1,bn2) to be 0 when bn2 > bn1, just as BOOST_PP_SUB does. Again, we assume that length(bn1) ≥ length(bn2) and fold over reverse(bn1); the associated recursive state definition is this:

statei = (borrowi,prefixi,seqi,resi);
state
0 = (0,0, reverse(bn2),empty sequence),
borrowi+1 = submsd(reverse(bn1)i,head(seqi),borrowi),
prefixi+1 =
‎ · if sublsd(reverse(bn1)i,head(seqi),borrowi) ≠ 0, 1,
‎‎ · else prefixi + 1.
seqi+1 = tail(seqi),
resi+1 = append(resi, sublsd(reverse(bn1)i,head(seqi),borrowi)),
submsd(x,y,borrow) = floor((10 + xyborrow) / 10) − 1,
sublsd(x,y,borrow) = (10 + xyborrow) mod 10.

prefixi keeps track of the leading zeros in the resi sequence; these zeros must be deleted on postprocessing time, since the normal representation for bignums always begins with a non-zero digit (except for the bignum corresponding to 0). The observant reader might have noticed that prefixi is actually the number of leading zeros plus one; this is merely an implementation artifact that helps us distinguish the special case when the result of the subtraction is 0. The algorithm allows us to easily detect when bn2 > bn1: this is the case if and only if the final borrowi is not zero. We will not show the whole coding of BN_SUB as a Boost.Preprocessor-based macro since it basically follows the same implementation patterns as BN_ADD.

Multiplication is a little harder. Although it is by no means the fastest algorithm possible, we will implement the O(n2) elementary school method, which consists of multiplying the first factor by every digit of the second factor, shifting appropriately and adding the intermediate results. So, we need first to devise a macro BN_DIGIT_MUL(bn,y) to multiply a bignum by a digit. No surprise by now, we can implement such operation by folding over reverse(bn) with this state structure:

statei = (y,carryi,resi);
state
0 = (y,0,empty sequence),
carryi+1 = mulmsd(reverse(bn1)i,y,carryi),
resi+1 = append(resi, mullsd(reverse(bn1)i,y,carryi)),
mulmsd(x,y,carry) = floor((xy + carry) /10),
mullsd(x,y,carry) = (xy + carry) mod 10.

Using BN_DIGIT_MUL, we can implement BN_MUL(bn1,bn2) by folding over reverse(bn1) as follows:

statei = (shifti,bn2,resi);
state
0 = (empty sequence,bn2,0),
shifti+1 = append(shifti,0),
resi+1 = resi + append(BN_DIGIT_MUL(bn2,reverse(bn1)i), shifti).

shifti is a sequence of increasingly many zero digits used to perform fast multiplication of the intermediate results by powers of 10 --multiplying a bignum bn by 10n reduces to appending a sequence of n zero digits to the representation of bn.

To keep this entry short, we will defer to later occasions the study of bignum division, when we will also provide complete code for all the arithmetic operations and make some performance measurements.

No comments :

Post a Comment