Skip to content

Commit cd8f350

Browse files
byrootpeterzhu2118
authored andcommitted
Dynamic ar_table size
Instead of always allocating `ar_table` backed hashes in 160B slots for them to have capacity of `RHASH_AR_TABLE_MAX_SIZE` (`8`), they can now be allocated in any slot size between `64B` and `160B`. `64B` is the minimum slot size that still allows transitioning to an `st_table` if necessary. The assumption is that there is a large number of literal hashes and keyword argument hashes being built, and the overwhelming majority of them aren't appended to. When this assumption hold, this patch saves memory and reduce GC pressure by allocating in a smaller slot than before. However when the assumption doesn't hold, it has the oppositve effect as the Hash will transition to an `st_table` when before it may have stayed an `ar_table`. Currently the main blocked for this change is that the compiler has a tendency to generate code that make the final Hash size unknown. For instance: `{a: 4, b: 4 + 1}` is compiled as: ``` 0000 duphash {a: 4} ( 1)[Li] 0002 putspecialobject 1 0004 swap 0005 putobject :b 0007 putobject 4 0009 putobject_INT2FIX_1_ 0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] 0012 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE> ``` Here `duphash` has no information about how large of a hash it needs to allocate. For this patch to be more efficient the compiler should generate something like: ``` 0000 putobject {a: 4} ( 1)[Li] 0002 putspecialobject 1 0004 swap 0005 putobject :b 0007 putobject 4 0009 putobject_INT2FIX_1_ 0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] 0012 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE> ``` So that it's `hash_merge_ptr` that is responsible for allocating the final hash, given it does have a much better clue of how large it would need to be. Demo: ```ruby require 'objspace' def size(hash) puts "size: #{hash.size}\tmemsize: #{ObjectSpace.memsize_of(hash)}" end size({}) size(a:1) size(a:1, b:2) size(a:1, b:2, c:3) size(a:1, b:2, c:3, d:4) size(a:1, b:2, c:3, d:4, e:5) size(a:1, b:2, c:3, d:4, e:5, f:6) size(a:1, b:2, c:3, d:4, e:5, f:6, g:7) size(a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8) size(a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9) ``` Before: ``` size: 0 memsize: 160 size: 1 memsize: 160 size: 2 memsize: 160 size: 3 memsize: 160 size: 4 memsize: 160 size: 5 memsize: 160 size: 6 memsize: 160 size: 7 memsize: 160 size: 8 memsize: 160 size: 9 memsize: 464 ``` After: ``` size: 0 memsize: 64 size: 1 memsize: 64 size: 2 memsize: 64 size: 3 memsize: 80 size: 4 memsize: 96 size: 5 memsize: 128 size: 6 memsize: 128 size: 7 memsize: 160 size: 8 memsize: 160 size: 9 memsize: 448 ```
1 parent 4b30340 commit cd8f350

7 files changed

Lines changed: 262 additions & 55 deletions

File tree

gc.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,10 +3351,17 @@ rb_gc_obj_optimal_size(VALUE obj)
33513351

33523352
case T_HASH:
33533353
{
3354-
if (RB_OBJ_FROZEN(obj) && RHASH_AR_TABLE_P(obj)) {
3355-
return sizeof(struct RHash) + offsetof(ar_table, pairs) + RHASH_AR_TABLE_BOUND(obj) * sizeof(ar_table_pair);
3354+
const size_t st_size = sizeof(struct RHash) + sizeof(st_table);
3355+
if (RHASH_ST_TABLE_P(obj)) {
3356+
return st_size;
33563357
}
3357-
return sizeof(struct RHash) + (RHASH_ST_TABLE_P(obj) ? sizeof(st_table) : sizeof(ar_table));
3358+
3359+
const size_t ar_size = sizeof(struct RHash) + offsetof(ar_table, pairs) + RHASH_AR_TABLE_BOUND(obj) * sizeof(ar_table_pair);
3360+
if (OBJ_FROZEN(obj) || ar_size > st_size) {
3361+
return ar_size;
3362+
}
3363+
3364+
return st_size;
33583365
}
33593366

33603367
default:

0 commit comments

Comments
 (0)