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)