Skip to content

Conversation

moberegger
Copy link

@moberegger moberegger commented Sep 25, 2025

Summary of changes:

  • Similar to what was done with _extract in (link PR), private _array and _set have been added to save in an additional memory allocation resulting from the extra *args splat that would happen when JbuilderTemplate#array! and JbuilderTemplate#set!'s called back up to super. With the new setup, the splat happens a single time.
  • Calls to ::Kernel.block_given? showed up as hotspots on our profiling. These have been replaced with a simple if block check, which performs faster. Normally you wouldn't see a difference with block_given?, but since Jbuilder is a BasicObject, ::Kernel.block_given? had to be used, and the extra module resolution apparently has some overhead.
  • Calls to one? showed up as hotspots in our profiling, which I believe it's an O(n) operation. The args.one? guards have been removed, as they appeared to not actually be necessary. There were guards like if args.one? && _partial_options?(options), and I presume the one? was intended to short circuit the checks against the options hash, but it's actually faster to just forgo the one? call. If the intent was to check if only one argument was provided, this isn't actually doing that; it is actually checking if one truthy argument was provided.

Some benchmarks against JbuilderTemplate comparing main (before) with this branch (after):

set!

# Simplest benchmark to exercise the changes
json.set! :foo, :bar
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before   446.710k i/100ms
               after   540.848k i/100ms
Calculating -------------------------------------
              before      5.251M (± 2.3%) i/s  (190.45 ns/i) -     26.356M in   5.022246s
               after      6.444M (± 6.9%) i/s  (155.17 ns/i) -     32.451M in   5.071314s
Comparison:
               after:  6444432.8 i/s
              before:  5250641.1 i/s - 1.23x  slower


Calculating -------------------------------------
              before    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
               after    40.000  memsize (     0.000  retained)
                         1.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
Comparison:
               after:         40 allocated
              before:         80 allocated - 2.00x more
# Where...
array = [1, 2, 3]
# Additionally benchmarks the underlying call to _array vs array!
json.set! :foo, array do |item|
  json.set! :bar, item
end
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before    73.584k i/100ms
               after    89.058k i/100ms
Calculating -------------------------------------
              before    734.510k (± 6.7%) i/s    (1.36 μs/i) -      3.679M in   5.041629s
               after    937.514k (± 4.7%) i/s    (1.07 μs/i) -      4.720M in   5.048528s
Comparison:
               after:   937513.6 i/s
              before:   734509.8 i/s - 1.28x  slower


Calculating -------------------------------------
              before     1.000k memsize (   520.000  retained)
                        16.000  objects (     4.000  retained)
                         0.000  strings (     0.000  retained)
               after   760.000  memsize (   520.000  retained)
                        10.000  objects (     4.000  retained)
                         0.000  strings (     0.000  retained)
Comparison:
               after:        760 allocated
              before:       1000 allocated - 1.32x more
# Where...
post = Post.new(1, 'Post 1', 'This is the body')
json.set! :post, post, :id, :title, :body
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before    96.148k i/100ms
               after   107.217k i/100ms
Calculating -------------------------------------
              before    993.402k (± 2.1%) i/s    (1.01 μs/i) -      5.000M in   5.035113s
               after      1.041M (± 9.9%) i/s  (960.98 ns/i) -      5.146M in   5.031288s
Comparison:
               after:  1040609.6 i/s
              before:   993401.9 i/s - same-ish: difference falls within error

Calculating -------------------------------------
              before   440.000  memsize (   160.000  retained)
                         3.000  objects (     1.000  retained)
                         0.000  strings (     0.000  retained)
               after   240.000  memsize (   160.000  retained)
                         2.000  objects (     1.000  retained)
                         0.000  strings (     0.000  retained)
Comparison:
               after:        240 allocated
              before:        440 allocated - 1.83x more

array!

# Where...
array = [1, 2, 3]
json.array! array
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before     1.643k i/100ms
               after     2.235k i/100ms
Calculating -------------------------------------
              before      7.167k (±20.9%) i/s  (139.53 μs/i) -     36.146k in   5.287060s
               after      7.384k (±24.2%) i/s  (135.44 μs/i) -     35.760k in   5.134774s
Comparison:
               after:     7383.5 i/s
              before:     7166.9 i/s - same-ish: difference falls within error


Calculating -------------------------------------
              before    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
               after    40.000  memsize (     0.000  retained)
                         1.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
               after:         40 allocated
              before:         80 allocated - 2.00x more

via method_missing

json.foo :bar
Warming up --------------------------------------
              before   327.202k i/100ms
               after   504.570k i/100ms
Calculating -------------------------------------
              before      3.800M (± 3.1%) i/s  (263.19 ns/i) -     18.978M in   4.999800s
               after      5.864M (± 1.5%) i/s  (170.52 ns/i) -     29.770M in   5.077434s
Comparison:
               after:  5864408.4 i/s
              before:  3799593.4 i/s - 1.54x  slower


Calculating -------------------------------------
              before    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
               after    40.000  memsize (     0.000  retained)
                         1.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
Comparison:
               after:         40 allocated
              before:         80 allocated - 2.00x more

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant