summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala')
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala311
1 files changed, 224 insertions, 87 deletions
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 64638ca34d..b9f593a4d8 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
@@ -25,49 +25,110 @@ class Inliner[BT <: BTypes](val btypes: BT) {
import inlinerHeuristics._
import backendUtils._
- case class InlineLog(request: InlineRequest, sizeBefore: Int, sizeAfter: Int, sizeInlined: Int, warning: Option[CannotInlineWarning])
- var inlineLog: List[InlineLog] = Nil
+ sealed trait InlineLog {
+ def request: InlineRequest
+ }
+ final case class InlineLogSuccess(request: InlineRequest, sizeBefore: Int, sizeInlined: Int) extends InlineLog {
+ var downstreamLog: mutable.Buffer[InlineLog] = mutable.ListBuffer.empty
+ }
+ final case class InlineLogFail(request: InlineRequest, warning: CannotInlineWarning) extends InlineLog
+ final case class InlineLogRollback(request: InlineRequest, warnings: List[CannotInlineWarning]) extends InlineLog
+
+ object InlineLog {
+ private def shouldLog(request: InlineRequest): Boolean = {
+ def logEnabled = compilerSettings.YoptLogInline.isSetByUser
+ def matchesName = {
+ val prefix = compilerSettings.YoptLogInline.value match {
+ case "_" => ""
+ case p => p
+ }
+ val name: String = request.callsite.callsiteClass.internalName + "." + request.callsite.callsiteMethod.name
+ name startsWith prefix
+ }
+ logEnabled && (upstream != null || (isTopLevel && matchesName))
+ }
- def runInliner(): Unit = {
- for (request <- collectAndOrderInlineRequests) {
- val Right(callee) = request.callsite.callee // collectAndOrderInlineRequests returns callsites with a known callee
+ // indexed by callsite method
+ private val logs = mutable.Map.empty[MethodNode, mutable.LinkedHashSet[InlineLog]]
- // 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!)
+ private var upstream: InlineLogSuccess = _
+ private var isTopLevel = true
- val warnings = inline(request)
- for (warning <- warnings) {
- if ((callee.annotatedInline && btypes.compilerSettings.optWarningEmitAtInlineFailed) || 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)
- }
+ def withInlineLogging[T](request: InlineRequest)(inlineRequest: => Unit)(inlinePost: => T): T = {
+ def doInlinePost(): T = {
+ val savedIsTopLevel = isTopLevel
+ isTopLevel = false
+ try inlinePost
+ finally isTopLevel = savedIsTopLevel
+ }
+ if (shouldLog(request)) {
+ val sizeBefore = request.callsite.callsiteMethod.instructions.size
+ inlineRequest
+ val log = InlineLogSuccess(request, sizeBefore, request.callsite.callee.get.callee.instructions.size)
+ apply(log)
+
+ val savedUpstream = upstream
+ upstream = log
+ try doInlinePost()
+ finally upstream = savedUpstream
+ } else {
+ inlineRequest
+ doInlinePost()
}
}
- if (compilerSettings.YoptLogInline.isSetByUser) {
- val methodPrefix = { val p = compilerSettings.YoptLogInline.value; if (p == "_") "" else p }
- val byCallsiteMethod = inlineLog.groupBy(_.request.callsite.callsiteMethod).toList.sortBy(_._2.head.request.callsite.callsiteClass.internalName)
- for ((m, mLogs) <- byCallsiteMethod) {
- val initialSize = mLogs.minBy(_.sizeBefore).sizeBefore
- val firstLog = mLogs.head
- val methodName = s"${firstLog.request.callsite.callsiteClass.internalName}.${m.name}"
- if (methodName.startsWith(methodPrefix)) {
- println(s"Inlining into $methodName (initially $initialSize instructions, ultimately ${m.instructions.size}):")
- val byCallee = mLogs.groupBy(_.request.callsite.callee.get).toList.sortBy(_._2.length).reverse
- for ((c, cLogs) <- byCallee) {
- val first = cLogs.head
- if (first.warning.isEmpty) {
- val num = if (cLogs.tail.isEmpty) "" else s" ${cLogs.length} times"
- println(s" - Inlined ${c.calleeDeclarationClass.internalName}.${c.callee.name} (${first.sizeInlined} instructions)$num: ${first.request.reason}")
- } else
- println(s" - Failed to inline ${c.calleeDeclarationClass.internalName}.${c.callee.name} (${first.request.reason}): ${first.warning.get}")
- }
- println()
- }
+ def apply(log: => InlineLog): Unit = if (shouldLog(log.request)) {
+ if (upstream != null) upstream.downstreamLog += log
+ else {
+ val methodLogs = logs.getOrElseUpdate(log.request.callsite.callsiteMethod, mutable.LinkedHashSet.empty)
+ methodLogs += log
+ }
+ }
+
+ def entryString(log: InlineLog, indent: Int = 0): String = {
+ val callee = log.request.callsite.callee.get
+ val calleeString = callee.calleeDeclarationClass.internalName + "." + callee.callee.name
+ val indentString = " " * indent
+ log match {
+ case s @ InlineLogSuccess(_, sizeBefore, sizeInlined) =>
+ val self = s"${indentString}inlined $calleeString. Before: $sizeBefore ins, inlined: $sizeInlined ins."
+ if (s.downstreamLog.isEmpty) self
+ else s.downstreamLog.iterator.map(entryString(_, indent + 2)).mkString(self + "\n", "\n", "")
+
+ case InlineLogFail(_, w) =>
+ s"${indentString}failed $calleeString. ${w.toString.replace('\n', ' ')}"
+
+ case InlineLogRollback(_, _) =>
+ s"${indentString}rolling back, nested inline failed."
+ }
+ }
+
+ def print(): Unit = if (compilerSettings.YoptLogInline.isSetByUser) {
+ val byClassAndMethod: List[(InternalName, mutable.Map[MethodNode, mutable.LinkedHashSet[InlineLog]])] = {
+ logs.
+ groupBy(_._2.head.request.callsite.callsiteClass.internalName).
+ toList.sortBy(_._1)
+ }
+ for {
+ (c, methodLogs) <- byClassAndMethod
+ (m, mLogs) <- methodLogs.toList.sortBy(_._1.name)
+ mLog <- mLogs // insertion order
+ } {
+ println(s"Inline into $c.${m.name}: ${entryString(mLog)}")
+ }
+ }
+ }
+
+ def runInliner(): Unit = {
+ for (request <- collectAndOrderInlineRequests) {
+ val Right(callee) = request.callsite.callee // collectAndOrderInlineRequests returns callsites with a known callee
+ val warnings = inline(request)
+ for (warning <- warnings) {
+ if (warning.emitWarning(compilerSettings))
+ backendReporting.inlinerWarning(request.callsite.callsitePosition, warning.toString)
}
}
+ InlineLog.print()
}
/**
@@ -221,26 +282,79 @@ class Inliner[BT <: BTypes](val btypes: BT) {
impl(post, mainCallsite)
}
+ class UndoLog(active: Boolean = true) {
+ import java.util.{ ArrayList => JArrayList }
+
+ private var actions = List.empty[() => Unit]
+ private var methodStateSaved = false
+
+ def apply(a: => Unit): Unit = if (active) actions = (() => a) :: actions
+ def rollback(): Unit = if (active) actions.foreach(_.apply())
+
+ def saveMethodState(methodNode: MethodNode): Unit = if (active && !methodStateSaved) {
+ methodStateSaved = true
+ val currentInstructions = methodNode.instructions.toArray
+ val currentLocalVariables = new JArrayList(methodNode.localVariables)
+ val currentTryCatchBlocks = new JArrayList(methodNode.tryCatchBlocks)
+ val currentMaxLocals = methodNode.maxLocals
+ val currentMaxStack = methodNode.maxStack
+
+ apply {
+ // `methodNode.instructions.clear()` doesn't work: it keeps the `prev` / `next` / `index` of
+ // instruction nodes. `instructions.removeAll(true)` would work, but is not public.
+ methodNode.instructions.iterator.asScala.toList.foreach(methodNode.instructions.remove)
+ for (i <- currentInstructions) methodNode.instructions.add(i)
+
+ methodNode.localVariables.clear()
+ methodNode.localVariables.addAll(currentLocalVariables)
+
+ methodNode.tryCatchBlocks.clear()
+ methodNode.tryCatchBlocks.addAll(currentTryCatchBlocks)
+
+ methodNode.maxLocals = currentMaxLocals
+ methodNode.maxStack = currentMaxStack
+ }
+ }
+ }
+
+ val NoUndoLogging = new UndoLog(active = false)
/**
* 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] = canInlineBody(request.callsite) match {
- case Some(w) =>
- if (compilerSettings.YoptLogInline.isSetByUser) {
- val size = request.callsite.callsiteMethod.instructions.size
- inlineLog ::= InlineLog(request, size, size, 0, Some(w))
+ def inline(request: InlineRequest, undo: UndoLog = NoUndoLogging): List[CannotInlineWarning] = {
+ def doInline(undo: UndoLog, callRollback: Boolean = false): List[CannotInlineWarning] = {
+ InlineLog.withInlineLogging(request) {
+ inlineCallsite(request.callsite, undo)
+ } {
+ val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite))
+ val warnings = postRequests.flatMap(inline(_, undo))
+ if (callRollback && warnings.nonEmpty) {
+ undo.rollback()
+ InlineLog(InlineLogRollback(request, warnings))
+ }
+ warnings
}
- List(w)
- case None =>
- val sizeBefore = request.callsite.callsiteMethod.instructions.size
- inlineCallsite(request.callsite)
- if (compilerSettings.YoptLogInline.isSetByUser)
- inlineLog ::= InlineLog(request, sizeBefore, request.callsite.callsiteMethod.instructions.size, request.callsite.callee.get.callee.instructions.size, None)
- val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite))
- postRequests flatMap inline
+ }
+
+ def inlinedByPost(insns: List[AbstractInsnNode]): Boolean =
+ insns.nonEmpty && insns.forall(ins => request.post.exists(_.callsite.callsiteInstruction == ins))
+
+ canInlineCallsite(request.callsite) match {
+ case None =>
+ doInline(undo)
+
+ case Some((_, illegalAccessInsns)) if inlinedByPost(illegalAccessInsns) =>
+ // speculatively inline, roll back if an illegalAccessInsn cannot be eliminated
+ if (undo == NoUndoLogging) doInline(new UndoLog(), callRollback = true)
+ else doInline(undo)
+
+ case Some((w, _)) =>
+ InlineLog(InlineLogFail(request, w))
+ List(w)
+ }
}
/**
@@ -253,7 +367,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): Unit = {
+ def inlineCallsite(callsite: Callsite, undo: UndoLog = NoUndoLogging): Unit = {
import callsite.{callsiteClass, callsiteMethod, callsiteInstruction, receiverKnownNotNull, callsiteStackHeight}
val Right(callsiteCallee) = callsite.callee
import callsiteCallee.{callee, calleeDeclarationClass, sourceFilePath}
@@ -380,6 +494,8 @@ class Inliner[BT <: BTypes](val btypes: BT) {
clonedInstructions.insert(postCallLabel, retVarLoad)
}
+ undo.saveMethodState(callsiteMethod)
+
callsiteMethod.instructions.insert(callsiteInstruction, clonedInstructions)
callsiteMethod.instructions.remove(callsiteInstruction)
@@ -406,7 +522,8 @@ class Inliner[BT <: BTypes](val btypes: BT) {
callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, math.max(stackHeightAtNullCheck, maxStackOfInlinedCode))
- addIndyLambdaImplMethod(callsiteClass.internalName, targetHandles)
+ val added = addIndyLambdaImplMethod(callsiteClass.internalName, targetHandles)
+ undo { removeIndyLambdaImplMethod(callsiteClass.internalName, added) }
callGraph.addIfMissing(callee, calleeDeclarationClass)
@@ -426,8 +543,13 @@ class Inliner[BT <: BTypes](val btypes: BT) {
argInfos = argInfos,
callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight
)
- originalCallsite.inlinedClones += ClonedCallsite(newCallsite, callsite)
+ val clonedCallsite = ClonedCallsite(newCallsite, callsite)
+ originalCallsite.inlinedClones += clonedCallsite
callGraph.addCallsite(newCallsite)
+ undo {
+ originalCallsite.inlinedClones -= clonedCallsite
+ callGraph.removeCallsite(newCallsite.callsiteInstruction, newCallsite.callsiteMethod)
+ }
}
callGraph.closureInstantiations(callee).valuesIterator foreach { originalClosureInit =>
@@ -440,10 +562,14 @@ class Inliner[BT <: BTypes](val btypes: BT) {
capturedArgInfos)
originalClosureInit.inlinedClones += newClosureInit
callGraph.addClosureInstantiation(newClosureInit)
+ undo {
+ callGraph.removeClosureInstantiation(newClosureInit.lambdaMetaFactoryCall.indy, newClosureInit.ownerMethod)
+ }
}
// Remove the elided invocation from the call graph
callGraph.removeCallsite(callsiteInstruction, callsiteMethod)
+ undo { callGraph.addCallsite(callsite) }
// Inlining a method body can render some code unreachable, see example above in this method.
unreachableCodeEliminated -= callsiteMethod
@@ -467,10 +593,10 @@ class Inliner[BT <: BTypes](val btypes: BT) {
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))
+ Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated))
} else if (isStrictfpMethod(callsiteMethod) != isStrictfpMethod(callee)) {
Some(StrictfpMismatch(
- calleeDeclarationClass.internalName, callee.name, callee.desc,
+ calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated,
callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
} else
None
@@ -486,9 +612,14 @@ class Inliner[BT <: BTypes](val btypes: BT) {
* 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
+ * Returns
+ * - `None` if the callsite can be inlined
+ * - `Some((message, Nil))` if there was an issue performing the access checks, for example
+ * because of a missing classfile
+ * - `Some((message, instructions))` if inlining `instructions` into the callsite method would
+ * cause an IllegalAccessError
*/
- def canInlineBody(callsite: Callsite): Option[CannotInlineWarning] = {
+ def canInlineCallsite(callsite: Callsite): Option[(CannotInlineWarning, List[AbstractInsnNode])] = {
import callsite.{callsiteInstruction, callsiteMethod, callsiteClass, callsiteStackHeight}
val Right(callsiteCallee) = callsite.callee
import callsiteCallee.{callee, calleeDeclarationClass}
@@ -519,23 +650,30 @@ class Inliner[BT <: BTypes](val btypes: BT) {
}
if (codeSizeOKForInlining(callsiteMethod, callee)) {
- Some(ResultingMethodTooLarge(
- calleeDeclarationClass.internalName, callee.name, callee.desc,
- callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
+ val warning = ResultingMethodTooLarge(
+ calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated,
+ callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)
+ Some((warning, Nil))
} else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) {
- Some(MethodWithHandlerCalledOnNonEmptyStack(
- calleeDeclarationClass.internalName, callee.name, callee.desc,
- callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
- } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) map {
- case (illegalAccessIns, None) =>
- IllegalAccessInstruction(
- calleeDeclarationClass.internalName, callee.name, callee.desc,
- callsiteClass.internalName, illegalAccessIns)
-
- case (illegalAccessIns, Some(warning)) =>
- IllegalAccessCheckFailed(
- calleeDeclarationClass.internalName, callee.name, callee.desc,
- callsiteClass.internalName, illegalAccessIns, warning)
+ val warning = MethodWithHandlerCalledOnNonEmptyStack(
+ calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated,
+ callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)
+ Some((warning, Nil))
+ } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) match {
+ case Right(Nil) =>
+ None
+
+ case Right(illegalAccessInsns) =>
+ val warning = IllegalAccessInstruction(
+ calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated,
+ callsiteClass.internalName, illegalAccessInsns.head)
+ Some((warning, illegalAccessInsns))
+
+ case Left((illegalAccessIns, cause)) =>
+ val warning = IllegalAccessCheckFailed(
+ calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated,
+ callsiteClass.internalName, illegalAccessIns, cause)
+ Some((warning, Nil))
}
}
@@ -624,13 +762,14 @@ class Inliner[BT <: BTypes](val btypes: BT) {
}
/**
- * Returns the first instruction in the `instructions` list that would cause a
- * [[java.lang.IllegalAccessError]] when inlined into the `destinationClass`.
- *
- * If validity of some instruction could not be checked because an error occurred, the instruction
- * is returned together with a warning message that describes the problem.
+ * Returns
+ * - `Right(Nil)` if all instructions can be safely inlined
+ * - `Right(insns)` if inlining any of `insns` would cause a [[java.lang.IllegalAccessError]]
+ * when inlined into the `destinationClass`
+ * - `Left((insn, warning))` if validity of some instruction could not be checked because an
+ * error occurred
*/
- def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
+ def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Either[(AbstractInsnNode, OptimizerWarning), List[AbstractInsnNode]] = {
/**
* Check if `instruction` can be transplanted to `destinationClass`.
*
@@ -759,17 +898,15 @@ class Inliner[BT <: BTypes](val btypes: BT) {
}
val it = instructions.iterator.asScala
- @tailrec def find: Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
- if (!it.hasNext) None // all instructions are legal
- else {
- val i = it.next()
- isLegal(i) match {
- case Left(warning) => Some((i, Some(warning))) // checking isLegal for i failed
- case Right(false) => Some((i, None)) // an illegal instruction was found
- case _ => find
- }
+ val illegalAccess = mutable.ListBuffer.empty[AbstractInsnNode]
+ while (it.hasNext) {
+ val i = it.next()
+ isLegal(i) match {
+ case Left(warning) => return Left((i, warning)) // checking isLegal for i failed
+ case Right(false) => illegalAccess += i // an illegal instruction was found
+ case _ =>
}
}
- find
+ Right(illegalAccess.toList)
}
}