davidmarkclements / fast-safe-stringify

Safely and quickly serialize JavaScript objects
MIT License
348 stars 27 forks source link

feature: improve stable stringify #28

Closed BridgeAR closed 6 years ago

BridgeAR commented 6 years ago

This is a performance improvement in many cases. For big / deep objects it will be a minor performance penalty in the case no options are used. If options are used (i.e. replacer or spacer) it will always be faster.

The implementation is side effect free, so it now works 1-to-1 as JSON.stringify.

For performance reasons this is a lot of duplicated code... I do not see a much nicer implementation right now that could do the same though :/.

No options:

old:   simple object x 916,356 ops/sec ±0.95% (95 runs sampled)
old:   circular      x 367,928 ops/sec ±1.00% (94 runs sampled)
old:   deep          x 16,704 ops/sec ±1.01% (91 runs sampled)
old:   deep circular x 16,234 ops/sec ±1.44% (91 runs sampled)

fast-safe-stringify:   simple object x 1,389,123 ops/sec ±0.63% (96 runs sampled)
fast-safe-stringify:   circular      x 477,570 ops/sec ±1.05% (90 runs sampled)
fast-safe-stringify:   deep          x 15,363 ops/sec ±1.00% (91 runs sampled)
fast-safe-stringify:   deep circular x 15,175 ops/sec ±0.90% (95 runs sampled)

Spacer

old:   simple object x 966,154 ops/sec ±1.09% (91 runs sampled)
old:   circular      x 295,570 ops/sec ±0.55% (95 runs sampled)
old:   deep          x 13,203 ops/sec ±0.81% (95 runs sampled)
old:   deep circular x 12,707 ops/sec ±1.10% (94 runs sampled)

fast-safe-stringify:   simple object x 1,214,236 ops/sec ±1.08% (93 runs sampled)
fast-safe-stringify:   circular      x 428,453 ops/sec ±0.82% (88 runs sampled)
fast-safe-stringify:   deep          x 14,071 ops/sec ±0.78% (92 runs sampled)
fast-safe-stringify:   deep circular x 13,856 ops/sec ±0.98% (92 runs sampled)

Replacer

old:   simple object x 939,815 ops/sec ±2.50% (90 runs sampled)
old:   circular      x 210,840 ops/sec ±0.72% (91 runs sampled)
old:   deep          x 12,708 ops/sec ±0.98% (91 runs sampled)
old:   deep circular x 12,358 ops/sec ±0.81% (90 runs sampled)

fast-safe-stringify:   simple object x 1,166,188 ops/sec ±0.71% (92 runs sampled)
fast-safe-stringify:   circular      x 445,552 ops/sec ±1.10% (92 runs sampled)
fast-safe-stringify:   deep          x 14,685 ops/sec ±1.04% (95 runs sampled)
fast-safe-stringify:   deep circular x 14,540 ops/sec ±0.89% (94 runs sampled)

Both

old:   simple object x 957,466 ops/sec ±1.20% (89 runs sampled)
old:   circular      x 194,075 ops/sec ±0.66% (93 runs sampled)
old:   deep          x 10,923 ops/sec ±0.86% (89 runs sampled)
old:   deep circular x 10,514 ops/sec ±1.26% (92 runs sampled)

fast-safe-stringify:   simple object x 1,117,194 ops/sec ±0.94% (92 runs sampled)
fast-safe-stringify:   circular      x 407,977 ops/sec ±1.06% (90 runs sampled)
fast-safe-stringify:   deep          x 13,700 ops/sec ±1.26% (93 runs sampled)
fast-safe-stringify:   deep circular x 13,570 ops/sec ±1.18% (95 runs sampled)
davidmarkclements commented 6 years ago

so... from what I can see you've reimplemented JSON.stringify

First to be clear, let me just say: awesome.

Before we can go forward though, we need to benchmark this excessively for as many scenarios as possible vs JSON.stringify

We know from past experiments that getting on par with JSON.stringify was pretty much impossible because of the TCO trick they use in the native implementation – so we need to be extremely thorough in establishing this is a performance improvement across the board

What we don't want is people using fast-safe-stringify having a noticeable slow-down in their use cases

@mcollina what are your thoughts?

BridgeAR commented 6 years ago

I understand the concerns about it. My main reason was to see how well it performs and i wanted to overcome the negative implementation parts.

I only implemented this for the stable version because the regular one is faster the way it is right now. With the added overhead in the stable version it makes sense to have this implementation though.

What we can do is to add lots of tests to make sure it works fine.

mcollina commented 6 years ago

How about we do: fast-stable-stringify? I prefer small modules with a single purpose.

