Skip to content

Commit e597df3

Browse files
committed
WIP
1 parent dcb4eb1 commit e597df3

1 file changed

Lines changed: 17 additions & 48 deletions

File tree

‎ruby_execution_and_jit_blog_post.md‎

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,22 @@ Now let's see what happens when a method has been JIT-compiled and the interpret
137137

138138
### The JIT_EXEC Gateway
139139

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?
143141

144142
```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();
148147
}
149148
```
150149

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-
153150
### How Methods Get JIT Compiled
154151

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:
156156

157157
```c
158158
if (body->jit_entry == NULL && rb_zjit_enabled_p) {
@@ -184,24 +184,17 @@ Method 'calculate' lifecycle:
184184
Calls: 0 ──────── 25 ──────── 30 ─────────────────►
185185
│ │ │
186186
Mode: └─Interpret┴─Profile──┴─Native Code (JIT compiled)
187-
188-
Timeline: Cold start Gathering Optimization complete,
189-
type info executing native code
190187
```
191188

192-
Key thresholds (ZJIT defaults, may change in the future):
193-
194-
- **Profile threshold**: 25 calls - Start gathering type information
195-
- **Compile threshold**: 30 calls - Generate optimized native code
196-
- **Steady state**: 30+ calls - Execute compiled code
197-
198-
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.
199190

200191
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/).
201192

202193
### Where JIT Code Lives
203194

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:
205198

206199
```text
207200
ISEQ (foo method)
@@ -216,9 +209,9 @@ After a method is called enough times and gets JIT-compiled:
216209
```text
217210
ISEQ (foo method)
218211
├── body
219-
│ ├── bytecode: [putself, opt_send_without_block, leave] // Still here!
212+
│ ├── bytecode: [putself, opt_send_without_block, leave] // Still here
220213
│ ├── jit_entry: 0x7f8b2c001000 // Pointer to native machine code
221-
│ └── jit_entry_calls: 1000 // Reached compilation threshold
214+
│ └── jit_entry_calls: 35 // Reached compilation threshold
222215
```
223216

224217
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.
@@ -253,29 +246,6 @@ Interpreter executing foo() [not JITted]:
253246
- No more bytecode interpretation
254247
```
255248

256-
### Visual: ISEQ State During Transition
257-
258-
Before the call to bar:
259-
260-
```text
261-
ISEQ (bar)
262-
├── body
263-
│ ├── jit_entry: 0x7f8b2c001000 ← JIT code exists!
264-
│ ├── jit_entry_calls: 1000 ← Reached threshold
265-
│ └── bytecode: [putobject 42, leave] ← Still available
266-
```
267-
268-
During the transition:
269-
270-
```text
271-
Method: Execution Mode:
272-
foo() → Interpreter (executing opt_send_without_block)
273-
↓
274-
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-
279249
### What Happens in JIT Code
280250

281251
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:
303273

304274
- Native CPU instructions execute directly
305275
- 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)
307277
- Direct jumps to other JIT code (no method lookup)
308-
- Register allocation instead of stack manipulation
309278

310279
The JIT code has full access to the Ruby VM's data structures and can:
311280

@@ -354,7 +323,7 @@ result = INT2FIX(FIX2INT(a) + FIX2INT(b));
354323
return result;
355324
```
356325

357-
The JIT inserts type guards using instructions like:
326+
The JIT inserts type guards using HIR (high-level intermediate representation) like:
358327

359328
```rust
360329
Insn::GuardType { val, guard_type: types::Fixnum, state }

0 commit comments

Comments
 (0)