Skip to content

Commit b01d130

Browse files
committed
Optimize JSON.dump argument parsing
`JSON.dump` looks terrible on micro-benchmarks because the way it handles arguments is quite allocation heavy compared to the actual JSON generation work. Profiling the `small hash` benchmarked show 14% of time spent in `Array#compact` and `34%` time spent in `JSON::Ext::GeneratorState.new`. Only `41%` in the actual `generate` function. By micro-optimizing `JSON.dump`, it can look much better: Before: ``` == Encoding small nested array (121 bytes) ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [arm64-darwin23] Warming up -------------------------------------- json 91.687k i/100ms oj 205.309k i/100ms rapidjson 161.648k i/100ms Calculating ------------------------------------- json 941.965k (± 1.4%) i/s (1.06 μs/i) - 4.768M in 5.062573s oj 2.138M (± 1.2%) i/s (467.82 ns/i) - 10.881M in 5.091254s rapidjson 1.678M (± 1.9%) i/s (596.04 ns/i) - 8.406M in 5.011931s Comparison: json: 941964.8 i/s oj: 2137586.5 i/s - 2.27x faster rapidjson: 1677737.1 i/s - 1.78x faster == Encoding small hash (65 bytes) ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [arm64-darwin23] Warming up -------------------------------------- json 141.737k i/100ms oj 676.871k i/100ms rapidjson 373.266k i/100ms Calculating ------------------------------------- json 1.491M (± 1.0%) i/s (670.78 ns/i) - 7.512M in 5.039463s oj 7.226M (± 1.4%) i/s (138.39 ns/i) - 36.551M in 5.059475s rapidjson 3.729M (± 2.2%) i/s (268.15 ns/i) - 18.663M in 5.007182s Comparison: json: 1490798.2 i/s oj: 7225766.2 i/s - 4.85x faster rapidjson: 3729192.2 i/s - 2.50x faster ``` After: ``` == Encoding small nested array (121 bytes) ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [arm64-darwin23] Warming up -------------------------------------- json 156.832k i/100ms oj 209.769k i/100ms rapidjson 162.922k i/100ms Calculating ------------------------------------- json 1.599M (± 2.5%) i/s (625.34 ns/i) - 7.998M in 5.005110s oj 2.137M (± 1.5%) i/s (467.99 ns/i) - 10.698M in 5.007806s rapidjson 1.677M (± 3.5%) i/s (596.31 ns/i) - 8.472M in 5.059515s Comparison: json: 1599141.2 i/s oj: 2136785.3 i/s - 1.34x faster rapidjson: 1676977.2 i/s - same-ish: difference falls within error == Encoding small hash (65 bytes) ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [arm64-darwin23] Warming up -------------------------------------- json 216.464k i/100ms oj 661.328k i/100ms rapidjson 324.434k i/100ms Calculating ------------------------------------- json 2.301M (± 1.7%) i/s (434.57 ns/i) - 11.689M in 5.081278s oj 7.244M (± 1.2%) i/s (138.05 ns/i) - 36.373M in 5.021985s rapidjson 3.323M (± 2.9%) i/s (300.96 ns/i) - 16.871M in 5.081696s Comparison: json: 2301142.2 i/s oj: 7243770.3 i/s - 3.15x faster rapidjson: 3322673.0 i/s - 1.44x faster ``` Now profiles of the `small hash` benchmark show 44% in `generate` and `45%` in `GeneratorState` allocation.
1 parent 02f79ef commit b01d130

File tree

1 file changed

+29
-13
lines changed

1 file changed

+29
-13
lines changed

lib/json/common.rb

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -613,26 +613,42 @@ class << self
613613
# Output:
614614
# {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}
615615
def dump(obj, anIO = nil, limit = nil, kwargs = nil)
616-
io_limit_opt = [anIO, limit, kwargs].compact
617-
kwargs = io_limit_opt.pop if io_limit_opt.last.is_a?(Hash)
618-
anIO, limit = io_limit_opt
619-
if anIO.respond_to?(:to_io)
620-
anIO = anIO.to_io
621-
elsif limit.nil? && !anIO.respond_to?(:write)
622-
anIO, limit = nil, anIO
616+
if kwargs.nil?
617+
if limit.nil?
618+
if anIO.is_a?(Hash)
619+
kwargs = anIO
620+
anIO = nil
621+
end
622+
elsif limit.is_a?(Hash)
623+
kwargs = limit
624+
limit = nil
625+
end
623626
end
627+
628+
unless anIO.nil?
629+
if anIO.respond_to?(:to_io)
630+
anIO = anIO.to_io
631+
elsif limit.nil? && !anIO.respond_to?(:write)
632+
anIO, limit = nil, anIO
633+
end
634+
end
635+
624636
opts = JSON.dump_default_options
625637
opts = opts.merge(:max_nesting => limit) if limit
626638
opts = merge_dump_options(opts, **kwargs) if kwargs
627-
result = generate(obj, opts)
628-
if anIO
639+
640+
result = begin
641+
generate(obj, opts)
642+
rescue JSON::NestingError
643+
raise ArgumentError, "exceed depth limit"
644+
end
645+
646+
if anIO.nil?
647+
result
648+
else
629649
anIO.write result
630650
anIO
631-
else
632-
result
633651
end
634-
rescue JSON::NestingError
635-
raise ArgumentError, "exceed depth limit"
636652
end
637653

638654
# Encodes string using String.encode.

0 commit comments

Comments
 (0)