1

I am developing a BigNumber library which allows for creation of large numbers (Integers and Floating). You can find the public repository here

I've implemented add and subtract for both BigFloating and BigInteger, however multiply and divide only for BigInteger

The bits for a floating point number are stored in a std::vector and are in the format:

sign (1 bit)|binary integer(variable bits)|binary fraction(variable bits)

Such that the number 5.5 would have the bits 0 101 1

What are some algorithms for multiplying and dividing numbers with this format?

i.e.

(5.5)   101 1
* (5.5) 101 1
-------------
= (30.25) 11110 01

or

(5.5)   101 1
/ (5.5) 101 1
-------------
= (1)     1

The functions to be implemented are found in BigCommon.cpp and are:

std::tuple<std::vector<bool>, size_t, size_t> BigCommon::multiplyBits(const std::vector<bool> &lhsBits, const std::vector<bool> &rhsBits, const size_t &integerBitsSize, const size_t &mantissaBitsSize)

and

std::tuple<std::vector<bool>, size_t, size_t> BigCommon::divideBits(const std::vector<bool> &lhsBits, const std::vector<bool> &rhsBits, const size_t &integerBitsSize, const size_t &mantissaBitsSize)

Update

I have implemented the multiplyBits algorithm like so:

std::tuple<std::vector<bool>, size_t, size_t> BigCommon::multiplyBits(const std::vector<bool> &lhsBits, const std::vector<bool> &rhsBits, const size_t &integerBitsSize, const size_t &mantissaBitsSize)
{
 std::vector<bool> result;
 result.insert(result.begin(), lhsBits[0] ^ rhsBits[0]);
 size_t newIntegerBitsSize = 0;
 size_t newMantissaBitsSize = mantissaBitsSize + mantissaBitsSize;
 std::vector<bool> lhsBinary(lhsBits.begin() + 1, lhsBits.end());
 std::vector<bool> rhsBinary(rhsBits.begin() + 1, rhsBits.end());
 std::vector<bool> multResult = multiplyBinaryVectors(lhsBinary, rhsBinary);
 newIntegerBitsSize = multResult.size() - newMantissaBitsSize;
 result.insert(result.begin() + 1, multResult.begin(), multResult.end());
 return {result, newIntegerBitsSize, newMantissaBitsSize};
};

Now just for divide!

Update 2

I have successfully implemented division using the following algorithm:

Code redacted in favour of answer.

Update 3

After some testing I found the division algorithm doesn't work with some types of numbers, here are some test cases: 5 / 0.27 10 / 100. Definitely to do with divideBinaryVectors

greybeard
  • 2,249
  • 8
  • 30
  • 66
