blob: 2eb5f6aaaffb276467ecd78e96e3e1b61bf55fc3 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
# Nock Interpreter Implementations
We have three Nock interpreters with different tradeoffs:
## 1. `nock.ml` - Hybrid (Trampoline + Recursion)
**Strategy**: Uses `loop()` trampoline for some tail calls, but still has recursive `nock_on` calls for distribution and op2.
**Pros**:
- Fastest for simple/medium computations
- Uses OCaml's native call stack for performance
**Cons**:
- **Stack overflow on deep computations** (solid pill lifecycle)
- Requires `OCAMLRUNPARAM='l=100M'` for deep recursion
- Not truly constant stack space
**Performance**: ⭐⭐⭐⭐⭐ (best when it works)
**Use case**: Default interpreter for normal operations
---
## 2. `nock_iter.ml` - Fully Iterative (Work/Result Stacks)
**Strategy**: Zero recursion. Uses explicit work and result stacks stored as lists.
**Pros**:
- **Never stack overflows** - O(1) stack space
- No `OCAMLRUNPARAM` needed
- Predictable memory usage
**Cons**:
- Slower than `nock.ml` (list push/pop overhead)
- More complex to understand
**Performance**: ⭐⭐⭐⭐ (solid, consistent)
**Use case**: When you need guaranteed no-overflow (e.g., untrusted inputs)
---
## 3. `nock_tail.ml` - Continuation-Passing Style (CPS)
**Strategy**: Uses explicit continuations as a queue. Each computation knows what to do with its result.
**Pros**:
- **Never stack overflows** - O(1) stack space
- No `OCAMLRUNPARAM` needed
- Elegant functional style
- Easy to add logging/tracing/debugging
**Cons**:
- Slower than `nock.ml` (continuation allocation overhead)
- Larger heap usage (continuations on heap)
**Performance**: ⭐⭐⭐⭐ (solid, elegant)
**Use case**: When you want beautiful code that never overflows
---
## Benchmark Results
### Simple Operations (1000× increment)
```
nock.ml 0.0001s ← Fastest
nock_iter.ml 0.0002s ← 2x slower
nock_tail.ml 0.0002s ← 2x slower
```
### Complex Operations (ivory pill lifecycle)
```
nock.ml Stack overflow! ✗ (without OCAMLRUNPARAM)
nock_iter.ml Works! ✓ (slow but works)
nock_tail.ml Works! ✓ (slow but works)
```
---
## Recommendation
**For production**: Use `nock.ml` with `OCAMLRUNPARAM='l=100M'` for best performance.
**For robustness**: Use `nock_iter.ml` or `nock_tail.ml` when you can't afford stack overflows.
**For learning/hacking**: Study `nock_tail.ml` - it's the clearest implementation of CPS.
---
## Technical Notes
### Why is `nock.ml` faster?
It uses OCaml's native call stack for tail calls, which is highly optimized in the OCaml runtime. The `loop()` pattern compiles to tight machine code.
### Why are `nock_iter` and `nock_tail` slower?
Every step requires:
- List allocation (`::` operator)
- Pattern matching on list head
- List garbage collection
Native call stack operations are just a stack pointer adjustment - orders of magnitude faster.
### The Fundamental Tradeoff
- **Stack-based** = Fast but can overflow
- **Heap-based** = Slower but never overflows
This is a classic CS tradeoff. You can't have both.
---
## Future Work
- **Jets**: Hand-optimized implementations of common Hoon stdlib functions
- **JIT compilation**: Compile hot paths to native code
- **Parallel execution**: Use `nock_parallel.ml` with Domainslib
- **Memory pooling**: Reuse continuation/stack objects instead of allocating new ones
|