skip to content

In Ruby 4.0, what does redefining Integer#+ in a patch cost at run time, and how do you make Ruby warn about it?

level: seniorimportance: nice to knowfreq 20%

answer

  1. specially optimized core methods
  2. a per-class redefined flag
  3. prepend counts as redefinition
  4. -W:performance or Warning[:performance]
  5. added as a warning in 3.4

basics

~20 s

Integer#+ is a core method the interpreter and JIT run without a method call; redefining it, by def or prepend, flags it so every Integer + becomes a full call. Ruby warns only under -W:performance or Warning[:performance] = true.

solid answer

~50 s

CRuby compiles `a + b` to a specialised instruction that adds two integers directly, and YJIT or ZJIT inline the same shortcut. The shortcut is valid only while `Integer#+` is the built-in method, so CRuby keeps a table of such **optimized core methods** — arithmetic and comparison on `Integer` and `Float`, `String#+` and `String#==`, `length`, `size` and `empty?` on `String`, `Array` and `Hash`, `Hash#[]`, `NilClass#nil?` and others. Redefining one, or prepending a module that defines it, sets a **redefined flag** for that method and class, and from then on every such call in the process takes the slow path. Since Ruby 3.4 this emits `Redefining 'Integer#+' disables interpreter and JIT optimizations` in the `performance` warning category, which is off by default, even under `-w`; enable it with `-W:performance` or `Warning[:performance] = true` before the patch loads. Adding a new method such as `Integer#minutes` triggers none of this.

code

bash · 6 lines
bash
$ ruby -W:performance -e 'class Integer; def +(other); super; end; end'
# prints a warning ending in:
# Redefining 'Integer#+' disables interpreter and JIT optimizations

$ ruby -w -e 'class Integer; def +(other); super; end; end'
# no output: -w alone does not enable the performance category

go deeper

for a junior

Recall that operators like + are methods, and that redefining the built-in ones on core classes has a hidden speed cost.

for a middle

Explain the specialised instruction, the per-class redefined flag, and the -W:performance switch that makes Ruby report it.

for a senior

Show how you would catch a dependency that patches a core operator, for example a CI boot with -W:performance, and what the cost is.

for a principal

Treat core-operator patches as a performance-budget decision; require measurement and a named owner before one is accepted.

## The shortcut Ruby takes for core operators In Ruby every operator is a method call: `1 + 2` sends `+` to `1`. A naive interpreter would look up `Integer#+` on every addition, which is slow. CRuby instead compiles common calls to **specialised instructions** (for `+` it is an `opt_plus`-style instruction) that check the receiver's class and, for an `Integer`, add the numbers directly without a method call. The JIT compilers, **YJIT** and the experimental **ZJIT** in Ruby 4.0, generate the same kind of direct code. The shortcut is only correct while the method is still the built-in one. So CRuby keeps a table of **optimized core methods** and, for each, a **redefined flag** per class. The optimized code checks the flag; if it is set, it falls back to a normal method call. ## Which methods are in the table The list lives in CRuby's VM source. A representative slice: | Class | Optimized methods (examples) | |---|---| | `Integer` | `+ - * / % == === < <= > >= >> [] succ & \| <=>` | | `Float` | `+ - * / % == === < <= > >= <=>` | | `String` | `+ == === << length size empty? succ =~ freeze -@ <=>` | | `Array` | `+ << [] []= length size empty? freeze max min hash include?` | | `Hash` | `[] []= length size empty? freeze default` | | others | `NilClass#nil?`, `Proc#call`, `Symbol#==`, `Regexp#=~` | New methods such as `Integer#minutes` or `String#titleize` are **not** in the table. Patching them costs a normal method definition and nothing else. ## What redefinition does When a method in the table is replaced — by reopening the class and writing `def +`, or by prepending a module that defines `+` — CRuby: 1. Sets the redefined flag for that operator on that class (for example `+` on `Integer`; `Float#+` keeps its shortcut). 2. Tells YJIT and ZJIT, which invalidate code compiled on the assumption. 3. Since Ruby 3.4, emits a **performance warning**: `Redefining 'Integer#+' disables interpreter and JIT optimizations`. The flag is process-wide and stays set: nothing in the VM resets it when the patch is later removed. Every `Integer +` anywhere in the application, including in gems and the standard library, now pays for a full method dispatch plus the patch's own body. In arithmetic-heavy code that is a measurable slowdown. ## Seeing the warning Ruby 3.3 introduced the `performance` warning category, and Ruby 3.4 started using it for core redefinitions. - The category is **off by default, even in verbose mode**: `ruby -w` alone does not show it. - Enable it with the command-line flag `-W:performance`, or `RUBYOPT=-W:performance` for a whole test suite. - Or set `Warning[:performance] = true` in code — early, before the patch loads, because the warning is emitted at the moment of redefinition. A useful CI step boots the application once with `-W:performance` and fails if the output contains `disables interpreter and JIT optimizations`, catching a dependency that starts patching an operator. ## What to do instead - **Do not patch core operators** to change their meaning; wrap the values in your own class and define the operator there. - If a gem does it, file an issue or replace the gem; there is no way to keep the shortcut while the patch is in place. - Measure before and after with a benchmark if you must keep such a patch. ## What interviewers want This is a senior-level question about the hidden cost of monkey patching. A strong answer explains the specialised-instruction shortcut, that redefinition (including via `prepend`) switches it off for that class, and how the 3.4 performance warning makes it visible.

  • Does prepending a module that defines Integer#+ avoid the slowdown?
    No. When a module is prepended, CRuby checks whether it now shadows an optimized method and sets the same redefined flag, so `Integer +` loses its shortcut exactly as with a direct `def +`. The performance warning is emitted in that case too.
  • Why can the slowdown persist after the patching code is removed at run time?
    The redefined flag is set once and never cleared in the running process; only a restart without the patch restores the specialised path. Removing the method with `remove_method` does not reset it.

saying these in an interview costs you the question

  • Adding Integer#minutes slows down all Integer arithmetic.
  • ruby -w is enough to show the core redefinition warning.
  • Using prepend instead of def avoids the deoptimization.
  • Redefining Integer#+ also slows down Float addition.
  • Removing the patch later restores the fast path in the same process.