Skip to content

puma_http11.c: pre-intern env keys#3825

Merged
joshuay03 merged 1 commit intopuma:mainfrom
byroot:parser-pre-intern-hash-key
Nov 19, 2025
Merged

puma_http11.c: pre-intern env keys#3825
joshuay03 merged 1 commit intopuma:mainfrom
byroot:parser-pre-intern-hash-key

Conversation

@byroot
Copy link
Contributor

@byroot byroot commented Nov 17, 2025

As explained in https://byroot.github.io/ruby/json/2025/01/12/optimizing-ruby-json-part-6.html when inserting a string key in a hash, Ruby will try to lookup an equivalent string in the interned string table.

As such, in C extensions, it's generally advantageous to pre-intern that string, or at least to pre-freeze it.

The gain is minor, but measurable. The gain is ~17% once common fields are interned too.

ruby 3.4.6 (2025-09-16 revision dbd83256b1) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
               after    27.498k i/100ms
Calculating -------------------------------------
               after    272.866k (± 1.5%) i/s    (3.66 μs/i) -      1.375M in   5.039945s

Comparison:
              before:   265747.8 i/s
               after:   272866.0 i/s - 1.03x  faster
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "benchmark-ips"
  gem "puma", path: "."
end

request = <<~HTTP.split("\n").join("\r\n").freeze
  GET /puma/puma HTTP/1.1
  Host: github.com
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-US,en;q=0.5
  Accept-Encoding: gzip, deflate, br, zstd
  Connection: keep-alive
  Cookie: user_session=Dsdfsdfdsfdsfdsfdsfdsfdfdfs; __Host-user_session_same_site=Zya7Q7Zndsfsdfdsfdsfdsfdsfdsf; logged_in=yes; dotcom_user=george; _octo=GH1.1.1796688136.1746271857; _device_id=dsfsdfsdfdsfdsfdsf1fb6c729; color_mode=%7B%22color_mode%22%3A%22auto%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark%22%2C%22color_mode%22%3A%22dark%22%7D%7D; GHCC=Required:1-Analytics:0-SocialMedia:0-Advertising:0; cpu_bucket=lg; preferred_color_mode=dark; tz=Europe%2FDublin; _gh_sess=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  Upgrade-Insecure-Requests: 1
  Sec-Fetch-Dest: document
  Sec-Fetch-Mode: navigate
  Sec-Fetch-Site: cross-site
  If-None-Match: W/"249a8aa7212f877b7ba603815b6f03d7"
  Priority: u=0, i
HTTP

def parse(request)
  http = Puma::HttpParser.new
  http.execute({}, request, 0)
  http.finished?
end

STAGE = ENV["STAGE"]
Benchmark.ips do |x|
  x.report(STAGE || "parse") do
    parse(request  + "\r\n\r\n")
  end
  if STAGE
    x.save!("/tmp/bench-puma-parser")
  end
  x.compare!(order: :baseline)
end

@byroot
Copy link
Contributor Author

byroot commented Nov 17, 2025

Actually, I just realized the "common fields" weren't interned either, the gain is much more substantial with them:

ruby 3.4.6 (2025-09-16 revision dbd83256b1) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
               after    30.315k i/100ms
Calculating -------------------------------------
               after    302.824k (± 1.5%) i/s    (3.30 μs/i) -      1.516M in   5.006506s

Comparison:
              before:   265747.8 i/s
               after:   302823.6 i/s - 1.14x  faster

@byroot
Copy link
Contributor Author

byroot commented Nov 17, 2025

Somehow TruffleRuby has rb_enc_interned_str_cstr but not rb_interned_str, that's weird.

I'll try to find time tomorrow to fix this.

@github-actions github-actions bot added the waiting-for-review Waiting on review from anyone label Nov 18, 2025
@byroot byroot force-pushed the parser-pre-intern-hash-key branch from 26d9567 to 51a9de0 Compare November 18, 2025 08:49
@byroot
Copy link
Contributor Author

byroot commented Nov 18, 2025

Alright, I fixed the Truffle Ruby version.

@joshuay03 joshuay03 removed the waiting-for-review Waiting on review from anyone label Nov 18, 2025
@byroot byroot force-pushed the parser-pre-intern-hash-key branch from 51a9de0 to f340c37 Compare November 18, 2025 18:31
@byroot byroot force-pushed the parser-pre-intern-hash-key branch from f340c37 to 7c2bdea Compare November 18, 2025 21:37
As explained in https://byroot.github.io/ruby/json/2025/01/12/optimizing-ruby-json-part-6.html
when inserting a string key in a hash, Ruby will try to lookup an equivalent string
in the interned string table.

As such, in C extensions, it's generally advantageous to pre-intern that string,
or at least to pre-freeze it.

```
ruby 3.4.6 (2025-09-16 revision dbd83256b1) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
               after    30.315k i/100ms
Calculating -------------------------------------
               after    302.824k (± 1.5%) i/s    (3.30 μs/i) -      1.516M in   5.006506s

Comparison:
              before:   265747.8 i/s
               after:   302823.6 i/s - 1.14x  faster
```

```ruby
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "benchmark-ips"
  gem "puma", path: "."
end

request = <<~HTTP.split("\n").join("\r\n").freeze
  GET /puma/puma HTTP/1.1
  Host: github.com
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-US,en;q=0.5
  Accept-Encoding: gzip, deflate, br, zstd
  Connection: keep-alive
  Cookie: user_session=Dsdfsdfdsfdsfdsfdsfdsfdfdfs; __Host-user_session_same_site=Zya7Q7Zndsfsdfdsfdsfdsfdsfdsf; logged_in=yes; dotcom_user=george; _octo=GH1.1.1796688136.1746271857; _device_id=dsfsdfsdfdsfdsfdsf1fb6c729; color_mode=%7B%22color_mode%22%3A%22auto%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark%22%2C%22color_mode%22%3A%22dark%22%7D%7D; GHCC=Required:1-Analytics:0-SocialMedia:0-Advertising:0; cpu_bucket=lg; preferred_color_mode=dark; tz=Europe%2FDublin; _gh_sess=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  Upgrade-Insecure-Requests: 1
  Sec-Fetch-Dest: document
  Sec-Fetch-Mode: navigate
  Sec-Fetch-Site: cross-site
  If-None-Match: W/"249a8aa7212f877b7ba603815b6f03d7"
  Priority: u=0, i
HTTP

def parse(request)
  http = Puma::HttpParser.new
  http.execute({}, request, 0)
  http.finished?
end

STAGE = ENV["STAGE"]
Benchmark.ips do |x|
  x.report(STAGE || "parse") do
    parse(request  + "\r\n\r\n")
  end
  if STAGE
    x.save!("/tmp/bench-puma-parser")
  end
  x.compare!(order: :baseline)
end
```
@byroot byroot force-pushed the parser-pre-intern-hash-key branch from 7c2bdea to d22e370 Compare November 18, 2025 21:54
@joshuay03
Copy link
Collaborator

Thank you!

@joshuay03 joshuay03 merged commit 253705b into puma:main Nov 19, 2025
122 of 124 checks passed
@headius headius mentioned this pull request Dec 13, 2025
13 tasks
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.

3 participants