Skip to content

Luke/bignum optimisation #1677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"clean": "rm -rf lib",
"test": "mocha -r ts-node/register tests/**/*.ts",
"test:inspect": "mocha --inspect-brk -r ts-node/register tests/**/*.ts",
"test:bignum": "mocha -r ts-node/register tests/bn/**/*.ts",
"test:bignum": "mocha -r ts-node/register tests/bn/test.ts",
"test:bignum-perf": "mocha -r ts-node/register tests/bn/performance.test.ts",
"test:ci": "mocha -r ts-node/register tests/ci/**/*.ts",
"patch-and-pub": "npm version patch --force && npm publish",
"prettify": "prettier --check './src/***/*.ts'",
Expand Down
129 changes: 74 additions & 55 deletions sdk/src/factory/bigNum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,64 +216,76 @@ export class BigNum {
'Tried to print a BN with precision lower than zero'
);

const isNeg = this.isNeg();
const plainString = this.abs().toString();
const precisionNum = this.precision.toNumber();

// Early return for zero precision
if (precisionNum === 0) {
return this.val.toString();
}

// make a string with at least the precisionNum number of zeroes
let printString = [
...Array(this.precision.toNumber()).fill(0),
...plainString.split(''),
].join('');
const isNeg = this.val.isNeg();
const plainString = this.val.abs().toString();

// inject decimal
printString =
printString.substring(0, printString.length - precisionNum) +
BigNum.delim +
printString.substring(printString.length - precisionNum);
// Build padded string with leading zeros
let printString = '0'.repeat(precisionNum) + plainString;

// remove leading zeroes
printString = printString.replace(/^0+/, '');
// Insert decimal point
const insertPos = printString.length - precisionNum;
printString = printString.substring(0, insertPos) + BigNum.delim + printString.substring(insertPos);

// add zero if leading delim
if (printString[0] === BigNum.delim) printString = `0${printString}`;
// Remove leading zeros but keep at least one digit before decimal
printString = printString.replace(/^0+(?=\d)/, '') || '0';

// Add minus if negative
if (isNeg) printString = `-${printString}`;
// Handle case where we have leading decimal after zero removal
if (printString[0] === BigNum.delim) {
printString = '0' + printString;
}

// remove trailing delim
if (printString[printString.length - 1] === BigNum.delim)
printString = printString.slice(0, printString.length - 1);
// Remove trailing decimal if present
if (printString.endsWith(BigNum.delim)) {
printString = printString.slice(0, -1);
}

return printString;
return isNeg ? `-${printString}` : printString;
}

