Yaffle / BigDecimal

a polyfill for decimal propocal
MIT License
51 stars 7 forks source link

optimize stringification #15

Closed imirkin closed 1 year ago

imirkin commented 1 year ago

This reduces the number of operations in bigDecimalToPlainString. Every little bit counts. With this, toString / toFixed are on par with big.js for a few sample values, and decimal.js's toFixed. Somehow decimal.js toString is still WAY faster.

The tests seem to pass with this, but I didn't do a ton of checking to make sure it was OK.

BigDecimal.BigDecimal x 1,028,157 ops/sec ±0.40% (84 runs sampled)
Decimal#constructor x 434,684 ops/sec ±1.51% (96 runs sampled)
Big#constructor x 681,729 ops/sec ±0.76% (89 runs sampled)
Fastest is BigDecimal.BigDecimal
BigDecimal#toFixed x 645,770 ops/sec ±0.44% (95 runs sampled)
Decimal#toFixed x 621,232 ops/sec ±1.62% (92 runs sampled)
Big#toFixed x 657,078 ops/sec ±1.68% (96 runs sampled)
Fastest is Big#toFixed
BigDecimal#toString x 1,544,764 ops/sec ±2.10% (89 runs sampled)
Decimal#toString x 2,602,547 ops/sec ±0.85% (94 runs sampled)
Big#toString x 1,282,278 ops/sec ±2.24% (90 runs sampled)
Fastest is Decimal#toString

For the values

let vals = [
  "98.987432987",
  "100000000000.00",
  "987344785.36",
  "1",
];
Yaffle commented 1 year ago

@imirkin what browser are you using?

imirkin commented 1 year ago

I've just been doing the microbenchmarks on Node v18.16, because it's easier. The ultimate target is Chrome, I've been hoping that the results carry over.

Looks like the "+" isn't a big perf difference. ZERO_CHAR is a big jump:

Without ZERO_CHAR, from a few runs:

BigDecimal#toFixed x 618,175 ops/sec ±0.63% (95 runs sampled)
BigDecimal#toFixed x 623,340 ops/sec ±0.96% (91 runs sampled)
BigDecimal#toFixed x 609,253 ops/sec ±0.50% (94 runs sampled)
BigDecimal#toFixed x 629,028 ops/sec ±0.43% (94 runs sampled)

With ZERO_CHAR (and the +):

BigDecimal#toFixed x 648,698 ops/sec ±0.59% (97 runs sampled)
BigDecimal#toFixed x 643,930 ops/sec ±0.77% (94 runs sampled)
BigDecimal#toFixed x 638,103 ops/sec ±0.37% (96 runs sampled)
BigDecimal#toFixed x 649,597 ops/sec ±0.43% (93 runs sampled)

I've tried my best to make the setup stable for benchmarking, but apparently still not -- enabled performance governor, forced a single cpu for node running. Even though the results are slightly unstable, they do seem to indicate non-overlapping ranges.

Perhaps the node v8 is older than the Chrome one, and some of this is for naught, but I'm not sure how to get proper benchmarks for this stuff going in Chrome.

Yaffle commented 1 year ago

@imirkin could you test on the latest code from the master?

imirkin commented 1 year ago

Did a couple runs on latest master (e4c3f3ee588ad4244664af353972c6a15bd5cef2):

BigDecimal.BigDecimal x 1,012,460 ops/sec ±1.43% (91 runs sampled)
Decimal#constructor x 438,019 ops/sec ±0.50% (97 runs sampled)
Big#constructor x 677,697 ops/sec ±0.95% (91 runs sampled)
Fastest is BigDecimal.BigDecimal
BigDecimal#toFixed x 843,683 ops/sec ±0.82% (96 runs sampled)
Decimal#toFixed x 626,912 ops/sec ±1.01% (95 runs sampled)
Big#toFixed x 656,388 ops/sec ±1.74% (94 runs sampled)
Fastest is BigDecimal#toFixed
BigDecimal#toString x 2,664,587 ops/sec ±0.60% (94 runs sampled)
Decimal#toString x 2,574,290 ops/sec ±0.52% (95 runs sampled)
Big#toString x 1,353,320 ops/sec ±0.38% (95 runs sampled)
Fastest is BigDecimal#toString

Looks like success!

imirkin commented 1 year ago

BTW, you could still remove one layer of Math.max() and allow "zeros" to be negative. IME that's worth some improvement, but ... up to you.