Skip to content

Commit c724f89

Browse files
committed
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 79209b1 commit c724f89

15 files changed

Lines changed: 913 additions & 306 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

bench/goldens/fnew_Bench.EffectStep2.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ function-body FNEW sites:
1212
Bench.EffectStep2.lua:9
1313
Bench.EffectStep2.lua:11
1414
Bench.EffectStep2.lua:11
15-
Bench.EffectStep2.lua:29
16-
Bench.EffectStep2.lua:40
15+
Bench.EffectStep2.lua:44
1716
Bench.EffectStep2.lua:43
18-
Bench.EffectStep2.lua:49
19-
Bench.EffectStep2.lua:56
17+
Bench.EffectStep2.lua:52
18+
Bench.EffectStep2.lua:51
19+
Bench.EffectStep2.lua:59

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

bench/goldens/trace_effect_step2.txt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,16 @@ runtime: LuaJIT 2.1.1741730670
33
workload: n=100000 reps=4 result=1500000
44
aborts (distinct site -- reason):
55
Bench.EffectStep2.lua:11 -- NYI: bytecode FNEW
6-
Bench.EffectStep2.lua:29 -- NYI: bytecode FNEW
7-
Bench.EffectStep2.lua:40 -- NYI: bytecode FNEW
8-
Bench.EffectStep2.lua:56 -- NYI: bytecode FNEW
6+
Bench.EffectStep2.lua:59 -- NYI: bytecode FNEW
97
Bench.EffectStep2.lua:8 -- NYI: bytecode FNEW
108
Bench.EffectStep2.lua:9 -- NYI: bytecode FNEW
119
bytecode end state (J*=compiled, I*=blacklisted):
1210
Bench.EffectStep2.lua:10 IFUNCF
1311
Bench.EffectStep2.lua:11 IFUNCF
1412
Bench.EffectStep2.lua:11 JFUNCF
1513
Bench.EffectStep2.lua:16 IFUNCF
16-
Bench.EffectStep2.lua:17 IFUNCF
17-
Bench.EffectStep2.lua:32 IFUNCF
18-
Bench.EffectStep2.lua:33 IFUNCF
14+
Bench.EffectStep2.lua:30 ILOOP
1915
Bench.EffectStep2.lua:8 JFUNCF
2016
Bench.EffectStep2.lua:9 IFUNCF
2117
Bench.EffectStep2.lua:9 JFUNCF
22-
counts: aborts=6 compiled=3 blacklisted=7
18+
counts: aborts=4 compiled=3 blacklisted=5
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)