UNPKG

@hastom/fixed-point

Version:

Light lib for fixed point math made around native bigint

499 lines (435 loc) 17.4 kB
/* eslint-disable no-console,@typescript-eslint/no-var-requires */ 'use strict' const Benchmark = require('benchmark') const { BigNumber } = require('bignumber.js') const dnum = require('dnum') const { FixedPoint, fpFromDecimal, Rounding } = require('../dist') // ─── Helpers ─────────────────────────────────────────────────────────────────── const PRECISION = 18 // common precision (e.g. Ethereum wei) const SCALE = 10n ** 18n // Pre-create values used across suites so construction time is not measured // FixedPoint values const fpA = fpFromDecimal('12345.678901234567890', PRECISION) // ~mid-size const fpB = fpFromDecimal('9876.543210987654321', PRECISION) const fpSmall = fpFromDecimal('0.000000000000000001', PRECISION) const fpLarge = fpFromDecimal('999999999999999999.999999999999999999', PRECISION) const fpNeg = fpFromDecimal('-42.123456789012345678', PRECISION) const fpSqrt = fpFromDecimal('2.000000000000000000', PRECISION) // BigNumber values (configured to match precision) BigNumber.config({ DECIMAL_PLACES: PRECISION, ROUNDING_MODE: BigNumber.ROUND_DOWN }) const bnA = new BigNumber('12345.678901234567890000') const bnB = new BigNumber('9876.543210987654321000') const bnSmall = new BigNumber('0.000000000000000001') const bnLarge = new BigNumber('999999999999999999.999999999999999999') const bnNeg = new BigNumber('-42.123456789012345678') const bnSqrt = new BigNumber('2.000000000000000000') // dnum values ([bigint, decimals] tuples) const dnA = dnum.from('12345.678901234567890', PRECISION) const dnB = dnum.from('9876.543210987654321', PRECISION) const dnSmall = dnum.from('0.000000000000000001', PRECISION) const dnLarge = dnum.from('999999999999999999.999999999999999999', PRECISION) const dnNeg = [-42123456789012345678n, PRECISION] const dnSqrt = dnum.from('2', PRECISION) // Plain BigInt values (scaled by 10^18 to represent the same decimals) // Wrapped in objects to prevent V8 constant-folding, which would make // the BigInt baseline unfairly fast vs any library that reads from objects. const bi = { a: 12345678901234567890n, // 12345.678901234567890 * 10^18 b: 9876543210987654321n, // 9876.543210987654321 * 10^18 small: 1n, large: 999999999999999999999999999999999999n, neg: -42123456789012345678n, } // Sink to prevent V8 dead-code elimination — all benchmark results must be // assigned here so the engine can't optimise away side-effect-free expressions. // eslint-disable-next-line @typescript-eslint/no-unused-vars let sink // ─── Suite runner ────────────────────────────────────────────────────────────── const suites = [] function makeSuite(name, setup) { const suite = new Benchmark.Suite(name) setup(suite) suites.push(suite) } // ─── 1. Creation / Parsing ───────────────────────────────────────────────────── makeSuite('Creation from decimal string', (suite) => { suite .add('FixedPoint fpFromDecimal(str)', () => { sink = fpFromDecimal('12345.678901234567890', 18) }) .add('BigNumber new BigNumber(str)', () => { sink = new BigNumber('12345.678901234567890000') }) .add('dnum dnum.from(str)', () => { sink = dnum.from('12345.678901234567890', 18) }) .add('BigInt BigInt(str)', () => { sink = BigInt('12345678901234567890') }) }) // ─── 2. Addition ─────────────────────────────────────────────────────────────── makeSuite('Addition', (suite) => { suite .add('FixedPoint a.add(b)', () => { sink = fpA.add(fpB) }) .add('dnum add(a, b)', () => { sink = dnum.add(dnA, dnB) }) .add('BigNumber a.plus(b)', () => { sink = bnA.plus(bnB) }) .add('BigInt a + b', () => { sink = bi.a + bi.b }) }) // ─── 3. Subtraction ──────────────────────────────────────────────────────────── makeSuite('Subtraction', (suite) => { suite .add('FixedPoint a.sub(b)', () => { sink = fpA.sub(fpB) }) .add('dnum sub(a, b)', () => { sink = dnum.subtract(dnA, dnB) }) .add('BigNumber a.minus(b)', () => { sink = bnA.minus(bnB) }) .add('BigInt a - b', () => { sink = bi.a - bi.b }) }) // ─── 4. Multiplication ───────────────────────────────────────────────────────── makeSuite('Multiplication', (suite) => { suite .add('FixedPoint a.mul(b)', () => { sink = fpA.mul(fpB) }) .add('dnum mul(a, b)', () => { sink = dnum.multiply(dnA, dnB, 18) }) .add('BigNumber a.times(b)', () => { sink = bnA.times(bnB) }) .add('BigInt a * b / SCALE', () => { sink = (bi.a * bi.b) / SCALE }) }) // ─── 5. Division ─────────────────────────────────────────────────────────────── makeSuite('Division', (suite) => { suite .add('FixedPoint a.div(b)', () => { sink = fpA.div(fpB) }) .add('dnum div(a, b)', () => { sink = dnum.divide(dnA, dnB, 18) }) .add('BigNumber a.div(b)', () => { sink = bnA.div(bnB) }) .add('BigInt (a * SCALE) / b', () => { sink = (bi.a * SCALE) / bi.b }) }) // ─── 6. Comparison (eq) ──────────────────────────────────────────────────────── makeSuite('Comparison (eq)', (suite) => { suite .add('FixedPoint a.eq(b)', () => { sink = fpA.eq(fpB) }) .add('dnum eq(a, b)', () => { sink = dnum.equal(dnA, dnB) }) .add('BigNumber a.eq(b)', () => { sink = bnA.eq(bnB) }) .add('BigInt a === b', () => { sink = bi.a === bi.b }) }) // ─── 7. Comparison (gt) ──────────────────────────────────────────────────────── makeSuite('Comparison (gt)', (suite) => { suite .add('FixedPoint a.gt(b)', () => { sink = fpA.gt(fpB) }) .add('dnum gt(a, b)', () => { sink = dnum.greaterThan(dnA, dnB) }) .add('BigNumber a.gt(b)', () => { sink = bnA.gt(bnB) }) .add('BigInt a > b', () => { sink = bi.a > bi.b }) }) // ─── 8. Comparison (lt) ──────────────────────────────────────────────────────── makeSuite('Comparison (lt)', (suite) => { suite .add('FixedPoint a.lt(b)', () => { sink = fpA.lt(fpB) }) .add('dnum lt(a, b)', () => { sink = dnum.lessThan(dnA, dnB) }) .add('BigNumber a.lt(b)', () => { sink = bnA.lt(bnB) }) .add('BigInt a < b', () => { sink = bi.a < bi.b }) }) // ─── 9. Comparison (gte) ────────────────────────────────────────────────────── makeSuite('Comparison (gte)', (suite) => { suite .add('FixedPoint a.gte(b)', () => { sink = fpA.gte(fpB) }) .add('BigNumber a.gte(b)', () => { sink = bnA.gte(bnB) }) .add('BigInt a >= b', () => { sink = bi.a >= bi.b }) }) // ─── 10. Negation ────────────────────────────────────────────────────────────── makeSuite('Negation', (suite) => { suite .add('FixedPoint a.neg()', () => { sink = fpA.neg() }) .add('BigNumber a.negated()', () => { sink = bnA.negated() }) .add('BigInt -a', () => { sink = -bi.a }) }) // ─── 11. Absolute value ──────────────────────────────────────────────────────── makeSuite('Absolute value', (suite) => { suite .add('FixedPoint a.abs()', () => { sink = fpNeg.abs() }) .add('dnum abs(a)', () => { sink = dnum.abs(dnNeg) }) .add('BigNumber a.abs()', () => { sink = bnNeg.abs() }) .add('BigInt manual abs', () => { sink = bi.neg < 0n ? -bi.neg : bi.neg }) }) // ─── 12. Square root ─────────────────────────────────────────────────────────── makeSuite('Square root', (suite) => { suite .add('FixedPoint a.sqrt()', () => { sink = fpSqrt.sqrt() }) .add('BigNumber a.sqrt()', () => { sink = bnSqrt.sqrt() }) // BigInt has no native sqrt — skip }) // ─── 13. Rounding ────────────────────────────────────────────────────────────── makeSuite('Rounding (half-up)', (suite) => { suite .add('FixedPoint a.round(ROUND_HALF_UP)', () => { sink = fpA.round(Rounding.ROUND_HALF_UP) }) .add('BigNumber a.integerValue(ROUND_HALF_UP)', () => { sink = bnA.integerValue(BigNumber.ROUND_HALF_UP) }) }) makeSuite('Rounding (floor)', (suite) => { suite .add('FixedPoint a.floor()', () => { sink = fpA.floor() }) .add('dnum floor(a)', () => { sink = dnum.floor(dnA) }) .add('BigNumber a.integerValue(ROUND_FLOOR)', () => { sink = bnA.integerValue(BigNumber.ROUND_FLOOR) }) }) makeSuite('Rounding (ceil)', (suite) => { suite .add('FixedPoint a.ceil()', () => { sink = fpA.ceil() }) .add('dnum ceil(a)', () => { sink = dnum.ceil(dnA) }) .add('BigNumber a.integerValue(ROUND_CEIL)', () => { sink = bnA.integerValue(BigNumber.ROUND_CEIL) }) }) // ─── 14. Precision change ────────────────────────────────────────────────────── makeSuite('Precision change (18 → 6)', (suite) => { suite .add('FixedPoint a.toPrecision(6)', () => { sink = fpA.toPrecision(6) }) .add('dnum setDecimals(a, 6)', () => { sink = dnum.setDecimals(dnA, 6) }) .add('BigNumber a.dp(6)', () => { sink = bnA.dp(6) }) .add('BigInt a / 10^12', () => { sink = bi.a / (10n ** 12n) }) }) // ─── 15. To string ───────────────────────────────────────────────────────────── makeSuite('To decimal string', (suite) => { suite .add('FixedPoint a.toDecimalString()', () => { sink = fpA.toDecimalString() }) .add('dnum toString(a)', () => { sink = dnum.toString(dnA) }) .add('BigNumber a.toFixed(18)', () => { sink = bnA.toFixed(18) }) .add('BigInt manual format', () => { const s = bi.a.toString() sink = s.slice(0, -18) + '.' + s.slice(-18) }) }) // ─── 16. isZero / isPositive / isNegative ────────────────────────────────────── makeSuite('Predicates (isZero, isPositive, isNegative)', (suite) => { suite .add('FixedPoint isZero+isPositive+isNegative', () => { sink = fpA.isZero(); sink = fpA.isPositive(); sink = fpA.isNegative() }) .add('BigNumber isZero+isPositive+isNegative', () => { sink = bnA.isZero(); sink = bnA.isPositive(); sink = bnA.isNegative() }) .add('BigInt === 0n, > 0n, < 0n', () => { sink = bi.a === 0n; sink = bi.a > 0n; sink = bi.a < 0n }) }) // ─── 17. Mixed-precision addition ────────────────────────────────────────────── const fpA6 = fpFromDecimal('12345.678901', 6) const fpB18 = fpFromDecimal('9876.543210987654321', 18) const bnA6 = new BigNumber('12345.678901') const bnB18 = new BigNumber('9876.543210987654321000') makeSuite('Addition (mixed precision: 6 + 18)', (suite) => { suite .add('FixedPoint a6.add(b18)', () => { sink = fpA6.add(fpB18) }) .add('BigNumber a6.plus(b18)', () => { sink = bnA6.plus(bnB18) }) .add('BigInt manual scale + add', () => { sink = (bi.a * (10n ** 12n)) + bi.b }) }) // ─── 18. Large-number stress ─────────────────────────────────────────────────── makeSuite('Large number multiplication', (suite) => { suite .add('FixedPoint large.mul(large)', () => { sink = fpLarge.mul(fpLarge) }) .add('dnum mul(large, large)', () => { sink = dnum.multiply(dnLarge, dnLarge, 18) }) .add('BigNumber large.times(large)', () => { sink = bnLarge.times(bnLarge) }) .add('BigInt large * large / SCALE', () => { sink = (bi.large * bi.large) / SCALE }) }) makeSuite('Large number division', (suite) => { suite .add('FixedPoint large.div(a)', () => { sink = fpLarge.div(fpA) }) .add('dnum div(large, a)', () => { sink = dnum.divide(dnLarge, dnA, 18) }) .add('BigNumber large.div(a)', () => { sink = bnLarge.div(bnA) }) .add('BigInt (large * SCALE) / a', () => { sink = (bi.large * SCALE) / bi.a }) }) // ─── 19. Chained operations ──────────────────────────────────────────────────── makeSuite('Chained: (a + b) * a - b', (suite) => { suite .add('FixedPoint', () => { sink = fpA.add(fpB).mul(fpA).sub(fpB) }) .add('dnum', () => { sink = dnum.subtract(dnum.multiply(dnum.add(dnA, dnB), dnA, 18), dnB) }) .add('BigNumber', () => { sink = bnA.plus(bnB).times(bnA).minus(bnB) }) .add('BigInt', () => { sink = (((bi.a + bi.b) * bi.a) / SCALE) - bi.b }) }) // ─── 20. Static min / max ────────────────────────────────────────────────────── makeSuite('Static min / max', (suite) => { suite .add('FixedPoint min + max', () => { sink = FixedPoint.min(fpA, fpB, fpSmall, fpLarge, fpNeg) sink = FixedPoint.max(fpA, fpB, fpSmall, fpLarge, fpNeg) }) .add('BigNumber min + max', () => { sink = BigNumber.min(bnA, bnB, bnSmall, bnLarge, bnNeg) sink = BigNumber.max(bnA, bnB, bnSmall, bnLarge, bnNeg) }) .add('BigInt manual min + max', () => { const vals = [bi.a, bi.b, bi.small, bi.large, bi.neg] sink = vals.reduce((m, e) => (e < m ? e : m)) sink = vals.reduce((m, e) => (e > m ? e : m)) }) }) // ─── Run all suites ──────────────────────────────────────────────────────────── function pad(str, len) { return str + ' '.repeat(Math.max(0, len - str.length)) } console.log('='.repeat(80)) console.log(' Fixed-Point Library Benchmarks') console.log(' FixedPoint vs dnum vs BigNumber.js vs plain BigInt') console.log(' Precision: 18 decimals') console.log(' Node.js ' + process.version) console.log('='.repeat(80)) console.log() let idx = 0 function runNext() { if (idx >= suites.length) { console.log('='.repeat(80)) console.log(' All benchmarks completed.') console.log('='.repeat(80)) return } const suite = suites[idx++] console.log(`── ${suite.name} ${'─'.repeat(Math.max(0, 76 - (suite.name || '').length))}`) console.log() suite .on('cycle', (event) => { const bench = event.target const hz = bench.hz ? bench.hz.toLocaleString('en', { maximumFractionDigits: 0 }) : '?' const rme = bench.stats ? bench.stats.rme.toFixed(2) : '?' const name = bench.name || '' console.log(` ${pad(name, 44)} ${pad(hz, 16)} ops/sec (\xb1${rme}%)`) }) .on('complete', function () { const fastest = this.filter('fastest').map('name') console.log() console.log(` => Fastest: ${fastest.join(', ')}`) console.log() runNext() }) .run({ async: true }) } runNext()