ZeunO8
  • 422
  • 6
  • 25
  • 1
    Why `0 101 1`? Shouldn't it be `0 101 101`? – NathanOliver Apr 26 '23 at 01:06
  • 1
    `0` for the sign bit (positive), `101` for the 5, `1` to represent .5 as a binary fraction – ZeunO8 Apr 26 '23 at 01:06
  • 1
    how will you represent 5.05 or 5.005 in this format? – Iłya Bursov Apr 26 '23 at 01:07
  • I get the `0`, it's the solitary `1` for the binary fraction part I don't get. How does `1` == `.5`? – NathanOliver Apr 26 '23 at 01:07
  • All is explained in the algorithms for `decimalStringToBinary` and `decimalFractionToBinary`: https://github.com/ZeunO8/BigNumber/blob/main/src/BigCommon.cpp#L534 – ZeunO8 Apr 26 '23 at 01:08
  • 1
    You really need comment in the code to explain what you are doing. I still don't comprehend how/why you are encoding `.5` to the value of `1` – NathanOliver Apr 26 '23 at 01:13
  • 1
    @NathanOliver `.1b` == `1b / 10b` == (1/2) the same way `0.5d` == `5d / 10d` == (1/2). Or think of it like you have a 1 in the 2^(-1)'s column. – Wyck Apr 26 '23 at 01:16
  • 1
    Each bit (binary digit) in the binary fraction represents a decreasing power of 2, with the first bit after the decimal point representing 2^-1 (which is 1/2), the second bit representing 2^-2 (which is 1/4), and so on. Each successive bit represents a fraction that is half the value of the previous bit, with the value of the fraction decreasing as the position of the bit moves further to the right. example, the binary fraction 0.101 represents the decimal number 5/8, where the first bit after the decimal point represents 1/2, the second bit represents 1/4, and the third bit represents 1/8. – ZeunO8 Apr 26 '23 at 01:18
  • @IłyaBursov 5.05 is represented as `0 101 00001100110011001100110011001100110011001100110011001100110011` – ZeunO8 Apr 26 '23 at 01:21
  • Aha, I was interpreting your `binary fraction(variable bits)` part as being an integer representing the fractional part (somehow I was thinking decimal) after the `.`, so `.5` being `101`. It might be a lot simpler if you do that. Then you could multiple the decimal part like the integer part and just add the carryover into the integer part. – NathanOliver Apr 26 '23 at 01:23
  • @NathanOliver but it needs to be represented as a binary fraction to get correct addition and subtraction, so I assume multiply and divide would use the same format – ZeunO8 Apr 26 '23 at 01:24
  • this format is essentially the same as decimal, so you can use exactly the same algorithms, check for example https://en.wikipedia.org/wiki/Binary_multiplier#Binary_long_multiplication - for floating point you just need to do appropriate padding to make numbers the same length (after point) – Iłya Bursov Apr 26 '23 at 01:47
  • still not sure how to do floating point multiplication with standard binary multiplication. wouldn't the point be shifted left causing incorrect results? – ZeunO8 Apr 26 '23 at 02:05
  • 1
    I have implemented the multiplyBits algorithm, now just for divide! – ZeunO8 Apr 26 '23 at 03:25
  • @ZeunO8, algorithm divide for BigFloating is like BigInteger. Consider it as a dividing 2 integers. Simply add a shift when done. – chux - Reinstate Monica Apr 26 '23 at 12:01
  • 1
    Division is just... hard. You can make things easier by choosing an appropriate representation. I would not recommend treating the integer and fraction portions of your number as two separate quantities, as you are here. If you instead use a pure fixed-point representation, or a pure floating-point representation, you can basically implement your addition, subtraction, multiplication, and division algorithms just once, as if for integers, then use appropriate shifts to adapt them for use on your fractions. – Steve Summit Apr 26 '23 at 12:09
  • By "pure fixed-point" I mean that you represent a fraction by an integer, with a known (constant) fractional multiplier or exponent. By "pure floating-point" I mean that you represent a fraction by an integer, with an exponent that you carry around with each fraction. For example, in fixed point with a constant multiplier of 2³², you'd represent the number 31.125 as 133680857088. In floating point, you might represent it as 249 with an exponent of -3. – Steve Summit Apr 26 '23 at 12:11
  • I have successfully implemented division! That completes addition, subtraction, multiplication and division for my BigNumber structs! – ZeunO8 Apr 26 '23 at 12:11
  • as mentioned You use fixed point not floating so you simply compute integer `+,-,*,/` and correct the bitshift of the result ... for division either use long binary division or approximation divider for example like this one [approximation divider](https://stackoverflow.com/a/18398246/2521214) for additional functions use either taylor/chebyshew series [sin example](https://stackoverflow.com/a/71748771/2521214) or binary search see [sqrt,pow,log,exp examples](https://stackoverflow.com/a/30962495/2521214) – Spektre Apr 30 '23 at 06:55
  • also Multiplication for big numbers can be done using Karatsuba or even faster algorithms see [Fast bignum square computation](https://stackoverflow.com/q/18465326/2521214). btw once I started to mess with floating and fixed point maths I created a template that computes these higher functions so I have the same base code for all related math types which ease up maintenance a lot... also once you start arbitrary maths you will certainly need to compute [pi](https://stackoverflow.com/a/22295383/2521214) and [e](https://stackoverflow.com/a/47670209/2521214) constants in high precision too – Spektre Apr 30 '23 at 07:02
  • (Failing?) `test cases: 5 / 0.27 & 10 / 100` you may be better off debugging `BigCommon::divideBinaryVectors()`. What's the overall goal in this exercise? – greybeard Apr 30 '23 at 10:54
  • You say you're looking for reputable sources, so you will definitely want to check out [Knuth](https://en.wikipedia.org/wiki/The_Art_of_Computer_Programming). See also [this previous Stack Overflow question](https://stackoverflow.com/questions/27801397). – Steve Summit Apr 30 '23 at 13:10
  • I'm looking for code or pseudo code that clearly explains how to do division of floating point numbers in the described format – ZeunO8 May 01 '23 at 01:20
  • Not sure if its of interest to you, but integer division can be implemented by multiplication by an appropriate number followed by division by the appropriate power of 2 (i.e. bit shift.) Meaning, you can implement division as multiplication, which given your scheme probably makes the most sense. Your implementation does not look amenable to an efficient implementation of euclid's algorithm or any other division algorithm. – ldog May 01 '23 at 23:04
  • If you're interested in fixed-point number representations, you might consider looking into how the Ada programming language and GNAT (an open-source Ada compiler) handle it. Ada supports declaring your own fixed-point types with user-defined sizes, etc. Depending on what you want you should consider writing your library in Ada and providing C or C++ wrappers. see https://learn.adacore.com/courses/intro-to-ada/chapters/fixed_point_types.html and https://www.adaic.org/resources/add_content/standards/05rm/html/RM-3-5-9.html – George May 04 '23 at 19:40

4 Answers4

4

What you describe is a binary fixed point representation with variable(unbounded) size both above and below the fixed binary point (the split between integer and fractional bits.

For this kind of representation, multiplication is pretty easy -- just concatenate the integer and fractional bits of each number, multiply, and split into integer/fraction again. If your input number have j and k fractional bits respectively, the result will have j+k fractional bits.

Divide is harder as you need to decide how far to continue the divide (how many additional fractional bits) to compute when the result is not exatly representable in binary. Fundamentally, it is just long division -- concatenate the integer/fraction bits, divide (a process that may not terminate) and then split the result. So for example, 10/5.5 gives you

           1.11010 00101 11010 ....
       /----------------------
101.1 / 1010.00000 00000 00000
         101 1
         100 10000 00000 00000
          10 11
           1 11000 00000 00000
           1 011
              1100 00000 00000
              1011
                 1 00000 00000
                   1011
                    1010 00000
                     101 1
                     100 10000
                      10 11
                       1 11000
                       1 011
                          1100
                          1011
                             1

with that repeating 10 bit value in the result. You need to decide when you want to cut it off and round the fraction.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • This is great! Could you possibly provide some code for the long division that works with a `std::vector` of combined bits? – ZeunO8 Apr 27 '23 at 05:05
  • Note that once you decide that the result may be rounded to some length (necessary for division) you might also add that as an option for other operations like multiplication. – Hans Olsson Apr 28 '23 at 12:43
1

My non-reputable answer to What are some algorithms for multiplying and dividing big numbers using a std::vector<bool> for sign and significand and a size_t for integer bits?

Best advice for evaluation faster than abysmally slow:
Convert to a useful format, calculate, and convert back.

For calculating "in" this format:
Multiplication is relatively immune to differences in number representations.
Algorithms to know are long multiplication and Toom, namely Toom-2/Karatsuba.

Non-performing/pre-checking division (lumped with restoring division more often than not) isn't much more effort than non-restoring due to comparison allowing early out: for every quotient bit an average about three bits need to be inspected.

Beyond missing specification, I find two problems with the implementation attempt in BigCommon.cpp as of 2023/05/04:

  • the way to terminate calculation based on parameter mantissaBitsSize is more than dubious
  • assuming remainder >= divisor does the right thing, use subtract.
    Just ex-oring remainder and divisor bits is meaningless regardless of direction of iteration.
greybeard
  • 2,249
  • 8
  • 30
  • 66
  • (in subtract, I'd use `borrow = !left && (borrow || right) || right && borrow` to stress the equivalence of/symmetry in *right* and *borrow*.) – greybeard May 04 '23 at 11:29
1

The cpp_bin_float class from the Boost.Multiprecision library implements arbitrarily large floating point arithmetic with binary representation as you seek. It also supports special edge cases for infinities and NaNs.

The division implementation is here. It's a bit complex, but you can run a simple example code under debugger to understand the logic. Basically, as was already said here, for division you just assume the numbers as integers and then calculate the location of the point separating the fractional part. But first you have to make the decision on the target precision you want to obtain. boost's implementation also uses arbitrary long integer arithmetic behind the scenes.

A note about your implementation:
It's strongly recomended to avoid usage of std::vector<bool>. A better alternative might be boost::dynamic_bitset.

vvv444
  • 2,764
  • 1
  • 14
  • 25
0

To perform division on fixed-point binary numbers we can use the following algorithm, a modified version of long-division that correctly updates integerBitsSize and fractionBitsSize

std::tuple<std::vector<bool>, size_t, size_t> BigCommon::divideBits(const std::vector<bool> &lhsBits, const std::vector<bool> &rhsBits, const size_t &integerBitsSize, const size_t &fractionBitsSize)
{
 if (isZero(rhsBits))
 {
  throw std::domain_error("Division by zero");
 }

 bool resultSign = lhsBits.front() ^ rhsBits.front();

 std::vector<bool> lhs(lhsBits.begin() + 1, lhsBits.end());
 std::vector<bool> rhs(rhsBits.begin() + 1, rhsBits.end());

 // Normalize dividend and divisor to remove leading zeros
 normalizeBits(lhs);
 normalizeBits(rhs);

 std::vector<bool> dividend = lhs;
 std::vector<bool> divisor = rhs;

 std::vector<bool> result;
 std::vector<bool> tempDividend;

 size_t resultIntegerBitsSize = 0;
 size_t resultFractionBitsSize = 0;

 size_t dividendSize = dividend.size();
 size_t dividendIndex = 0;

 while (true)
 {
  tempDividend.insert(tempDividend.end(), dividendIndex < dividendSize ? dividend[dividendIndex] : false);
  dividendIndex++;
  if (binaryVectorLessThanOrEqualsTo(divisor, tempDividend))
  {
   result.insert(result.end(), true);
   tempDividend = subtractBinaryVectors(tempDividend, divisor);
  }
  else
  {
   result.insert(result.end(), false);
  }
  if (dividendIndex <= dividendSize)
  {
   resultIntegerBitsSize++;
  }
  else
  {
   resultFractionBitsSize++;
   if (resultFractionBitsSize >= 128)
   {
    break;
   }
  }
 }

 while (resultIntegerBitsSize > 0 && !result.front())
 {
  result.erase(result.begin());
  resultIntegerBitsSize--;
 }

 while (resultFractionBitsSize > 0 && !result.back())
 {
  result.erase(result.end() - 1);
  resultFractionBitsSize--;
 }

 result.insert(result.begin(), resultSign);
 return {result, resultIntegerBitsSize, resultFractionBitsSize};
};

It has been tested on the failing test cases 5 / 0.27 and 10 / 100 and is returning correct results.

ZeunO8
  • 422
  • 6
  • 25