Zulip Chat Archive
Stream: lean4
Topic: Suboptimal compilation of `bind` in `IO` monad
Jovan Gerbscheid (Sep 09 2024 at 14:15):
I was looking at some output of set_option trace.compiler.ir.result true
, and noticed that withReducible
was compiled into a more complicated term than the equivalent withReader ({· with config.transparency := .reducible})
. Minimizing this example leads to this suboptimally compiled function:
def foo (x : IO α) : IO α := do
let a ← x
pure a
which should be just the identity function, but gets compiled to
def foo._rarg (x_1 : obj) (x_2 : obj) : obj :=
let x_3 : obj := app x_1 x_2;
case x_3 : obj of
EStateM.Result.ok →
let x_4 : u8 := isShared x_3;
case x_4 : u8 of
Bool.false →
ret x_3
Bool.true →
let x_5 : obj := proj[0] x_3;
let x_6 : obj := proj[1] x_3;
inc x_6;
inc x_5;
dec x_3;
let x_7 : obj := ctor_0[EStateM.Result.ok] x_5 x_6;
ret x_7
EStateM.Result.error →
let x_8 : u8 := isShared x_3;
case x_8 : u8 of
Bool.false →
ret x_3
Bool.true →
let x_9 : obj := proj[0] x_3;
let x_10 : obj := proj[1] x_3;
inc x_10;
inc x_9;
dec x_3;
let x_11 : obj := ctor_1[EStateM.Result.error] x_9 x_10;
ret x_11
After inling the functions that are inlined by the compiler, we get the definition
def foo (x : IO α) : IO α :=
fun s => match x s with
| EStateM.Result.ok a s => EStateM.Result.ok a s
| EStateM.Result.error e s => EStateM.Result.error e s
Now we can see that the problem is that the match
expression is acting like the identity function, but this isn't seen by the compiler. If we replace either of | EStateM.Result.ok a s => EStateM.Result.ok a s
or | EStateM.Result.error e s => EStateM.Result.error e s
with | x => x
, then the corresponding match
branch gets compiled more efficiently, and if we do this for both branches, foo
is compiled as the identity function.
This optimization wouldn't just help to reduce away a combination of bind
and pure
. It would improve the compilation of every single bind
, because looking at the definition of bind
:
def bind (x : EStateM ε σ α) (f : α → EStateM ε σ β) : EStateM ε σ β := fun s =>
match x s with
| Result.ok a s => f a s
| Result.error e s => Result.error e s
we see that we still have | Result.error e s => Result.error e s
, and this part gets compiled to
EStateM.Result.error →
dec x_2;
let x_8 : u8 := isShared x_4;
case x_8 : u8 of
Bool.false →
ret x_4
Bool.true →
let x_9 : obj := proj[0] x_4;
let x_10 : obj := proj[1] x_4;
inc x_10;
inc x_9;
dec x_4;
let x_11 : obj := ctor_1[EStateM.Result.error] x_9 x_10;
ret x_11
which instead should just be
EStateM.Result.error →
dec x_2;
ret x_4
So I think that implementing this would make the compiled size of functions in IO
, MetaM
, etc. significantly smaller.
Specifically, what the compiler simplifier should do is, for every match branch, check if the result is exactly the thing that is being matched against (or a constructor of the same shape with the same fields), and in that case make this branch simply return that thing that is being matched against.
Henrik Böving (Sep 09 2024 at 14:27):
Many issues like this are known with the old compiler and tracked in the core issue tracker. They will eventually get fixed with the new compiler but there is no plan to invest time into the old one anymore. If you want you can add your thing onto the heap of TODOs
Mauricio Collares (Sep 09 2024 at 14:40):
Item 7 in the year 2 roadmap says "Bandwidth-permitting, we will also deliver a new code generator", if you want a rough timeline
Henrik Böving (Sep 09 2024 at 15:00):
Emphasis on bandwith-permitting^^
Eric Wieser (Sep 09 2024 at 15:01):
Does the "new compiler" already exist in any form?
Henrik Böving (Sep 09 2024 at 16:56):
Its been 70% done a couple of years ago mhm
Henrik Böving (Sep 09 2024 at 16:57):
70% done to the point of "it would work end to end" that is
Eric Wieser (Sep 09 2024 at 17:13):
Is this "70% done and in some unused file" or "70% done in a (private?) branch"?
Henrik Böving (Sep 09 2024 at 17:33):
Lean.Compiler.LCNF
Last updated: May 02 2025 at 03:31 UTC