Skip to content

Optimize internal extract! calls to save on memory allocation #7

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

Merged
merged 2 commits into from
Jun 2, 2025

Conversation

moberegger
Copy link

@moberegger moberegger commented May 30, 2025

Simply optimizes internal calls to extract! to avoid extra memory allocations to splat the *attributes. Both _extract_hash_values and _extract_method_values accept an array for attributes, so no need to re-splat it by calling extract!, which then splats it again.

Solution is to implement a private _extract that just accepts an array, which can be used internally.

A simple way to compare it is with the call DSL, which was just running extract! underneath.

# person = { first_name: 'John', last_name: 'Doe', age: 30, city: 'New York' }

json.(person, :first_name, :last_name, :age, :city)
Warming up --------------------------------------
              before   156.678k i/100ms
               after   176.882k i/100ms
Calculating -------------------------------------
              before      1.674M (± 1.6%) i/s  (597.37 ns/i) -      8.461M in   5.055454s
               after      1.802M (± 2.0%) i/s  (554.82 ns/i) -      9.021M in   5.007138s

Comparison:
               after:  1802399.6 i/s
              before:  1673994.1 i/s - 1.08x  slower
Calculating -------------------------------------
              before   320.000  memsize (     0.000  retained)
                         6.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
               after   240.000  memsize (     0.000  retained)
                         5.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)

Comparison:
               after:        240 allocated
              before:        320 allocated - 1.33x more

This change has no impact in using json.extract! directly.

# person = { first_name: 'John', last_name: 'Doe', age: 30, city: 'New York' }

json.extract! person, :first_name, :last_name, :age, :city
Calculating -------------------------------------
              before   240.000  memsize (     0.000  retained)
                         5.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
               after   240.000  memsize (     0.000  retained)
                         5.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)

Comparison:
              before:        240 allocated
               after:        240 allocated - same

Filed upstream: rails#598

@moberegger moberegger marked this pull request as ready for review May 30, 2025 17:18
@moberegger moberegger changed the title Optimize calls to extract! to save on memory allocation Optimize internal extract! calls to save on memory allocation May 30, 2025
@moberegger moberegger requested review from mscrivo and Insomniak47 May 30, 2025 17:19
lib/jbuilder.rb Outdated
def _extract_hash_values(object, attributes)
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
attributes.each{ |key| _set_value key, _format_keys(object[key]) }
Copy link
Author

@moberegger moberegger May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also using [] instead of fetch. It's a bit faster.

ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
                  []     1.582M i/100ms
               fetch     1.627M i/100ms
Calculating -------------------------------------
                  []     26.015M (± 4.2%) i/s   (38.44 ns/i) -    129.730M in   4.999980s
               fetch     21.967M (± 4.2%) i/s   (45.52 ns/i) -    110.641M in   5.047802s

Comparison:
                  []: 26014972.6 i/s
               fetch: 21966990.7 i/s - 1.18x  slower

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the behaviour identical on missing keys?

Copy link
Author

@moberegger moberegger Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. fetch actually throws if the key doesn't exist, vs returning nil. Not clear if this is the intended behaviour or just happenstance. The coding style for the project seems to prefer fetch. I git blamed back 10+ years to see if there was any reasoning for this, but saw nothing of note before losing interest.

Mixed feelings on this. The current implementation would result in 500 errors if/when this occurs, which is not something I would expect or desire from our tooling. But given my intent here was simply performance - not a behavioural change - I will switch this back to fetch; the overall savings still fall within the margin of error (according to ips) and not worth the risk and drift from the upstream project.

lib/jbuilder.rb Outdated
def _extract_hash_values(object, attributes)
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
attributes.each{ |key| _set_value key, _format_keys(object[key]) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the behaviour identical on missing keys?

@moberegger moberegger merged commit 87f2915 into main Jun 2, 2025
30 checks passed
@moberegger moberegger deleted the moberegger/optimize_extract branch June 13, 2025 18:54
@moberegger moberegger mentioned this pull request Jun 13, 2025
@moberegger moberegger restored the moberegger/optimize_extract branch June 17, 2025 02:05
@moberegger moberegger mentioned this pull request Jun 17, 2025
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.

2 participants