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

Commit 33f6b34

Browse files
committed
Statically resolve Apply calls when possible.
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.
1 parent e6cb628 commit 33f6b34

File tree

2 files changed

+47
-35
lines changed

2 files changed

+47
-35
lines changed

Diff for: wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala

+31-35
Original file line numberDiff line numberDiff line change
@@ -334,36 +334,35 @@ private class WasmExpressionBuilder private (
334334
instrs += UNREACHABLE // trap
335335
IRTypes.NothingType
336336

337-
case prim: IRTypes.PrimType =>
338-
// statically resolved call with non-null argument
339-
val receiverClassName = IRTypes.PrimTypeToBoxedClass(prim)
340-
genApplyStatically(
341-
IRTrees.ApplyStatically(t.flags, t.receiver, receiverClassName, t.method, t.args)(t.tpe)(
342-
t.pos
343-
)
344-
)
345-
346-
case IRTypes.ClassType(className) if IRNames.HijackedClasses.contains(className) =>
347-
// statically resolved call with maybe-null argument
348-
genApplyStatically(
349-
IRTrees.ApplyStatically(t.flags, t.receiver, className, t.method, t.args)(t.tpe)(t.pos)
350-
)
351-
352337
case _ if t.method.name.isReflectiveProxy =>
353338
genReflectiveCall(t)
354339

355-
case IRTypes.ArrayType(_) =>
356-
/* Array classes always inherit all their methods from jl.Object, so we
357-
* can always statically resolve the call.
358-
*/
359-
genApplyStatically(
360-
IRTrees.ApplyStatically(t.flags, t.receiver, IRNames.ObjectClass, t.method, t.args)(
361-
t.tpe
362-
)(t.pos)
363-
)
364-
365340
case _ =>
366-
genApplyNonPrim(t)
341+
val receiverClassName = t.receiver.tpe match {
342+
case prim: IRTypes.PrimType => IRTypes.PrimTypeToBoxedClass(prim)
343+
case IRTypes.ClassType(cls) => cls
344+
case IRTypes.AnyType => IRNames.ObjectClass
345+
case IRTypes.ArrayType(_) => IRNames.ObjectClass
346+
case tpe: IRTypes.RecordType => throw new AssertionError(s"Invalid receiver type $tpe")
347+
}
348+
val receiverClassInfo = ctx.getClassInfo(receiverClassName)
349+
350+
val canUseStaticallyResolved = {
351+
receiverClassInfo.kind == ClassKind.HijackedClass ||
352+
t.receiver.tpe.isInstanceOf[IRTypes.ArrayType] ||
353+
receiverClassInfo.resolvedMethodInfos.get(t.method.name).exists(_.isEffectivelyFinal)
354+
}
355+
if (canUseStaticallyResolved) {
356+
genApplyStatically(
357+
IRTrees.ApplyStatically(t.flags, t.receiver, receiverClassName, t.method, t.args)(
358+
t.tpe
359+
)(
360+
t.pos
361+
)
362+
)
363+
} else {
364+
genApplyWithDispatch(t, receiverClassInfo)
365+
}
367366
}
368367
}
369368

@@ -409,21 +408,18 @@ private class WasmExpressionBuilder private (
409408
// done
410409
}
411410

412-
/** Generates the code an `Apply` call where the receiver's type is not statically known to be a
413-
* primitive or hijacked class.
411+
/** Generates the code an `Apply` call that requires dynamic dispatch.
414412
*
415413
* In that case, there is always at least a vtable/itable-based dispatch. It may also contain
416414
* primitive-based dispatch if the receiver's type is an ancestor of a hijacked class.
417415
*/
418-
private def genApplyNonPrim(t: IRTrees.Apply): IRTypes.Type = {
416+
private def genApplyWithDispatch(
417+
t: IRTrees.Apply,
418+
receiverClassInfo: WasmContext.WasmClassInfo
419+
): IRTypes.Type = {
419420
implicit val pos: Position = t.pos
420421

421-
val receiverClassName = t.receiver.tpe match {
422-
case ClassType(className) => className
423-
case IRTypes.AnyType => IRNames.ObjectClass
424-
case _ => throw new Error(s"Invalid receiver type ${t.receiver.tpe}")
425-
}
426-
val receiverClassInfo = ctx.getClassInfo(receiverClassName)
422+
val receiverClassName = receiverClassInfo.name
427423

428424
/* Similar to transformType(t.receiver.tpe), but:
429425
* - it is non-null,

Diff for: wasm/src/main/scala/wasm4s/WasmContext.scala

+16
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,9 @@ object WasmContext {
735735
case None => Map.empty
736736
}
737737

738+
for (methodName <- classConcretePublicMethodNames)
739+
inherited.get(methodName).foreach(_.markOverridden())
740+
738741
classConcretePublicMethodNames.foldLeft(inherited) { (prev, methodName) =>
739742
prev.updated(methodName, new ConcreteMethodInfo(name, methodName))
740743
}
@@ -815,8 +818,14 @@ object WasmContext {
815818
_.tableMethodInfos
816819
)
817820

821+
/* When computing the table entries to add for this class, exclude:
822+
* - methods that are already in the super class' table entries, and
823+
* - methods that are effectively final, since they will always be
824+
* statically resolved instead of using the table dispatch.
825+
*/
818826
val newTableEntries = methodsCalledDynamically.toList
819827
.filter(!superTableMethodInfos.contains(_))
828+
.filterNot(m => resolvedMethodInfos.get(m).exists(_.isEffectivelyFinal))
820829
.sorted // for stability
821830

822831
val baseIndex = superTableMethodInfos.size
@@ -871,6 +880,13 @@ object WasmContext {
871880
val methodName: IRNames.MethodName
872881
) {
873882
val wasmName = WasmFunctionName(IRTrees.MemberNamespace.Public, ownerClass, methodName)
883+
884+
private var effectivelyFinal: Boolean = true
885+
886+
private[WasmContext] def markOverridden(): Unit =
887+
effectivelyFinal = false
888+
889+
def isEffectivelyFinal: Boolean = effectivelyFinal
874890
}
875891

876892
final class TableMethodInfo(val methodName: IRNames.MethodName, val tableIndex: Int)

0 commit comments

Comments
 (0)