diff options
Diffstat (limited to 'src')
12 files changed, 315 insertions, 120 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index a42332f7f2..c4eb6e1b42 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -16,6 +16,7 @@ import java.lang.invoke.LambdaMetafactory import scala.tools.asm import GenBCode._ import BackendReporting._ +import scala.tools.asm.tree.MethodInsnNode /* * @@ -706,6 +707,21 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { } else { genCallMethod(sym, invokeStyle, app.pos, hostClass) + // Check if the Apply tree has an InlineAnnotatedAttachment, added by the typer + // for callsites marked `f(): @inline/noinline`. For nullary calls, the attachment + // is on the Select node (not on the Apply node added by UnCurry). + def checkInlineAnnotated(t: Tree): Unit = { + if (t.hasAttachment[InlineAnnotatedAttachment]) bc.jmethod.instructions.getLast match { + case m: MethodInsnNode => + if (app.hasAttachment[NoInlineCallsiteAttachment.type]) noInlineAnnotatedCallsites += m + else inlineAnnotatedCallsites += m + case _ => + } else t match { + case Apply(fun, _) => checkInlineAnnotated(fun) + case _ => + } + } + checkInlineAnnotated(app) } } // end of genNormalMethodCall() diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index f1b515910f..92aaf991bf 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -82,6 +82,14 @@ abstract class BTypes { val callsitePositions: concurrent.Map[MethodInsnNode, Position] = recordPerRunCache(TrieMap.empty) /** + * Stores callsite instructions of invocatinos annotated `f(): @inline/noinline`. + * Instructions are added during code generation (BCodeBodyBuilder). The maps are then queried + * when building the CallGraph, every Callsite object has an annotated(No)Inline field. + */ + val inlineAnnotatedCallsites: mutable.Set[MethodInsnNode] = recordPerRunCache(mutable.Set.empty) + val noInlineAnnotatedCallsites: mutable.Set[MethodInsnNode] = recordPerRunCache(mutable.Set.empty) + + /** * Contains the internal names of all classes that are defined in Java source files of the current * compilation run (mixed compilation). Used for more detailed error reporting. */ diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala index 005d01f187..05cc484135 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala @@ -44,7 +44,7 @@ object BackendReporting { implicit class RightBiasedEither[A, B](val v: Either[A, B]) extends AnyVal { def map[U](f: B => U) = v.right.map(f) def flatMap[BB](f: B => Either[A, BB]) = v.right.flatMap(f) - def filter(f: B => Boolean)(implicit empty: A): Either[A, B] = v match { + def withFilter(f: B => Boolean)(implicit empty: A): Either[A, B] = v match { case Left(_) => v case Right(e) => if (f(e)) v else Left(empty) // scalaz.\/ requires an implicit Monoid m to get m.empty } @@ -86,8 +86,8 @@ object BackendReporting { def emitWarning(settings: ScalaSettings): Boolean } - // Method filter in RightBiasedEither requires an implicit empty value. Taking the value here - // in scope allows for-comprehensions that desugar into filter calls (for example when using a + // Method withFilter in RightBiasedEither requires an implicit empty value. Taking the value here + // in scope allows for-comprehensions that desugar into withFilter calls (for example when using a // tuple de-constructor). implicit object emptyOptimizerWarning extends OptimizerWarning { def emitWarning(settings: ScalaSettings): Boolean = false diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index a8f1e43071..66810176a1 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -67,6 +67,8 @@ class CallGraph[BT <: BTypes](val btypes: BT) { callsites(callsite.callsiteMethod) = methodCallsites + (callsite.callsiteInstruction -> callsite) } + def containsCallsite(callsite: Callsite): Boolean = callsites(callsite.callsiteMethod) contains callsite.callsiteInstruction + def removeClosureInstantiation(indy: InvokeDynamicInsnNode, methodNode: MethodNode): Option[ClosureInstantiation] = { val methodClosureInits = closureInstantiations(methodNode) val newClosureInits = methodClosureInits - indy @@ -130,8 +132,8 @@ class CallGraph[BT <: BTypes](val btypes: BT) { val callee: Either[OptimizerWarning, Callee] = for { (method, declarationClass) <- byteCodeRepository.methodNode(call.owner, call.name, call.desc): Either[OptimizerWarning, (MethodNode, InternalName)] (declarationClassNode, source) <- byteCodeRepository.classNodeAndSource(declarationClass): Either[OptimizerWarning, (ClassNode, Source)] - declarationClassBType = classBTypeFromClassNode(declarationClassNode) } yield { + val declarationClassBType = classBTypeFromClassNode(declarationClassNode) val CallsiteInfo(safeToInline, safeToRewrite, annotatedInline, annotatedNoInline, samParamTypes, warning) = analyzeCallsite(method, declarationClassBType, call.owner, source) Callee( callee = method, @@ -159,7 +161,9 @@ class CallGraph[BT <: BTypes](val btypes: BT) { argInfos = argInfos, callsiteStackHeight = a.frameAt(call).getStackSize, receiverKnownNotNull = receiverNotNull, - callsitePosition = callsitePositions.getOrElse(call, NoPosition) + callsitePosition = callsitePositions.getOrElse(call, NoPosition), + annotatedInline = inlineAnnotatedCallsites(call), + annotatedNoInline = noInlineAnnotatedCallsites(call) ) case LambdaMetaFactoryCall(indy, samMethodType, implMethod, instantiatedMethodType) if a.frameAt(indy) != null => @@ -346,7 +350,14 @@ class CallGraph[BT <: BTypes](val btypes: BT) { */ final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType, callee: Either[OptimizerWarning, Callee], argInfos: IntMap[ArgInfo], - callsiteStackHeight: Int, receiverKnownNotNull: Boolean, callsitePosition: Position) { + callsiteStackHeight: Int, receiverKnownNotNull: Boolean, callsitePosition: Position, + annotatedInline: Boolean, annotatedNoInline: Boolean) { + /** + * Contains callsites that were created during inlining by cloning this callsite. Used to find + * corresponding callsites when inlining post-inline requests. + */ + val inlinedClones = mutable.Set.empty[ClonedCallsite] + override def toString = "Invocation of" + s" ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}" + @@ -354,6 +365,8 @@ class CallGraph[BT <: BTypes](val btypes: BT) { s" in ${callsiteClass.internalName}.${callsiteMethod.name}" } + final case class ClonedCallsite(callsite: Callsite, clonedWhenInlining: Callsite) + /** * Information about invocation arguments, obtained through data flow analysis of the callsite method. */ @@ -399,6 +412,10 @@ class CallGraph[BT <: BTypes](val btypes: BT) { * graph when re-writing a closure invocation to the body method. */ final case class ClosureInstantiation(lambdaMetaFactoryCall: LambdaMetaFactoryCall, ownerMethod: MethodNode, ownerClass: ClassBType, capturedArgInfos: IntMap[ArgInfo]) { + /** + * Contains closure instantiations that were created during inlining by cloning this instantiation. + */ + val inlinedClones = mutable.Set.empty[ClosureInstantiation] override def toString = s"ClosureInstantiation($lambdaMetaFactoryCall, ${ownerMethod.name + ownerMethod.desc}, $ownerClass)" } final case class LambdaMetaFactoryCall(indy: InvokeDynamicInsnNode, samMethodType: Type, implMethod: Handle, instantiatedMethodType: Type) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index a7c4c27a97..4203a93f2e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -263,7 +263,9 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { argInfos = argInfos, callsiteStackHeight = invocationStackHeight, receiverKnownNotNull = true, // see below (*) - callsitePosition = originalCallsite.map(_.callsitePosition).getOrElse(NoPosition) + callsitePosition = originalCallsite.map(_.callsitePosition).getOrElse(NoPosition), + annotatedInline = false, + annotatedNoInline = false ) // (*) The documentation in class LambdaMetafactory says: // "if implMethod corresponds to an instance method, the first capture argument diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index f88c131e8d..449a56fdd1 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -38,27 +38,18 @@ class Inliner[BT <: BTypes](val btypes: BT) { rewriteFinalTraitMethodInvocations() for (request <- collectAndOrderInlineRequests) { - val callsite = request.callsite - val Right(callee) = callsite.callee // collectAndOrderInlineRequests returns callsites with a known callee - - // Inlining a method can create unreachable code. Example: - // def f = throw e - // def g = f; println() // println is unreachable after inlining f - // If we have an inline request for a call to g, and f has been already inlined into g, we - // need to run DCE before inlining g. - eliminateUnreachableCodeAndUpdateCallGraph(callee.callee, callee.calleeDeclarationClass.internalName) - - // DCE above removes unreachable callsites from the call graph. If the inlining request denotes - // such an eliminated callsite, do nothing. - if (callGraph.callsites(callsite.callsiteMethod).contains(callsite.callsiteInstruction)) { - val warnings = inline(request) - - for (warning <- warnings) { - if ((callee.annotatedInline && btypes.compilerSettings.YoptWarningEmitAtInlineFailed) || warning.emitWarning(compilerSettings)) { - val annotWarn = if (callee.annotatedInline) " is annotated @inline but" else "" - val msg = s"${BackendReporting.methodSignature(callee.calleeDeclarationClass.internalName, callee.callee)}$annotWarn could not be inlined:\n$warning" - backendReporting.inlinerWarning(callsite.callsitePosition, msg) - } + val Right(callee) = request.callsite.callee // collectAndOrderInlineRequests returns callsites with a known callee + + // TODO: if the request has downstream requests, create a snapshot to which we could roll back in case some downstream callsite cannot be inlined + // (Needs to revert modifications to the callee method, but also the call graph) + // (This assumes that inlining a request only makes sense if its downstream requests are satisfied - sync with heuristics!) + + val warnings = inline(request) + for (warning <- warnings) { + if ((callee.annotatedInline && btypes.compilerSettings.YoptWarningEmitAtInlineFailed) || warning.emitWarning(compilerSettings)) { + val annotWarn = if (callee.annotatedInline) " is annotated @inline but" else "" + val msg = s"${BackendReporting.methodSignature(callee.calleeDeclarationClass.internalName, callee.callee)}$annotWarn could not be inlined:\n$warning" + backendReporting.inlinerWarning(request.callsite.callsitePosition, msg) } } } @@ -163,10 +154,8 @@ class Inliner[BT <: BTypes](val btypes: BT) { if (selfParamType.info.get.inlineInfo.sam.isEmpty) samParamTypes - 0 else samParamTypes.updated(0, selfParamType) } - val staticCallsite = Callsite( + val staticCallsite = callsite.copy( callsiteInstruction = newCallsiteInstruction, - callsiteMethod = callsite.callsiteMethod, - callsiteClass = callsite.callsiteClass, callee = Right(Callee( callee = implClassMethod, calleeDeclarationClass = implClassBType, @@ -175,11 +164,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { annotatedInline = annotatedInline, annotatedNoInline = annotatedNoInline, samParamTypes = staticCallSamParamTypes, - calleeInfoWarning = infoWarning)), - argInfos = callsite.argInfos, - callsiteStackHeight = callsite.callsiteStackHeight, - receiverKnownNotNull = callsite.receiverKnownNotNull, - callsitePosition = callsite.callsitePosition + calleeInfoWarning = infoWarning)) ) callGraph.addCallsite(staticCallsite) } @@ -261,30 +246,93 @@ class Inliner[BT <: BTypes](val btypes: BT) { } /** - * Inline the callsites of an inlining request and its post-inlining requests. + * Given an InlineRequest(mainCallsite, post = List(postCallsite)), the postCallsite is a callsite + * in the method `mainCallsite.callee`. Once the mainCallsite is inlined into the target method + * (mainCallsite.callsiteMethod), we need to find the cloned callsite that corresponds to the + * postCallsite so we can inline that into the target method as well. + * + * However, it is possible that there is no cloned callsite at all that corresponds to the + * postCallsite, for example if the corresponding callsite already inlined. Example: + * + * def a() = 1 + * def b() = a() + 2 + * def c() = b() + 3 + * def d() = c() + 4 + * + * We have the following callsite objects in the call graph: + * + * c1 = a() in b + * c2 = b() in c + * c3 = c() in d + * + * Assume we have the following inline request + * r = InlineRequest(c3, + * post = List(InlineRequest(c2, + * post = List(InlineRequest(c1, post = Nil))))) + * + * But before inlining r, assume a separate InlineRequest(c2, post = Nil) is inlined first. We get + * + * c1' = a() in c // added to the call graph + * c1.inlinedClones += (c1' at c2) // remember that c1' was created when inlining c2 + * ~c2~ // c2 is removed from the call graph + * + * If we now inline r, we first inline c3. We get + * + * c1'' = a() in d // added to call graph + * c1'.inlinedClones += (c1'' at c3) // remember that c1'' was created when inlining c3 + * ~c3~ + * + * Now we continue with the post-requests for r, i.e. c2. + * - we try to find the clone of c2 that was created when inlining c3 - but there is none. c2 + * was already inlined before + * - we continue with the post-request of c2: c1 + * - we search for the callsite of c1 that was cloned when inlining c2, we find c1' + * - recursively we search for the callsite of c1' that was cloned when inlining c3, we find c1'' + * - so we create an inline request for c1'' + */ + def adaptPostRequestForMainCallsite(post: InlineRequest, mainCallsite: Callsite): List[InlineRequest] = { + def impl(post: InlineRequest, at: Callsite): List[InlineRequest] = { + post.callsite.inlinedClones.find(_.clonedWhenInlining == at) match { + case Some(clonedCallsite) => + List(InlineRequest(clonedCallsite.callsite, post.post)) + case None => + post.post.flatMap(impl(_, post.callsite)).flatMap(impl(_, at)) + } + } + impl(post, mainCallsite) + } + + + /** + * Inline the callsite of an inlining request and its post-inlining requests. * * @return An inliner warning for each callsite that could not be inlined. */ - def inline(request: InlineRequest): List[CannotInlineWarning] = canInline(request.callsite) match { - case Some(warning) => List(warning) + def inline(request: InlineRequest): List[CannotInlineWarning] = canInlineBody(request.callsite) match { + case Some(w) => List(w) case None => - val instructionsMap = inlineCallsite(request.callsite) - val postRequests = request.post.flatMap(post => { - // the post-request invocation instruction might not exist anymore: it might have been - // inlined itself, or eliminated by DCE. - for { - inlinedInvocationInstr <- instructionsMap.get(post.callsiteInstruction).map(_.asInstanceOf[MethodInsnNode]) - inlinedCallsite <- callGraph.callsites(request.callsite.callsiteMethod).get(inlinedInvocationInstr) - } yield InlineRequest(inlinedCallsite, post.post) - }) - postRequests flatMap inline + // Inlining a method can create unreachable code. Example: + // def f = throw e + // def g = f; println() // println is unreachable after inlining f + // If we have an inline request for a call to g, and f has been already inlined into g, we + // need to run DCE before inlining g. + val Right(callee) = request.callsite.callee + eliminateUnreachableCodeAndUpdateCallGraph(callee.callee, callee.calleeDeclarationClass.internalName) + // Skip over DCE'd callsites + if (callGraph.containsCallsite(request.callsite)) { + inlineCallsite(request.callsite) + val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite)) + postRequests flatMap inline + } else { + Nil + } } /** * Copy and adapt the instructions of a method to a callsite. * * Preconditions: - * - The callsite can safely be inlined (canInline is true) + * - The callsite can safely be inlined (canInlineBody is true) * - The maxLocals and maxStack values of the callsite method are correctly computed * - The callsite method contains no unreachable basic blocks, i.e., running an Analyzer does * not produce any `null` frames @@ -292,7 +340,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { * @return A map associating instruction nodes of the callee with the corresponding cloned * instruction in the callsite method. */ - def inlineCallsite(callsite: Callsite): Map[AbstractInsnNode, AbstractInsnNode] = { + def inlineCallsite(callsite: Callsite): Unit = { import callsite.{callsiteClass, callsiteMethod, callsiteInstruction, receiverKnownNotNull, callsiteStackHeight} val Right(callsiteCallee) = callsite.callee import callsiteCallee.{callee, calleeDeclarationClass} @@ -447,28 +495,27 @@ class Inliner[BT <: BTypes](val btypes: BT) { callGraph.callsites(callee).valuesIterator foreach { originalCallsite => val newCallsiteIns = instructionMap(originalCallsite.callsiteInstruction).asInstanceOf[MethodInsnNode] val argInfos = originalCallsite.argInfos flatMap mapArgInfo - callGraph.addCallsite(Callsite( + val newCallsite = originalCallsite.copy( callsiteInstruction = newCallsiteIns, callsiteMethod = callsiteMethod, callsiteClass = callsiteClass, - callee = originalCallsite.callee, argInfos = argInfos, - callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight, - receiverKnownNotNull = originalCallsite.receiverKnownNotNull, - callsitePosition = originalCallsite.callsitePosition - )) + callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight + ) + originalCallsite.inlinedClones += ClonedCallsite(newCallsite, callsite) + callGraph.addCallsite(newCallsite) } callGraph.closureInstantiations(callee).valuesIterator foreach { originalClosureInit => val newIndy = instructionMap(originalClosureInit.lambdaMetaFactoryCall.indy).asInstanceOf[InvokeDynamicInsnNode] val capturedArgInfos = originalClosureInit.capturedArgInfos flatMap mapArgInfo - callGraph.addClosureInstantiation( - ClosureInstantiation( - originalClosureInit.lambdaMetaFactoryCall.copy(indy = newIndy), - callsiteMethod, - callsiteClass, - capturedArgInfos) - ) + val newClosureInit = ClosureInstantiation( + originalClosureInit.lambdaMetaFactoryCall.copy(indy = newIndy), + callsiteMethod, + callsiteClass, + capturedArgInfos) + originalClosureInit.inlinedClones += newClosureInit + callGraph.addClosureInstantiation(newClosureInit) } // Remove the elided invocation from the call graph @@ -476,15 +523,48 @@ class Inliner[BT <: BTypes](val btypes: BT) { // Inlining a method body can render some code unreachable, see example above (in runInliner). unreachableCodeEliminated -= callsiteMethod + } + + /** + * Check whether an inlining can be performed. This method performs tests that don't change even + * if the body of the callee is changed by the inliner / optimizer, so it can be used early + * (when looking at the call graph and collecting inline requests for the program). + * + * The tests that inspect the callee's instructions are implemented in method `canInlineBody`, + * which is queried when performing an inline. + * + * @return `Some(message)` if inlining cannot be performed, `None` otherwise + */ + def earlyCanInlineCheck(callsite: Callsite): Option[CannotInlineWarning] = { + import callsite.{callsiteMethod, callsiteClass} + val Right(callsiteCallee) = callsite.callee + import callsiteCallee.{callee, calleeDeclarationClass} - instructionMap + if (isSynchronizedMethod(callee)) { + // Could be done by locking on the receiver, wrapping the inlined code in a try and unlocking + // in finally. But it's probably not worth the effort, scala never emits synchronized methods. + Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc)) + } else if (isStrictfpMethod(callsiteMethod) != isStrictfpMethod(callee)) { + Some(StrictfpMismatch( + calleeDeclarationClass.internalName, callee.name, callee.desc, + callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) + } else + None } /** - * Check whether an inling can be performed. + * Check whether the body of the callee contains any instructions that prevent the callsite from + * being inlined. See also method `earlyCanInlineCheck`. + * + * The result of this check depends on changes to the callee method's body. For example, if the + * callee initially invokes a private method, it cannot be inlined into a different class. If the + * private method is inlined into the callee, inlining the callee becomes possible. Therefore + * we don't query it while traversing the call graph and selecting callsites to inline - it might + * rule out callsites that can be inlined just fine. + * * @return `Some(message)` if inlining cannot be performed, `None` otherwise */ - def canInline(callsite: Callsite): Option[CannotInlineWarning] = { + def canInlineBody(callsite: Callsite): Option[CannotInlineWarning] = { import callsite.{callsiteInstruction, callsiteMethod, callsiteClass, callsiteStackHeight} val Right(callsiteCallee) = callsite.callee import callsiteCallee.{callee, calleeDeclarationClass} @@ -518,14 +598,6 @@ class Inliner[BT <: BTypes](val btypes: BT) { Some(ResultingMethodTooLarge( calleeDeclarationClass.internalName, callee.name, callee.desc, callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) - } else if (isSynchronizedMethod(callee)) { - // Could be done by locking on the receiver, wrapping the inlined code in a try and unlocking - // in finally. But it's probably not worth the effort, scala never emits synchronized methods. - Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc)) - } else if (isStrictfpMethod(callsiteMethod) != isStrictfpMethod(callee)) { - Some(StrictfpMismatch( - calleeDeclarationClass.internalName, callee.name, callee.desc, - callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) { Some(MethodWithHandlerCalledOnNonEmptyStack( calleeDeclarationClass.internalName, callee.name, callee.desc, diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/InlinerHeuristics.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlinerHeuristics.scala index e559b63c09..89a768fd9c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/InlinerHeuristics.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlinerHeuristics.scala @@ -12,14 +12,17 @@ import scala.tools.asm.Type import scala.tools.asm.tree.{MethodNode, MethodInsnNode} import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BackendReporting.OptimizerWarning class InlinerHeuristics[BT <: BTypes](val bTypes: BT) { import bTypes._ import inliner._ import callGraph._ - case class InlineRequest(callsite: Callsite, post: List[PostInlineRequest]) - case class PostInlineRequest(callsiteInstruction: MethodInsnNode, post: List[PostInlineRequest]) + case class InlineRequest(callsite: Callsite, post: List[InlineRequest]) { + // invariant: all post inline requests denote callsites in the callee of the main callsite + for (pr <- post) assert(pr.callsite.callsiteMethod == callsite.callee.get.callee, s"Callsite method mismatch: main $callsite - post ${pr.callsite}") + } /** * Select callsites from the call graph that should be inlined, grouped by the containing method. @@ -38,28 +41,35 @@ class InlinerHeuristics[BT <: BTypes](val bTypes: BT) { compilingMethods.map(methodNode => { var requests = Set.empty[InlineRequest] callGraph.callsites(methodNode).valuesIterator foreach { - case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, _, warning)), _, _, _, pos) => - val request = inlineRequest(callsite) - requests ++= request - if (request.isEmpty) { - if (annotatedInline && bTypes.compilerSettings.YoptWarningEmitAtInlineFailed) { - // if the callsite is annotated @inline, we report an inline warning even if the underlying - // reason is, for example, mixed compilation (which has a separate -Yopt-warning flag). - def initMsg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)} is annotated @inline but cannot be inlined" - def warnMsg = warning.map(" Possible reason:\n" + _).getOrElse("") - if (doRewriteTraitCallsite(callsite)) - backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + warnMsg) - else if (!safeToInline) - backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg) - else - backendReporting.inlinerWarning(pos, s"$initMsg." + warnMsg) - } else if (warning.isDefined && warning.get.emitWarning(compilerSettings)) { - // when annotatedInline is false, and there is some warning, the callsite metadata is possibly incomplete. - backendReporting.inlinerWarning(pos, s"there was a problem determining if method ${callee.name} can be inlined: \n"+ warning.get) - } + case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, calleeAnnotatedInline, _, _, callsiteWarning)), _, _, _, pos, _, _) => + inlineRequest(callsite) match { + case Some(Right(req)) => requests += req + case Some(Left(w)) => + if ((calleeAnnotatedInline && bTypes.compilerSettings.YoptWarningEmitAtInlineFailed) || w.emitWarning(compilerSettings)) { + val annotWarn = if (calleeAnnotatedInline) " is annotated @inline but" else "" + val msg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)}$annotWarn could not be inlined:\n$w" + backendReporting.inlinerWarning(callsite.callsitePosition, msg) + } + + case None => + if (calleeAnnotatedInline && !callsite.annotatedNoInline && bTypes.compilerSettings.YoptWarningEmitAtInlineFailed) { + // if the callsite is annotated @inline, we report an inline warning even if the underlying + // reason is, for example, mixed compilation (which has a separate -Yopt-warning flag). + def initMsg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)} is annotated @inline but cannot be inlined" + def warnMsg = callsiteWarning.map(" Possible reason:\n" + _).getOrElse("") + if (doRewriteTraitCallsite(callsite)) + backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + warnMsg) + else if (!safeToInline) + backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg) + else + backendReporting.inlinerWarning(pos, s"$initMsg." + warnMsg) + } else if (callsiteWarning.isDefined && callsiteWarning.get.emitWarning(compilerSettings)) { + // when annotatedInline is false, and there is some warning, the callsite metadata is possibly incomplete. + backendReporting.inlinerWarning(pos, s"there was a problem determining if method ${callee.name} can be inlined: \n"+ callsiteWarning.get) + } } - case Callsite(ins, _, _, Left(warning), _, _, _, pos) => + case Callsite(ins, _, _, Left(warning), _, _, _, pos, _, _) => if (warning.emitWarning(compilerSettings)) backendReporting.inlinerWarning(pos, s"failed to determine if ${ins.name} should be inlined:\n$warning") } @@ -73,27 +83,39 @@ class InlinerHeuristics[BT <: BTypes](val bTypes: BT) { * * The resulting inline request may contain post-inlining requests of callsites that in turn are * also selected as individual inlining requests. + * + * @return `None` if this callsite should not be inlined according to the active heuristic + * `Some(Left)` if the callsite cannot be inlined (for example because that would cause + * an IllegalAccessError) but should be according to the heuristic + * TODO: what if a downstream inline request would cause an IAE and we don't create an + * InlineRequest for the original callsite? new subclass of OptimizerWarning. + * `Some(Right)` if the callsite should be and can be inlined */ - def inlineRequest(callsite: Callsite): Option[InlineRequest] = compilerSettings.YoptInlineHeuristics.value match { - case "everything" => - if (callsite.callee.get.safeToInline) Some(InlineRequest(callsite, Nil)) - else None - - case "at-inline-annotated" => - val callee = callsite.callee.get - if (callee.safeToInline && callee.annotatedInline) Some(InlineRequest(callsite, Nil)) - else None - - case "default" => - val callee = callsite.callee.get - if (callee.safeToInline && !callee.annotatedNoInline) { - val shouldInlineHO = callee.samParamTypes.nonEmpty && (callee.samParamTypes exists { - case (index, _) => callsite.argInfos.contains(index) - }) - - if (shouldInlineHO || callee.annotatedInline) Some(InlineRequest(callsite, Nil)) + def inlineRequest(callsite: Callsite): Option[Either[OptimizerWarning, InlineRequest]] = { + val callee = callsite.callee.get + def requestIfCanInline(callsite: Callsite): Either[OptimizerWarning, InlineRequest] = inliner.earlyCanInlineCheck(callsite) match { + case Some(w) => Left(w) + case None => Right(InlineRequest(callsite, Nil)) + } + + compilerSettings.YoptInlineHeuristics.value match { + case "everything" => + if (callee.safeToInline) Some(requestIfCanInline(callsite)) else None - } else None + + case "at-inline-annotated" => + if (callee.safeToInline && callee.annotatedInline) Some(requestIfCanInline(callsite)) + else None + + case "default" => + if (callee.safeToInline && !callee.annotatedNoInline && !callsite.annotatedNoInline) { + def shouldInlineHO = callee.samParamTypes.nonEmpty && (callee.samParamTypes exists { + case (index, _) => callsite.argInfos.contains(index) + }) + if (callee.annotatedInline || callsite.annotatedInline || shouldInlineHO) Some(requestIfCanInline(callsite)) + else None + } else None + } } /* diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 26e04edcca..45ebbd532d 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -4130,6 +4130,14 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper ann setType arg1.tpe.withAnnotation(annotInfo) } val atype = ann.tpe + // For `f(): @inline/noinline` callsites, add the InlineAnnotatedAttachment. TypeApplys + // are eliminated by erasure, so add it to the underlying function in this case. + def setInlineAttachment(t: Tree, att: InlineAnnotatedAttachment): Unit = t match { + case TypeApply(fun, _) => setInlineAttachment(fun, att) + case _ => t.updateAttachment(att) + } + if (atype.hasAnnotation(definitions.ScalaNoInlineClass)) setInlineAttachment(arg1, NoInlineCallsiteAttachment) + else if (atype.hasAnnotation(definitions.ScalaInlineClass)) setInlineAttachment(arg1, InlineCallsiteAttachment) Typed(arg1, resultingTypeTree(atype)) setPos tree.pos setType atype } } diff --git a/src/library/scala/inline.scala b/src/library/scala/inline.scala index a21cced928..dc55af301c 100644 --- a/src/library/scala/inline.scala +++ b/src/library/scala/inline.scala @@ -11,8 +11,30 @@ package scala /** - * An annotation on methods that requests that the compiler should - * try especially hard to inline the annotated method. + * An annotation on methods that requests that the compiler should try especially hard to inline the + * annotated method. The annotation can be used at definition site or at callsite. + * + * {{{ + * @inline final def f1(x: Int) = x + * @noinline final def f2(x: Int) = x + * final def f3(x: Int) = x + * + * def t1 = f1(1) // inlined if possible + * def t2 = f2(1) // not inlined + * def t3 = f3(1) // may be inlined (heuristics) + * def t4 = f1(1): @noinline // not inlined (override at callsite) + * def t5 = f2(1): @inline // not inlined (cannot override the @noinline at f2's definition) + * def t6 = f3(1): @inline // inlined if possible + * def t7 = f3(1): @noinline // not inlined + * } + * }}} + * + * Note: parentheses are required when annotating a callsite withing a larger expression. + * + * {{{ + * def t1 = f1(1) + f1(1): @noinline // equivalent to (f1(1) + f1(1)): @noinline + * def t2 = f1(1) + (f1(1): @noinline) // the second call to f1 is not inlined + * }}} * * @author Lex Spoon * @version 1.0, 2007-5-21 diff --git a/src/library/scala/noinline.scala b/src/library/scala/noinline.scala index 38fd4c39d6..a427e170f4 100644 --- a/src/library/scala/noinline.scala +++ b/src/library/scala/noinline.scala @@ -11,8 +11,30 @@ package scala /** - * An annotation on methods that forbids the compiler to inline the - * method, no matter how safe the inlining appears to be. + * An annotation on methods that forbids the compiler to inline the method, no matter how safe the + * inlining appears to be. The annotation can be used at definition site or at callsite. + * + * {{{ + * @inline final def f1(x: Int) = x + * @noinline final def f2(x: Int) = x + * final def f3(x: Int) = x + * + * def t1 = f1(1) // inlined if possible + * def t2 = f2(1) // not inlined + * def t3 = f3(1) // may be inlined (heuristics) + * def t4 = f1(1): @noinline // not inlined (override at callsite) + * def t5 = f2(1): @inline // not inlined (cannot override the @noinline at f2's definition) + * def t6 = f3(1): @inline // inlined if possible + * def t7 = f3(1): @noinline // not inlined + * } + * }}} + * + * Note: parentheses are required when annotating a callsite withing a larger expression. + * + * {{{ + * def t1 = f1(1) + f1(1): @noinline // equivalent to (f1(1) + f1(1)): @noinline + * def t2 = f1(1) + (f1(1): @noinline) // the second call to f1 is not inlined + * }}} * * @author Lex Spoon * @version 1.0, 2007-5-21 diff --git a/src/reflect/scala/reflect/internal/StdAttachments.scala b/src/reflect/scala/reflect/internal/StdAttachments.scala index cca33253be..8358c1295c 100644 --- a/src/reflect/scala/reflect/internal/StdAttachments.scala +++ b/src/reflect/scala/reflect/internal/StdAttachments.scala @@ -52,4 +52,8 @@ trait StdAttachments { /** Untyped list of subpatterns attached to selector dummy. */ case class SubpatternsAttachment(patterns: List[Tree]) + + abstract class InlineAnnotatedAttachment + case object NoInlineCallsiteAttachment extends InlineAnnotatedAttachment + case object InlineCallsiteAttachment extends InlineAnnotatedAttachment } diff --git a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala index 1e9a4fe8a5..0132fff17c 100644 --- a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala +++ b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala @@ -41,6 +41,8 @@ trait JavaUniverseForce { self: runtime.JavaUniverse => this.ForAttachment this.SyntheticUnitAttachment this.SubpatternsAttachment + this.NoInlineCallsiteAttachment + this.InlineCallsiteAttachment this.noPrint this.typeDebug this.Range |