Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/zjit-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
RUN_OPTS: ${{ matrix.run_opts }}
SPECOPTS: ${{ matrix.specopts }}
TESTOPTS: ${{ matrix.testopts }}
ZJIT_RB_BUG: 1

runs-on: macos-14

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/zjit-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
RUBY_DEBUG: ci
BUNDLE_JOBS: 8 # for yjit-bench
RUST_BACKTRACE: 1
ZJIT_RB_BUG: 1

runs-on: ubuntu-22.04

Expand Down
2 changes: 1 addition & 1 deletion doc/yjit/yjit.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ There are multiple test suites:
- `make test-all`
- `make test-spec`
- `make check` runs all of the above
- `make yjit-smoke-test` runs quick checks to see that YJIT is working correctly
- `make yjit-check` runs quick checks to see that YJIT is working correctly

The tests can be run in parallel like this:

Expand Down
43 changes: 43 additions & 0 deletions doc/zjit.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,49 @@ You can also run a single test case by matching the method name:
make test-all TESTS="test/ruby/test_zjit.rb -n TestZJIT#test_putobject"
```

## Statistics Collection

ZJIT provides detailed statistics about JIT compilation and execution behavior.

### Basic Stats

Run with basic statistics printed on exit:

```bash
./miniruby --zjit-stats script.rb
```

Collect stats without printing (access via `RubyVM::ZJIT.stats` in Ruby):

```bash
./miniruby --zjit-stats=quiet script.rb
```

### Accessing Stats in Ruby

```ruby
# Check if stats are enabled
if RubyVM::ZJIT.stats_enabled?
stats = RubyVM::ZJIT.stats
puts "Compiled ISEQs: #{stats[:compiled_iseq_count]}"
puts "Failed ISEQs: #{stats[:failed_iseq_count]}"

