Use getblockparamproxy for block#[], block#yield, block#===

Make block#[], block#yield, block#=== faster when inside a method and the block is a
block parameter. Treat it like an optimized method just like block#call was already.
This avoids the proc allocation unless the method is redefined.

```
Summary (YJIT disabled):

┌──────────────────────┬────────────┬────────────┬─────────────────────────────────┐
│          op          │   before   │   after    │             speedup             │
├──────────────────────┼────────────┼────────────┼─────────────────────────────────┤
│ block.call (control) │ 8.287M i/s │ 8.298M i/s │ ~1.00× (unchanged, as expected) │
├──────────────────────┼────────────┼────────────┼─────────────────────────────────┤
│ block[]              │ 3.786M i/s │ 8.289M i/s │ ~2.19×                          │
├──────────────────────┼────────────┼────────────┼─────────────────────────────────┤
│ block.yield          │ 3.760M i/s │ 8.269M i/s │ ~2.20×                          │
└──────────────────────┴────────────┴────────────┴─────────────────────────────────┘

Summary (YJIT enabled)

┌──────────────────────┬─────────────┬─────────────┬────────────────────┐
│          op          │   before    │    after    │      speedup       │
├──────────────────────┼─────────────┼─────────────┼────────────────────┤
│ block.call (control) │ 32.736M i/s │ 32.749M i/s │ ~1.00× (unchanged) │
├──────────────────────┼─────────────┼─────────────┼────────────────────┤
│ block[]              │ 9.112M i/s  │ 32.012M i/s │ ~3.51×             │
├──────────────────────┼─────────────┼─────────────┼────────────────────┤
│ block.yield          │ 9.119M i/s  │ 32.683M i/s │ ~3.58×             │
└──────────────────────┴─────────────┴─────────────┴────────────────────┘
```
This commit is contained in:
Luke Gruber authored and Luke Gruber committed 2026-07-07 12:49:29 -04:00
1 parent c51b1596c0
commit f66235e2bb
9 files changed
+100 -7
Notes: git 2026-07-07 16:49:59 +00:00

No files matched your search

