mirror of
https://github.com/ruby/ruby.git
synced 2026-11-08 01:20:26 +08:00
Optimize Hash.new(capacity:)
Since the dynamic AR-table refactoring, `Hash.new(capacity:)` no longer allocate the ideal size for capacity `<= 8`, because the capacity allocation happens in `#initialize`, hence after the slot has been allocated. To solve that, we have to define a custom `.new` method, so we can parse the `capacity:` argument prior to the slot allocation, similar to how it's done for `String`. However defining a custom `new` disable the `opt_new` optimization, so to counter-act that we have to add a special branch in `opt_new` to handle hash specifically, and parse the `capacity:` argument without having to allocate a keyword arguments hash. | |compare-ruby|built-ruby| |:-------------------|-----------:|---------:| |new | 21.185M| 24.041M| | | -| 1.13x| |new_8 | 10.419M| 19.930M| | | -| 1.91x| |new_index_8 | 22.113k| 22.534k| | | -| 1.02x| |new_capa_index_8 | 21.786k| 22.455k| | | -| 1.03x| |to_h_8 | 21.510k| 20.655k| | | 1.04x| -| |new_with_capa_1k | 21.047k| 21.933k| | | -| 1.04x| |new_with_capa_100k | 209.952| 207.936| | | 1.01x| -|
This commit is contained in:
parent
c08130556a
commit
7142a3debb
Notes:
git
2026-10-29 10:15:06 +00:00
@ -1,16 +1,38 @@
|
||||
prelude: |
|
||||
has_hash_with_capa = Hash.instance_method(:initialize).parameters.include?([:key, :capacity])
|
||||
sym_8 = 1_000.times.map { |i| i.to_s.to_sym }
|
||||
strings_1k = 1_000.times.map { |i| -i.to_s.freeze }
|
||||
strings_100k = 100_000.times.map { |i| -i.to_s.freeze }
|
||||
|
||||
benchmark:
|
||||
new: Hash.new
|
||||
new: |
|
||||
Hash.new
|
||||
|
||||
new_8: |
|
||||
Hash.new(capacity: 8)
|
||||
|
||||
new_index_8: |
|
||||
h = {}
|
||||
sym_8.each do |x|
|
||||
h[x] = true
|
||||
end
|
||||
|
||||
new_capa_index_8: |
|
||||
h = Hash.new(capacity: 8)
|
||||
sym_8.each do |x|
|
||||
h[x] = true
|
||||
end
|
||||
|
||||
to_h_8: |
|
||||
sym_8.to_h { |k| [k, true] }
|
||||
|
||||
new_with_capa_1k: |
|
||||
h = has_hash_with_capa ? Hash.new(capacity: strings_1k.size) : {}
|
||||
h = Hash.new(capacity: strings_1k.size)
|
||||
strings_1k.each do |x|
|
||||
h[x] = true
|
||||
end
|
||||
|
||||
new_with_capa_100k: |
|
||||
h = has_hash_with_capa ? Hash.new(capacity: strings_100k.size) : {}
|
||||
h = Hash.new(capacity: strings_100k.size)
|
||||
strings_100k.each do |x|
|
||||
h[x] = true
|
||||
end
|
||||
|
||||
@ -18,7 +18,9 @@ firstline, predefined = __LINE__+1, %[\
|
||||
singleton_method_removed
|
||||
method_undefined
|
||||
singleton_method_undefined
|
||||
new
|
||||
length
|
||||
capacity
|
||||
size
|
||||
gets
|
||||
succ
|
||||
|
||||
39
hash.c
39
hash.c
@ -1590,6 +1590,12 @@ hash_alloc_capa(VALUE klass, size_t size)
|
||||
return hash_alloc(klass, 0, Qnil, size, false);
|
||||
}
|
||||
|
||||
VALUE
|
||||
rb_hash_alloc_capa(rb_len_t capa)
|
||||
{
|
||||
return hash_alloc_capa(rb_cHash, capa);
|
||||
}
|
||||
|
||||
VALUE
|
||||
rb_hash_alloc_copy(VALUE klass, VALUE src)
|
||||
{
|
||||
@ -1946,6 +1952,7 @@ rb_hash_init(rb_execution_context_t *ec, VALUE hash, VALUE capa_value, VALUE ifn
|
||||
{
|
||||
rb_hash_modify(hash);
|
||||
|
||||
|
||||
if (capa_value != INT2FIX(0)) {
|
||||
rb_len_t capa = NUM2LEN(capa_value);
|
||||
if (capa > 0 && RHASH_AR_TABLE_P(hash) && RHASH_SIZE(hash) == 0 &&
|
||||
@ -1970,6 +1977,37 @@ rb_hash_init(rb_execution_context_t *ec, VALUE hash, VALUE capa_value, VALUE ifn
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* :nodoc: */
|
||||
static VALUE
|
||||
rb_hash_s_new(int argc, VALUE *argv, VALUE klass)
|
||||
{
|
||||
if (klass != rb_cHash) {
|
||||
return rb_class_new_instance_pass_kw(argc, argv, klass);
|
||||
}
|
||||
|
||||
static ID keyword_ids[1];
|
||||
VALUE orig, opt, capacity = Qnil;
|
||||
VALUE kwargs[2];
|
||||
|
||||
rb_scan_args(argc, argv, "01:", &orig, &opt);
|
||||
if (NIL_P(opt)) {
|
||||
return rb_class_new_instance_pass_kw(argc, argv, klass);
|
||||
}
|
||||
|
||||
CONST_ID(keyword_ids[0], "capacity");
|
||||
rb_get_kwargs(opt, keyword_ids, 0, 1, kwargs);
|
||||
capacity = kwargs[0];
|
||||
|
||||
if (UNDEF_P(capacity) || !RB_TYPE_P(capacity, T_FIXNUM)) {
|
||||
return rb_class_new_instance_pass_kw(argc, argv, klass);
|
||||
}
|
||||
|
||||
rb_len_t capa = NUM2LEN(capacity);
|
||||
VALUE hash = hash_alloc_capa(klass, capa);
|
||||
rb_obj_call_init_kw(hash, argc, argv, RB_PASS_CALLED_KEYWORDS);
|
||||
return hash;
|
||||
}
|
||||
|
||||
static VALUE rb_hash_to_a(VALUE hash);
|
||||
static VALUE hash_new_with_bulk_insert(VALUE klass, rb_len_t argc, const VALUE *argv);
|
||||
|
||||
@ -7677,6 +7715,7 @@ Init_Hash(void)
|
||||
rb_include_module(rb_cHash, rb_mEnumerable);
|
||||
|
||||
rb_define_alloc_func(rb_cHash, empty_hash_alloc);
|
||||
rb_define_singleton_method(rb_cHash, "new", rb_hash_s_new, -1);
|
||||
rb_define_singleton_method(rb_cHash, "[]", rb_hash_s_create, -1);
|
||||
rb_define_singleton_method(rb_cHash, "try_convert", rb_hash_s_try_convert, 1);
|
||||
rb_define_method(rb_cHash, "initialize_copy", rb_hash_replace, 1);
|
||||
|
||||
17
insns.def
17
insns.def
@ -924,13 +924,26 @@ opt_new
|
||||
()
|
||||
// attr bool leaf = false;
|
||||
{
|
||||
VALUE argc = vm_ci_argc(cd->ci);
|
||||
int argc = vm_ci_argc(cd->ci);
|
||||
VALUE val = TOPN(argc);
|
||||
|
||||
// The bookkeeping slot should be empty.
|
||||
RUBY_ASSERT(TOPN(argc + 1) == Qnil);
|
||||
|
||||
if (vm_method_cfunc_is(GET_CFP(), cd, val, rb_class_new_instance_pass_kw)) {
|
||||
if (val == rb_cHash && BASIC_OP_UNREDEFINED_P(BOP_NEW, HASH_REDEFINED_OP_FLAG)) {
|
||||
RB_DEBUG_COUNTER_INC(opt_new_hit);
|
||||
|
||||
rb_len_t capa = 0;
|
||||
VALUE capacity = vm_call_get_kwarg(reg_cfp, cd, idCapacity);
|
||||
if (FIXNUM_P(capacity)) {
|
||||
capa = NUM2LEN(capacity);
|
||||
}
|
||||
|
||||
val = rb_hash_alloc_capa(capa);
|
||||
TOPN(argc) = val;
|
||||
TOPN(argc + 1) = val;
|
||||
}
|
||||
else if (vm_method_cfunc_is(GET_CFP(), cd, val, rb_class_new_instance_pass_kw)) {
|
||||
RB_DEBUG_COUNTER_INC(opt_new_hit);
|
||||
val = rb_obj_alloc(val);
|
||||
TOPN(argc) = val;
|
||||
|
||||
@ -27,6 +27,7 @@ enum ruby_basic_operators {
|
||||
BOP_GTGT,
|
||||
BOP_NOT,
|
||||
BOP_NEQ,
|
||||
BOP_NEW,
|
||||
BOP_MATCH,
|
||||
BOP_FREEZE,
|
||||
BOP_UMINUS,
|
||||
|
||||
@ -109,6 +109,7 @@ RUBY_SYMBOL_EXPORT_BEGIN
|
||||
/* hash.c (export) */
|
||||
VALUE rb_hash_delete_entry(VALUE hash, VALUE key);
|
||||
VALUE rb_ident_hash_new(void);
|
||||
VALUE rb_hash_alloc_capa(rb_len_t capa);
|
||||
int rb_hash_stlike_foreach(VALUE hash, st_foreach_callback_func *func, st_data_t arg);
|
||||
int rb_hash_stlike_foreach_with_replace(VALUE hash, st_foreach_check_callback_func *func, st_update_callback_func *replace, st_data_t arg);
|
||||
RUBY_SYMBOL_EXPORT_END
|
||||
|
||||
4
vm.c
4
vm.c
@ -2541,6 +2541,9 @@ add_opt_method_entry_bop(const rb_method_entry_t *me, ID mid, enum ruby_basic_op
|
||||
static void
|
||||
add_opt_method(VALUE klass, ID mid, enum ruby_basic_operators bop)
|
||||
{
|
||||
if (bop == BOP_NEW) {
|
||||
klass = rb_singleton_class(klass);
|
||||
}
|
||||
const rb_method_entry_t *me = rb_method_entry_at(klass, mid);
|
||||
|
||||
if (me && vm_redefinition_check_method_type(me)) {
|
||||
@ -2573,6 +2576,7 @@ vm_init_redefined_flag(void)
|
||||
|
||||
#define OP(mid_, bop_) (mid = id##mid_, bop = BOP_##bop_, ruby_vm_redefined_flag[bop] = 0)
|
||||
#define C(k) add_opt_method(rb_c##k, mid, bop)
|
||||
OP(New, NEW), (C(Hash));
|
||||
OP(PLUS, PLUS), (C(Integer), C(Float), C(String), C(Array));
|
||||
OP(MINUS, MINUS), (C(Integer), C(Float));
|
||||
OP(MULT, MULT), (C(Integer), C(Float));
|
||||
|
||||
@ -3984,6 +3984,31 @@ rb_splat_or_kwargs_p(const struct rb_callinfo *restrict ci)
|
||||
return IS_ARGS_SPLAT(ci) || IS_ARGS_KW_OR_KW_SPLAT(ci);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
vm_call_get_kwarg(rb_control_frame_t *reg_cfp, CALL_DATA cd, ID kw)
|
||||
{
|
||||
|
||||
VALUE sym = ID2SYM(kw);
|
||||
if (vm_ci_flag(cd->ci) & VM_CALL_KW_SPLAT) {
|
||||
VALUE keywords = TOPN(0);
|
||||
if (UNLIKELY(!RB_TYPE_P(keywords, T_HASH))) {
|
||||
TOPN(0) = keywords = rb_to_hash_type(keywords);
|
||||
}
|
||||
return rb_hash_lookup2(keywords, sym, Qundef);
|
||||
}
|
||||
else {
|
||||
const struct rb_callinfo_kwarg *kwarg = vm_ci_kwarg(cd->ci);
|
||||
if (kwarg) {
|
||||
for (int index = 0; index < kwarg->keyword_len; index++) {
|
||||
if (kwarg->keywords[index] == sym) {
|
||||
return TOPN(kwarg->keyword_len - index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Qundef;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling)
|
||||
{
|
||||
|
||||
31
yjit/src/cruby_bindings.inc.rs
generated
31
yjit/src/cruby_bindings.inc.rs
generated
@ -358,21 +358,22 @@ pub const BOP_GE: ruby_basic_operators = 18;
|
||||
pub const BOP_GTGT: ruby_basic_operators = 19;
|
||||
pub const BOP_NOT: ruby_basic_operators = 20;
|
||||
pub const BOP_NEQ: ruby_basic_operators = 21;
|
||||
pub const BOP_MATCH: ruby_basic_operators = 22;
|
||||
pub const BOP_FREEZE: ruby_basic_operators = 23;
|
||||
pub const BOP_UMINUS: ruby_basic_operators = 24;
|
||||
pub const BOP_MAX: ruby_basic_operators = 25;
|
||||
pub const BOP_MIN: ruby_basic_operators = 26;
|
||||
pub const BOP_HASH: ruby_basic_operators = 27;
|
||||
pub const BOP_CALL: ruby_basic_operators = 28;
|
||||
pub const BOP_AND: ruby_basic_operators = 29;
|
||||
pub const BOP_OR: ruby_basic_operators = 30;
|
||||
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_YIELD: ruby_basic_operators = 35;
|
||||
pub const BOP_LAST_: ruby_basic_operators = 36;
|
||||
pub const BOP_NEW: ruby_basic_operators = 22;
|
||||
pub const BOP_MATCH: ruby_basic_operators = 23;
|
||||
pub const BOP_FREEZE: ruby_basic_operators = 24;
|
||||
pub const BOP_UMINUS: ruby_basic_operators = 25;
|
||||
pub const BOP_MAX: ruby_basic_operators = 26;
|
||||
pub const BOP_MIN: ruby_basic_operators = 27;
|
||||
pub const BOP_HASH: ruby_basic_operators = 28;
|
||||
pub const BOP_CALL: ruby_basic_operators = 29;
|
||||
pub const BOP_AND: ruby_basic_operators = 30;
|
||||
pub const BOP_OR: ruby_basic_operators = 31;
|
||||
pub const BOP_CMP: ruby_basic_operators = 32;
|
||||
pub const BOP_DEFAULT: ruby_basic_operators = 33;
|
||||
pub const BOP_PACK: ruby_basic_operators = 34;
|
||||
pub const BOP_INCLUDE_P: ruby_basic_operators = 35;
|
||||
pub const BOP_YIELD: ruby_basic_operators = 36;
|
||||
pub const BOP_LAST_: ruby_basic_operators = 37;
|
||||
pub type ruby_basic_operators = u32;
|
||||
pub type rb_serial_t = ::std::os::raw::c_ulonglong;
|
||||
pub const METHOD_VISI_UNDEF: rb_method_visibility_t = 0;
|
||||
|
||||
31
zjit/src/cruby_bindings.inc.rs
generated
31
zjit/src/cruby_bindings.inc.rs
generated
@ -482,21 +482,22 @@ pub const BOP_GE: ruby_basic_operators = 18;
|
||||
pub const BOP_GTGT: ruby_basic_operators = 19;
|
||||
pub const BOP_NOT: ruby_basic_operators = 20;
|
||||
pub const BOP_NEQ: ruby_basic_operators = 21;
|
||||
pub const BOP_MATCH: ruby_basic_operators = 22;
|
||||
pub const BOP_FREEZE: ruby_basic_operators = 23;
|
||||
pub const BOP_UMINUS: ruby_basic_operators = 24;
|
||||
pub const BOP_MAX: ruby_basic_operators = 25;
|
||||
pub const BOP_MIN: ruby_basic_operators = 26;
|
||||
pub const BOP_HASH: ruby_basic_operators = 27;
|
||||
pub const BOP_CALL: ruby_basic_operators = 28;
|
||||
pub const BOP_AND: ruby_basic_operators = 29;
|
||||
pub const BOP_OR: ruby_basic_operators = 30;
|
||||
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_YIELD: ruby_basic_operators = 35;
|
||||
pub const BOP_LAST_: ruby_basic_operators = 36;
|
||||
pub const BOP_NEW: ruby_basic_operators = 22;
|
||||
pub const BOP_MATCH: ruby_basic_operators = 23;
|
||||
pub const BOP_FREEZE: ruby_basic_operators = 24;
|
||||
pub const BOP_UMINUS: ruby_basic_operators = 25;
|
||||
pub const BOP_MAX: ruby_basic_operators = 26;
|
||||
pub const BOP_MIN: ruby_basic_operators = 27;
|
||||
pub const BOP_HASH: ruby_basic_operators = 28;
|
||||
pub const BOP_CALL: ruby_basic_operators = 29;
|
||||
pub const BOP_AND: ruby_basic_operators = 30;
|
||||
pub const BOP_OR: ruby_basic_operators = 31;
|
||||
pub const BOP_CMP: ruby_basic_operators = 32;
|
||||
pub const BOP_DEFAULT: ruby_basic_operators = 33;
|
||||
pub const BOP_PACK: ruby_basic_operators = 34;
|
||||
pub const BOP_INCLUDE_P: ruby_basic_operators = 35;
|
||||
pub const BOP_YIELD: ruby_basic_operators = 36;
|
||||
pub const BOP_LAST_: ruby_basic_operators = 37;
|
||||
pub type ruby_basic_operators = u32;
|
||||
pub type rb_serial_t = ::std::os::raw::c_ulonglong;
|
||||
pub const METHOD_VISI_UNDEF: rb_method_visibility_t = 0;
|
||||
|
||||
@ -7035,30 +7035,10 @@ mod hir_opt_tests {
|
||||
PatchPoint StableConstantNames(0x1000, Hash)
|
||||
v13:ClassSubclass[Hash@0x1008] = Const Value(VALUE(0x1008))
|
||||
PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010)
|
||||
v42:HashExact = ObjectAllocClass Hash:VALUE(0x1008)
|
||||
PatchPoint NoSingletonClass(Hash@0x1008)
|
||||
PatchPoint MethodRedefined(Hash@0x1008, initialize@0x1038, cme:0x1040)
|
||||
v46:Fixnum[0] = Const Value(0)
|
||||
v96:Fixnum[0] = Const Value(0)
|
||||
v97:NilClass = Const Value(nil)
|
||||
PushInlineFrame :initialize, v42 (0x1068), num_args=1
|
||||
v62:TrueClass = Const Value(true)
|
||||
v80:CPtr = GetEP 0
|
||||
v81:CUInt64 = LoadField v80, :VM_ENV_DATA_INDEX_FLAGS@0x1088
|
||||
v82:CBool = IsBlockParamModified v81
|
||||
CondBranch v82, bb11(), bb12()
|
||||
bb11():
|
||||
v84:BasicObject = LoadField v80, :block@0x1089
|
||||
Jump bb13(v84)
|
||||
bb12():
|
||||
v86:BasicObject = GetBlockParam :block, l0, EP@4
|
||||
Jump bb13(v86)
|
||||
bb13(v79:BasicObject):
|
||||
v89:BasicObject = InvokeBuiltin rb_hash_init, v42, v46, v62, v62, v79
|
||||
PatchPoint NoEPEscape(initialize)
|
||||
PopInlineFrame
|
||||
PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010)
|
||||
v51:BasicObject = CCallVariadic v13, :Hash.new@0x1040
|
||||
CheckInterrupts
|
||||
Return v42
|
||||
Return v51
|
||||
");
|
||||
assert_snapshot!(inspect("test"), @"{}");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user