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