Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Statically resolve Apply calls when possible. #116

Merged
merged 5 commits into from
Apr 23, 2024

Conversation

sjrd
Copy link
Collaborator

@sjrd sjrd commented Apr 21, 2024

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.

Copy link
Owner

@tanishiking tanishiking left a 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))
Copy link
Owner

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)
Copy link
Owner

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?

for (clazz <- classes) {
ctx.getClassInfo(clazz.className).buildMethodTable()

Copy link
Collaborator Author

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

/* 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 types of the structs 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.

sjrd added 5 commits April 23, 2024 10:10
* 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.
@sjrd sjrd force-pushed the static-dispatch-when-possible branch from c1252ba to 33f6b34 Compare April 23, 2024 08:15
@sjrd sjrd merged commit 7fd86c1 into tanishiking:main Apr 23, 2024
1 check passed
@sjrd sjrd deleted the static-dispatch-when-possible branch April 23, 2024 08:20
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants