-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Rework tree transforms #3366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework tree transforms #3366
Conversation
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3366/ to see the changes. Benchmarks is based on merging with master (0438e48) |
test performance please |
performance test scheduled: 2 job(s) in queue, 1 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3366/ to see the changes. Benchmarks is based on merging with master (e1365d9) |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3366/ to see the changes. Benchmarks is based on merging with master (e1365d9) |
Could you give a list of differences between TreeTransformer and SuperPhase? |
* This is an evolution of the previous "TreeTransformers.scala", which was written by @DarkDimius and | ||
* is described in his thesis. | ||
*/ | ||
object SuperPhase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bikeshed: I'm not a big fan of the name SuperPhase
since super
already has a very specific meaning in Scala. Some possible alternatives: MegaPhase
,FusedPhase
, JoinedPhase
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, let's go with MegaPhase. I like the sound of that :-)
} | ||
|
||
{ implicit val ctx = nestedCtx | ||
tag match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be tag: @switch
|
||
/** Transform full tree using all phases in this group that have idxInGroup >= start */ | ||
def transformTree(tree: Tree, start: Int)(implicit ctx: Context): Tree = { | ||
val tag = tree.tag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could avoid the megamorphic dispatch here if we had something like:
abstract class Tree[-T >: Untyped](val tag: TreeTag) ...
// ...
class Ident(...) extends Tree(Tag.Ident) with ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but at the cost of one field per tree. It might still be worth it, though. One needs to experiment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have benchmarks now that support this optimization?
I should be able to review this on Wednesday. |
The motivation from the change came from me trying to understand (again) TreeTransformers.scala when I did the work on running all transforms at group end. I noted that things were very convoluted, and repetitive and that, as a consequence it was hard to find the right way to do changes. For instance there was the issue that TreeTransforms, but not TeeTransformers used cpyBetweenPhases. I believe that was not intentional (or was it?), but rather caused by the fact that due to its length, TreeTransformers is hard to maintain. The new set of changes cuts the size of TreeTransformers to roughly half of what it was before and clearly separates actual implementation logic (~200loc) from initialization code. There was also the motivation to play with the implementation a bit to see whether we could wring more performance out of it. To be able to judge performance, I believe we have to get #3350 in first and then compare. I am not yet sure performance is better or worse. But to play with it I felt it was necessary to simplify it first. In detail, the changes can be summarized as follows: MegaPhase is a simplified version of TreeTransformer
The simplifications are largely due to three changes
|
For both review and performance comparisons it would be best if we got #3350 in first. |
a51f0fb
to
e73974a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is another base class MacroTransform
in the code, which is only used once. I think it's not justified to have a separate concept for it. Is it possible to get rid of it as well?
def newLocation[T](): (Location[T], Store) = { | ||
val elems1 = new Array[AnyRef](elems.length + 1) | ||
System.arraycopy(elems, 0, elems1, 0, elems.length) | ||
(new Location(elems.length), new Store(elems1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For another PR: minor optimisations here to double the size of array and keep track of size?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be complicated because arrays are purely functional. I.e you have to copy anyway on an update.
|
||
final val TypedSplice = 40 | ||
final val ModuleDef = 41 | ||
final val ParsedTry = 42 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add a comment describing which tag numbers can be reused and why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 It's not clear for me why are they reused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no re-use: NumTypeTreeTags is a counter, not a tag. I'll add a comment to make that clearer.
type TreeTag = Int | ||
|
||
object Tag { | ||
final val Ident = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use the same tags that are used in tasty. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could try, but then the switches will not be dense anymore, so will most likely not be implemented by a table switch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, in that case unpickled trees would not have tags, so we could not use tags for e.g. typer.
@@ -357,6 +425,7 @@ object Trees { | |||
case class Ident[-T >: Untyped] private[ast] (name: Name) | |||
extends RefTree[T] { | |||
type ThisTree[-T >: Untyped] = Ident[T] | |||
def tag = Tag.Ident |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Final.
|
||
/** Transform full tree using all phases in this group that have idxInGroup >= start */ | ||
def transformTree(tree: Tree, start: Int)(implicit ctx: Context): Tree = { | ||
val tag = tree.tag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have benchmarks now that support this optimization?
val rhs = transformTree(tree1.rhs, start) | ||
trans(cpy.DefDef(tree1)(tree1.name, tparams, vparamss, tpt, rhs)) | ||
} | ||
transformDefDef(localContext) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're trying to get all possible performance here - localContext has extra branch that DefDef never needs.
else { | ||
var clsMethods = clsMethodsCache.get(cls) | ||
if (clsMethods eq null) { | ||
clsMethods = cls.getDeclaredMethods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
proposal: add map(_.getName)
here and use contains
instead of exists
below.
private def newTransformerArray[T, R]( | ||
methName: String, elemFn: IndexedTransformer[T, R], last: Transformer[T, R]) = { | ||
val trans = new Array[Transformer[T, R]](miniPhases.length + 1) | ||
trans(miniPhases.length) = last |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:-) Now it's actually closer to version described in the thesis.
private val nodeTransformer: Array[Array[Transformer[Tree, Tree]]] = | ||
Array.fill(Tag.NumTags)(otherNodeTransformer) | ||
|
||
private def init(name: String, tag: TreeTag, ctf: IndexedTransformer[_, Context], ntf: IndexedTransformer[_, Tree]): Unit = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think init
is too generic name and also counterintuitive given that it is called more than once.
|
||
final val TypedSplice = 40 | ||
final val ModuleDef = 41 | ||
final val ParsedTry = 42 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 It's not clear for me why are they reused.
I like the idea of stores inside Context instead of mutable state inside phases. |
I've left comments on individual commits. I'm very curious to see what impact on performance will manual handling of tags have compared to |
SuperPhase is a simplified version of TreeTransformer - about half of the size - no need for separate TransformerInfo arguments - simpler wiring logic The simplifications are largely due to two changes - use of function values, where wiring is done by a sort of staging - prepare... operations return a Context instead of a TreeTranform
... and provide a way to initialize contexts when phases are linked.
Convert phases that created new TreeTransforms before to use the new Store concept instead of Properties.
and add @switch annotation to core match in it.
The switch on tags in transformTree was a lookup switch with the previous distribution of tags. By moving tags that get eliminated before pickling to the end we can turn it into a table switch.
e73974a
to
0bd23f8
Compare
test performance please |
performance test scheduled: 2 job(s) in queue, 1 running. |
Mixin changes the number of parameters of trait initializers and therefore needs relaxed typing. This was not discovered before because TreeTransformers did not use a time travelling tree copier.
3a8cde2
to
5a41b3e
Compare
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3366/ to see the changes. Benchmarks is based on merging with master (07e4968) |
scala#3366 did lose some performance relative to the previous tree transform scheme. http://dotty-bench.epfl.ch/3366/ shows a loss of about 4% for scalapb and dotty. This commit is trying to get it back, using two tweaks: - We don't call an empty context or node transformer, instead we represent such transformers now by null and return immediately if we hit a null transformer - After a transform, we do a test whether we still have a tree of the same tag before computing the treeTag of the transformed tree. This avoids the megamorphic dispatch in the common case where the tag is the same.
Based on #3350.