-
Notifications
You must be signed in to change notification settings - Fork 3
Statically resolve Apply calls when possible. #116
Conversation
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 left one comment, but otherwise LGTM! Nice work 🎉
|
||
val newTableEntries = methodsCalledDynamically.toList | ||
.filter(!superTableMethodInfos.contains(_)) | ||
.filterNot(m => resolvedMethodInfos.get(m).exists(_.isEffectivelyFinal)) |
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.
[note]
Filter out effectively final methods because they will be called statically, thus we don't need them in vtable
case ClassKind.Class | ClassKind.ModuleClass | ClassKind.HijackedClass => | ||
val superClassInfo = superClass.map(ctx.getClassInfo(_)) | ||
val superTableEntries = | ||
superClassInfo.fold[List[IRNames.MethodName]](Nil)(_.tableEntries) |
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 seems like we need to run buildMethodTable
in order of superClass -> subClass.
Are we sure that the given classes are topologically sorted by subclass relations here?
scala-wasm/wasm/src/main/scala/ir2wasm/Preprocessor.scala
Lines 29 to 30 in c1252ba
for (clazz <- classes) { | |
ctx.getClassInfo(clazz.className).buildMethodTable() |
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, this is not the first thing that requires such a topological order. It is enforced by sorting classes by their .ancestors.size
at
scala-wasm/wasm/src/main/scala/WebAssemblyLinkerBackend.scala
Lines 81 to 88 in 1be5a2b
/* Sort by ancestor count so that superclasses always appear before | |
* subclasses, then tie-break by name for stability. | |
*/ | |
val sortedClasses = onlyModule.classDefs.sortWith { (a, b) => | |
val cmp = Integer.compare(a.ancestors.size, b.ancestors.size) | |
if (cmp != 0) cmp < 0 | |
else a.className.compareTo(b.className) < 0 | |
} |
We also need this order for building the list of fields of a class, for example. Indirectly we also need it to generate the
type
s of the struct
s for the classes, since they declare a subtyping relationship at the Wasm level and therefore must appear in inheritance order.
So yes, it's pretty pervasive and we can/have to rely on it.
* Method calls on `AnyType` and `ArrayType`. * Method calls inside top-level exported method defs.
…tations. Previously, we put in `classInfo.methods` two different things: * concrete implementations of methods, and * abstract definitions required for dynamic calls. Now, we separate these two categories, and store them respectively in `resolvedMethodInfos` and `tableEntries`/`tableMethodInfos`. We also fill in everything in a set order from the preprocessor, instead of computing `VTable`s lazily. The separation can lead to creating smaller vtables, as not all public methods are called dynamically. It is possible for public methods to only be called through `ApplyStatically` instead. This effect will be hightened in the following commit, in which we will generate `Apply`s as `ApplyStatically` when possible (when the target method is never overridden).
When the statically resolved method of an `Apply` node exists and is "effectively final" (i.e., it is never overridden), we can use the compilation scheme of `ApplyStatically` instead. This also allows to remove methods from the vtables if they are always called in such a situation. The analysis is not optimal: consider 3 classes A, B, C. B and C both extend A, but only B overrides method m. Then a call on a `(c: C).m` will "resolve" to `A.m` which is not effectively final, so it won't be optimized. A better analysis would be costly in our setup, but will be free to obtain from the Scala.js Optimizer when we can enable it, so we don't push the design too much for now. Nevertheless, it is worth doing now in its limited form. It reduces the size of the fastLink output by 18% and the fullLink output by 28% for the Scala.js test suite. Not to mention the likely performance improvements.
c1252ba
to
33f6b34
Compare
When the statically resolved method of an
Apply
node exists and is "effectively final" (i.e., it is never overridden), we can use the compilation scheme ofApplyStatically
instead.This also allows to remove methods from the vtables if they are always called in such a situation.
The analysis is not optimal: consider 3 classes A, B, C. B and C both extend A, but only B overrides method m. Then a call on a
(c: C).m
will "resolve" toA.m
which is not effectively final, so it won't be optimized. A better analysis would be costly in our setup, but will be free to obtain from the Scala.js Optimizer when we can enable it, so we don't push the design too much for now.Nevertheless, it is worth doing now in its limited form. It reduces the size of the fastLink output by 18% and the fullLink output by
28% for the Scala.js test suite. Not to mention the likely performance improvements.