Improve method table performance by rehashing at 75% load factor.

The method table (mt_tbl) in src/class.c previously only rehashed when it became completely full. With linear probing, this could lead to significant performance degradation for lookups and insertions as the table approached full capacity.

This change introduces a load factor (MT_LOAD_FACTOR_NUM/MT_LOAD_FACTOR_DEN, set to 3/4 or 0.75). The mt_put function now checks if adding a new element would cause the table's size to meet or exceed this load factor relative to its allocated capacity. If so, it triggers a rehash before inserting the new element.

This helps maintain more empty slots in the hash table, improving the average-case performance of linear probing and reducing the likelihood of worst-case scenarios. The existing initial allocation size and doubling strategy for rehashing are retained.
This commit is contained in:
google-labs-jules[bot]
2025-05-24 22:50:29 +00:00
parent 0c51a72bfe
commit 8a91c91a69
+31 -14
View File
@@ -16,6 +16,9 @@
#include <mruby/data.h>
#include <mruby/istruct.h>
#include <mruby/opcode.h>
#define MT_LOAD_FACTOR_NUM 3 // Numerator for load factor (represents 0.75)
#define MT_LOAD_FACTOR_DEN 4 // Denominator for load factor
#include <mruby/internal.h>
#include <mruby/presym.h>
@@ -102,41 +105,55 @@ mt_put(mrb_state *mrb, mt_tbl *t, mrb_sym sym, mrb_sym flags, union mt_ptr ptr)
{
int pos, start, dpos = -1;
if (t->alloc == 0) {
// Rehash if t->alloc is 0 (initial allocation)
// or if (t->size + 1) / t->alloc would meet or exceed the load factor.
// This check helps maintain performance by keeping the table from getting too full.
if (t->alloc == 0 || ((t->size + 1) * MT_LOAD_FACTOR_DEN >= t->alloc * MT_LOAD_FACTOR_NUM)) {
mt_rehash(mrb, t);
}
// These assignments MUST come AFTER any potential mt_rehash call,
// as mt_rehash changes t->ptr and t->alloc.
mrb_sym *keys = (mrb_sym*)&t->ptr[t->alloc];
union mt_ptr *vals = t->ptr;
int hash = mrb_int_hash_func(mrb, sym);
start = pos = hash & (t->alloc-1);
start = pos = hash & (t->alloc-1); // t->alloc is always a power of 2 after mt_rehash
for (;;) {
mrb_sym key = keys[pos];
if (MT_KEY_SYM(key) == sym) {
value_set:
if (MT_KEY_SYM(key) == sym) { // Key found, update existing method
value_set: // Label to jump to for setting the value
keys[pos] = MT_KEY(sym, flags);
vals[pos] = ptr;
return;
}
else if (key == MT_EMPTY) {
t->size++;
else if (key == MT_EMPTY) { // Empty slot found
if (dpos != -1) { // If we passed a deleted slot, use that one instead
pos = dpos;
}
t->size++; // A new element is being added, so increment size.
goto value_set;
}
else if (key == MT_DELETED && dpos < 0) {
dpos = pos;
else if (key == MT_DELETED) { // Deleted slot found
if (dpos == -1) { // Store the first deleted slot found
dpos = pos;
}
}
pos = (pos+1) & (t->alloc-1);
if (pos == start) { /* not found */
if (dpos > 0) {
t->size++;
pos = (pos+1) & (t->alloc-1); // Move to the next slot (linear probing)
if (pos == start) { // Cycled through all slots
if (dpos != -1) { // If we found a deleted slot during the cycle, use it
pos = dpos;
t->size++; // We are using a deleted slot for a new entry.
goto value_set;
}
/* no room */
// No empty or deleted slot found in the probe sequence. The table is full.
// This rehash is a fallback. The load factor check should ideally prevent this.
mt_rehash(mrb, t);
start = pos = hash & (t->alloc-1);
// Recalculate pointers and hash position after rehash
keys = (mrb_sym*)&t->ptr[t->alloc];
vals = t->ptr;
start = pos = hash & (t->alloc-1);
dpos = -1; // Reset dpos as the table structure changed
}
}
}