From 279bf05f99f8e1ea3d96b63ddde2c691bb80c871 Mon Sep 17 00:00:00 2001 From: dthwaite Date: Fri, 4 Mar 2016 20:45:16 +0000 Subject: [PATCH 1/3] Changes to store numbers to a higher base than the original base 10 (typically base 1000000). This significantly speeds up all operations (by quite a few factors). Also Made division by relatively low divisor numbers much faster. Extended tests to include a few choice big number operations. --- lib/big-number.js | 118 ++++++++++++++++++++++++++++++++++------------ test/test.js | 17 +++++++ 2 files changed, 105 insertions(+), 30 deletions(-) diff --git a/lib/big-number.js b/lib/big-number.js index 72f6335..86dc74f 100644 --- a/lib/big-number.js +++ b/lib/big-number.js @@ -11,6 +11,14 @@ !(function() { 'use strict'; + // The higher the base the more efficient this library + // Note that we cannot have a base greater than the square root of the maximum safe integer size + // because we can natively square a "digit" during multiplication + var maxsafeinteger=Number.MAX_SAFE_INTEGER || Math.pow(2,50); + var log10base=Math.floor(Math.log(Math.sqrt(maxsafeinteger))/Math.log(10)); + // The base to which we store the number. + var base=Math.round(Math.pow(10,log10base)); + // Helper function which tests if a given character is a digit var testDigit = function(digit) { return (/^\d$/.test(digit)); @@ -41,12 +49,24 @@ // from an integer, a string, an array or other BigNumber object function BigNumber(initialNumber) { var index; + var position=0; + + // Method removed from public API and made local within this context + function addDigit(digit,number) { + if (testDigit(digit)) { + if (position%log10base==0) number.push(0); + number[number.length-1]+=(digit*Math.pow(10,position%log10base)); + position++; + return true; + } + return false; + } if (!(this instanceof BigNumber)) { return new BigNumber(initialNumber); } - this.number = [] + this.number = []; this.sign = 1; this.rest = 0; @@ -68,8 +88,10 @@ initialNumber.shift(0); } for (index = initialNumber.length - 1; index >= 0; index--) { - if (!this.addDigit(initialNumber[index])) + if (!addDigit(initialNumber[index],this.number)) { + this.number = errors['invalid']; return; + } } } else { initialNumber = initialNumber.toString(); @@ -79,22 +101,21 @@ } for (index = initialNumber.length - 1; index >= 0; index--) { - if (!this.addDigit(parseInt(initialNumber.charAt(index), 10))) { + if (!addDigit(parseInt(initialNumber.charAt(index), 10),this.number)) { + this.number = errors['invalid']; return; } } } - }; - - BigNumber.prototype.addDigit = function(digit) { - if (testDigit(digit)) { - this.number.push(digit); - } else { - this.number = errors['invalid']; - return false; - } + } - return this; + // Statistics on the number of operations performed (useful for checking performance) + BigNumber.stats={ + divs:{count:0,iterations:0}, + mults:{count:0,iterations:0}, + adds:{count:0,iterations:0}, + subs:{count:0,iterations:0}, + pows:{count:0,iterations:0} }; // returns: @@ -210,14 +231,16 @@ // adds two positive BigNumbers BigNumber._add = function(a, b) { var index; + var remainder = 0; var length = Math.max(a.number.length, b.number.length); for (index = 0; index < length || remainder > 0; index++) { - a.number[index] = (remainder += (a.number[index] || 0) + (b.number[index] || 0)) % 10; - remainder = Math.floor(remainder / 10); + a.number[index] = (remainder += (a.number[index] || 0) + (b.number[index] || 0)) % base; + remainder = Math.floor(remainder / base); + BigNumber.stats.adds.iterations++; } - + BigNumber.stats.adds.count++; return a.number; }; @@ -230,7 +253,8 @@ for (index = 0; index < length; index++) { a.number[index] -= (b.number[index] || 0) + remainder; - a.number[index] += (remainder = (a.number[index] < 0) ? 1 : 0) * 10; + a.number[index] += (remainder = (a.number[index] < 0) ? 1 : 0) * base; + BigNumber.stats.subs.iterations++; } // Count the zeroes which will be removed index = 0; @@ -241,6 +265,7 @@ if (index > 0) { a.number.splice(-index); } + BigNumber.stats.subs.count++; return a.number; }; @@ -264,12 +289,14 @@ // multiply the numbers for (index = 0; index < this.number.length; index++) { for (remainder = 0, givenNumberIndex = 0; givenNumberIndex < bigNumber.number.length || remainder > 0; givenNumberIndex++) { - result[index + givenNumberIndex] = (remainder += (result[index + givenNumberIndex] || 0) + this.number[index] * (bigNumber.number[givenNumberIndex] || 0)) % 10; - remainder = Math.floor(remainder / 10); + result[index + givenNumberIndex] = (remainder += (result[index + givenNumberIndex] || 0) + this.number[index] * (bigNumber.number[givenNumberIndex] || 0)) % base; + remainder = Math.floor(remainder / base); + BigNumber.stats.mults.iterations++; } } this.number = result; + BigNumber.stats.mults.count++; return this; }; @@ -284,6 +311,8 @@ var length; var result = []; var rest = BigNumber(); + var nativebigNumber=0; + var nativerest=0; // test if one of the numbers is zero if (bigNumber.isZero()) { @@ -300,13 +329,33 @@ if (bigNumber.number.length === 1 && bigNumber.number[0] === 1) return this; + // If our divisor is less than the base then we can perform the division using native arithmetic + if (bigNumber.lt(base)) nativebigNumber=Number(bigNumber.val()); + for (index = this.number.length - 1; index >= 0; index--) { - rest.multiply(10); - rest.number[0] = this.number[index]; - result[index] = 0; - while (bigNumber.lte(rest)) { - result[index]++; - rest.subtract(bigNumber); + + if (nativebigNumber) { + // If we are dividing by a small number (less than our base) then we can do this division step natively + // which is much faster + nativerest*=base; + nativerest+=this.number[index]; + result[index]=Math.floor(nativerest/nativebigNumber); + nativerest-=(result[index]*nativebigNumber); + } else { + // Otherwise we need to use BigNumber arithmetic + var digit=this.number[index]; + result[index] = 0; + // Go into base 10 mode as per original logic to maintain efficiency + for (var subindex=base/10;subindex>=1;subindex/=10) { + rest.multiply(10); + rest.add(Math.floor(digit/subindex)); + digit=digit%subindex; + while (bigNumber.lte(rest)) { + result[index]+=subindex; + rest.subtract(bigNumber); + BigNumber.stats.divs.iterations++; + } + } } } @@ -318,9 +367,10 @@ if (index > 0) { result.splice(-index); } - - this.rest = rest; + if (nativebigNumber) this.rest=BigNumber(nativerest); + else this.rest = rest; this.number = result; + BigNumber.stats.divs.count++; return this; }; @@ -346,6 +396,7 @@ this.number = [1]; while (number > 0) { + BigNumber.stats.pows.iterations++; if (number % 2 === 1) { this.multiply(bigNumber); number--; @@ -354,7 +405,7 @@ bigNumber.multiply(bigNumber); number = Math.floor(number / 2); } - + BigNumber.stats.pows.count++; return this; }; @@ -385,13 +436,20 @@ } for (index = this.number.length - 1; index >= 0; index--) { - str += this.number[index]; + var digit=this.number[index]; + var group=""; + for (var digits=0;digits 0) ? str : ('-' + str); }; - // Use shorcuts for functions names + // Use shortcuts for functions names BigNumber.prototype.plus = BigNumber.prototype.add; BigNumber.prototype.minus = BigNumber.prototype.subtract; BigNumber.prototype.div = BigNumber.prototype.divide; diff --git a/test/test.js b/test/test.js index 59c92fc..4dca6d7 100644 --- a/test/test.js +++ b/test/test.js @@ -292,4 +292,21 @@ describe('BigNumber.js', function () { BigNumber(5).plus(97).minus(53).plus(434).multiply(5435423).add(321453).multiply(21).div(2).val().should.equal("27569123001"); }); }); + + describe('very big numbers', function () { + it('should test main arithmetic on very large integers', function () { + var a=BigNumber("43216789895328312675790036721362674782378923750950234212368965373910384654"); + var b=BigNumber("85764636478697019837453424264758699685764562524345456576868"); + var x = BigNumber(a); + x.add(b).val().should.equal("43216789895328398440426515418382512235803188509649919976931489719366961522"); + x = BigNumber(a); + x.minus(b).val().should.equal("43216789895328226911153558024342837328954658992250548447806441028453807786"); + x = BigNumber(a); + x.multiply(b).val().should.equal("3706472275149059366968352499608225426876935061337924498923340460238964318011366196170940767807131779369992004353943009853967998583672"); + x = BigNumber(a); + x.divide(b).val().should.equal("503899878431396"); + x = BigNumber(a); + x.divide(b).rest.val().should.equal("13662369696833870468897163678348569097398678349966371836926"); + }); + }); }); From a513f76a75a147cf5f2b18a78ffef9c6f987f69d Mon Sep 17 00:00:00 2001 From: dthwaite Date: Sat, 5 Mar 2016 23:23:11 +0000 Subject: [PATCH 2/3] Made the choice of base completely generic - can set to anything from 2 to 2^25, the latter fully optimising the procedures. Division logic improved yet more for further performance gains --- lib/big-number.js | 186 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 139 insertions(+), 47 deletions(-) diff --git a/lib/big-number.js b/lib/big-number.js index 86dc74f..38c404b 100644 --- a/lib/big-number.js +++ b/lib/big-number.js @@ -11,13 +11,66 @@ !(function() { 'use strict'; - // The higher the base the more efficient this library - // Note that we cannot have a base greater than the square root of the maximum safe integer size - // because we can natively square a "digit" during multiplication - var maxsafeinteger=Number.MAX_SAFE_INTEGER || Math.pow(2,50); - var log10base=Math.floor(Math.log(Math.sqrt(maxsafeinteger))/Math.log(10)); - // The base to which we store the number. - var base=Math.round(Math.pow(10,log10base)); + /** + * The base to which we hold the big number + * + * This must be set to no more than the square root of the maximum native integer that JS can represent accurately + * It can be any number from base 2 up this maximum + * The larger the number the more efficient the big number arithmetic will be + * However, for division, the choice of number also has a significant impact. Infact for maximum efficiency + * the base should have as many common factors as possible. This is maximised by having the base to be a power + * of 2. JS typically holds integers accurately to at least up to 2^50 so a value of 2^25 would be the optimal + * base to use. Setting the base to a prime number will have the worst effect on performance so far as division + * is concerned. + * + * The original version was hard coded with a base of 10. If we set our base to this now it will have similar + * performance, though the division routine has been optimised such that even with this base it could be up + * to twice as fast. + * + * Even with the base set to 2 (i.e. the numbers are held in binary!) the arithmetic is surprisingly fast. + * But nothing like as fast as with a base of 2^25 + * + * @type {number} + */ + var base=Math.pow(2,25); + + /* + * Factorise the base - these then become the most efficient way to segment the division process + * Essentially, the most efficient base is the one with the most factors + * Which means that a base of a power of 2 is the best choice + * + * This is somewhat over the top, but is a good exercise to ensure that division is performed + * with maximum efficiency whatever the base :-) + */ + var factors=(function() { + var result=[]; + var primes=[]; + var factorisingbase=base; + var lastprime=2; + + // Check number for prime (assumes previous primes have been found) + function isprime(prime) { + // Only need to test divisors so far as the square root of the number under test + for (var i=0;i1) { + if (factorisingbase%lastprime==0) { + result.push(lastprime); + factorisingbase=Math.floor(factorisingbase/lastprime); + } + else { + do { + lastprime=(lastprime==2 ? 3 : lastprime+2); + } while (!isprime(lastprime)); + primes.push(lastprime); + } + } + return result; + })(); // Helper function which tests if a given character is a digit var testDigit = function(digit) { @@ -45,22 +98,17 @@ "division by zero": "Invalid Number - Division By Zero" }; - // Constructor function which creates a new BigNumber object - // from an integer, a string, an array or other BigNumber object - function BigNumber(initialNumber) { - var index; - var position=0; - // Method removed from public API and made local within this context - function addDigit(digit,number) { - if (testDigit(digit)) { - if (position%log10base==0) number.push(0); - number[number.length-1]+=(digit*Math.pow(10,position%log10base)); - position++; - return true; - } - return false; - } + /* + * Constructor function which creates a new BigNumber object from an integer, a string, an array or other BigNumber object + * + * IMPORTANT NOTE: Once we allow for any base, the routines to initiate a number from a string of decimal numbers + * and to output a number in decimal form (.val()) now use big number arithmetic. This has potential to get + * us into trouble if we are to build numbers from within internal routines that are already manipulating numbers. It can cause + * a stack overflow. This situation is now avoided by optimising the initialisation to handle more specifically + * (and efficiently) numbers constructed from existing BigNumber objects or native numbers. + */ + function BigNumber(initialNumber) { if (!(this instanceof BigNumber)) { return new BigNumber(initialNumber); @@ -70,25 +118,60 @@ this.sign = 1; this.rest = 0; + // No parameter initialises the number to zero if (!initialNumber) { this.number = [0]; return; } + // A big number parameter initialises this number to the same value + if (initialNumber instanceof BigNumber) { + this.number = initialNumber.number.slice(); + this.sign = initialNumber.sign; + this.rest = initialNumber.rest; + return; + } + + // A native number makes for a fast initialisation of the number + if (typeof initialNumber=="number") { + this.sign=initialNumber<0 ? -1 : 1; + initialNumber=Math.abs(initialNumber); + while (initialNumber>0) { + this.number.push(initialNumber%base); + initialNumber=Math.floor(initialNumber/base); + } + return; + } + + // Otherwise, now create the number from a String or Array + var index; + var multipleoftens=BigNumber(1); + var ten=BigNumber(10); + + // Method removed from public API and made local within this context + function addDigit(digit) { + if (testDigit(digit)) { + this.add(BigNumber(digit).mult(multipleoftens)); + multipleoftens.mult(ten); + return true; + } + return false; + } + // The initial number can be an array or object // e.g. array : [3,2,1], ['+',3,2,1], ['-',3,2,1] // number : 312 // string : '321', '+321', -321' // BigNumber : BigNumber(321) // Every character except the first must be a digit - + var sign=1; if (isArray(initialNumber)) { if (initialNumber.length && initialNumber[0] === '-' || initialNumber[0] === '+') { - this.sign = initialNumber[0] === '+' ? 1 : -1; + sign = initialNumber[0] === '+' ? 1 : -1; initialNumber.shift(0); } for (index = initialNumber.length - 1; index >= 0; index--) { - if (!addDigit(initialNumber[index],this.number)) { + if (!addDigit.call(this,initialNumber[index])) { this.number = errors['invalid']; return; } @@ -96,17 +179,18 @@ } else { initialNumber = initialNumber.toString(); if (initialNumber.charAt(0) === '-' || initialNumber.charAt(0) === '+') { - this.sign = initialNumber.charAt(0) === '+' ? 1 : -1; + sign = initialNumber.charAt(0) === '+' ? 1 : -1; initialNumber = initialNumber.substring(1); } for (index = initialNumber.length - 1; index >= 0; index--) { - if (!addDigit(parseInt(initialNumber.charAt(index), 10),this.number)) { + if (!addDigit.call(this,parseInt(initialNumber.charAt(index), 10))) { this.number = errors['invalid']; return; } } } + this.sign=sign; } // Statistics on the number of operations performed (useful for checking performance) @@ -311,7 +395,7 @@ var length; var result = []; var rest = BigNumber(); - var nativebigNumber=0; + var nativebigNumber=bigNumber.number.length==1 ? bigNumber.number[0] : 0; var nativerest=0; // test if one of the numbers is zero @@ -329,9 +413,6 @@ if (bigNumber.number.length === 1 && bigNumber.number[0] === 1) return this; - // If our divisor is less than the base then we can perform the division using native arithmetic - if (bigNumber.lt(base)) nativebigNumber=Number(bigNumber.val()); - for (index = this.number.length - 1; index >= 0; index--) { if (nativebigNumber) { @@ -345,13 +426,15 @@ // Otherwise we need to use BigNumber arithmetic var digit=this.number[index]; result[index] = 0; - // Go into base 10 mode as per original logic to maintain efficiency - for (var subindex=base/10;subindex>=1;subindex/=10) { - rest.multiply(10); - rest.add(Math.floor(digit/subindex)); - digit=digit%subindex; + // Segment the division by the factors of the base + var divisor=base; + for (var i=0;i= 0; index--) { - var digit=this.number[index]; - var group=""; - for (var digits=0;digits 0) ? str : ('-' + str); }; From 9efbde5973c090013fb97cd2b82ad72069465916 Mon Sep 17 00:00:00 2001 From: dthwaite Date: Thu, 10 Mar 2016 19:56:06 +0000 Subject: [PATCH 3/3] ESLint accommodations --- lib/big-number.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/big-number.js b/lib/big-number.js index 38c404b..92e1fcd 100644 --- a/lib/big-number.js +++ b/lib/big-number.js @@ -51,8 +51,8 @@ // Check number for prime (assumes previous primes have been found) function isprime(prime) { // Only need to test divisors so far as the square root of the number under test - for (var i=0;i0) { @@ -195,11 +195,11 @@ // Statistics on the number of operations performed (useful for checking performance) BigNumber.stats={ - divs:{count:0,iterations:0}, - mults:{count:0,iterations:0}, - adds:{count:0,iterations:0}, - subs:{count:0,iterations:0}, - pows:{count:0,iterations:0} + divs: {count: 0,iterations: 0}, + mults: {count: 0,iterations: 0}, + adds: {count: 0,iterations: 0}, + subs: {count: 0,iterations: 0}, + pows: {count: 0,iterations: 0} }; // returns: @@ -428,7 +428,7 @@ result[index] = 0; // Segment the division by the factors of the base var divisor=base; - for (var i=0;i 0) ? str : ('-' + str); };