@hastom/fixed-point
Version:
Light lib for fixed point math made around native bigint
499 lines (435 loc) • 17.4 kB
JavaScript
/* eslint-disable no-console,@typescript-eslint/no-var-requires */
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()