summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukas Rytz <lukas.rytz@typesafe.com>2015-10-20 21:19:43 +0200
committerLukas Rytz <lukas.rytz@typesafe.com>2015-10-20 21:19:43 +0200
commit6df88b024f720a10d3f7451750ab620addf725b9 (patch)
tree684f42edc5e42319910f1e6d4b98a4192e5fbaa1
parentc99e53e8a05fc5d45f8e8a28da68d3977be65bfa (diff)
parent62be5705b91cb233bf922021f102a08bccc95af5 (diff)
downloadscala-6df88b024f720a10d3f7451750ab620addf725b9.tar.gz
scala-6df88b024f720a10d3f7451750ab620addf725b9.tar.bz2
scala-6df88b024f720a10d3f7451750ab620addf725b9.zip
Merge pull request #4801 from lrytz/opt/inlinerCleanups
Various cleanups in the Inliner
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala16
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala8
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala6
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala23
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala4
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala208
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/InlinerHeuristics.scala104
-rw-r--r--src/compiler/scala/tools/nsc/typechecker/Typers.scala8
-rw-r--r--src/library/scala/inline.scala26
-rw-r--r--src/library/scala/noinline.scala26
-rw-r--r--src/reflect/scala/reflect/internal/StdAttachments.scala4
-rw-r--r--src/reflect/scala/reflect/runtime/JavaUniverseForce.scala2
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala3
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala4
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala19
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala347
16 files changed, 539 insertions, 269 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
diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
index 769236ae49..1f2ec274d3 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
@@ -169,6 +169,9 @@ object CodeGenTools {
def getSingleMethod(classNode: ClassNode, name: String): Method =
convertMethod(classNode.methods.asScala.toList.find(_.name == name).get)
+ def findAsmMethods(c: ClassNode, p: String => Boolean) = c.methods.iterator.asScala.filter(m => p(m.name)).toList.sortBy(_.name)
+ def findAsmMethod(c: ClassNode, name: String) = findAsmMethods(c, _ == name).head
+
/**
* Instructions that match `query` when textified.
* If `query` starts with a `+`, the next instruction is returned.
diff --git a/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala
index f9a55bb26e..e57e95bac4 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala
@@ -16,8 +16,8 @@ object DefaultMethodTest extends ClearAfterClass.Clearable {
}
class DefaultMethodTest extends ClearAfterClass {
- ClearAfterClass.stateToClear = DirectCompileTest
- val compiler = DirectCompileTest.compiler
+ ClearAfterClass.stateToClear = DefaultMethodTest
+ val compiler = DefaultMethodTest.compiler
@Test
def defaultMethodsViaGenBCode(): Unit = {
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala
index efd88f10c3..3857d3e8ce 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala
@@ -50,9 +50,6 @@ class CallGraphTest extends ClearAfterClass {
compileClasses(compiler)(code, allowMessage = allowMessage).map(c => byteCodeRepository.classNode(c.name).get)
}
- def getMethods(c: ClassNode, p: String => Boolean) = c.methods.iterator.asScala.filter(m => p(m.name)).toList.sortBy(_.name)
- def getMethod(c: ClassNode, name: String) = getMethods(c, _ == name).head
-
def callsInMethod(methodNode: MethodNode): List[MethodInsnNode] = methodNode.instructions.iterator.asScala.collect({
case call: MethodInsnNode => call
}).toList
@@ -121,10 +118,10 @@ class CallGraphTest extends ClearAfterClass {
val List(cCls, cMod, dCls, testCls) = compile(code, checkMsg)
assert(msgCount == 6, msgCount)
- val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = getMethods(cCls, _.startsWith("f"))
- val List(df1, df3) = getMethods(dCls, _.startsWith("f"))
- val g1 = getMethod(cMod, "g1")
- val List(t1, t2) = getMethods(testCls, _.startsWith("t"))
+ val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = findAsmMethods(cCls, _.startsWith("f"))
+ val List(df1, df3) = findAsmMethods(dCls, _.startsWith("f"))
+ val g1 = findAsmMethod(cMod, "g1")
+ val List(t1, t2) = findAsmMethods(testCls, _.startsWith("t"))
val List(cf1Call, cf2Call, cf3Call, cf4Call, cf5Call, cf6Call, cf7Call, cg1Call) = callsInMethod(t1)
val List(df1Call, df2Call, df3Call, df4Call, df5Call, df6Call, df7Call, dg1Call) = callsInMethod(t2)
@@ -160,7 +157,7 @@ class CallGraphTest extends ClearAfterClass {
|}
""".stripMargin
val List(c) = compile(code)
- val m = getMethod(c, "m")
+ val m = findAsmMethod(c, "m")
val List(fn) = callsInMethod(m)
val forNameMeth = byteCodeRepository.methodNode("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;").get._1
val classTp = classBTypeFromInternalName("java/lang/Class")
@@ -168,12 +165,6 @@ class CallGraphTest extends ClearAfterClass {
checkCallsite(fn, m, forNameMeth, classTp, safeToInline = false, atInline = false, atNoInline = false)
}
- /**
- * NOTE: if this test fails for you when running within the IDE, it's probably because you're
- * using 2.12.0-M2 for compilining within the IDE, which doesn't add SAM information to the
- * InlineInfo attribute. So the InlineInfo in the classfile for Function1 doesn't say that
- * it's a SAM type. The test passes when running with ant (which does a full bootstrap).
- */
@Test
def checkArgInfos(): Unit = {
val code =
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala
index 1108a37266..1e01627969 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala
@@ -6,17 +6,11 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.Test
import scala.collection.generic.Clearable
-import scala.collection.immutable.IntMap
-import scala.collection.mutable.ListBuffer
-import scala.reflect.internal.util.{NoPosition, BatchSourceFile}
import scala.tools.asm.Opcodes._
import org.junit.Assert._
import scala.tools.asm.tree._
-import scala.tools.asm.tree.analysis._
-import scala.tools.nsc.io._
import scala.tools.nsc.reporters.StoreReporter
-import scala.tools.testing.AssertUtil._
import CodeGenTools._
import scala.tools.partest.ASMConverters
@@ -69,6 +63,7 @@ class InlinerTest extends ClearAfterClass {
val compiler = InlinerTest.compiler
import compiler.genBCode.bTypes._
import compiler.genBCode.bTypes.backendUtils._
+ import inlinerHeuristics._
def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = {
InlinerTest.notPerRun.foreach(_.clear())
@@ -88,44 +83,25 @@ class InlinerTest extends ClearAfterClass {
assert(callsite.callee.get.callee == callee, callsite.callee.get.callee.name)
}
- def makeInlineRequest( callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType,
- callee: MethodNode, calleeDeclarationClass: ClassBType,
- callsiteStackHeight: Int, receiverKnownNotNull: Boolean,
- post: List[inlinerHeuristics.PostInlineRequest] = Nil) = inlinerHeuristics.InlineRequest(
- callsite = callGraph.Callsite(
- callsiteInstruction = callsiteInstruction,
- callsiteMethod = callsiteMethod,
- callsiteClass = callsiteClass,
- callee = Right(callGraph.Callee(callee = callee, calleeDeclarationClass = calleeDeclarationClass, safeToInline = true, safeToRewrite = false, annotatedInline = false, annotatedNoInline = false, samParamTypes = IntMap.empty, calleeInfoWarning = None)),
- argInfos = IntMap.empty,
- callsiteStackHeight = callsiteStackHeight,
- receiverKnownNotNull = receiverKnownNotNull,
- callsitePosition = NoPosition),
- post = post)
-
- // inline first invocation of f into g in class C
- def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, List[CannotInlineWarning]) = {
- val List(cls) = compile(code)
- mod(cls)
- val clsBType = classBTypeFromParsedClassfile(cls.name)
-
- val List(f, g) = cls.methods.asScala.filter(m => Set("f", "g")(m.name)).toList.sortBy(_.name)
- val fCall = g.instructions.iterator.asScala.collect({ case i: MethodInsnNode if i.name == "f" => i }).next()
-
- val analyzer = new AsmAnalyzer(g, clsBType.internalName)
-
- val request = makeInlineRequest(
- callsiteInstruction = fCall,
- callsiteMethod = g,
- callsiteClass = clsBType,
- callee = f,
- calleeDeclarationClass = clsBType,
- callsiteStackHeight = analyzer.frameAt(fCall).getStackSize,
- receiverKnownNotNull = true
- )
-
- val r = inliner.inline(request)
- (g, r)
+ def getCallsite(method: MethodNode, calleeName: String) = callGraph.callsites(method).valuesIterator.find(_.callee.get.callee.name == calleeName).get
+
+ def gMethAndFCallsite(code: String, mod: ClassNode => Unit = _ => ()) = {
+ val List(c) = compile(code)
+ mod(c)
+ val gMethod = findAsmMethod(c, "g")
+ val fCall = getCallsite(gMethod, "f")
+ (gMethod, fCall)
+ }
+
+ def canInlineTest(code: String, mod: ClassNode => Unit = _ => ()): Option[OptimizerWarning] = {
+ val cs = gMethAndFCallsite(code, mod)._2
+ inliner.earlyCanInlineCheck(cs) orElse inliner.canInlineBody(cs)
+ }
+
+ def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): MethodNode = {
+ val (gMethod, fCall) = gMethAndFCallsite(code, mod)
+ inliner.inline(InlineRequest(fCall, Nil))
+ gMethod
}
@Test
@@ -137,7 +113,7 @@ class InlinerTest extends ClearAfterClass {
|}
""".stripMargin
- val (g, _) = inlineTest(code)
+ val g = inlineTest(code)
val gConv = convertMethod(g)
assertSameCode(gConv.instructions.dropNonOp,
@@ -171,7 +147,7 @@ class InlinerTest extends ClearAfterClass {
// See also discussion around ATHROW in BCodeBodyBuilder
- val (g, _) = inlineTest(code)
+ val g = inlineTest(code)
val expectedInlined = List(
VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this
Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ???
@@ -192,11 +168,11 @@ class InlinerTest extends ClearAfterClass {
|}
""".stripMargin
- val (_, can) = inlineTest(code, cls => {
+ val can = canInlineTest(code, cls => {
val f = cls.methods.asScala.find(_.name == "f").get
f.access |= ACC_SYNCHRONIZED
})
- assert(can.length == 1 && can.head.isInstanceOf[SynchronizedMethod], can)
+ assert(can.nonEmpty && can.get.isInstanceOf[SynchronizedMethod], can)
}
@Test
@@ -207,7 +183,7 @@ class InlinerTest extends ClearAfterClass {
| def g = f + 1
|}
""".stripMargin
- val (_, r) = inlineTest(code)
+ val r = canInlineTest(code)
assert(r.isEmpty, r)
}
@@ -221,8 +197,8 @@ class InlinerTest extends ClearAfterClass {
| def g = println(f)
|}
""".stripMargin
- val (_, r) = inlineTest(code)
- assert(r.length == 1 && r.head.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r)
+ val r = canInlineTest(code)
+ assert(r.nonEmpty && r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r)
}
@Test
@@ -242,31 +218,10 @@ class InlinerTest extends ClearAfterClass {
""".stripMargin
val List(c, d) = compile(code)
-
- val cTp = classBTypeFromParsedClassfile(c.name)
- val dTp = classBTypeFromParsedClassfile(d.name)
-
- val g = c.methods.asScala.find(_.name == "g").get
- val h = d.methods.asScala.find(_.name == "h").get
- val gCall = h.instructions.iterator.asScala.collect({
- case m: MethodInsnNode if m.name == "g" => m
- }).next()
-
- val analyzer = new AsmAnalyzer(h, dTp.internalName)
-
- val request = makeInlineRequest(
- callsiteInstruction = gCall,
- callsiteMethod = h,
- callsiteClass = dTp,
- callee = g,
- calleeDeclarationClass = cTp,
- callsiteStackHeight = analyzer.frameAt(gCall).getStackSize,
- receiverKnownNotNull = true
- )
-
- val r = inliner.inline(request)
-
- assert(r.length == 1 && r.head.isInstanceOf[IllegalAccessInstruction], r)
+ val hMeth = findAsmMethod(d, "h")
+ val gCall = getCallsite(hMeth, "g")
+ val r = inliner.canInlineBody(gCall)
+ assert(r.nonEmpty && r.get.isInstanceOf[IllegalAccessInstruction], r)
}
@Test
@@ -403,27 +358,14 @@ class InlinerTest extends ClearAfterClass {
""".stripMargin
val List(c) = compile(code)
- val f = c.methods.asScala.find(_.name == "f").get
- val callsiteIns = f.instructions.iterator().asScala.collect({ case c: MethodInsnNode => c }).next()
- val clsBType = classBTypeFromParsedClassfile(c.name)
- val analyzer = new AsmAnalyzer(f, clsBType.internalName)
-
- val integerClassBType = classBTypeFromInternalName("java/lang/Integer")
- val lowestOneBitMethod = byteCodeRepository.methodNode(integerClassBType.internalName, "lowestOneBit", "(I)I").get._1
-
- val request = makeInlineRequest(
- callsiteInstruction = callsiteIns,
- callsiteMethod = f,
- callsiteClass = clsBType,
- callee = lowestOneBitMethod,
- calleeDeclarationClass = integerClassBType,
- callsiteStackHeight = analyzer.frameAt(callsiteIns).getStackSize,
- receiverKnownNotNull = false
- )
-
- val r = inliner.inline(request)
- assert(r.isEmpty, r)
- val ins = instructionsFromMethod(f)
+ val fMeth = findAsmMethod(c, "f")
+ val call = getCallsite(fMeth, "lowestOneBit")
+
+ val warning = inliner.canInlineBody(call)
+ assert(warning.isEmpty, warning)
+
+ inliner.inline(InlineRequest(call, Nil))
+ val ins = instructionsFromMethod(fMeth)
// no invocations, lowestOneBit is inlined
assertNoInvoke(ins)
@@ -1065,49 +1007,124 @@ class InlinerTest extends ClearAfterClass {
| final def f = 10
| final def g = f + 19
| final def h = g + 29
+ | final def i = h + 39
|}
""".stripMargin
val List(c) = compile(code)
+ val hMeth = findAsmMethod(c, "h")
+ val gMeth = findAsmMethod(c, "g")
+ val iMeth = findAsmMethod(c, "i")
+ val fCall = getCallsite(gMeth, "f")
+ val gCall = getCallsite(hMeth, "g")
+ val hCall = getCallsite(iMeth, "h")
+
+ val warning = inliner.canInlineBody(gCall)
+ assert(warning.isEmpty, warning)
+
+ inliner.inline(InlineRequest(hCall,
+ post = List(InlineRequest(gCall,
+ post = List(InlineRequest(fCall, Nil))))))
+ assertNoInvoke(convertMethod(iMeth)) // no invoke in i: first h is inlined, then the inlined call to g is also inlined, etc for f
+ assertInvoke(convertMethod(gMeth), "C", "f") // g itself still has the call to f
+ }
- val cTp = classBTypeFromParsedClassfile(c.name)
-
- val f = c.methods.asScala.find(_.name == "f").get
- val g = c.methods.asScala.find(_.name == "g").get
- val h = c.methods.asScala.find(_.name == "h").get
-
- val gCall = h.instructions.iterator.asScala.collect({
- case m: MethodInsnNode if m.name == "g" => m
- }).next()
- val fCall = g.instructions.iterator.asScala.collect({
- case m: MethodInsnNode if m.name == "f" => m
- }).next()
-
- val analyzer = new AsmAnalyzer(h, cTp.internalName)
-
- val request = makeInlineRequest(
- callsiteInstruction = gCall,
- callsiteMethod = h,
- callsiteClass = cTp,
- callee = g,
- calleeDeclarationClass = cTp,
- callsiteStackHeight = analyzer.frameAt(gCall).getStackSize,
- receiverKnownNotNull = false,
- post = List(inlinerHeuristics.PostInlineRequest(fCall, Nil))
- )
-
- val r = inliner.inline(request)
- assertNoInvoke(getSingleMethod(c, "h")) // no invoke in h: first g is inlined, then the inlined call to f is also inlined
- assertInvoke(getSingleMethod(c, "g"), "C", "f") // g itself still has the call to f
- assert(r.isEmpty, r)
+ @Test
+ def postRequestSkipAlreadyInlined(): Unit = {
+ val code =
+ """class C {
+ | final def a = 10
+ | final def b = a + 20
+ | final def c = b + 30
+ | final def d = c + 40
+ |}
+ """.stripMargin
+
+ val List(cl) = compile(code)
+ val List(b, c, d) = List("b", "c", "d").map(findAsmMethod(cl, _))
+ val aCall = getCallsite(b, "a")
+ val bCall = getCallsite(c, "b")
+ val cCall = getCallsite(d, "c")
+
+ inliner.inline(InlineRequest(bCall, Nil))
+
+ val req = InlineRequest(cCall,
+ List(InlineRequest(bCall,
+ List(InlineRequest(aCall, Nil)))))
+ inliner.inline(req)
+
+ assertNoInvoke(convertMethod(d))
+ }
+
+ @Test
+ def inlineAnnotatedCallsite(): Unit = {
+ val code =
+ """class C {
+ | final def a(x: Int, f: Int => Int): Int = f(x)
+ | final def b(x: Int) = x
+ | final def c = 1
+ | final def d[T] = 2
+ | final def e[T](x: T) = c
+ | final def f[T](x: T) = println(x)
+ | final def g(x: Int)(y: Int) = x
+ |
+ | def t1 = a(10, _ + 1)
+ | def t2 = a(10, _ + 1): @noinline
+ | def t3 = b(3)
+ | def t4 = b(3): @inline
+ | def t5 = c: @inline
+ | def t6 = d[Int]: @inline
+ | def t7 = e[Int](2): @inline
+ | def t8 = f[Int](2): @inline
+ | def t9 = g(1)(2): @inline
+ |}
+ """.stripMargin
+
+ val List(c) = compile(code)
+ assertInvoke(getSingleMethod(c, "t1"), "C", "C$$$anonfun$1")
+ assertInvoke(getSingleMethod(c, "t2"), "C", "a")
+ assertInvoke(getSingleMethod(c, "t3"), "C", "b")
+ assertNoInvoke(getSingleMethod(c, "t4"))
+ assertNoInvoke(getSingleMethod(c, "t5"))
+ assertNoInvoke(getSingleMethod(c, "t6"))
+ assertInvoke(getSingleMethod(c, "t7"), "C", "c")
+ assertInvoke(getSingleMethod(c, "t8"), "scala/Predef$", "println")
+ assertNoInvoke(getSingleMethod(c, "t9"))
+ }
+
+ @Test
+ def inlineNoInlineOverride(): Unit = {
+ val code =
+ """class C {
+ | @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
+ | def t2 = f2(1) // not inlined
+ | def t3 = f1(1): @noinline // not inlined
+ | def t4 = f2(1): @inline // not inlined (cannot override the def-site @noinline)
+ | def t5 = f3(1): @inline // inlined
+ | def t6 = f3(1): @noinline // not inlined
+ |
+ | def t7 = f1(1) + (f3(1): @inline) // without parenthesis, the ascription encloses the entire expression..
+ | def t8 = f1(1) + (f1(1): @noinline)
+ | def t9 = f1(1) + f1(1) : @noinline // the ascription goes on the entire expression, so on the + invocation.. both f1 are inlined
+ |}
+ """.stripMargin
+
+ val List(c) = compile(code)
+ assertNoInvoke(getSingleMethod(c, "t1"))
+ assertInvoke(getSingleMethod(c, "t2"), "C", "f2")
+ assertInvoke(getSingleMethod(c, "t3"), "C", "f1")
+ assertInvoke(getSingleMethod(c, "t4"), "C", "f2")
+ assertNoInvoke(getSingleMethod(c, "t5"))
+ assertInvoke(getSingleMethod(c, "t6"), "C", "f3")
+ assertNoInvoke(getSingleMethod(c, "t7"))
+ assertInvoke(getSingleMethod(c, "t8"), "C", "f1")
+ assertNoInvoke(getSingleMethod(c, "t9"))
}
- /**
- * NOTE: if this test fails for you when running within the IDE, it's probably because you're
- * using 2.12.0-M2 for compilining within the IDE, which doesn't add SAM information to the
- * InlineInfo attribute. So the InlineInfo in the classfile for Function1 doesn't say that
- * it's a SAM type. The test passes when running with ant (which does a full bootstrap).
- */
@Test
def inlineHigherOrder(): Unit = {
val code =
@@ -1134,4 +1151,68 @@ class InlinerTest extends ClearAfterClass {
assertInvoke(getSingleMethod(c, "t4"), "scala/Function1", "apply$mcII$sp")
assertInvoke(getSingleMethod(c, "t5"), "C", "h")
}
+
+ @Test
+ def twoStepNoInlineHandler(): Unit = {
+ val code =
+ """class C {
+ | @inline final def f = try 1 catch { case _: Throwable => 2 }
+ | @inline final def g = f
+ | def t = println(g) // cannot inline g onto non-empty stack once that f was inlined into g
+ |}
+ """.stripMargin
+
+ val warn =
+ """C::g()I is annotated @inline but could not be inlined:
+ |The operand stack at the callsite in C::t()V contains more values than the
+ |arguments expected by the callee C::g()I. These values would be discarded
+ |when entering an exception handler declared in the inlined method.""".stripMargin
+
+ val List(c) = compile(code, allowMessage = _.msg contains warn)
+ assertInvoke(getSingleMethod(c, "t"), "C", "g")
+ }
+
+ @Test
+ def twoStepNoInlinePrivate(): Unit = {
+ val code =
+ """class C {
+ | @inline final def g = {
+ | @noinline def f = 0
+ | f
+ | }
+ | @inline final def h = g // after inlining g, h has an invocate of private method f$1
+ |}
+ |class D {
+ | def t(c: C) = c.h // cannot inline
+ |}
+ """.stripMargin
+
+ val warn =
+ """C::h()I is annotated @inline but could not be inlined:
+ |The callee C::h()I contains the instruction INVOKESPECIAL C.f$1 ()I
+ |that would cause an IllegalAccessError when inlined into class D.""".stripMargin
+
+ val List(c, d) = compile(code, allowMessage = _.msg contains warn)
+ assertInvoke(getSingleMethod(c, "h"), "C", "f$1")
+ assertInvoke(getSingleMethod(d, "t"), "C", "h")
+ }
+
+ @Test
+ def twoStepInlinePrivate(): Unit = {
+ val code =
+ """class C {
+ | @inline final def g = { // initially, g invokes the private method f$1, but then f$1 is inlined
+ | @inline def f = 0
+ | f
+ | }
+ |}
+ |class D {
+ | def t(c: C) = c.g // can inline
+ |}
+ """.stripMargin
+
+ val List(c, d) = compile(code)
+ assertNoInvoke(getSingleMethod(c, "g"))
+ assertNoInvoke(getSingleMethod(d, "t"))
+ }
}