ruby/lib/json/ext/generator/state.rb
Jean Boussier 0e2ac46584 Optimize Ext::Generator::State#configure
If we assume that most of the time the `opts` hash is small
it's faster to go over the provided keys with a `case` than
to test all possible keys one by one.

Before:

```
== 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
```

After:

```
== Encoding small nested array (121 bytes)
ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
                json   168.087k i/100ms
                  oj   208.872k i/100ms
           rapidjson   149.909k i/100ms
Calculating -------------------------------------
                json      1.761M (± 1.1%) i/s  (567.90 ns/i) -      8.909M in   5.059794s
                  oj      2.144M (± 0.9%) i/s  (466.37 ns/i) -     10.861M in   5.065903s
           rapidjson      1.692M (± 1.7%) i/s  (591.04 ns/i) -      8.545M in   5.051808s

Comparison:
                json:  1760868.2 i/s
                  oj:  2144205.9 i/s - 1.22x  faster
           rapidjson:  1691941.1 i/s - 1.04x  slower

== Encoding small hash (65 bytes)
ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
                json   242.957k i/100ms
                  oj   675.217k i/100ms
           rapidjson   355.040k i/100ms
Calculating -------------------------------------
                json      2.569M (± 1.5%) i/s  (389.22 ns/i) -     12.877M in   5.013095s
                  oj      7.128M (± 2.3%) i/s  (140.30 ns/i) -     35.787M in   5.023594s
           rapidjson      3.656M (± 3.1%) i/s  (273.50 ns/i) -     18.462M in   5.054558s

Comparison:
                json:  2569217.5 i/s
                  oj:  7127705.6 i/s - 2.77x  faster
           rapidjson:  3656285.0 i/s - 1.42x  faster
```
2024-10-17 11:35:33 +00:00

135 lines
4.1 KiB
Ruby

# frozen_string_literal: true
module JSON
module Ext
module Generator
class State
# call-seq: new(opts = {})
#
# Instantiates a new State object, configured by _opts_.
#
# _opts_ can have the following keys:
#
# * *indent*: a string used to indent levels (default: ''),
# * *space*: a string that is put after, a : or , delimiter (default: ''),
# * *space_before*: a string that is put before a : pair delimiter (default: ''),
# * *object_nl*: a string that is put at the end of a JSON object (default: ''),
# * *array_nl*: a string that is put at the end of a JSON array (default: ''),
# * *allow_nan*: true if NaN, Infinity, and -Infinity should be
# generated, otherwise an exception is thrown, if these values are
# encountered. This options defaults to false.
# * *ascii_only*: true if only ASCII characters should be generated. This
# option defaults to false.
# * *buffer_initial_length*: sets the initial length of the generator's
# internal buffer.
def initialize(opts = nil)
if opts && !opts.empty?
configure(opts)
end
end
# call-seq: configure(opts)
#
# Configure this State instance with the Hash _opts_, and return
# itself.
def configure(opts)
unless opts.is_a?(Hash)
if opts.respond_to?(:to_hash)
opts = opts.to_hash
elsif opts.respond_to?(:to_h)
opts = opts.to_h
else
raise TypeError, "can't convert #{opts.class} into Hash"
end
end
opts.each do |key, value|
case key
when :indent
self.indent = value
when :space
self.space = value
when :space_before
self.space_before = value
when :array_nl
self.array_nl = value
when :object_nl
self.object_nl = value
when :max_nesting
self.max_nesting = value || 0
when :depth
self.depth = value
when :buffer_initial_length
self.buffer_initial_length = value
when :allow_nan
self.allow_nan = value
when :ascii_only
self.ascii_only = value
when :script_safe, :escape_slash
self.script_safe = value
when :strict
self.strict = value
end
end
self
end
alias_method :merge, :configure
# call-seq: to_h
#
# Returns the configuration instance variables as a hash, that can be
# passed to the configure method.
def to_h
result = {
indent: indent,
space: space,
space_before: space_before,
object_nl: object_nl,
array_nl: array_nl,
allow_nan: allow_nan?,
ascii_only: ascii_only?,
max_nesting: max_nesting,
script_safe: script_safe?,
strict: strict?,
depth: depth,
buffer_initial_length: buffer_initial_length,
}
instance_variables.each do |iv|
iv = iv.to_s[1..-1]
result[iv.to_sym] = self[iv]
end
result
end
alias_method :to_hash, :to_h
# call-seq: [](name)
#
# Returns the value returned by method +name+.
def [](name)
if respond_to?(name)
__send__(name)
else
instance_variable_get("@#{name}") if
instance_variables.include?("@#{name}".to_sym) # avoid warning
end
end
# call-seq: []=(name, value)
#
# Sets the attribute name to value.
def []=(name, value)
if respond_to?(name_writer = "#{name}=")
__send__ name_writer, value
else
instance_variable_set "@#{name}", value
end
end
end
end
end
end