mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 08:25:20 +08:00
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:
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
@@ -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));
|
||||
|
||||
@@ -48,6 +48,7 @@ firstline, predefined = __LINE__+1, %[\
|
||||
bt
|
||||
bt_locations
|
||||
call
|
||||
yield
|
||||
mesg
|
||||
exception
|
||||
locals
|
||||
|
||||
@@ -40,6 +40,7 @@ enum ruby_basic_operators {
|
||||
BOP_DEFAULT,
|
||||
BOP_PACK,
|
||||
BOP_INCLUDE_P,
|
||||
BOP_YIELD,
|
||||
|
||||
BOP_LAST_
|
||||
};
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Generated
+2
-1
@@ -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;
|
||||
|
||||
Generated
+2
-1
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user