diff --git a/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala b/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala index 96204fe7a998..01cb30da08bd 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/ClosureOptimizer.scala @@ -240,7 +240,10 @@ class ClosureOptimizer(optimizerUtils: OptimizerUtils, receiverProducers.size == 1 && receiverProducers.head == indy } - def isSpecializedVersion(specName: String, nonSpecName: String) = specName.startsWith(nonSpecName) && specializationSuffix.pattern.matcher(specName.substring(nonSpecName.length)).matches + def isSpecializedVersion(specName: String, nonSpecName: String) = + specName.startsWith(nonSpecName) + && ((specName == "applyVoid" && nonSpecName == "apply" && invocation.owner.startsWith("scala/Function")) + || specializationSuffix.pattern.matcher(specName.substring(nonSpecName.length)).matches) def sameOrSpecializedType(specTp: Type, nonSpecTp: Type) = { specTp == nonSpecTp || { diff --git a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala index 1cc721f94bf7..262b1d315088 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/CopyProp.scala @@ -28,7 +28,7 @@ import org.objectweb.asm class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: Inliner, ts: OptimizerKnownBTypes, settings: OptimizerSettings) { - private val modulesAllowSkipInitialization = + private val modulesAllowSkipInitialization: InternalName => Boolean = if settings.optAllowSkipCoreModuleInit then optimizerUtils.modulesAllowSkipInitialization else Set.empty /** @@ -309,8 +309,8 @@ class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: In // A queue of instructions producing a value that has to be eliminated. If possible, the // instruction (and its inputs) will be removed, otherwise a POP is inserted after val queue = mutable.Queue.empty[ProducedValue] - // Contains constructor invocations for values that can be eliminated if unused. - val sideEffectFreeConstructorCalls = mutable.ArrayBuffer.empty[MethodInsnNode] + // Contains the creation of values that can be eliminated if unused. + val sideEffectFreeCreations = mutable.ArrayBuffer.empty[MethodInsnNode] // instructions to remove (we don't change the bytecode while analyzing it. this allows // running the ProdConsAnalyzer only once.) @@ -398,7 +398,7 @@ class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: In case INVOKESPECIAL => val mi = insn.asInstanceOf[MethodInsnNode] - if (optimizerUtils.isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi + if (optimizerUtils.isSideEffectFreeConstructorOrFactoryCall(mi)) sideEffectFreeCreations += mi case _ => } @@ -545,11 +545,11 @@ class CopyProp(optimizerUtils: OptimizerUtils, callGraph: CallGraph, inliner: In def removeConstructorCall(mi: MethodInsnNode): Unit = { toRemove += mi callGraph.removeCallsite(mi, method) - sideEffectFreeConstructorCalls -= mi + sideEffectFreeCreations -= mi changed = true } - for (mi <- sideEffectFreeConstructorCalls.toList) { // toList to allow removing elements while traversing + for (mi <- sideEffectFreeCreations.toList) { // toList to allow removing elements while traversing val frame = prodCons.frameAt(mi) val stackTop = frame.stackTop val numArgs = Type.getArgumentTypes(mi.desc).length diff --git a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala index 67b56164bf0b..2b0970a4a5f2 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/OptimizerUtils.scala @@ -17,6 +17,8 @@ import org.objectweb.asm.{Handle, Opcodes, Type} * This component hosts tools and utilities used in the optimizer that require access to an `OptimizerKnownBTypes` instance. */ class OptimizerUtils(val ts: OptimizerKnownBTypes) { + // Contains methods that get instances of various standard library modules, such as `Either$`, `Range$`, etc. + private val ScalaPackageObject = "scala/package$" private val indyLambdaImplMethods: ConcurrentHashMap[InternalName, mutable.Map[MethodNode, mutable.Map[InvokeDynamicInsnNode, asm.Handle]]] = new ConcurrentHashMap @@ -135,9 +137,9 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { def runtimeRefClassBoxedType(refClass: InternalName): asm.Type = asm.Type.getArgumentTypes(ts.srRefCreateMethods(refClass).methodType.descriptor)(0) - def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { - insn.name == BCodeUtils.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc)) - } + def isSideEffectFreeConstructorOrFactoryCall(insn: MethodInsnNode): Boolean = + insn.owner == ScalaPackageObject || + (insn.name == BCodeUtils.INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc))) def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode): Boolean = { insn.getOpcode == Opcodes.NEW && { @@ -149,14 +151,14 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { def isSideEffectFreeCall(mi: MethodInsnNode): Boolean = { isScalaBox(mi) || // not Scala unbox, it may CCE isJavaBox(mi) || // not Java unbox, it may NPE - isSideEffectFreeConstructorCall(mi) || + isSideEffectFreeConstructorOrFactoryCall(mi) || AnalysisUtils.isClassTagApply(mi) } // methods that are known to return a non-null result def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) || AnalysisUtils.isClassTagApply(mi) || - isTupleApply(mi) + isTupleApply(mi) || mi.owner == ScalaPackageObject } // unused objects created by these constructors are eliminated by pushPop @@ -172,16 +174,42 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { lazy val modulesAllowSkipInitialization: Set[InternalName] = Set( + // Please keep sorted + ScalaPackageObject, + "scala/Array$", "scala/Predef$", + "scala/TupleXXL$", + "scala/collection/ArrayOps$", + "scala/collection/Iterable$", + "scala/collection/Iterator$", + "scala/collection/StringOps$", + "scala/collection/immutable/IndexedSeq$", + "scala/collection/immutable/LazyList$", + "scala/collection/immutable/List$", + "scala/collection/immutable/Nil$", + "scala/collection/immutable/Range$", + "scala/collection/immutable/Seq$", + "scala/collection/immutable/Stream$", + "scala/collection/immutable/Vector$", + "scala/collection/mutable/StringBuilder$", + "scala/math/BigDecimal$", + "scala/math/BigInt$", + "scala/math/Equiv$", + "scala/math/Fractional$", + "scala/math/Integral$", + "scala/math/Numeric$", + "scala/math/Ordered$", + "scala/math/Ordering$", + "scala/runtime/Arrays$", "scala/runtime/ScalaRunTime$", "scala/runtime/Scala3RunTime$", "scala/reflect/ClassTag$", "scala/reflect/ManifestFactory$", - "scala/Array$", - "scala/collection/ArrayOps$", - "scala/collection/StringOps$", - "scala/TupleXXL$" - ) ++ (1 to Definitions.MaxTupleArity).map(n => s"scala/Tuple$n$$") ++ AnalysisUtils.primitiveTypes.keysIterator + "scala/util/Either$", + "scala/util/Left$", + "scala/util/Right$", + ) ++ (1 to Definitions.MaxTupleArity).map(n => s"scala/Tuple$n$$") + ++ AnalysisUtils.primitiveTypes.keysIterator.map(n => s"scala/$n$$") private lazy val classesOfSideEffectFreeConstructors: Set[String] = sideEffectFreeConstructors.map(_._1) @@ -229,8 +257,10 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { val t = i.getType if (t == AbstractInsnNode.METHOD_INSN) { val mi = i.asInstanceOf[MethodInsnNode] - // invokespecial has, well, special semantics that depend on the class it's being invoked in, see, e.g., https://stackoverflow.com/a/8950564 - if (!allowPrivateCalls && i.getOpcode == Opcodes.INVOKESPECIAL && mi.name != BCodeUtils.INSTANCE_CONSTRUCTOR_NAME) { + if (mi.owner == ScalaPackageObject && modulesAllowSkipInitialization.exists(mi.desc.contains)) { + // Ignore calls that, e.g., load the Range module -- that's still part of a forwarder + } else if (!allowPrivateCalls && i.getOpcode == Opcodes.INVOKESPECIAL && mi.name != BCodeUtils.INSTANCE_CONSTRUCTOR_NAME) { + // invokespecial has, well, special semantics that depend on the class it's being invoked in, see, e.g., https://stackoverflow.com/a/8950564 numCallsOrNew = 2 // stop here: don't inline forwarders with a private or super call } else { if (isScalaBox(mi) || isScalaUnbox(mi) || isPredefAutoBox(mi) || isPredefAutoUnbox(mi) || isJavaBox(mi) || isJavaUnbox(mi)) @@ -242,8 +272,9 @@ class OptimizerUtils(val ts: OptimizerKnownBTypes) { } } else if (nonForwarderInstructionTypes(t)) { if (i.getOpcode == Opcodes.GETSTATIC) { - if (!allowPrivateCalls && owner == i.asInstanceOf[FieldInsnNode].owner) + if (!allowPrivateCalls && owner == i.asInstanceOf[FieldInsnNode].owner) { numCallsOrNew = 2 // stop here: not forwarder or trivial + } } else { numCallsOrNew = 2 // stop here: not forwarder or trivial } diff --git a/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala index 7b2145a7e5bf..a40083f0bd48 100644 --- a/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/OptimizationBytecodeTests.scala @@ -78,7 +78,7 @@ class OptimizationBytecodeTests extends DottyBytecodeTest { val instructions = instructionsFromMethod(meth) for instr <- instructions do instr match { case AsmConverters.Invoke(_, owner, name, _, _) => assert(allowedCalls(owner, name), s"Found invoke to $owner.$name in:\n${instructions.mkString("\n")}") - case AsmConverters.InvokeDynamic(_, name, _, _, _) => assert(false, s"Found dynamic invoke to $name in:\n${instructions.mkString("\n")}") + case AsmConverters.InvokeDynamic(_, _, _, bsm, _) => assert(allowedCalls(bsm.owner, bsm.name), s"Found dynamic invoke to ${bsm.owner}.${bsm.name} in:\n${instructions.mkString("\n")}") case _ => () } } @@ -720,6 +720,25 @@ class OptimizationBytecodeTests extends DottyBytecodeTest { ) + @Test def inlineIntTo = + assertCalls(Calls.noneToClasses("Test"), + "extension (n: Int) { def myTo(m: Int): Range.Inclusive = Range.inclusive(n, m) }; (0 myTo 49)", + returnType = "Range.Inclusive" + ) + + // requires understanding `applyVoid` as a specialization on Function* + @Test def inlineRangeForeachArrayForeach = + assertCalls(Calls.noneToClasses("java/lang/invoke/LambdaMetafactory"), + """ + |final class C() { @noinline def test(): Boolean = false } + |val r = Range.inclusive(0, 49) + |val a = Array.fill(50)(new C()) + |var x = 1 + |r.foreach { n => a.foreach { c => if (c.test()) { x += 1 } } } + |x""".stripMargin + ) + + @Test def inlineSingleAbstractMethod = assertEquivalence( "if i eq null then throw null; 42",