3

so, I'm trying to build a simple big integer class, I've read some pages on the internet and all that, but I'm stuck. I know the theory and I know that I need a carry but all examples I've seen, they focuded more in chars and in base 10 and well, I'm using a different approach to make it a bit more faster. I would appreciate some help with the plus assignment operator, the rest of it I'll try to figure it out by myself.

#include <iostream>
#include <string>
#include <vector>

using std::cout;
using std::endl;

class big_integer {
    using box = std::vector<int unsigned>;
    box data {0};

    box split(std::string const & p_input) const {
        box output;
        for (size_t i {}; i < p_input.size(); i += 8) {
            output.push_back(stoi(p_input.substr(i, 8)));
        }
        return output;
    }

public:
    big_integer(std::string const & p_data)
        : data {split(p_data)}
    {}

    big_integer(int unsigned const p_data)
        : data {p_data}
    {}

    big_integer & operator +=(big_integer const & p_input) {
        int carry {};

        for (size_t i {}; i < data.size(); ++i) {

            //Need help here!
            //All examples I see either use primitive arrays
            //or are too esoteric for me to understand.             
            //data[i] += p_input.data[i] + carry;

        }

        return *this;
    }

    std::string to_string() const {
        std::string output;
        output.reserve(data.size() * 8);
        for (auto const i : data) {
            output.append(std::to_string(i));
        }
        return output;
    }
};

std::ostream & operator <<(std::ostream & p_output, big_integer const & p_input) {
    return p_output << p_input.to_string();
}

int main() {
    big_integer test1 {"126355316523"};
    big_integer test2 {255};

    test1 += test1;

    cout << test1 << endl;
    cout << test2 << endl;
    return 0;
}
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
João Pires
  • 927
  • 1
  • 5
  • 16
  • 3
    What exactly is your problem? Vectors behave very much like simple arrays in that respect. Also, the base does not matter much. The approach stays the same. – Nico Schertler Dec 10 '16 at 16:21
  • 1
    What base have you decided to use if not 10? – Mark Ransom Dec 10 '16 at 16:22
  • Oh, I get it know, I'm such a stupid person :( so, will `data[i] = (data[i] + p_input.data[i] + carry) % 10; carry = (data[i] + p_input.data[i] + carry) / 10` do the trick, or something in those lines? – João Pires Dec 10 '16 at 16:27
  • Yes, probably. You have to make sure that the vector is long enough, though. – Nico Schertler Dec 10 '16 at 16:30
  • if you want to make it fast you'll have to use base 2^64 (or base 2^32 on 32-bit computers). Using an int to store a decimal digit is extremely inefficient. Look at big integer libraries for more information http://stackoverflow.com/q/11548070/995714 http://www.numberworld.org/y-cruncher/internals/representation.html – phuclv Dec 10 '16 at 16:34
  • @LưuVĩnhPhúc you can use other powers of 10 as well, such as base 1000000. – Mark Ransom Dec 10 '16 at 17:00
  • But how do I implement operator += correctly? I need someone to guide me on at least one operator. – João Pires Dec 10 '16 at 17:02
  • @MarkRansom yes but the only better thing in power-of-10 bases compared to power-of-2 bases is faster IO, all computations on them will be much slower. – phuclv Dec 10 '16 at 17:09
  • @LưuVĩnhPhúc as the number of digits per element rises, the efficiency difference grows smaller. At 2^31 vs. 10^10 I suspect it's insignificant. – Mark Ransom Dec 10 '16 at 17:18
  • 3
    @MarkRansom in long computations it'll be significant. Imagine you have to divide by 10^9 (not 10) and take remainder every time which is very slow whereas using 2^32 (instead of 31) you already have the carry flag and remainder available so you don't need to do anything – phuclv Dec 10 '16 at 17:23
  • 1
    You know, this is one of the rare cases in which assembler programming is both easy and performs best. You essentially only need a few registers and a loop that uses the add-with-carry instruction `adc`. If you've got no experience with assembler it's a very appropriate first exercise. – Iwillnotexist Idonotexist Dec 10 '16 at 18:20
  • @LưuVĩnhPhúc you may be surprised at how fast a modern processor can do remainders. I wouldn't rule out powers of 10 before doing a side-by-side benchmark, and for a learning exercise it wouldn't make much difference anyway. Thank you for the correction, 10^9 is correct. – Mark Ransom Dec 11 '16 at 00:33

1 Answers1

4

Right. So the basic problem is how to do unsigned + unsigned + carry to give unsigned and carry. If we consider 16-bit integers (it works the same in 32-bits, but is more typing), on the first digit, 0xFFFF + 0xFFFF == 0xFFFE + a carry of 1. On subsequent digits 0xFFFF + 0xFFFF + carry == 0xFFFF + carry. Hence carry can only be one. The algorithm is:

    unsigned lhs, rhs, carry_in;  // Input
    unsigned sum, carry_out;      // Output

    sum = lhs + rhs;
    carry_out = sum < lhs ;
    sum += carry_in;
    carry_out = carry_out || sum < lhs;

Basically, the idea is to do the addition in unsigned, and then detect wrapping (and hence carry). What is very annoying, is that this is masses of conditional logic and multiple instructions to implement "add with carry", which is an instruction in every instruction set I have ever used. (Note: it may be worth making the final calculation of carry_out use | rather than || - it saves branching, which is bad for performance. As always, profile to see if it helps.)

If you are going to eventually support multiplication, you need a type which is twice as wide as your "digit" - in which case, you might as well use it for addition too. Using the variables from above, and assuming "unsigned long long" is your "wide" type:

    const auto temp = (unsigned long long)lhs + rhs + carry_in;
    sum = temp; // Will truncate.
    carry_out = temp > UINT_MAX;

Choosing your "wide" type can be tricky. As a first pass, it's probably best to use uint32_t for your digits and uint64_t for your wide type.

For more details on implementing multi-precision arithmetic, Chapter 14 from Handbook of Applied Cryptography looks useful.

  • Use of `|` instead of `||` eliminates branches in the above. This may or may not be worth the extra data reads and writes, but it definitely makes the performance more consistent. – Yakk - Adam Nevraumont Dec 10 '16 at 17:36
  • @BeyelerStudios: Ah, thank you. I was doing this off the top of my head, and couldn't convince myself that only one was necessary. I am pretty sure I need to check both before *and* after adding the carry though. – Martin Bonner supports Monica Dec 10 '16 at 18:07
  • Actually, I observe that in any base, (10-1)+(10-1) == 20 - 2, so the maximum carry from the first digit is 1, subseqently we have (10-1)+(10-1)+1 == 20 - 1, so the carry cannot exceed 1. – Martin Bonner supports Monica Dec 10 '16 at 18:12
  • You concept of two carries, and of checking if `sum < lhs` is exactly what I use in my (Delphi language) version of BigInteger. I have seen many other ways to emulate a carry, including the use of double sized (e.g. 64 bit when adding 32 bit unsigneds) integers, but this is the only proper way (except for assembler, which is the alternative in my implementation). Very good! – Rudy Velthuis Dec 10 '16 at 20:20