Skip to content

Commit 734e65c

Browse files
authored
feat(ir): absorb the magic-do thunk into early-split effect workers (#358)
* test(golden): pin the call-then-force shape of a multi-argument effect statement (#265) An Effect action of two or more real arguments is saturated at its real arity before magic-do runs, so the uncurrying split fires there and the thunk magic-do builds ends up inside an already n-ary worker. Every fully applied statement site therefore compiles to a worker call that allocates the thunk plus the run that forces it. Golden.EffectWorkerThunk pins that shape from four sides: a plain case whose wrapper is dead, one whose wrapper survives as a value, a let-bound local worker, and an action bound to a name and run later — the shape whose arity may not be widened. The eval oracle pins the runtime output. * bench(effect): add the two-argument effect-step macrobenchmark (#265) Bench.EffectStep drives a hot ST loop through a unary effect action, the case the late uncurry run splits. Bench.EffectStep2 is its two-argument sibling: with two real arguments the spine is already saturated when the early uncurry run measures it, so the split fires at the real arity and the thunk sits inside the worker, out of the late run reach. Every iteration pays the thunk allocation plus the call that forces it, which the committed counter oracles record as two NYI: bytecode FNEW trace aborts and an interpreted driver. * feat(ir): absorb the magic-do thunk into early-split effect workers (#265) An Effect/ST action of two or more real arguments is saturated at that arity before magic-do runs, so the uncurrying worker/wrapper split fires at the real arity and magic-do afterwards rewrites the worker's body into the nullary thunk an Effect value is. Every fully applied statement site then allocates that thunk and immediately forces it, two Lua calls with a closure in between. The late uncurry run cannot repair it: it splits manifest lambda chains, and the thunk is inside a worker that is already n-ary. absorbEffectThunk widens such a worker in place — the thunk's parameter joins its parameter list, its body becomes the worker's body — and rewrites every forced site w(a...)(run) to the single call w(a..., run), still an effect run by its trailing marker. Each wrapper delegating to the worker grows one parameter, which the delegate passes on, so a partial application still evaluates to a closure. The precondition is that every reference is a forced site or such a delegate, with at least one forced site: the Lua backend drops the worker's trailing unused parameter run, so any other reference the wider arity leaves under-applied would become a saturated Lua call and run the effect at construction time. Absorbing the marker also makes a recursive driver's self-call a genuine tail call, so the native-loop lowering turns it into a Lua while loop. Bench.EffectStep2 gains 1.22x under PUC Lua 5.1 and 1.28x under LuaJIT; Bench.EffectStep, whose two-argument driver is the same case, gains 1.10x/1.14x, and both trace reports lose their thunk-allocation aborts with the driver ending as a compiled ILOOP. The pass runs last, after the late uncurry run and the dce that follows it: nothing later moves a call, so the reference census is final, and the wrappers whose sites all went to their workers are already gone.
1 parent 22a7431 commit 734e65c

22 files changed

Lines changed: 1432 additions & 130 deletions

File tree

bench/goldens/fnew_Bench.EffectStep.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ function-body FNEW sites:
1313
Bench.EffectStep.lua:11
1414
Bench.EffectStep.lua:11
1515
Bench.EffectStep.lua:32
16-
Bench.EffectStep.lua:43
17-
Bench.EffectStep.lua:46
18-
Bench.EffectStep.lua:55
16+
Bench.EffectStep.lua:49
17+
Bench.EffectStep.lua:48
18+
Bench.EffectStep.lua:58
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
chunk: Bench.EffectStep2.lua
2+
runtime: LuaJIT 2.1.1741730670
3+
main-chunk FNEW: 11
4+
function-body FNEW: 12
5+
total FNEW: 23
6+
prototypes: 24
7+
function-body FNEW sites:
8+
Bench.EffectStep2.lua:3
9+
Bench.EffectStep2.lua:5
10+
Bench.EffectStep2.lua:5
11+
Bench.EffectStep2.lua:8
12+
Bench.EffectStep2.lua:9
13+
Bench.EffectStep2.lua:11
14+
Bench.EffectStep2.lua:11
15+
Bench.EffectStep2.lua:44
16+
Bench.EffectStep2.lua:43
17+
Bench.EffectStep2.lua:52
18+
Bench.EffectStep2.lua:51
19+
Bench.EffectStep2.lua:59
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
chunk: Bench.EffectStep2.lua
2+
runtime: LuaJIT 2.1.1741730670
3+
main-chunk TNEW+TDUP: 3
4+
function-body TNEW+TDUP: 1
5+
total TNEW+TDUP: 4
6+
prototypes: 24
7+
function-body TNEW+TDUP sites:
8+
Bench.EffectStep2.lua:8 TDUP

bench/goldens/trace_effect_step.txt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@ runtime: LuaJIT 2.1.1741730670
33
workload: n=100000 reps=4 result=1500000
44
aborts (distinct site -- reason):
55
Bench.EffectStep.lua:11 -- NYI: bytecode FNEW
6-
Bench.EffectStep.lua:43 -- NYI: bytecode FNEW
7-
Bench.EffectStep.lua:55 -- NYI: bytecode FNEW
6+
Bench.EffectStep.lua:58 -- NYI: bytecode FNEW
87
Bench.EffectStep.lua:8 -- NYI: bytecode FNEW
98
Bench.EffectStep.lua:9 -- NYI: bytecode FNEW
109
bytecode end state (J*=compiled, I*=blacklisted):
1110
Bench.EffectStep.lua:10 IFUNCF
1211
Bench.EffectStep.lua:11 IFUNCF
1312
Bench.EffectStep.lua:11 JFUNCF
1413
Bench.EffectStep.lua:16 IFUNCF
15-
Bench.EffectStep.lua:35 IFUNCF
16-
Bench.EffectStep.lua:36 IFUNCF
14+
Bench.EffectStep.lua:35 ILOOP
1715
Bench.EffectStep.lua:8 JFUNCF
1816
Bench.EffectStep.lua:9 IFUNCF
1917
Bench.EffectStep.lua:9 JFUNCF
20-
counts: aborts=5 compiled=3 blacklisted=6
18+
counts: aborts=4 compiled=3 blacklisted=5
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
spec: effect_step2
2+
runtime: LuaJIT 2.1.1741730670
3+
workload: n=100000 reps=4 result=1500000
4+
aborts (distinct site -- reason):
5+
Bench.EffectStep2.lua:11 -- NYI: bytecode FNEW
6+
Bench.EffectStep2.lua:59 -- NYI: bytecode FNEW
7+
Bench.EffectStep2.lua:8 -- NYI: bytecode FNEW
8+
Bench.EffectStep2.lua:9 -- NYI: bytecode FNEW
9+
bytecode end state (J*=compiled, I*=blacklisted):
10+
Bench.EffectStep2.lua:10 IFUNCF
11+
Bench.EffectStep2.lua:11 IFUNCF
12+
Bench.EffectStep2.lua:11 JFUNCF
13+
Bench.EffectStep2.lua:16 IFUNCF
14+
Bench.EffectStep2.lua:30 ILOOP
15+
Bench.EffectStep2.lua:8 JFUNCF
16+
Bench.EffectStep2.lua:9 IFUNCF
17+
Bench.EffectStep2.lua:9 JFUNCF
18+
counts: aborts=4 compiled=3 blacklisted=5

bench/macro/effect_step2.lua

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- A hot ST loop whose per-iteration step is a two-argument effect action,
2+
-- always fully applied and immediately run. Two real arguments saturate the
3+
-- spine before magicDo runs, so the uncurry split fires at the real arity and
4+
-- the thunk ends up inside the worker: every iteration allocates that closure
5+
-- and pays a second call to force it. With the thunk parameter absorbed into
6+
-- the worker the iteration is one n-ary call and no allocation.
7+
-- `effect_step.lua` is the unary sibling, which the late uncurry run covers.
8+
return {
9+
artifact = "Bench.EffectStep2",
10+
n = 100000,
11+
drive = function(mod, n)
12+
return mod.run(n)
13+
end,
14+
ideal = function(n)
15+
local acc = 0
16+
for _ = 1, n do acc = acc + 15 end
17+
return acc
18+
end,
19+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
### Added
2+
3+
- Effect/ST actions of two or more arguments no longer allocate a closure
4+
per executed statement (#265). Such an action is already saturated at
5+
its real arity when the uncurrying worker/wrapper split measures it, so
6+
the split fires there and magic-do only afterwards rewrites the worker's
7+
body into the nullary thunk an Effect value is — leaving every fully
8+
applied statement site to allocate that thunk and immediately force it.
9+
The late uncurry run cannot repair this: it splits manifest lambda
10+
chains, and the thunk sits inside a worker that is already n-ary. The
11+
new `absorbEffectThunk` pass instead widens the worker in place, moving
12+
the thunk's parameter onto its parameter list, so the site is one n-ary
13+
call:
14+
15+
```lua
16+
local Golden_EffectWorkerThunk_Test_report_S_w = function(tag, n)
17+
return function()
18+
local _ = Effect_Console_log(tag)()
19+
local _ = Effect_Console_log(Data_Show_showIntImpl(n))()
20+
return Effect_Console_log("-")()
21+
end
22+
end
23+
local _ = Golden_EffectWorkerThunk_Test_report_S_w("a", 1)()
24+
```
25+
26+
becomes
27+
28+
```lua
29+
local Golden_EffectWorkerThunk_Test_report_S_w = function(tag, n)
30+
local _ = Effect_Console_log(tag)()
31+
local _ = Effect_Console_log(Data_Show_showIntImpl(n))()
32+
return Effect_Console_log("-")()
33+
end
34+
local _ = Golden_EffectWorkerThunk_Test_report_S_w("a", 1)
35+
```
36+
37+
The action's curried wrapper grows one parameter so a partial
38+
application still evaluates to a closure. Taking the run marker into
39+
the call also makes a recursive driver's self-call a genuine tail call,
40+
which the native-loop lowering then turns into a Lua `while` — the
41+
driver of `Bench.EffectStep` went from
42+
43+
```lua
44+
local Bench_EffectStep_go_S_w
45+
Bench_EffectStep_go_S_w = function(i, ref)
46+
return function()
47+
local _ = Bench_EffectStep_step_S_w(ref)
48+
if i >= 1 and i ~= 1 then
49+
return Bench_EffectStep_go_S_w(i - 1, ref)()
50+
else
51+
return Control_Monad_ST_Internal_read(ref)()
52+
end
53+
end
54+
end
55+
```
56+
57+
to
58+
59+
```lua
60+
local Bench_EffectStep_go_S_w = function(i, ref)
61+
while true do
62+
local _ = Bench_EffectStep_step_S_w(ref)
63+
if i >= 1 and i ~= 1 then
64+
i, ref = i - 1, ref
65+
else
66+
return Control_Monad_ST_Internal_read(ref)()
67+
end
68+
end
69+
end
70+
```
71+
72+
1.22× faster under PUC Lua 5.1 and 1.28× under LuaJIT on the new
73+
`Bench.EffectStep2` macrobenchmark, and 1.10×/1.14× on the pre-existing
74+
`Bench.EffectStep`, whose two-argument driver is the same case; that
75+
spec's trace report loses one `NYI: bytecode FNEW` abort and its driver
76+
ends compiled as an `ILOOP` instead of interpreted as an `IFUNCF`.
77+
78+
A worker whose call is bound as an action value and run later keeps the
79+
call-then-force shape, as does any other reference the widened arity
80+
would leave under-applied: the Lua backend drops a trailing unused
81+
parameter run, so an under-applied worker call would run the effect at
82+
construction time. `Golden.EffectWorkerThunk` pins both sides, including
83+
a `let`-bound local worker.

0 commit comments

Comments
 (0)