Skip to content

Optimize _blank? - #622

Open
moberegger wants to merge 3 commits into
rails:mainfrom
moberegger:moberegger/optimize-blank
Open

moberegger wants to merge 3 commits into
rails:mainfrom
moberegger:moberegger/optimize-blank

Conversation

@moberegger

@moberegger moberegger commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Switches _blank? from BLANK == value to Blank === value.

Originally _blank? did a Ruby-level dispatch to Blank#==, which itself calls super (for an identity check) and then Blank === other. We can short circuit this to simply be Blank === other. This gives us the same result for less work because Blank#== would always return true for any Blank instance. We don't actually need to an identity check.

blank1 = Jbuilder::Blank.new
blank2 = Jbuilder::Blank.new

puts blank1 == blank2 # true
puts blank2 == blank1 # true

Below is an IPS benchmark against a simple builder that exercises _blank a few times.

# frozen_string_literal: true

require 'benchmark/ips'
require_relative 'lib/jbuilder'

json = Jbuilder.new

Benchmark.ips do |x|
  x.report('old') do |n|
    i = 0
    while i < n
      i += 1

      json.set! :foo do
        json.set! :bar, 123
      end
    end
  end

  x.report('new') do |n|
    i = 0
    while i < n
      i += 1

      json.set! :foo do
        json.set! :bar, 123
      end
    end
  end

  x.hold! 'ips_results'
  x.compare! order: :baseline
end

Without YJIT

ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin23]
Warming up --------------------------------------
                 old    55.923k i/100ms
                 new    70.938k i/100ms
Calculating -------------------------------------
                 old    563.384k (± 1.4%) i/s    (1.77 μs/i) -      2.852M in   5.062397s
                 new    716.586k (± 1.4%) i/s    (1.40 μs/i) -      3.618M in   5.048717s

Comparison:
old:   563383.9 i/s
new:   716585.6 i/s - 1.27x  faster

With YJIT

ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
                 old   169.546k i/100ms
                 new   178.359k i/100ms
Calculating -------------------------------------
                 old      1.712M (± 2.3%) i/s  (584.09 ns/i) -      8.647M in   5.050534s
Calculating -------------------------------------
                 new      1.783M (± 3.3%) i/s  (560.71 ns/i) -      8.918M in   5.000406s

Comparison:
old:  1712065.7 i/s
new:  1783445.2 i/s - same-ish: difference falls within error

Comment thread lib/jbuilder/blank.rb
Comment on lines -5 to -8
def ==(other)
super || Blank === other
end

@moberegger moberegger Aug 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not necessary for the optimization, but figured I'd remove it since it is no longer running in the gem.

I suppose it's possible that a user is doing something with Jbuilder::Blank directly that may require this, but I believe this class was intended to be an implementation detail.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this is technically a change to the public API (Jbuilder::Blank is not marked with # :nodoc:), I think it'd be best (and increase the likelihood of this PR being reviewed and merged) to maintain the method and open a separate PR to deprecate it.

@moberegger moberegger Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yup, I can put it back no problem!

To clarify:

but I believe this class was intended to be an implementation detail.

I failed to mention that my only rationale there was that the Readme doesn't mention Blank.

Comment thread lib/jbuilder/blank.rb
Comment on lines -5 to -8
def ==(other)
super || Blank === other
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this is technically a change to the public API (Jbuilder::Blank is not marked with # :nodoc:), I think it'd be best (and increase the likelihood of this PR being reviewed and merged) to maintain the method and open a separate PR to deprecate it.

@seanpdoyle

Copy link
Copy Markdown

Could you share the source for the IPS benchmark mentioned in the pull request description?

@moberegger

moberegger commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@seanpdoyle Updated the PR description to add the bench source. Also updated the results for Ruby 4.0.7 (originally they were run on 4.0.6). Also included a run with YJIT enabled (which was missing before).

YJIT is a tad faster, but still falls within the margin of error according to the benchmark/ips tool.

@moberegger

moberegger commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Interestingly, when initializing a new Jbuilder object in each pass

Benchmark.ips do |x|
  x.report('old') do |n|
    i = 0
    while i < n
      i += 1

      json = Jbuilder.new
      json.set! :foo do
        json.set! :bar, 123
      end
    end
  end

  x.report('new') do |n|
    i = 0
    while i < n
      i += 1

      json = Jbuilder.new
      json.set! :foo do
        json.set! :bar, 123
      end
    end
  end

  x.hold! 'ips_results'
  x.compare! order: :baseline
end
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
                 old   193.640k i/100ms
                 new   195.000k i/100ms
Calculating -------------------------------------
                 old      2.228M (± 4.2%) i/s  (448.84 ns/i) -     11.231M in   5.040934s
                 new      2.405M (± 2.8%) i/s  (415.75 ns/i) -     12.090M in   5.026368s

Comparison:
old:  2227983.9 i/s
new:  2405315.3 i/s - 1.08x  faster

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