Skip to content

Optimize method_missing via alias_method #596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

moberegger
Copy link

@moberegger moberegger commented May 29, 2025

jbuilder provides a nice DSL where something like

json.foo :foo

is simply sugar for the set! API

json.set! :foo, :foo

The sugar doesn't come for free, though; set! performs substantially better in both CPU and memory. Consider the following benchmark:

json = Jbuilder.new
name = 'John Doe'

Benchmark.ips do |x|
  x.report('method_missing') { json.name name }
  x.report('set!') { json.set! :name, name }
  x.compare!
end

Benchmark.memory do |x|
  x.report('method_missing') { json.name name }
  x.report('set!') { json.set! :name, name }
  x.compare!
end
ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
      method_missing   293.497k i/100ms
                set!   481.601k i/100ms
Calculating -------------------------------------
      method_missing      3.694M (± 2.1%) i/s  (270.70 ns/i) -     18.490M in   5.007609s
                set!      5.426M (± 1.6%) i/s  (184.31 ns/i) -     27.451M in   5.060849s

Comparison:
                set!:  5425603.3 i/s
      method_missing:  3694079.2 i/s - 1.47x  slower
Calculating -------------------------------------
      method_missing   120.000  memsize (     0.000  retained)
                         3.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)
                set!    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)

Comparison:
                set!:         80 allocated
      method_missing:        120 allocated - 1.50x more

It's around 1.5x slower to use the DSL over set!! The main culprit for this is the overhead inherent in method_missing. The DSL also results in an additional memory allocation to allocate for *args, which is just passed off to set. This is something that adds up for larger responses.

What this PR does is simply defines method_missing as an alias to set!. This saves on some busy work that existed in the method originally (ex: it was doing a ::Kernel.block_given? check, even though set! already does this.). It also saves on the memory allocation for *args.

The before after comparison for the method_missing implementation

ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before   350.380k i/100ms
               after   431.436k i/100ms
Calculating -------------------------------------
              before      3.699M (± 1.4%) i/s  (270.36 ns/i) -     18.570M in   5.021595s
               after      4.727M (± 1.6%) i/s  (211.56 ns/i) -     23.729M in   5.021313s

Comparison:
               after:  4726868.4 i/s
              before:  3698805.2 i/s - 1.28x  slower
Calculating -------------------------------------
              before   120.000  memsize (     0.000  retained)
                         3.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)
               after    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)

Comparison:
               after:         80 allocated
              before:        120 allocated - 1.50x more

Final comparison between method_missing and set!

ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
      method_missing   428.508k i/100ms
                set!   474.806k i/100ms
Calculating -------------------------------------
      method_missing      4.725M (± 1.3%) i/s  (211.63 ns/i) -     23.996M in   5.079342s
                set!      5.200M (± 1.5%) i/s  (192.30 ns/i) -     26.114M in   5.022865s

Comparison:
                set!:  5200304.9 i/s
      method_missing:  4725192.8 i/s - 1.10x  slower
Calculating -------------------------------------
      method_missing    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)
                set!    80.000  memsize (     0.000  retained)
                         2.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)

Comparison:
      method_missing:         80 allocated
                set!:         80 allocated - same

This gets the DSL to be nearly on par with a regular set! call.

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