Skip to content

Optimize JSON escaping #54484

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 1 commit into from
Feb 12, 2025
Merged

Conversation

etiennebarrie
Copy link
Contributor

@etiennebarrie etiennebarrie commented Feb 10, 2025

Instead of using multiple gsub! calls, this uses binary encoding to do the regexp gsub with map, then forces encoding back to UTF-8.

This uses the same approach as before ebe0c40, with binary encoding, which performs better.

# frozen_string_literal: true

require "bundler/inline"

gemfile do
  source 'https://rubygems.org'
  gem 'benchmark-ips'
  gem 'rails', path: '.'
  gem "json", path: "../../ruby/json"
  gem "vernier"
end

require "active_support"
require "active_support/json/encoding"

obj = JSON.load_file("../../ruby/json/benchmark/data/twitter.json")

Benchmark.ips do |x|
  x.report "obj.to_json" do
    obj.to_json
  end
end

obj = "a"*obj.to_json.bytesize
Benchmark.ips do |x|
  x.report "obj.to_json nothing to escape" do
    obj.to_json
  end
end

Before:

ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
         obj.to_json    28.000 i/100ms
Calculating -------------------------------------
         obj.to_json    290.133 (± 2.4%) i/s    (3.45 ms/i) -      1.456k in   5.020937s
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
obj.to_json nothing to escape
                       100.000 i/100ms
Calculating -------------------------------------
obj.to_json nothing to escape
                          1.027k (± 3.7%) i/s  (973.57 μs/i) -      5.200k in   5.069529s

After:

ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
         obj.to_json    61.000 i/100ms
Calculating -------------------------------------
         obj.to_json    610.559 (± 4.6%) i/s    (1.64 ms/i) -      3.050k in   5.007098s
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
obj.to_json nothing to escape
                        97.000 i/100ms
Calculating -------------------------------------
obj.to_json nothing to escape
                          1.004k (± 2.4%) i/s  (996.13 μs/i) -      5.044k in   5.027348s

We can see it's a bit slower without anything to escape, but within the margin of error. We also think we can optimize gsub with a map on the Ruby side to avoid the allocations mentioned in #48669. cc @jhawthorn

@byroot
Copy link
Member

byroot commented Feb 11, 2025

We also think we can optimize gsub with a map on the Ruby side to avoid the allocations mentioned in #48669.

Ruby PR to get rid of the string allocations: ruby/ruby#12730, but I'll try to follow it up to try to also reduce MatchData allocations.

Instead of using multiple gsub! calls, this uses binary encoding to do
the regexp gsub with map, then forces encoding back to UTF-8.

This uses the same approach as before
ebe0c40, with binary encoding, which
performs better.

Co-authored-by: Jean Boussier <[email protected]>
@byroot byroot force-pushed the optimize-json-escape branch from de343ba to 75b4380 Compare February 12, 2025 09:23
@byroot byroot merged commit 9621e59 into rails:main Feb 12, 2025
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants