Commitc13dc096Recorded15 Jan 2026Repositorysigil-app
docs: Update performance optimization documentation
Message
- Document hash table and type-specialized primitives implementations
- Update recommended progression table with actual results
- Add performance improvements to release-0.5.0 notes
Changed
notes/design/jit-support.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------
notes/design/release-0.5.0.md | 26 ++++++++++++++++++++++++++
2 files changed, 109 insertions(+), 40 deletions(-)Diff
notes/design/jit-support.mdmodified
@@ -1291,75 +1291,118 @@ A full implementation was created on the `feature/computed-goto` branch. Benchma
1291
- Implementing superinstructions that benefit from explicit dispatch control 1292
- Building toward a tracing JIT where dispatch control matters 1293
−1294
### Alternative 2: Inline Caching for Module Bindings+1294
### Alternative 2: Hash Table for Module Bindings ✓ IMPLEMENTED 1295
−1296
`OP_PUSH_BINDING` currently does a hash table lookup for every global variable access. Add inline caching:+1296
Module binding lookup was using O(n) linear scan, which became a major bottleneck since bindings are looked up on every global variable access at runtime (not just at import time).+1297
+1298
#### Implementation (January 2026)+1299
+1300
Added a 256-bucket hash table with chaining to the module structure. Symbols already have precomputed hashes, so this was straightforward:+1301
+1302
```c+1303
// In SigilBinding struct+1304
int hash_next; // Next binding index in hash chain, or -1 if end+1305
+1306
// In SigilModule struct+1307
int binding_hash[256]; // Hash buckets (indices into bindings array)+1308
+1309
// Lookup is now O(1) average case+1310
static inline int symbol_bucket(Value sym) {+1311
SigilSymbol *s = (SigilSymbol *)sigil_as_ptr(sym);+1312
return s->hash & (MODULE_BINDING_HASH_SIZE - 1);+1313
}+1314
```+1315
+1316
#### Benchmark Results+1317
+1318
| Metric | Before | After |+1319
|--------|--------|-------|+1320
| Time (dispatch benchmark) | ~5.9s | ~0.63s |+1321
| Avg iterations per lookup | 334 | 1.8 |+1322
| **Speedup** | - | **9.4x** |+1323
+1324
This was by far the most impactful optimization - a 9.4x speedup on dispatch-heavy code.+1325
+1326
Also added optional profiling via `SIGIL_PROFILE_BINDINGS=1` for future performance debugging.+1327
+1328
### Alternative 3: Inline Caching for Binding Cells+1329
+1330
With hash table lookup now fast, further optimization could cache the actual cell pointer to avoid even the hash lookup: 1331
1332
```c 1333
typedef struct { 1334
SigilModule *cached_module; 1335
uint32_t cached_version;−1302
Value cached_value;+1336
Value cached_cell; // Direct pointer to binding cell 1337
} BindingCache; 1338
1339
// In compiled bytecode, allocate cache slots 1340
// At runtime: 1341
if (cache->cached_module == current_module && 1342
cache->cached_version == module->version) {−1309
push(vm, cache->cached_value); // Fast path+1343
push(vm, cell_value(cache->cached_cell)); // Fast path 1344
} else {−1311
// Slow path: lookup and update cache+1345
// Slow path: hash lookup and update cache 1346
} 1347
``` 1348
1349
**Effort**: 2-4 weeks−1316
**Expected speedup**: 30-50% for code with many global references+1350
**Expected speedup**: 10-20% (less impactful now that hash lookup is fast) 1351
**Portability**: Fully portable 1352
−1319
### Alternative 3: Type-Specialized Primitives+1353
### Alternative 4: Type-Specialized Primitives ✓ IMPLEMENTED 1354
−1321
Add fast paths in the interpreter for common type patterns:+1355
Add fast paths in the interpreter for common type patterns.+1356
+1357
#### Implementation (January 2026)+1358
+1359
Added `BINARY_OP_FAST` and `COMPARE_OP_FAST` macros that check if both operands are fixnums and skip the double conversion: 1360
1361
```c−1324
// Current: generic add−1325
case OP_ADD: {−1326
Value b = pop(vm);−1327
Value a = pop(vm);−1328
if (!check_number(vm, a, "+") || !check_number(vm, b, "+"))−1329
goto error;−1330
push(vm, sigil_number(as_number(a) + as_number(b)));−1331
break;−1332
}−1333
−1334
// Improved: fast path for fixnums−1335
case OP_ADD: {−1336
Value b = pop(vm);−1337
Value a = pop(vm);−1338
if (is_fixnum(a) && is_fixnum(b)) {−1339
// Fast path: no overflow possible for most additions−1340
push(vm, sigil_fixnum(as_fixnum(a) + as_fixnum(b)));−1341
} else {−1342
// Slow path: handle flonums, bignums, type errors−1343
push(vm, sigil_generic_add(vm, a, b));−1344
}−1345
break;−1346
}+1362
#define BINARY_OP_FAST(op, name) \+1363
do { \+1364
Value b = pop(vm); \+1365
Value a = pop(vm); \+1366
if (sigil_is_fixnum(a) && sigil_is_fixnum(b)) { \+1367
int64_t ia = sigil_as_fixnum(a); \+1368
int64_t ib = sigil_as_fixnum(b); \+1369
int64_t result = ia op ib; \+1370
if (result >= SIGIL_FIXNUM_MIN && result <= SIGIL_FIXNUM_MAX) { \+1371
push(vm, sigil_fixnum(result)); \+1372
break; \+1373
} \+1374
push(vm, sigil_flonum((double)result)); \+1375
break; \+1376
} \+1377
/* Slow path: at least one flonum */ \+1378
if (!check_number(vm, a, name) || !check_number(vm, b, name)) \+1379
goto vm_error_abort; \+1380
push(vm, number_value(as_number(a) op as_number(b))); \+1381
} while (0) 1382
``` 1383
−1349
**Effort**: 1-2 weeks−1350
**Expected speedup**: 10-20% for numeric code+1384
Applied to: `OP_ADD`, `OP_SUB`, `OP_LT`, `OP_LE`, `OP_GT`, `OP_GE`, `OP_NUM_EQ`.+1385
+1386
#### Benchmark Results+1387
+1388
| Benchmark | Before | After | Improvement |+1389
|-----------|--------|-------|-------------|+1390
| Pure fixnum arithmetic | ~0.83s | ~0.73s | **~12%** |+1391
| Mixed workload | ~0.81s | ~0.79s | ~2-3% |+1392
1393
**Portability**: Fully portable 1394
1395
### Recommended Progression 1396
−1355
| Phase | Optimization | Cumulative Speedup | Complexity |−1356
|-------|-------------|-------------------|------------|−1357
| 1 | Computed goto dispatch | ~25% | Low |−1358
| 2 | Type-specialized primitives | ~40% | Low |−1359
| 3 | Inline caching for bindings | ~60% | Medium |−1360
| 4 | Method JIT (this document) | ~200-500% | High |+1397
| Phase | Optimization | Speedup | Status |+1398
|-------|-------------|---------|--------|+1399
| 1 | Computed goto dispatch | ~1-2% | Tested, not worth complexity |+1400
| 2 | Hash table for bindings | **9.4x** | ✓ Implemented |+1401
| 3 | Type-specialized primitives | ~12% | ✓ Implemented |+1402
| 4 | Inline caching for binding cells | ~10-20% | Not yet implemented |+1403
| 5 | Method JIT (this document) | ~200-500% | Future | 1404
−1362
**Recommendation**: Implement phases 1-3 first. They provide significant speedups with low risk and full portability. Only proceed to JIT if profiling shows the interpreter is still the bottleneck for real-world Sigil applications.+1405
**Findings**: The hash table optimization provided far more benefit than expected (9.4x vs anticipated 30-50%). Computed goto showed negligible improvement on modern CPUs. Type-specialized primitives provide ~12% improvement for pure fixnum arithmetic. 1406
1407
### When JIT Makes Sense 1408
notes/design/release-0.5.0.mdmodified
@@ -116,6 +116,32 @@ Common patterns that would benefit from built-in helpers:
116
base-url: "/users" target: "#user-list") 117
``` 118
+119
## Performance+120
+121
### Hash table for module binding lookup+122
+123
Replaced O(n) linear scan with O(1) hash lookup for module bindings. Symbols already have precomputed hashes, so we added a 256-bucket hash table with chaining to the module structure.+124
+125
Benchmark results on dispatch-heavy workload:+126
- Before (linear scan): ~5.9s+127
- After (hash table): ~0.63s+128
- **Speedup: 9.4x**+129
+130
The key insight from profiling was that module bindings are looked up on every global variable access at runtime, not just at import time. With modules having hundreds of bindings, the linear scan was a major bottleneck.+131
+132
Also added optional profiling via `SIGIL_PROFILE_BINDINGS=1` for future performance debugging.+133
+134
### Type-specialized fixnum arithmetic+135
+136
Added fast paths for arithmetic and comparison operations when both operands are fixnums. Skips double conversion for the common case of integer arithmetic.+137
+138
Benchmark results on pure fixnum arithmetic:+139
- Before: ~0.83s+140
- After: ~0.73s+141
- **Speedup: ~12%**+142
+143
Affects: `+`, `-`, `<`, `<=`, `>`, `>=`, `=`+144
145
## Other Items 146
147
(To be populated as 0.5.0 planning progresses)