Experiment: conserve in TrampolinedRewrite#1382
Conversation
|
Looks great and that gives me an idea: can't we even have a combinator like def rewrite[T <: AnyRef](t: T)(run: T => T): T that does what you already implemented? Then we can even write phases manually like: def rewrite(s: Stmt): Stmt = rewrite(s) {
case If(c, t, e) =>
If(c, rewrite(t), rewrite(e))
...
}and it would automagically work as long as one does not pattern match more than one level. This would strike the balance of making even @phischu happy, because he can still pattern match and reconstruct. We could even implement it like: def rewrite[T <: AnyRef](t: T)(run: T => T): T =
val res = run(t)
if res == t then t else reswhich would even work when pattern matching more than one level! It seems crazy at first since one might think that we structurally compare at every recursive level. However, if we are lucky case classes have a fast path with eq and thus this would end up pretty similar to your code. |
|
I also now have a version where One thing I like about it a lot is that it's opt-in: if you forget it, you "only" get worse perf. (And YIL/TIL that But I agree that the experience could be better, I just wanted to quickly put smth together |
|
This very simple thing seems to work https://scastie.scala-lang.org/ZhwS41kgRa2PYtCIYLILkA One could try to look at the generated equals instance with something like the "-Vprint:typer" option. |
|
Here are the results of my benchmarks:
The total runtimes were: NONE 3.41s+-0.55, RUNTIME 3.55s+-0.51, MACRO 3.20s+-0.42. So if at all, it should be a macro. |
|
Thanks for measuring! I am wondering:
I investigated the generated which does look good to me. First check |
If my benchmarks are to be believed, constructing the RHS of the |
Yep, I hope I did it correctly: $ effekt --no-optimize --build examples/benchmarks/other/unify.effekt
[warning] before vs after: true, trueclass Identity extends core.Tree.TrampolinedRewrite {
override def rewrite(m: ModuleDecl): ModuleDecl = m match {
case m @ ModuleDecl(path, includes, declarations, externs, definitions, exports) =>
m.rebuild(ModuleDecl(path, includes, declarations, externs, definitions, exports))
}
}
object Identity extends Phase[CoreTransformed, CoreTransformed] {
val phaseName: String = "identity"
def run(input: CoreTransformed)(using Context): Option[CoreTransformed] =
input match {
case CoreTransformed(source, tree, mod, core) =>
val identical = Context.timed("identity", source.name) {
new Identity().rewrite(core)
}
Context.warning(pretty"before vs after: ${core eq identical}, ${core == identical}")
Some(CoreTransformed(source, tree, mod, identical))
}
}
yep, plus #1381 where I want to benchmark it -- o/w that PR feels really slow |
|
Given your numbers, we talk about 10% slowdown, right? I wonder whether this starts to amortize only after using this pattern extensively over the code base. Other developers (personal communication) that also maintain object id reported that they needed to carefully inspect reconstruction in rewrites to actually observe benefits. |
|
I can't write the macro version correctly and can't prove that the I also considered a different variant (runtime): extension [T <: Product](original: T) {
def rebuild(trampolined: Trampoline[T]): Trampoline[T] =
trampolined.map { rebuilt =>
val arity = original.productArity
var i = 0
var changed = false
while (i < arity && !changed) {
if !(original.productElement(i).asInstanceOf[AnyRef] eq rebuilt.productElement(i).asInstanceOf[AnyRef]) then
changed = true
i += 1
}
if changed then rebuilt else original
}
}used as case s @ Stmt.If(cond, thn, els) => s.rebuild {
for {
cond2 <- rewrite(cond)
thn2 <- rewrite(thn)
els2 <- rewrite(els)
} yield Stmt.If(cond2, thn2, els2)
}but I don't think it's better than the one in this PR. |
I tried to put together a rewrite of TrampolinedRewrite that (tries to?) rebuild.
It currently always makes a very short lived allocation, this could be alleviated by using a proper macro.
Nevertheless, this is likely worth checking out. :)