public prettyPrint(
useTradePrecision?: boolean,
precisionOverride?: number
): string {
const [leftSide, rightSide] = this.printShort(
useTradePrecision,
precisionOverride
).split(BigNum.delim);

let formattedLeftSide = leftSide;
const printVal = this.printShort(useTradePrecision, precisionOverride);
const delimIndex = printVal.indexOf(BigNum.delim);

let leftSide: string;
let rightSide: string;

if (delimIndex === -1) {
leftSide = printVal;
rightSide = '';
} else {
leftSide = printVal.substring(0, delimIndex);
rightSide = printVal.substring(delimIndex + 1);
}

const isNeg = formattedLeftSide.includes('-');
const isNeg = leftSide.startsWith('-');
if (isNeg) {
formattedLeftSide = formattedLeftSide.replace('-', '');
leftSide = leftSide.substring(1);
}

let index = formattedLeftSide.length - 3;

while (index >= 1) {
const formattedLeftSideArray = formattedLeftSide.split('');

formattedLeftSideArray.splice(index, 0, BigNum.spacer);

formattedLeftSide = formattedLeftSideArray.join('');
if (leftSide.length <= 3) {
return `${isNeg ? '-' : ''}${leftSide}${
rightSide ? `${BigNum.delim}${rightSide}` : ''
}`;
}

index -= 3;
let formattedLeftSide = '';
const len = leftSide.length;

for (let i = 0; i < len; i++) {
if (i > 0 && (len - i) % 3 === 0) {
formattedLeftSide += BigNum.spacer;
}
formattedLeftSide += leftSide[i];
}

return `${isNeg ? '-' : ''}${formattedLeftSide}${
Expand Down Expand Up @@ -317,15 +329,24 @@ export class BigNum {
}

const printString = this.print();
const delimIndex = printString.indexOf(BigNum.delim);

let leftSide: string;
let rightSide: string;

if (delimIndex === -1) {
leftSide = printString;
rightSide = '';
} else {
leftSide = printString.substring(0, delimIndex);
rightSide = printString.substring(delimIndex + 1);
}

const [leftSide, rightSide] = printString.split(BigNum.delim);

const filledRightSide = [
...(rightSide ?? '').slice(0, fixedPrecision),
...Array(fixedPrecision).fill('0'),
]
.slice(0, fixedPrecision)
.join('');
const truncatedRightSide = rightSide.length > fixedPrecision
? rightSide.substring(0, fixedPrecision)
: rightSide;

const filledRightSide = truncatedRightSide.padEnd(fixedPrecision, '0');

return `${leftSide}${BigNum.delim}${filledRightSide}`;
}
Expand Down Expand Up @@ -613,14 +634,12 @@ export class BigNum {

// Must convert any non-US delimiters and spacers to US format before using parseFloat
if (BigNum.delim !== '.' || BigNum.spacer !== ',') {
printedValue = printedValue
.split('')
.map((char) => {
if (char === BigNum.delim) return '.';
if (char === BigNum.spacer) return ',';
return char;
})
.join('');
if (BigNum.delim !== '.') {
printedValue = printedValue.replace(new RegExp('\\' + BigNum.delim, 'g'), '.');
}
if (BigNum.spacer !== ',') {
printedValue = printedValue.replace(new RegExp('\\' + BigNum.spacer, 'g'), ',');
}
}

return parseFloat(printedValue);
Expand Down
130 changes: 130 additions & 0 deletions sdk/tests/bn/performance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { BN } from '../../src/index';
import { BigNum } from '../../src/factory/bigNum';

const BIGNUM_VERSION = 'v1.3';

describe('BigNum Performance Tests', () => {
const testValues = [
{ val: '123', precision: 0 },
{ val: '1234567', precision: 0 },
{ val: '123456789', precision: 3 },
{ val: '1000000000123', precision: 6 },
{ val: '999999999999999', precision: 9 },
{ val: '123456789012345', precision: 12 },
];

const createTestBigNums = () => {
return testValues.map(({ val, precision }) =>
BigNum.from(new BN(val), new BN(precision))
);
};

const performanceTest = (name: string, fn: () => void, iterations: number) => {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn();
}
const end = performance.now();
const duration = end - start;
console.log(`[${BIGNUM_VERSION}] ${name}: ${duration.toFixed(2)}ms (${iterations} iterations)`);
return duration;
};

it('should benchmark print() method', () => {
const bigNums = createTestBigNums();

performanceTest('print() method', () => {
bigNums.forEach(bn => bn.print());
}, 100000);
});

it('should benchmark prettyPrint() method', () => {
const bigNums = createTestBigNums();

performanceTest('prettyPrint() method', () => {
bigNums.forEach(bn => bn.prettyPrint());
}, 100000);
});

it('should benchmark toFixed() method', () => {
const bigNums = createTestBigNums();

performanceTest('toFixed() method', () => {
bigNums.forEach(bn => bn.toFixed(4));
}, 100000);
});

it('should benchmark toNum() method', () => {
const bigNums = createTestBigNums();

performanceTest('toNum() method', () => {
bigNums.forEach(bn => bn.toNum());
}, 100000);
});

it('should benchmark printShort() method', () => {
const bigNums = createTestBigNums();

performanceTest('printShort() method', () => {
bigNums.forEach(bn => bn.printShort());
}, 100000);
});

it('should stress test with large numbers', () => {
const largeBigNums = [
BigNum.from(new BN('999999999999999999999999999999'), new BN(15)),
BigNum.from(new BN('123456789012345678901234567890'), new BN(18)),
BigNum.from(new BN('987654321098765432109876543210'), new BN(20)),
];

performanceTest('Large number print()', () => {
largeBigNums.forEach(bn => bn.print());
}, 50000);

performanceTest('Large number prettyPrint()', () => {
largeBigNums.forEach(bn => bn.prettyPrint());
}, 50000);

performanceTest('Large number toFixed()', () => {
largeBigNums.forEach(bn => bn.toFixed(4));
}, 50000);
});

it('should test repeated operations on single instance', () => {
const bigNum = BigNum.from(new BN('123456789'), new BN(6));

performanceTest('Repeated print() operations', () => {
for (let i = 0; i < 1000; i++) {
bigNum.print();
}
}, 1000);

performanceTest('Repeated prettyPrint() operations', () => {
for (let i = 0; i < 1000; i++) {
bigNum.prettyPrint();
}
}, 1000);

performanceTest('Repeated toFixed() operations', () => {
for (let i = 0; i < 1000; i++) {
bigNum.toFixed(2);
}
}, 1000);
});

it('should benchmark locale-specific formatting', () => {
const bigNums = createTestBigNums();

performanceTest('toNum() with default locale', () => {
bigNums.forEach(bn => bn.toNum());
}, 100000);

BigNum.setLocale('de-DE');

performanceTest('toNum() with custom locale', () => {
bigNums.forEach(bn => bn.toNum());
}, 100000);

BigNum.setLocale('en-US');
});
});
Loading