views:

600

answers:

8

I need some help deciding what is better performance wise. I'm working with bigints (more then 5 million digits) and most of the computation (if not all) is in the part of doubling the current bigint. So i wanted to know is it better to multiply every cell (part of the bigint) by 2 then mod it and you know the rest. Or is it better just add the bigint to itself.

I'm thinking a bit about the ease of implementation too (addition of 2 bigints is more complicated then multiplication by 2) , but I'm more concerned about the performance rather then the size of code or ease of implementation.

Other info: I'll code it in C++ , I'm fairly familiar with bigints (just never came across this problem). I'm not in the need of any source code or similar i just need a nice opinion and explanation/proof of it , since i need to make a good decision form the start as the project will be fairly large and mostly built around this part it depends heavily on what i chose now.

Thanks.

+8  A: 

Try bitshifting each bit. That is probably the fastest method. When you bitshift an integer to the left, then you double it (multiply by 2). If you have several long integers in a chain, then you need to store the most significant bit, because after shifting it, it will be gone, and you need to use it as the least significant bit on the next long integer.

This doesn't actually matter a whole lot. Modern 64bit computers can add two integers in the same time it takes to bitshift them (1 clockcycle), so it will take just as long. I suggest you try different methods, and then report back if there is any major time differences. All three methods should be easy to implement, and generating a 5mb number should also be easy, using a random number generator.

Marius
+1  A: 

did you try shifting the bits?
<< multiplies by 2
>> divides by 2

Robert Greiner
Ehm, << - multiply, >> - division ;)
Ravadre
Well i know about that , and in fact most modern C++ compiler when compiling source code exchange *2 and /2 with >>1 and <<1. So that's not really an improvement.And both , you and Marius missed the point of the question , as i wanted to know is it more efficient to add the bigint to itself or is it better to multiply the bigint by 2.
Krunch
@Ravadre haha, thanks. I'm dumb
Robert Greiner
A: 

You should check out this question: http://stackoverflow.com/questions/235072/do-modern-compilers-optimize-the-x-2-operation-to-x-1

Bordering on duplicate ...

Brian Rasmussen
Thanks on finding this question/article , it was just what I was looking for (well sort of , the actual answer lies in one of the articles that have been copied from some site) and it shows that x*2 and <<1 is most likely to end up as x+x , so in general it doesn't matter if you write x*2 , x<<1 or x+x.Performance wise addition and multiplication by 2 are pretty much the same , so I'll go with the easier one to implement (the multiplication).
Krunch
-1, that applies to builtins, not bigints. For UDTs the compiler can't make assumptions on the behavior of operator<<(T) versus operator*(T)
MSalters
The answer to this question is totally different for bignums.
Nelson
It is totally different when looking at bigints as a whole , but when you break it down , you will see that its implemented using features like + - * / and others using normal 32 bit ints (or some other data type you choose or make).
Krunch
+1  A: 

Left bit shifting by one is the same as a multiplication by two !
This link explains the mecanism and give examples.

int A = 10; //...01010 = 10
int B = A<<1; //..010100 = 20
Matthieu
+4  A: 

To store a 5 million digit integer, you'll need quite a few bits -- 5 million if you were referring to binary digits, or ~17 million bits if those were decimal digits. Let's assume the numbers are stored in a binary representation, and your arithmetic happens in chunks of some size, e.g. 32 bits or 64 bits.

  • If adding the number to itself, each chunk is added to itself and to the carry from the addition of the previous chunk. Any carry forward is kept for the next chunk. That's a couple of addition operation, and some book keeping for tracking the carry.

  • If multiplying by two by left-shifting, that's one left-shift operation for the multiplication, and one right-shift operation + and with 1 to obtain the carry. Carry book keeping is a little simpler.

