Commitb0479cdcRecorded15 Jan 2026Repositorysigil-app
perf: Add hash table for module binding lookup
Message
Replace O(n) linear scan with O(1) hash lookup for module bindings. Symbols already have precomputed hashes, so we just add a hash table with chaining to the module structure.
Benchmark results on dispatch-heavy workload: - Before (linear scan): ~5.9s - After (hash table): ~0.63s - Speedup: 9.4x
Also adds optional profiling (SIGILPROFILEBINDINGS=1) for future performance debugging.
Changed
packages/sigil-lib/include/sigil/sigil.h | 7 +++++++
packages/sigil-lib/src/module.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
packages/sigil-lib/src/vm.c | 25 +++++++++++++++++++++++++
3 files changed, 98 insertions(+), 12 deletions(-)Diff
packages/sigil-lib/include/sigil/sigil.hmodified
@@ -1042,8 +1042,12 @@ typedef struct {
1042
Value name; /* Symbol */ 1043
Value cell; /* SigilUpvalueCell containing the actual value */ 1044
bool owned; /* true = defined in this module, false = imported (read-only) */+1045
int hash_next; /* Next binding index in hash chain, or -1 if end */ 1046
} SigilBinding; 1047
+1048
/* Hash table size for module binding lookup (must be power of 2) */+1049
#define MODULE_BINDING_HASH_SIZE 256+1050
1051
/* Module macro binding - maps symbol to transformer closure */ 1052
typedef struct { 1053
Value name; /* Symbol */@@ -1080,6 +1084,9 @@ struct SigilModule {
1084
SigilBinding bindings[SIGIL_MODULE_MAX_BINDINGS]; 1085
int binding_count; 1086
+1087
/* Hash table for fast binding lookup (indices into bindings array, -1 = empty) */+1088
int binding_hash[MODULE_BINDING_HASH_SIZE];+1089
1090
/* Exports (what other modules can see) */ 1091
SigilExport exports[SIGIL_MODULE_MAX_EXPORTS]; 1092
int export_count;packages/sigil-lib/src/module.cmodified
@@ -8,6 +8,19 @@
8
#include <stdio.h> 9
#include <stdlib.h> 10
#include <string.h>+11
#include <time.h>+12
+13
/* Profiling counters (defined in vm.c, enabled via SIGIL_PROFILE_BINDINGS=1) */+14
extern int profile_bindings_enabled;+15
extern uint64_t binding_lookup_count;+16
extern uint64_t binding_lookup_iterations;+17
extern double binding_lookup_time_ns;+18
+19
static inline double module_get_time_ns(void) {+20
struct timespec ts;+21
clock_gettime(CLOCK_MONOTONIC, &ts);+22
return ts.tv_sec * 1e9 + ts.tv_nsec;+23
} 24
25
/* 26
* Helper to compare symbols by name@@ -21,6 +34,15 @@ static bool symbols_equal(Value a, Value b)
34
return memcmp(sa->name, sb->name, sa->length) == 0; 35
} 36
+37
/*+38
* Get hash bucket index for a symbol+39
*/+40
static inline int symbol_bucket(Value sym)+41
{+42
SigilSymbol *s = (SigilSymbol *)sigil_as_ptr(sym);+43
return s->hash & (MODULE_BINDING_HASH_SIZE - 1);+44
}+45
46
/* 47
* Convert a library name list to a canonical string. 48
* E.g., (foo bar baz) -> "(foo bar baz)"@@ -166,6 +188,12 @@ Value sigil_module_create(SigilVM *vm, Value name_list)
188
module->bindings[i].name = SIGIL_UNDEFINED; 189
module->bindings[i].cell = SIGIL_UNDEFINED; 190
module->bindings[i].owned = false;+191
module->bindings[i].hash_next = -1;+192
}+193
+194
/* Initialize binding hash table */+195
for (int i = 0; i < MODULE_BINDING_HASH_SIZE; i++) {+196
module->binding_hash[i] = -1; 197
} 198
199
/* Initialize exports */@@ -219,8 +247,10 @@ bool sigil_module_define(SigilVM *vm, SigilModule *module, Value name, Value val
247
{ 248
if (!sigil_is_symbol(name)) return false; 249
−222
/* Check if binding already exists */−223
for (int i = 0; i < module->binding_count; i++) {+250
int bucket = symbol_bucket(name);+251
+252
/* Check if binding already exists via hash chain */+253
for (int i = module->binding_hash[bucket]; i != -1; i = module->bindings[i].hash_next) { 254
if (symbols_equal(module->bindings[i].name, name)) { 255
if (!module->bindings[i].owned) { 256
/* Imported binding - shadow it with a new owned binding.@@ -243,9 +273,13 @@ bool sigil_module_define(SigilVM *vm, SigilModule *module, Value name, Value val
273
exit(1); 274
} 275
−246
module->bindings[module->binding_count].name = name;−247
module->bindings[module->binding_count].cell = make_cell(vm, value);−248
module->bindings[module->binding_count].owned = true;+276
int idx = module->binding_count;+277
module->bindings[idx].name = name;+278
module->bindings[idx].cell = make_cell(vm, value);+279
module->bindings[idx].owned = true;+280
/* Insert into hash chain */+281
module->bindings[idx].hash_next = module->binding_hash[bucket];+282
module->binding_hash[bucket] = idx; 283
module->binding_count++; 284
return true; 285
}@@ -260,8 +294,10 @@ bool sigil_module_import_binding(SigilModule *module, Value name, Value cell)
294
if (!sigil_is_symbol(name)) return false; 295
if (!sigil_is_upvalue_cell(cell)) return false; 296
−263
/* Check if binding already exists */−264
for (int i = 0; i < module->binding_count; i++) {+297
int bucket = symbol_bucket(name);+298
+299
/* Check if binding already exists via hash chain */+300
for (int i = module->binding_hash[bucket]; i != -1; i = module->bindings[i].hash_next) { 301
if (symbols_equal(module->bindings[i].name, name)) { 302
/* Replace existing binding with imported cell */ 303
module->bindings[i].cell = cell;@@ -277,9 +313,13 @@ bool sigil_module_import_binding(SigilModule *module, Value name, Value cell)
313
exit(1); 314
} 315
−280
module->bindings[module->binding_count].name = name;−281
module->bindings[module->binding_count].cell = cell;−282
module->bindings[module->binding_count].owned = false;+316
int idx = module->binding_count;+317
module->bindings[idx].name = name;+318
module->bindings[idx].cell = cell;+319
module->bindings[idx].owned = false;+320
/* Insert into hash chain */+321
module->bindings[idx].hash_next = module->binding_hash[bucket];+322
module->binding_hash[bucket] = idx; 323
module->binding_count++; 324
return true; 325
}@@ -292,7 +332,20 @@ Value sigil_module_lookup(SigilModule *module, Value name)
332
{ 333
if (!sigil_is_symbol(name)) return SIGIL_UNDEFINED; 334
−295
for (int i = 0; i < module->binding_count; i++) {+335
/* Check if profiling is enabled (lazy init) */+336
if (profile_bindings_enabled < 0) {+337
profile_bindings_enabled = getenv("SIGIL_PROFILE_BINDINGS") != NULL;+338
}+339
+340
if (profile_bindings_enabled) {+341
binding_lookup_count++;+342
}+343
+344
int bucket = symbol_bucket(name);+345
for (int i = module->binding_hash[bucket]; i != -1; i = module->bindings[i].hash_next) {+346
if (profile_bindings_enabled) {+347
binding_lookup_iterations++;+348
} 349
if (symbols_equal(module->bindings[i].name, name)) { 350
return module->bindings[i].cell; 351
}@@ -320,7 +373,8 @@ bool sigil_module_binding_owned(SigilModule *module, Value name)
373
{ 374
if (!sigil_is_symbol(name)) return false; 375
−323
for (int i = 0; i < module->binding_count; i++) {+376
int bucket = symbol_bucket(name);+377
for (int i = module->binding_hash[bucket]; i != -1; i = module->bindings[i].hash_next) { 378
if (symbols_equal(module->bindings[i].name, name)) { 379
return module->bindings[i].owned; 380
}packages/sigil-lib/src/vm.cmodified
@@ -8,6 +8,19 @@
8
#include <string.h> 9
#include <stdarg.h> 10
#include <math.h>+11
#include <time.h>+12
+13
/* Profiling counters (enabled via SIGIL_PROFILE_BINDINGS=1) */+14
int profile_bindings_enabled = -1; /* -1 = unchecked */+15
uint64_t binding_lookup_count = 0;+16
uint64_t binding_lookup_iterations = 0;+17
double binding_lookup_time_ns = 0;+18
+19
static inline double get_time_ns(void) {+20
struct timespec ts;+21
clock_gettime(CLOCK_MONOTONIC, &ts);+22
return ts.tv_sec * 1e9 + ts.tv_nsec;+23
} 24
25
#ifdef _WIN32 26
#include <io.h>@@ -5165,6 +5178,18 @@ void sigil_vm_destroy(SigilVM *vm)
5178
free(sigil_as_ptr(vm->vm_error_prompt_tag)); 5179
} 5180
+5181
/* Print binding lookup profiling stats if enabled */+5182
if (profile_bindings_enabled > 0 && binding_lookup_count > 0) {+5183
double avg_iters = (double)binding_lookup_iterations / binding_lookup_count;+5184
fprintf(stderr, "\n=== Binding Lookup Profile ===\n");+5185
fprintf(stderr, " Total lookups: %lu\n", (unsigned long)binding_lookup_count);+5186
fprintf(stderr, " Total iterations: %lu (%.1fM)\n",+5187
(unsigned long)binding_lookup_iterations,+5188
binding_lookup_iterations / 1e6);+5189
fprintf(stderr, " Avg iterations: %.1f per lookup\n", avg_iters);+5190
fprintf(stderr, "==============================\n");+5191
}+5192
5193
free(vm); 5194
} 5195