BridgeAR commented 6 years ago

The original intention against it was that there are multiple out there (all with "fast" in their name...) but I will go ahead and create a new package when I am at home. We can reference to it :)

BridgeAR commented 6 years ago

Ok, I just created https://github.com/BridgeAR/safe-stable-stringify

I fixed bugs, rewrote some more stuff and improved the performance further. Here a comparison to the current solution to the new one:

Old:

simple:   simple object x 950,953 ops/sec ±1.21% (91 runs sampled)
simple:   circular      x 369,246 ops/sec ±0.91% (89 runs sampled)
simple:   deep          x 15,143 ops/sec ±1.17% (88 runs sampled)
simple:   deep circular x 13,962 ops/sec ±3.04% (87 runs sampled)

replacer:   simple object x 465,644 ops/sec ±1.73% (86 runs sampled)
replacer:   circular      x 213,329 ops/sec ±0.75% (92 runs sampled)
replacer:   deep          x 11,723 ops/sec ±1.56% (92 runs sampled)
replacer:   deep circular x 11,364 ops/sec ±1.82% (90 runs sampled)

array:   simple object x 714,791 ops/sec ±0.31% (95 runs sampled)
array:   circular      x 446,081 ops/sec ±1.69% (88 runs sampled)
array:   deep          x 25,168 ops/sec ±0.61% (93 runs sampled)
array:   deep circular x 23,010 ops/sec ±3.50% (85 runs sampled)

full replacer:   simple object x 395,189 ops/sec ±0.95% (91 runs sampled)
full replacer:   circular      x 195,702 ops/sec ±1.14% (89 runs sampled)
full replacer:   deep          x 9,581 ops/sec ±1.11% (91 runs sampled)
full replacer:   deep circular x 9,901 ops/sec ±1.26% (93 runs sampled)

full array:   simple object x 616,108 ops/sec ±0.88% (96 runs sampled)
full array:   circular      x 410,485 ops/sec ±1.24% (90 runs sampled)
full array:   deep          x 24,772 ops/sec ±1.32% (89 runs sampled)
full array:   deep circular x 23,860 ops/sec ±1.80% (94 runs sampled)

indentation:   simple object x 754,916 ops/sec ±1.38% (94 runs sampled)
indentation:   circular      x 309,293 ops/sec ±1.12% (96 runs sampled)
indentation:   deep          x 12,311 ops/sec ±1.46% (94 runs sampled)
indentation:   deep circular x 12,174 ops/sec ±1.88% (93 runs sampled)

New

simple:   simple object x 1,733,045 ops/sec ±1.82% (86 runs sampled)
simple:   circular      x 717,021 ops/sec ±0.78% (91 runs sampled)
simple:   deep          x 17,674 ops/sec ±0.77% (94 runs sampled)
simple:   deep circular x 17,396 ops/sec ±0.70% (93 runs sampled)

replacer:   simple object x 1,126,942 ops/sec ±2.22% (91 runs sampled)
replacer:   circular      x 541,243 ops/sec ±0.87% (94 runs sampled)
replacer:   deep          x 17,229 ops/sec ±0.90% (94 runs sampled)
replacer:   deep circular x 16,948 ops/sec ±0.86% (97 runs sampled)

array:   simple object x 1,470,751 ops/sec ±0.84% (95 runs sampled)
array:   circular      x 1,360,269 ops/sec ±2.94% (91 runs sampled)
array:   deep          x 1,289,785 ops/sec ±2.82% (87 runs sampled)
array:   deep circular x 1,400,577 ops/sec ±1.00% (92 runs sampled)

full replacer:   simple object x 993,352 ops/sec ±1.49% (92 runs sampled)
full replacer:   circular      x 467,264 ops/sec ±0.68% (95 runs sampled)
full replacer:   deep          x 14,855 ops/sec ±0.82% (91 runs sampled)
full replacer:   deep circular x 14,608 ops/sec ±1.68% (95 runs sampled)

full array:   simple object x 1,233,430 ops/sec ±0.58% (92 runs sampled)
full array:   circular      x 1,205,360 ops/sec ±1.33% (90 runs sampled)
full array:   deep          x 1,175,758 ops/sec ±0.63% (92 runs sampled)
full array:   deep circular x 1,171,813 ops/sec ±1.08% (92 runs sampled)

indentation:   simple object x 1,385,853 ops/sec ±2.18% (94 runs sampled)
indentation:   circular      x 598,650 ops/sec ±1.26% (92 runs sampled)
indentation:   deep          x 16,060 ops/sec ±0.76% (93 runs sampled)
indentation:   deep circular x 15,784 ops/sec ±1.31% (95 runs sampled)