+1 -1
View File
@@ -9600,7 +9600,7 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
if (type == NODE_CALL || type == NODE_OPCALL || type == NODE_QCALL) {
int idx, level;
if (mid == idCall &&
if ((mid == idCall || mid == idAREF || mid == idYield || mid == idEqq) &&
nd_type_p(get_nd_recv(node), NODE_LVAR) &&
iseq_block_param_id_p(iseq, RNODE_LVAR(get_nd_recv(node))->nd_vid, &idx, &level)) {
ADD_INSN2(recv, get_nd_recv(node), getblockparamproxy, INT2FIX(idx + VM_ENV_DATA_SIZE - 1), INT2FIX(level));
+1
View File
@@ -48,6 +48,7 @@ firstline, predefined = __LINE__+1, %[\
bt
bt_locations
call
yield
mesg
exception
locals
+1
View File
@@ -40,6 +40,7 @@ enum ruby_basic_operators {
BOP_DEFAULT,
BOP_PACK,
BOP_INCLUDE_P,
BOP_YIELD,
BOP_LAST_
};
+1 -1
View File
@@ -7500,7 +7500,7 @@ pm_compile_call_node(rb_iseq_t *iseq, const pm_call_node_t *node, LINK_ANCHOR *c
PUSH_INSN(ret, location, putself);
}
else {
if (method_id == idCall && PM_NODE_TYPE_P(node->receiver, PM_LOCAL_VARIABLE_READ_NODE)) {
if ((method_id == idCall || method_id == idAREF || method_id == idYield || method_id == idEqq) && PM_NODE_TYPE_P(node->receiver, PM_LOCAL_VARIABLE_READ_NODE)) {
const pm_local_variable_read_node_t *read_node_cast = (const pm_local_variable_read_node_t *) node->receiver;
uint32_t node_id = node->receiver->node_id;
int idx, level;
+76
View File
@@ -945,6 +945,82 @@ class TestRubyOptimization < Test::Unit::TestCase
END
end
def bptest_call(&b) = b.call(1, 2)
def bptest_aref(&b) = b[1, 2]
def bptest_dot_yield(&b) = b.yield(1, 2)
def bptest_eqq(&b) = b === 2
def test_block_param_proxy_aref_yield_and_eqq
%i[bptest_call bptest_aref bptest_dot_yield bptest_eqq].each do |name|
assert_include(RubyVM::InstructionSequence.of(method(name)).disasm,
"getblockparamproxy", "#{name} should use the block param proxy")
end
assert_equal(3, bptest_call {|a, b| a + b})
assert_equal(3, bptest_aref {|a, b| a + b})
assert_equal(3, bptest_dot_yield {|a, b| a + b})
assert_equal(3, bptest_eqq {|a| a + 1})
end
def test_block_param_proxy_should_not_create_objects
assert_separately [], <<-END
def viacall(&b) = b.call
def viaaref(&b) = b[]
def viayield(&b) = b.yield
def viaeqq(&b) = b === 1
viacall{}; viaaref{}; viayield{}; viaeqq{} # warm up caches
h1 = {}; h2 = {}
ObjectSpace.count_objects(h1)
GC.start; GC.disable
ObjectSpace.count_objects(h1)
1000.times { viacall{}; viaaref{}; viayield{}; viaeqq{} }
ObjectSpace.count_objects(h2)
# The proxy avoids materializing a Proc (T_DATA), so allocations must
# not scale with the number of calls.
assert_operator(h2[:T_DATA] - h1[:T_DATA], :<, 1000)
END
end
def test_block_param_proxy_honors_redefinition
assert_separately [], <<-END
$VERBOSE = nil # silence "method redefined" warnings
def viacall(&b) = b.call
def viaaref(&b) = b[]
def viayield(&b) = b.yield
def viaeqq(&b) = b === 1
assert_equal(:orig, viacall { :orig })
assert_equal(:orig, viaaref { :orig })
assert_equal(:orig, viayield { :orig })
assert_equal(:orig, viaeqq { :orig })
class Proc
def [](*) = :redef_aref
end
assert_equal(:redef_aref, viaaref { :orig }, "redefining Proc#[] must deopt blk[]")
assert_equal(:orig, viacall { :orig }, "Proc#[] redefinition must not affect blk.call")
assert_equal(:orig, viayield { :orig }, "Proc#[] redefinition must not affect blk.yield")
assert_equal(:orig, viaeqq { :orig }, "Proc#[] redefinition must not affect blk === x")
class Proc
def yield(*) = :redef_yield
end
assert_equal(:redef_yield, viayield { :orig }, "redefining Proc#yield must deopt blk.yield")
assert_equal(:orig, viacall { :orig }, "Proc#yield redefinition must not affect blk.call")
class Proc
def ===(*) = :redef_eqq
end
assert_equal(:redef_eqq, viaeqq { :orig }, "redefining Proc#=== must deopt blk === x")
assert_equal(:orig, viacall { :orig }, "Proc#=== redefinition must not affect blk.call")
class Proc
def call(*) = :redef_call
end
assert_equal(:redef_call, viacall { :orig }, "redefining Proc#call must deopt blk.call")
END
end
def test_peephole_optimization_without_trace
assert_ruby_status [], <<-END
RubyVM::InstructionSequence.compile_option = {trace_instruction: false}
+7 -2
View File
@@ -2462,6 +2462,7 @@ vm_init_redefined_flag(void)
OP(Min, MIN), (C(Array));
OP(Hash, HASH), (C(Array));
OP(Call, CALL), (C(Proc));
OP(Yield, YIELD), (C(Proc));
OP(And, AND), (C(Integer));
OP(Or, OR), (C(Integer));
OP(NilP, NIL_P), (C(NilClass));
@@ -2502,6 +2503,7 @@ vm_redefinition_bop_for_id(ID mid)
OP(Min, MIN);
OP(Hash, HASH);
OP(Call, CALL);
OP(Yield, YIELD);
OP(And, AND);
OP(Or, OR);
OP(NilP, NIL_P);
@@ -4621,8 +4623,11 @@ Init_VM(void)
vm_init_redefined_flag();
rb_block_param_proxy = rb_obj_alloc(rb_cObject);
rb_add_method_optimized(rb_singleton_class(rb_block_param_proxy), idCall,
OPTIMIZED_METHOD_TYPE_BLOCK_CALL, 0, METHOD_VISI_PUBLIC);
VALUE proxy_singleton = rb_singleton_class(rb_block_param_proxy);
rb_add_method_optimized(proxy_singleton, idCall, OPTIMIZED_METHOD_TYPE_BLOCK_CALL, 0, METHOD_VISI_PUBLIC);
rb_add_method_optimized(proxy_singleton, idAREF, OPTIMIZED_METHOD_TYPE_BLOCK_CALL, 0, METHOD_VISI_PUBLIC);
rb_add_method_optimized(proxy_singleton, idYield, OPTIMIZED_METHOD_TYPE_BLOCK_CALL, 0, METHOD_VISI_PUBLIC);
rb_add_method_optimized(proxy_singleton, idEqq, OPTIMIZED_METHOD_TYPE_BLOCK_CALL, 0, METHOD_VISI_PUBLIC);
rb_obj_freeze(rb_block_param_proxy);
rb_vm_register_global_object(rb_block_param_proxy);
+9 -1
View File
@@ -4652,7 +4652,15 @@ vm_call_opt_block_call(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
VALUE block_handler = VM_ENV_BLOCK_HANDLER(VM_CF_LEP(reg_cfp));
const struct rb_callinfo *ci = calling->cd->ci;
if (BASIC_OP_UNREDEFINED_P(BOP_CALL, PROC_REDEFINED_OP_FLAG)) {
enum ruby_basic_operators bop;
switch (vm_ci_mid(ci)) {
case idAREF: bop = BOP_AREF; break;
case idYield: bop = BOP_YIELD; break;
case idEqq: bop = BOP_EQQ; break;
default: bop = BOP_CALL; break;
}
if (BASIC_OP_UNREDEFINED_P(bop, PROC_REDEFINED_OP_FLAG)) {
return vm_invoke_block_opt_call(ec, reg_cfp, calling, ci, block_handler);
}
else {
+2 -1
View File
@@ -337,7 +337,8 @@ pub const BOP_CMP: ruby_basic_operators = 31;
pub const BOP_DEFAULT: ruby_basic_operators = 32;
pub const BOP_PACK: ruby_basic_operators = 33;
pub const BOP_INCLUDE_P: ruby_basic_operators = 34;
pub const BOP_LAST_: ruby_basic_operators = 35;
pub const BOP_YIELD: ruby_basic_operators = 35;
pub const BOP_LAST_: ruby_basic_operators = 36;
pub type ruby_basic_operators = u32;
pub type rb_serial_t = ::std::os::raw::c_ulonglong;
pub const imemo_env: imemo_type = 0;
+2 -1
View File
@@ -439,7 +439,8 @@ pub const BOP_CMP: ruby_basic_operators = 31;
pub const BOP_DEFAULT: ruby_basic_operators = 32;
pub const BOP_PACK: ruby_basic_operators = 33;
pub const BOP_INCLUDE_P: ruby_basic_operators = 34;
pub const BOP_LAST_: ruby_basic_operators = 35;
pub const BOP_YIELD: ruby_basic_operators = 35;
pub const BOP_LAST_: ruby_basic_operators = 36;
pub type ruby_basic_operators = u32;
pub type rb_serial_t = ::std::os::raw::c_ulonglong;
#[repr(C)]