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:
Jean Boussier 2026-10-26 12:12:19 +01:00 committed by Jean Boussier
parent c08130556a
commit 7142a3debb
Notes: git 2026-10-29 10:15:06 +00:00
11 changed files with 148 additions and 59 deletions

View File

@ -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

View File

@ -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
View File

@ -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);

View File

@ -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;

View File

@ -27,6 +27,7 @@ enum ruby_basic_operators {
BOP_GTGT,
BOP_NOT,
BOP_NEQ,
BOP_NEW,
BOP_MATCH,
BOP_FREEZE,
BOP_UMINUS,

View File

@ -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
View File

@ -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));

View File

@ -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)
{

View File

@ -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;

View File

@ -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;

View File

@ -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"), @"{}");
}