You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e597df3
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: ruby_execution_and_jit_blog_post.md
+17-48Lines changed: 17 additions & 48 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -137,22 +137,22 @@ Now let's see what happens when a method has been JIT-compiled and the interpret
137
137
138
138
### The JIT_EXEC Gateway
139
139
140
-
Remember the `JIT_EXEC` check we saw in Flow 1? It appeared after every method call setup, always checking for JIT code even when none existed. Now, this same check becomes the gateway to native code.
141
-
142
-
When a method has been JIT-compiled, the check finally finds something:
140
+
Remember the `JIT_EXEC` check we saw in Flow 1? It appeared after every method call setup, always checking for JIT code even when none existed. Now, what happens when the ISEQ has JIT-compiled code?
143
141
144
142
```text
145
-
JIT_EXEC logic (method has JIT code):
146
-
if (method_was_set_up && method_has_jit_code) {
147
-
jump_to_native_code();
143
+
if (method_was_called && method_has_jit_code) {
144
+
jump_to_jit_code();
145
+
} else {
146
+
continue_interpreting();
148
147
}
149
148
```
150
149
151
-
The difference from Flow 1? The second condition is now true - `jit_compile()` finds actual compiled code at the method's `jit_entry` pointer instead of NULL.
152
-
153
150
### How Methods Get JIT Compiled
154
151
155
-
Methods don't start with JIT code. They earn it through usage. ZJIT uses a two-phase approach:
152
+
Normally, methods don't get compiled by JIT compilers right away. They only get compiled after
153
+
it's been executed by the interpreter enough times to pass a threshold.
154
+
155
+
If we use ZJIT as the example, it uses a two-phase approach:
156
156
157
157
```c
158
158
if (body->jit_entry == NULL && rb_zjit_enabled_p) {
This is why JIT compilers need time to "warm up" to reach optimal speed - they need to observe your code's behavior before optimizing it.
189
+
This is why JIT compilers need time to "warm up" to reach optimal performance - they need to observe your code's behavior before optimizing it.
199
190
200
191
And to learn more about how ZJIT compiles Ruby code, I recommend reading [ZJIT has been merged into Ruby](https://railsatscale.com/2025-05-14-merge-zjit/).
201
192
202
193
### Where JIT Code Lives
203
194
204
-
I used to think that JIT-compiled code would replace the bytecode as it's "faster", "better optimized"...etc. But it's actually not true - it lives alongside it in the ISEQ structure. Here's what an ISEQ looks like internally:
195
+
So how does Ruby store the JIT compiled code? I used to think the compiled code would replace the bytecode as it's "faster". But it's actually not true - it lives alongside it in the ISEQ structure.
196
+
197
+
Here's what an ISEQ looks like internally:
205
198
206
199
```text
207
200
ISEQ (foo method)
@@ -216,9 +209,9 @@ After a method is called enough times and gets JIT-compiled:
216
209
```text
217
210
ISEQ (foo method)
218
211
├── body
219
-
│ ├── bytecode: [putself, opt_send_without_block, leave] // Still here!
212
+
│ ├── bytecode: [putself, opt_send_without_block, leave] // Still here
220
213
│ ├── jit_entry: 0x7f8b2c001000 // Pointer to native machine code
The `jit_entry` field is the gateway to native code. When it's NULL, Ruby interprets bytecode. When it points to compiled code, Ruby jumps directly to machine instructions.
bar() → JIT (about to execute native code at 0x7f8b2c001000)
275
-
```
276
-
277
-
The transition is remarkably simple - it's just a function pointer call. The interpreter jumps directly to the machine code address stored in `jit_entry`.
278
-
279
249
### What Happens in JIT Code
280
250
281
251
Once execution transfers to JIT code, native CPU instructions execute directly. Here's a glimpse of what the `bar` method looks like after JIT compilation (ARM64 assembly):
@@ -303,9 +273,8 @@ Key differences from interpreter execution:
303
273
304
274
- Native CPU instructions execute directly
305
275
- No instruction fetch/decode overhead
306
-
- Guards ensure type safety with conditional branches
276
+
- Guards ensure type safety with conditional branches (more on this later)
307
277
- Direct jumps to other JIT code (no method lookup)
308
-
- Register allocation instead of stack manipulation
309
278
310
279
The JIT code has full access to the Ruby VM's data structures and can:
311
280
@@ -354,7 +323,7 @@ result = INT2FIX(FIX2INT(a) + FIX2INT(b));
354
323
return result;
355
324
```
356
325
357
-
The JIT inserts type guards using instructions like:
326
+
The JIT inserts type guards using HIR (high-level intermediate representation) like:
358
327
359
328
```rust
360
329
Insn::GuardType { val, guard_type:types::Fixnum, state }
0 commit comments