# You can also reset stats during execution
RubyVM::ZJIT.reset_stats!
end
```

### Performance Ratio

The `ratio_in_zjit` stat shows the percentage of Ruby instructions executed in JIT code vs interpreter. This metric only appears when ZJIT is built with `--enable-zjit=stats` (which enables `rb_vm_insn_count` tracking) and represents a key performance indicator for ZJIT effectiveness.

To build with stats support:

```bash
./configure --enable-zjit=stats
make -j
```

## ZJIT Glossary

This glossary contains terms that are helpful for understanding ZJIT.
Expand Down
2 changes: 1 addition & 1 deletion test/ruby/test_require.rb
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ def to_str
def to_path = @path
end

def create_ruby_file = Tempfile.create(["test", ".rb"]).path
def create_ruby_file = Tempfile.open(["test", ".rb"]).path

require MyString.new(create_ruby_file)
$LOADED_FEATURES.unshift(create_ruby_file)
Expand Down
55 changes: 55 additions & 0 deletions test/ruby/test_zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,21 @@ def entry = [test(1), test(3, 4)]
}, call_threshold: 2
end

def test_forwardable_iseq
assert_compiles '1', %q{
def test(...) = 1
test
}
end

def test_sendforward
assert_runs '[1, 2]', %q{
def callee(a, b) = [a, b]
def test(...) = callee(...)
test(1, 2)
}, insns: [:sendforward]
end

def test_iseq_with_optional_arguments
assert_compiles '[[1, 2], [3, 4]]', %q{
def test(a, b = 2) = [a, b]
Expand Down Expand Up @@ -2569,6 +2584,46 @@ def test(x)
}, insns: [:opt_case_dispatch]
end

def test_invokeblock
assert_compiles '42', %q{
def test
yield
end
test { 42 }
}, insns: [:invokeblock]
end

def test_invokeblock_with_args
assert_compiles '3', %q{
def test(x, y)
yield x, y
end
test(1, 2) { |a, b| a + b }
}, insns: [:invokeblock]
end

def test_invokeblock_no_block_given
assert_compiles ':error', %q{
def test
yield rescue :error
end
test
}, insns: [:invokeblock]
end

def test_invokeblock_multiple_yields
assert_compiles "[1, 2, 3]", %q{
results = []
def test
yield 1
yield 2
yield 3
end
test { |x| results << x }
results
}, insns: [:invokeblock]
end

private

# Assert that every method call in `test_script` can be compiled by ZJIT
Expand Down
29 changes: 16 additions & 13 deletions vm_insnhelper.c
Original file line number Diff line number Diff line change
Expand Up @@ -6139,26 +6139,29 @@ rb_vm_opt_send_without_block(rb_execution_context_t *ec, rb_control_frame_t *reg

VALUE
rb_vm_invokesuper(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, CALL_DATA cd, ISEQ blockiseq)
{
stack_check(ec);

VALUE bh = vm_caller_setup_arg_block(ec, GET_CFP(), cd->ci, blockiseq, true);
VALUE val = vm_sendish(ec, GET_CFP(), cd, bh, mexp_search_super);

VM_EXEC(ec, val);
return val;
}

VALUE
rb_vm_invokesuperforward(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, CALL_DATA cd, ISEQ blockiseq)
{
stack_check(ec);
struct rb_forwarding_call_data adjusted_cd;
struct rb_callinfo adjusted_ci;

VALUE bh;
VALUE val;

if (vm_ci_flag(cd->ci) & VM_CALL_FORWARDING) {
bh = vm_caller_setup_fwd_args(GET_EC(), GET_CFP(), cd, blockiseq, true, &adjusted_cd, &adjusted_ci);
VALUE bh = vm_caller_setup_fwd_args(GET_EC(), GET_CFP(), cd, blockiseq, true, &adjusted_cd, &adjusted_ci);

val = vm_sendish(ec, GET_CFP(), &adjusted_cd.cd, bh, mexp_search_super);
VALUE val = vm_sendish(ec, GET_CFP(), &adjusted_cd.cd, bh, mexp_search_super);

if (cd->cc != adjusted_cd.cd.cc && vm_cc_markable(adjusted_cd.cd.cc)) {
RB_OBJ_WRITE(GET_ISEQ(), &cd->cc, adjusted_cd.cd.cc);
}
}
else {
bh = vm_caller_setup_arg_block(ec, GET_CFP(), cd->ci, blockiseq, true);
val = vm_sendish(ec, GET_CFP(), cd, bh, mexp_search_super);
if (cd->cc != adjusted_cd.cd.cc && vm_cc_markable(adjusted_cd.cd.cc)) {
RB_OBJ_WRITE(GET_ISEQ(), &cd->cc, adjusted_cd.cd.cc);
}

VM_EXEC(ec, val);
Expand Down
20 changes: 18 additions & 2 deletions yjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9731,7 +9731,7 @@ fn gen_invokesuper(
return Some(status);
}

// Otherwise, fallback to dynamic dispatch using the interpreter's implementation of send
// Otherwise, fallback to dynamic dispatch using the interpreter's implementation of invokesuper
let blockiseq = jit.get_arg(1).as_iseq();
gen_send_dynamic(jit, asm, cd, unsafe { rb_yjit_sendish_sp_pops((*cd).ci) }, |asm| {
extern "C" {
Expand All @@ -9748,7 +9748,23 @@ fn gen_invokesuperforward(
jit: &mut JITState,
asm: &mut Assembler,
) -> Option<CodegenStatus> {
return gen_invokesuper(jit, asm);
// Generate specialized code if possible
let cd = jit.get_arg(0).as_ptr();
if let Some(status) = gen_invokesuper_specialized(jit, asm, cd) {
return Some(status);
}

// Otherwise, fallback to dynamic dispatch using the interpreter's implementation of invokesuperforward
let blockiseq = jit.get_arg(1).as_iseq();
gen_send_dynamic(jit, asm, cd, unsafe { rb_yjit_sendish_sp_pops((*cd).ci) }, |asm| {
extern "C" {
fn rb_vm_invokesuperforward(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
}
asm.ccall(
rb_vm_invokesuperforward as *const u8,
vec![EC, CFP, (cd as usize).into(), VALUE(blockiseq as usize).into()],
)
})
}

fn gen_invokesuper_specialized(
Expand Down
4 changes: 2 additions & 2 deletions yjit/yjit.mk
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ endif
RUST_VERSION = +1.58.0

# Gives quick feedback about YJIT. Not a replacement for a full test run.
.PHONY: yjit-smoke-test
yjit-smoke-test:
.PHONY: yjit-check
yjit-check:
ifneq ($(strip $(CARGO)),)
$(CARGO) test --all-features -q --manifest-path='$(top_srcdir)/yjit/Cargo.toml'
endif
Expand Down
26 changes: 26 additions & 0 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state)),
Insn::SendWithoutBlockDirect { cme, iseq, self_val, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(self_val), opnds!(args), &function.frame_state(*state)),
&Insn::InvokeSuper { cd, blockiseq, state, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state)),
Insn::InvokeBlock { cd, state, .. } => gen_invoke_block(jit, asm, *cd, &function.frame_state(*state)),
// Ensure we have enough room fit ec, self, and arguments
// TODO remove this check when we have stack args (we can use Time.new to test it)
Insn::InvokeBuiltin { bf, state, .. } if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) => return Err(*state),
Expand Down Expand Up @@ -1093,6 +1094,31 @@ fn gen_send_without_block_direct(
ret
}

/// Compile for invokeblock
fn gen_invoke_block(
jit: &mut JITState,
asm: &mut Assembler,
cd: *const rb_call_data,
state: &FrameState,
) -> lir::Opnd {
gen_incr_counter(asm, Counter::dynamic_send_count);

// Save PC and SP, spill locals and stack
gen_prepare_call_with_gc(asm, state);
gen_save_sp(asm, state.stack().len());
gen_spill_locals(jit, asm, state);
gen_spill_stack(jit, asm, state);

asm_comment!(asm, "call invokeblock");
unsafe extern "C" {
fn rb_vm_invokeblock(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE;
}
asm.ccall(
rb_vm_invokeblock as *const u8,
vec![EC, CFP, (cd as usize).into()],
)
}

/// Compile a dynamic dispatch for `super`
fn gen_invokesuper(
jit: &mut JITState,
Expand Down
Loading