{"article":{"slug":"rails-is-a-jit-compiler","title":"Rails is a JIT compiler","subtitle":null,"summary":"Max Bernstein on Rails at Scale shows how Rails enum predicate methods were made dramatically faster by generating static code at runtime instead of dynamic public_send dispatch, with benchmarks and lessons about fixing performance upstream.","content_type":"blog_post","language":"en","canonical_url":"https://railsatscale.com/2026-10-21-rails-is-a-jit-compiler/","author":{"name":"Max Bernstein","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Rails at Scale","url":"https://railsatscale.com/","listing_slug":null,"listing":null},"topics":[{"name":"Performance","slug":"performance","url":"https://listedarticles.com/topics/performance"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Web Development","slug":"web-development","url":"https://listedarticles.com/topics/web-development"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":788,"reading_minutes":3,"published_at":"2026-10-21T00:00:00.000Z","added_at":"2026-10-22T02:14:34.229Z","updated_at":"2026-10-22T02:14:34.229Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/rails-is-a-jit-compiler","markdown_url":"https://listedarticles.com/articles/rails-is-a-jit-compiler.md","example":false,"citation":"Max Bernstein, Rails at Scale. \"Rails is a JIT compiler.\" 21 Oct 2026. https://railsatscale.com/2026-10-21-rails-is-a-jit-compiler/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://railsatscale.com/2026-10-21-rails-is-a-jit-compiler/"},"body_markdown":"# Rails is a JIT compiler\n\n*Or, you wouldn’t believe how Aaron made enums faster with one weird trick.*\n\nThe title was bait to get you to open this article. But fear not! We will be\ngenerating code at run-time today. (See [the\nPR](https://github.com/rails/rails/pull/59250) for a sneak peek.)\n\n[Sam](https://railsatscale.com/authors/sam-schmidt/) popped into the JIT channel asking how we might\noptimize Rails enums in Roundhouse, motivated by the following code snippet:\n\n```\nclass Order < ActiveRecord::Base\n  enum :status, { pending: 0, paid: 1, shipped: 2, refunded: 3 }\n\n  def shippable?\n    paid? && !refunded?\n  end\nend\n```\n\nIn particular, there is a lot of dynamic behavior going on with the code\ngenerated by Rails `enum` and Sam wanted to know if inlining it all away in an\n(external-to-Rails) build step would help.\n\nThat didn’t sound right to me because I thought that:\n\n1. Rails got a lot better in recent years about removing these dynamic lookups\n   in favor of creating and caching methods\n2. ZJIT should be able to handle dispatch to methods (generated or otherwise)\n   just fine\n\nSo I inquired and Sam presented to us the [implementation of\n`enum`](https://github.com/rails/rails/blob/36e0f96b9d7a022755a53a2a9fb7a67ad4e3669e/activerecord/lib/active_record/enum.rb#L335)\nwhich contained this gem:\n\n```\nvalue = 0\ndefine_method(\"#{value_method_name}?\") {\n  public_send(:\"#{name}_for_database\") == value\n}\n```\n\nNow fortunately, this is not the fully dynamic lookup I was worried about! No\nhashes, etc. This is Rails using the `enum` to generate a method on the\n`ActiveRecord` class. So in our case, for example, it turns `pending` into a\nmethod on `Order`:\n\n```\nvalue = 0\nOrder.define_method(\"pending?\") {\n  public_send(:status_for_database) == value\n}\n```\n\n*Unfortunately,* it is still using very dynamic behavior. There is no reason to\nbe using `public_send` for method dispatch if you know the name statically: it\ndoesn’t get cached and has a lot more machinery than we need. There is also no\nreason to use a bmethod (`define_method` with a block that captures the\nenvironment) and look up the `value` dynamically; the enum values are burned in\nat `enum` definition time.\n\nI wanted to make it use `module_eval` with a string instead to generate a\nnormal looking method. I don’t know anything about Rails so I nerd sniped\n[Aaron](https://railsatscale.com/authors/aaron-patterson/) into fixing it.\n\n## Nerd sniping Aaron\n\nThe change to the the code started off fairly mechanical: we should be able to\ndo `module_eval` to generate a similar looking method:\n\n```\ncode = []\ncode << \"def #{value_method_name}?; #{name}_for_database == #{value.inspect}; end\"\n# ...\nmodule_eval(code.join(\"\\n\"), __FILE__, __LINE__) unless code.empty?\n```\n\nThe differences here being:\n\n1. Direct dispatch to the method instead of going through `public_send`\n2. Burning the `value` into the method instead of looking it up from the parent\n   environment of the block\n\nIt passed almost all the tests but then we ran into a couple of weird cases:\n\n1. People have the darndest enum names\n   1. The Rails test suite, for example, uses fake timezones: `enum :timezone, [:\"Etc/GMT+1\", :\"Etc/GMT-1\"]`\n   2. Our application, has an instance with a capital letter: `enum :thing, [:Foo, :Bar]`\n   3. Probably other stuff that wouldn’t be a identifier in Ruby\n2. Floats. Naturally. While all the enum values are required to be serializable\n   (and therefore reifiable in Ruby source code), the roundtrippability gets a\n   little weird with floats. So we have this irritating check: `!(Float === value && !value.finite?)`\n\nIn the end it wasn’t quite a one-line change because\n[Ufuk](https://railsatscale.com/authors/ufuk-kayserilioglu/) also pointed out that we should be using\n`ActiveSupport::CodeGenerator` instead of yolo-ing it. But it was pretty close\nto it.\n\nCheck out [the PR](https://github.com/rails/rails/pull/59250).\n\n## Performance numbers\n\nGo check out the PR! But you’re still here, so I’ll repeat the numbers here.\nThe following microbenchmark of the code at the beginning of the article:\n\n```\nIPS.ips do |x|\n  x.report(\"shippable?\") { order.shippable? }\nend\n```\n\ngot a *lot* better.\n\nHere are the performance numbers for `main`:\n\n```\naaron@Aarons-MBP ~/g/rails (main)> ruby -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -> 0.0073s\n          shippable?:     1.721M i/s (± 2.3%, GC  4.4%)\naaron@Aarons-MBP ~/g/rails (main)> ruby --yjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -> 0.0048s\n          shippable?:     3.568M i/s (± 2.6%, GC  8.2%)\naaron@Aarons-MBP ~/g/rails (main)> ruby --zjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -> 0.0029s\n          shippable?:     3.812M i/s (± 2.8%, GC  8.9%)\n```\n\nand for Aaron’s PR:\n\n```\naaron@Aarons-MBP ~/g/rails (fix-enum)> ruby -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -> 0.0073s\n          shippable?:     3.145M i/s (± 1.5%)\naaron@Aarons-MBP ~/g/rails (fix-enum)> ruby --yjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -> 0.0065s\n          shippable?:    16.274M i/s (± 1.3%)\naaron@Aarons-MBP ~/g/rails (fix-enum)> ruby --zjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -> 0.0027s\n          shippable?:    18.509M i/s (± 2.2%)\n```\n\nWahoo!\n\n## Lessons\n\n- Static is better than dynamic. Burn in those constants.\n- In particular, `public_send` is slow!\n- We can fix performance issues upstream! Especially if the upstream libraries\n  already do code generation. Not all problems require a custom compiler.\n","body_html":"<h1 id=\"rails-is-a-jit-compiler\">Rails is a JIT compiler</h1>\n<p><em>Or, you wouldn’t believe how Aaron made enums faster with one weird trick.</em></p>\n<p>The title was bait to get you to open this article. But fear not! We will be\ngenerating code at run-time today. (See <a href=\"https://github.com/rails/rails/pull/59250\" rel=\"nofollow ugc noopener\">the\nPR</a> for a sneak peek.)</p>\n<p><a href=\"https://railsatscale.com/authors/sam-schmidt/\" rel=\"nofollow ugc noopener\">Sam</a> popped into the JIT channel asking how we might\noptimize Rails enums in Roundhouse, motivated by the following code snippet:</p>\n<pre><code>class Order &lt; ActiveRecord::Base\n  enum :status, { pending: 0, paid: 1, shipped: 2, refunded: 3 }\n\n  def shippable?\n    paid? &amp;&amp; !refunded?\n  end\nend</code></pre>\n<p>In particular, there is a lot of dynamic behavior going on with the code\ngenerated by Rails <code>enum</code> and Sam wanted to know if inlining it all away in an\n(external-to-Rails) build step would help.</p>\n<p>That didn’t sound right to me because I thought that:</p>\n<ol><li><p>Rails got a lot better in recent years about removing these dynamic lookups</p><p> in favor of creating and caching methods</p></li><li><p>ZJIT should be able to handle dispatch to methods (generated or otherwise)</p><p> just fine</p></li></ol>\n<p>So I inquired and Sam presented to us the <a href=\"https://github.com/rails/rails/blob/36e0f96b9d7a022755a53a2a9fb7a67ad4e3669e/activerecord/lib/active_record/enum.rb#L335\" rel=\"nofollow ugc noopener\">implementation of\n<code>enum</code></a>\nwhich contained this gem:</p>\n<pre><code>value = 0\ndefine_method(&quot;#{value_method_name}?&quot;) {\n  public_send(:&quot;#{name}_for_database&quot;) == value\n}</code></pre>\n<p>Now fortunately, this is not the fully dynamic lookup I was worried about! No\nhashes, etc. This is Rails using the <code>enum</code> to generate a method on the\n<code>ActiveRecord</code> class. So in our case, for example, it turns <code>pending</code> into a\nmethod on <code>Order</code>:</p>\n<pre><code>value = 0\nOrder.define_method(&quot;pending?&quot;) {\n  public_send(:status_for_database) == value\n}</code></pre>\n<p><em>Unfortunately,</em> it is still using very dynamic behavior. There is no reason to\nbe using <code>public_send</code> for method dispatch if you know the name statically: it\ndoesn’t get cached and has a lot more machinery than we need. There is also no\nreason to use a bmethod (<code>define_method</code> with a block that captures the\nenvironment) and look up the <code>value</code> dynamically; the enum values are burned in\nat <code>enum</code> definition time.</p>\n<p>I wanted to make it use <code>module_eval</code> with a string instead to generate a\nnormal looking method. I don’t know anything about Rails so I nerd sniped\n<a href=\"https://railsatscale.com/authors/aaron-patterson/\" rel=\"nofollow ugc noopener\">Aaron</a> into fixing it.</p>\n<h2 id=\"nerd-sniping-aaron\">Nerd sniping Aaron</h2>\n<p>The change to the the code started off fairly mechanical: we should be able to\ndo <code>module_eval</code> to generate a similar looking method:</p>\n<pre><code>code = []\ncode &lt;&lt; &quot;def #{value_method_name}?; #{name}_for_database == #{value.inspect}; end&quot;\n# ...\nmodule_eval(code.join(&quot;\\n&quot;), __FILE__, __LINE__) unless code.empty?</code></pre>\n<p>The differences here being:</p>\n<ol><li>Direct dispatch to the method instead of going through <code>public_send</code></li><li><p>Burning the <code>value</code> into the method instead of looking it up from the parent</p><p> environment of the block</p></li></ol>\n<p>It passed almost all the tests but then we ran into a couple of weird cases:</p>\n<ol><li>People have the darndest enum names<ol><li>The Rails test suite, for example, uses fake timezones: <code>enum :timezone, [:&quot;Etc/GMT+1&quot;, :&quot;Etc/GMT-1&quot;]</code></li><li>Our application, has an instance with a capital letter: <code>enum :thing, [:Foo, :Bar]</code></li><li>Probably other stuff that wouldn’t be a identifier in Ruby</li></ol></li><li><p>Floats. Naturally. While all the enum values are required to be serializable</p><p> (and therefore reifiable in Ruby source code), the roundtrippability gets a\n little weird with floats. So we have this irritating check: <code>!(Float === value &amp;&amp; !value.finite?)</code></p></li></ol>\n<p>In the end it wasn’t quite a one-line change because\n<a href=\"https://railsatscale.com/authors/ufuk-kayserilioglu/\" rel=\"nofollow ugc noopener\">Ufuk</a> also pointed out that we should be using\n<code>ActiveSupport::CodeGenerator</code> instead of yolo-ing it. But it was pretty close\nto it.</p>\n<p>Check out <a href=\"https://github.com/rails/rails/pull/59250\" rel=\"nofollow ugc noopener\">the PR</a>.</p>\n<h2 id=\"performance-numbers\">Performance numbers</h2>\n<p>Go check out the PR! But you’re still here, so I’ll repeat the numbers here.\nThe following microbenchmark of the code at the beginning of the article:</p>\n<pre><code>IPS.ips do |x|\n  x.report(&quot;shippable?&quot;) { order.shippable? }\nend</code></pre>\n<p>got a <em>lot</em> better.</p>\n<p>Here are the performance numbers for <code>main</code>:</p>\n<pre><code>aaron@Aarons-MBP ~/g/rails (main)&gt; ruby -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -&gt; 0.0073s\n          shippable?:     1.721M i/s (± 2.3%, GC  4.4%)\naaron@Aarons-MBP ~/g/rails (main)&gt; ruby --yjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -&gt; 0.0048s\n          shippable?:     3.568M i/s (± 2.6%, GC  8.2%)\naaron@Aarons-MBP ~/g/rails (main)&gt; ruby --zjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -&gt; 0.0029s\n          shippable?:     3.812M i/s (± 2.8%, GC  8.9%)</code></pre>\n<p>and for Aaron’s PR:</p>\n<pre><code>aaron@Aarons-MBP ~/g/rails (fix-enum)&gt; ruby -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -&gt; 0.0073s\n          shippable?:     3.145M i/s (± 1.5%)\naaron@Aarons-MBP ~/g/rails (fix-enum)&gt; ruby --yjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -&gt; 0.0065s\n          shippable?:    16.274M i/s (± 1.3%)\naaron@Aarons-MBP ~/g/rails (fix-enum)&gt; ruby --zjit -I~/git/ips/lib test.rb\n-- create_table(:orders)\n   -&gt; 0.0027s\n          shippable?:    18.509M i/s (± 2.2%)</code></pre>\n<p>Wahoo!</p>\n<h2 id=\"lessons\">Lessons</h2>\n<ul><li>Static is better than dynamic. Burn in those constants.</li><li>In particular, <code>public_send</code> is slow!</li><li><p>We can fix performance issues upstream! Especially if the upstream libraries</p><p>already do code generation. Not all problems require a custom compiler.</p></li></ul>","headings":[{"level":1,"text":"Rails is a JIT compiler","id":"rails-is-a-jit-compiler"},{"level":2,"text":"Nerd sniping Aaron","id":"nerd-sniping-aaron"},{"level":2,"text":"Performance numbers","id":"performance-numbers"},{"level":2,"text":"Lessons","id":"lessons"}]}}