Superficially, the shift version appears slightly faster. The overall cost of doubling the number, however, is highly influenced by the size of the number. A 17 million bits number exceeds the cpu's L1 cache, and processing time is likely overwhelmed by memory fetch operations. On modern PC hardware, memory fetch is orders of magnitude slower than addition and shifting.

With that, you might want to pick the one that's simpler for you to implement. I'm leaning towards the left-shift version.

Oren Trutner
I'm well aware of the memory transfer bottleneck but I'll have to live with it as it is to expect that the number will grow over 10 million digits with time , but that is jet to come.
Krunch
The key point is that the CPU is bottlenecked on memory fetch, not processing. It doesn't matter whether it's adding or shifting and what optimizations the compiler makes, because the time is dominated by waiting for memory transactions to complete.
Oren Trutner
True , but between the memory fetches (while actually processing) i can make it work less so therefore reduce the time processing and with that reduce total time.
Krunch
Prefetching, out-of-order execution, etc. should hopefully make it so that there is no point where its not waiting on memory.
derobert
A: 

If it really matters, you need to write all three methods (including bit-shift!), and profile them, on various input. (Use small numbers, large numbers, and random numbers, to avoid biasing the results.)

Sorry for the "Do it yourself" answer, but that's really the best way. No one cares about this result more than you, which just makes you the best person to figure it out.

ojrac
Well I asked it because I thought somebody has already but it doesn't seem so.There's a first time for everything so it looks like it's up to me to find the truth.
Krunch
In general, profiling is very environment-dependent. Someone else's results from a year ago are probably wrong for you, your compiler, and the optimizations you use.
ojrac
A: 

most of the computation (if not all) is in the part of doubling the current bigint

If all your computation is in doubling the number, why don't you just keep a distinct (base-2) scale field? Then just add one to scale, which can just be a plain-old int. This will surely be faster than any manipulation of some-odd million bits.

IOW, use a bigfloat.

random benchmark

use Math::GMP;
use Time::HiRes qw(clock_gettime CLOCK_REALTIME CLOCK_PROCESS_CPUTIME_ID);

my $n = Math::GMP->new(2);
$n = $n ** 1_000_000;

my $m = Math::GMP->new(2);
$m = $m ** 10_000;

my $str;
for ($bits = 1_000_000; $bits <= 2_000_000; $bits += 10_000) {
    my $start = clock_gettime(CLOCK_PROCESS_CPUTIME_ID);
    $str = "$n" for (1..3);
    my $stop = clock_gettime(CLOCK_PROCESS_CPUTIME_ID);
    print "$bits,@{[($stop-$start)/3]}\n";
    $n = $n * $m;
}

Seems to show that somehow GMP is doing its conversion in O(n) time (where n the number of bits in the binary number). This may be due to the special case of having a 1 followed by a million (or two) zeros; the GNU MP docs say it should be slower (but still better than O(N^2).

derobert
Yes , that was my first idea , but i have to be able to write it out in base 10 and write it to a file , so imagine having to calculate 2^1000000 then 2^999999 and so on , that's one big problem (well a bit smaller if you do it the reverse way like 2^0 then 2^1) , but its still a gigantic task and calculating 2^1000000 comes to the same problem i asked then , that is i need to double a bigint countless times.
Krunch
GMP can convert 2^1,000,000 to a decimal in 0.2 seconds on my relatively slow (P4) machine. How many of these are you outputting?!
derobert
A: 

Well implemented multiplication of BigNums is O(N log(N) log(log(N)). Addition is O(n). Therefore, adding to itself should be faster than multiplying by two. However that's only true if you're multiplying two arbitrary bignums; if your library knows you're multiplying a bignum by a small integer it may be able to optimize to O(n).

As others have noted, bit-shifting is also an option. It should be O(n) as well but faster constant time. But that will only work if your bignum library supports bit shifting.

Nelson
I'll code everything myself so i can add countless features to is to test it.Like I said in some previous comments , I'll post some benching results after i code out a few versions.
Krunch