-
Notifications
You must be signed in to change notification settings - Fork 444
Optimize array!
and set!
#604
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
base: main
Are you sure you want to change the base?
Optimize array!
and set!
#604
Conversation
|
||
BLANK = Blank.new | ||
BLANK = Blank.new.freeze | ||
EMPTY_ARRAY = [].freeze |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were quite a few spots allocating a new empty array when dealing with an empty collection. Figured it was worthwhile to re-use the same Array
instance. This does assume that no call sites attempt to mutate the output from Jbuilder#target!
.
if _blank?(value) | ||
# json.comments { ... } | ||
# { "comments": ... } | ||
_merge_block key, &block | ||
else | ||
# json.comments @post.comments { |comment| ... } | ||
# { "comments": [ { ... }, { ... } ] } | ||
_scope { _array value, &block } | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition used to be the inverse, with a if !_blank?
. I think this improves control flow, and it saves a tiny bit in processing.
if _blank?(value) | ||
# json.comments { ... } | ||
# { "comments": ... } | ||
_merge_block key, &block |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used to be
_merge_block(key){ yield self }
def call(object, *attributes, &block) | ||
if ::Kernel.block_given? | ||
array! object, &block | ||
if block |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A quick micro benchmark to show the difference between these
module Foo
def self.if_block_given?
block_given? ? true : false
end
def self.if_kernel_block_given?
::Kernel.block_given? ? true : false
end
def self.if_block?(&block)
block ? true : false
end
end
Benchmark.ips do |x|
x.report('block_given?') { Foo.if_block_given? }
x.report('Kernel.block_given?') { Foo.if_kernel_block_given? }
x.report('block?') { Foo.if_block? }
x.compare!
end
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
block_given? 2.450M i/100ms
Kernel.block_given? 2.270M i/100ms
block? 2.516M i/100ms
Calculating -------------------------------------
block_given? 44.851M (± 1.9%) i/s (22.30 ns/i) - 225.368M in 5.026585s
Kernel.block_given? 39.434M (± 1.5%) i/s (25.36 ns/i) - 197.469M in 5.008676s
block? 45.037M (± 2.8%) i/s (22.20 ns/i) - 226.436M in 5.032194s
Comparison:
block?: 45036992.5 i/s
block_given?: 44851312.9 i/s - same-ish: difference falls within error
Kernel.block_given?: 39434092.2 i/s - 1.14x slower
Some small adjustments to reduce latency and memory allocations on calls to
set!
andarray!
. A summary of changes:_extract
in (link PR), private_array
and_set
methods have been added to save on a memory allocation resulting from the extra*args
splat that would happen whenJbuilderTemplate#array!
andJbuilderTemplate#set!
's called back up tosuper
. With the new setup, the splat happens a single time.::Kernel.block_given?
showed up as hotspots in our profiling. These have been replaced with a simpleif block
check, which performs a little bit faster. Normally you wouldn't see a difference withblock_given?
, but sinceJbuilder
is aBasicObject
,::Kernel.block_given?
had to be used, and the extra module resolution apparently has some overhead.one?
showed up as hotspots in our profiling, which I believe is an O(n) operation. Theargs.one?
guards have been removed, as they appeared to not actually be necessary. There were guards likeif args.one? && _partial_options?(options)
, and I presume theone?
was intended to short circuit the checks against theoptions
hash, but it's actually faster to just forgo theone?
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
comparingmain
(before) with this branch (after):set!
The simplest benchmark to exercise the changes under
set!
.Simple benchmark when
set!
is provided a collection. Additionally benchmarks the underlying call to_array
A benchmark for when
set!
is provided a list of attributes. Intent here to measure theargs.one?
change. Was hoping to see a larger improvement in IPS.array!
A simple benchmark for
array!
to exercise the changes. Was hoping for a larger improvement in IPS. This does still save on memory, though.A benchmark for when
array!
is provided a list of attributes. Intent here to measure theargs.one?
change. Was hoping to see a larger improvement in IPS.via
method_missing
To showcase that the optimizations impact the DSL offered via
method_missing
. Not sure why there is a larger improvement here compared toset!
.