scala.tools.nsc.backend.jvm.opt.CallGraph.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scala-compiler Show documentation
Show all versions of scala-compiler Show documentation
Compiler for the Scala Programming Language
The newest version!
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala.tools.nsc
package backend.jvm
package opt
import scala.collection.concurrent.TrieMap
import scala.collection.immutable.IntMap
import scala.collection.{concurrent, mutable}
import scala.jdk.CollectionConverters._
import scala.reflect.internal.util.{NoPosition, Position}
import scala.tools.asm.tree._
import scala.tools.asm.{Opcodes, Type}
import scala.tools.nsc.backend.jvm.BTypes.InternalName
import scala.tools.nsc.backend.jvm.BackendReporting._
import scala.tools.nsc.backend.jvm.analysis.BackendUtils.LambdaMetaFactoryCall
import scala.tools.nsc.backend.jvm.analysis.TypeFlowInterpreter.{LMFValue, ParamValue}
import scala.tools.nsc.backend.jvm.analysis._
import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._
abstract class CallGraph {
val postProcessor: PostProcessor
import postProcessor._
import bTypes._
import bTypesFromClassfile._
import frontendAccess.recordPerRunCache
/**
* The call graph contains the callsites in the program being compiled.
*
* Indexing the call graph by the containing MethodNode and the invocation MethodInsnNode allows
* finding callsites efficiently. For example, an inlining heuristic might want to know all
* callsites within a callee method.
*
* Note that the call graph is not guaranteed to be complete: callsites may be missing. In
* particular, if a method is very large, all of its callsites might not be in the hash map.
* The reason is that adding a method to the call graph requires running an ASM analyzer, which
* can be too slow.
*
* Note that call graph entries (Callsite instances) keep a reference to the invocation
* MethodInsnNode, which keeps all AbstractInsnNodes of the method reachable. Adding classes
* from the classpath to the call graph (in addition to classes being compiled) may prevent
* method instruction nodes from being GCd. The ByteCodeRepository has a fixed size cache for
* parsed ClassNodes - keeping all ClassNodes alive consumed too much memory.
* The call graph is less problematic because only methods being called are kept alive, not entire
* classes. But we should keep an eye on this.
*/
val callsites: mutable.Map[MethodNode, Map[MethodInsnNode, Callsite]] = recordPerRunCache(concurrent.TrieMap.empty withDefaultValue Map.empty)
/**
* Closure instantiations in the program being compiled.
*
* Indexing closure instantiations by the containing MethodNode is beneficial for the closure
* optimizer: finding callsites to re-write requires running a producers-consumers analysis on
* the method. Here the closure instantiations are already grouped by method.
*/
//currently single threaded access only
val closureInstantiations: mutable.Map[MethodNode, Map[InvokeDynamicInsnNode, ClosureInstantiation]] = recordPerRunCache(concurrent.TrieMap.empty withDefaultValue Map.empty)
/**
* Store the position of every MethodInsnNode during code generation. This allows each callsite
* in the call graph to remember its source position, which is required for inliner warnings.
*/
val callsitePositions: concurrent.Map[MethodInsnNode, Position] = recordPerRunCache(TrieMap.empty)
/**
* Stores callsite instructions of invocations 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.
*/
//currently single threaded access only
val inlineAnnotatedCallsites: mutable.Set[MethodInsnNode] = recordPerRunCache(mutable.Set.empty)
//currently single threaded access only
val noInlineAnnotatedCallsites: mutable.Set[MethodInsnNode] = recordPerRunCache(mutable.Set.empty)
// Contains `INVOKESPECIAL` instructions that were cloned by the inliner and need to be resolved
// statically by the call graph. See Inliner.maybeInlinedLater.
val staticallyResolvedInvokespecial: mutable.Set[MethodInsnNode] = recordPerRunCache(mutable.Set.empty)
def isStaticCallsite(call: MethodInsnNode): Boolean = {
val opc = call.getOpcode
opc == Opcodes.INVOKESTATIC || opc == Opcodes.INVOKESPECIAL && staticallyResolvedInvokespecial(call)
}
def removeCallsite(invocation: MethodInsnNode, methodNode: MethodNode): Option[Callsite] = {
val methodCallsites = callsites(methodNode)
val newCallsites = methodCallsites - invocation
if (newCallsites.isEmpty) callsites.remove(methodNode)
else callsites(methodNode) = newCallsites
methodCallsites.get(invocation)
}
def addCallsite(callsite: Callsite): Unit = {
val methodCallsites = callsites(callsite.callsiteMethod)
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
if (newClosureInits.isEmpty) closureInstantiations.remove(methodNode)
else closureInstantiations(methodNode) = newClosureInits
methodClosureInits.get(indy)
}
def addClass(classNode: ClassNode): Unit = {
val classType = classBTypeFromClassNode(classNode)
classNode.methods.asScala.foreach(addMethod(_, classType))
}
def refresh(methodNode: MethodNode, definingClass: ClassBType): Unit = {
callsites.remove(methodNode)
closureInstantiations.remove(methodNode)
// callsitePositions, inlineAnnotatedCallsites, noInlineAnnotatedCallsites, staticallyResolvedInvokespecial
// are left unchanged. They contain individual instructions, the state for those remains valid in case
// the inliner performs a rollback.
addMethod(methodNode, definingClass)
}
def addMethod(methodNode: MethodNode, definingClass: ClassBType): Unit = {
if (!BytecodeUtils.isAbstractMethod(methodNode) && !BytecodeUtils.isNativeMethod(methodNode) && AsmAnalyzer.sizeOKForBasicValue(methodNode)) {
lazy val typeAnalyzer = new NonLubbingTypeFlowAnalyzer(methodNode, definingClass.internalName)
var methodCallsites = Map.empty[MethodInsnNode, Callsite]
var methodClosureInstantiations = Map.empty[InvokeDynamicInsnNode, ClosureInstantiation]
methodNode.instructions.iterator.asScala foreach {
case call: MethodInsnNode if typeAnalyzer.frameAt(call) != null => // skips over unreachable code
// JVMS 6.5 invokespecial: " If all of the following are true, let C be the direct superclass of the current class"
def isSuperCall: Boolean =
call.getOpcode == Opcodes.INVOKESPECIAL &&
call.name != GenBCode.INSTANCE_CONSTRUCTOR_NAME && {
val owner = call.owner
definingClass.internalName != owner && {
var nextSuper = definingClass.info.get.superClass
while (nextSuper.nonEmpty) {
if (nextSuper.get.internalName == owner) return true
nextSuper = nextSuper.get.info.get.superClass
}
false
}
}
val paramTps = FLazy(Type.getArgumentTypes(call.desc))
// This is the type where method lookup starts (implemented in byteCodeRepository.methodNode)
val preciseOwner =
if (isStaticCallsite(call)) call.owner
else if (isSuperCall) definingClass.info.get.superClass.get.internalName
else if (call.getOpcode == Opcodes.INVOKESPECIAL) call.owner
else {
// invokevirtual, invokeinterface: start search at the type of the receiver
val f = typeAnalyzer.frameAt(call)
// Not Type.getArgumentsAndReturnSizes: in asm.Frame, size-2 values use a single stack slot
val numParams = paramTps.get.length
f.peekStack(numParams).getType.getInternalName
}
val callee: Either[OptimizerWarning, Callee] = {
for {
(method, declarationClass) <- byteCodeRepository.methodNode(preciseOwner, call.name, call.desc): Either[OptimizerWarning, (MethodNode, InternalName)]
(declarationClassNode, calleeSourceFilePath) <- byteCodeRepository.classNodeAndSourceFilePath(declarationClass): Either[OptimizerWarning, (ClassNode, Option[String])]
} yield {
val declarationClassBType = classBTypeFromClassNode(declarationClassNode)
val info = analyzeCallsite(method, declarationClassBType, call, paramTps, calleeSourceFilePath, definingClass)
import info._
Callee(
callee = method,
calleeDeclarationClass = declarationClassBType,
isStaticallyResolved = isStaticallyResolved,
sourceFilePath = sourceFilePath,
annotatedInline = annotatedInline,
annotatedNoInline = annotatedNoInline,
samParamTypes = info.samParamTypes,
calleeInfoWarning = warning)
}
}
val argInfos = computeArgInfos(callee, call, paramTps, typeAnalyzer)
// A nullness analysis could be used to prevent emitting unnecessary receiver null checks
// when inlining non-static callsites. However, LocalOpt's nullness cleanup will also do
// it after the fact, so we can avoid running the nullness analysis when building the call
// graph (or when inlining).
val receiverNotNull = call.getOpcode == Opcodes.INVOKESTATIC
methodCallsites += call -> Callsite(
callsiteInstruction = call,
callsiteMethod = methodNode,
callsiteClass = definingClass,
callee = callee,
argInfos = argInfos,
callsiteStackHeight = typeAnalyzer.frameAt(call).getStackSize,
receiverKnownNotNull = receiverNotNull,
callsitePosition = callsitePositions.getOrElse(call, NoPosition),
annotatedInline = inlineAnnotatedCallsites(call),
annotatedNoInline = noInlineAnnotatedCallsites(call)
)
case LambdaMetaFactoryCall(indy, samMethodType, implMethod, instantiatedMethodType, indyParamTypes) if typeAnalyzer.frameAt(indy) != null =>
val lmf = LambdaMetaFactoryCall(indy, samMethodType, implMethod, instantiatedMethodType)
val capturedArgInfos = computeCapturedArgInfos(lmf, indyParamTypes, typeAnalyzer)
methodClosureInstantiations += indy -> ClosureInstantiation(
lmf,
methodNode,
definingClass,
capturedArgInfos)
case _ =>
}
callsites(methodNode) = methodCallsites
closureInstantiations(methodNode) = methodClosureInstantiations
}
}
def computeArgInfos(callee: Either[OptimizerWarning, Callee], callsiteInsn: MethodInsnNode, paramTps: FLazy[Array[Type]], typeAnalyzer: NonLubbingTypeFlowAnalyzer): IntMap[ArgInfo] = {
if (callee.isLeft) IntMap.empty
else {
val numArgs = FLazy(paramTps.get.length + (if (callsiteInsn.getOpcode == Opcodes.INVOKESTATIC) 0 else 1))
argInfosForSams(callee.get.samParamTypes, callsiteInsn, numArgs, typeAnalyzer)
}
}
def computeCapturedArgInfos(lmf: LambdaMetaFactoryCall, indyParamTypes: Array[Type], typeAnalyzer: NonLubbingTypeFlowAnalyzer): IntMap[ArgInfo] = {
val capturedTypes = indyParamTypes.map(t => bTypeForDescriptorFromClassfile(t.getDescriptor))
val capturedSams = samTypes(capturedTypes)
argInfosForSams(capturedSams, lmf.indy, FLazy(indyParamTypes.length), typeAnalyzer)
}
private def argInfosForSams(sams: IntMap[ClassBType], consumerInsn: AbstractInsnNode, numConsumed: FLazy[Int], typeAnalyzer: NonLubbingTypeFlowAnalyzer): IntMap[ArgInfo] = {
lazy val consumerFrame = typeAnalyzer.frameAt(consumerInsn)
lazy val firstConsumedSlot = consumerFrame.stackTop - numConsumed.get + 1
val samInfos: IntMap[ArgInfo] = sams flatMap {
case (index, _) =>
val argInfo = consumerFrame.getValue(firstConsumedSlot + index) match {
case _: LMFValue => Some(FunctionLiteral)
case p: ParamValue => Some(ForwardedParam(p.local))
case _ => None
}
argInfo.map((index, _))
}
val isArrayLoadOrUpdateOnKnownArray = BackendUtils.isRuntimeArrayLoadOrUpdate(consumerInsn) &&
consumerFrame.getValue(firstConsumedSlot + 1).getType.getSort == Type.ARRAY
if (isArrayLoadOrUpdateOnKnownArray) samInfos.updated(1, StaticallyKnownArray)
else samInfos
}
def samParamTypes(methodNode: MethodNode, paramTps: Array[Type], receiverType: ClassBType): IntMap[ClassBType] = {
val paramTypes = {
val params = paramTps.map(t => bTypeForDescriptorFromClassfile(t.getDescriptor))
val isStatic = BytecodeUtils.isStaticMethod(methodNode)
if (isStatic) params else receiverType +: params
}
samTypes(paramTypes)
}
private def samTypes(types: Array[BType]): IntMap[ClassBType] = {
var res = IntMap.empty[ClassBType]
for (i <- types.indices) {
types(i) match {
case c: ClassBType =>
if (c.info.get.inlineInfo.sam.isDefined) res = res.updated(i, c)
case _ =>
}
}
res
}
final class FLazy[@specialized(Int) T](_init: => T) {
private[this] var init = () => _init
private[this] var v: T = _
def get: T = {
if (init != null) {
v = init()
init = null
}
v
}
}
object FLazy {
def apply[T](init: => T): FLazy[T] = new FLazy(init)
}
/**
* Just a named tuple used as return type of `analyzeCallsite`.
*/
private case class CallsiteInfo(isStaticallyResolved: Boolean, sourceFilePath: Option[String],
annotatedInline: Boolean, annotatedNoInline: Boolean,
samParamTypes: IntMap[ClassBType],
warning: Option[CalleeInfoWarning])
/**
* Analyze a callsite and gather meta-data that can be used for inlining decisions.
*/
private def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, call: MethodInsnNode, paramTps: FLazy[Array[Type]], calleeSourceFilePath: Option[String], callsiteClass: ClassBType): CallsiteInfo = {
val methodSignature = (calleeMethodNode.name, calleeMethodNode.desc)
try {
// The inlineInfo.methodInfos of a ClassBType holds an InlineInfo for each method *declared*
// within a class (not for inherited methods). Since we already have the classBType of the
// callee, we only check there for the methodInlineInfo, we should find it there.
calleeDeclarationClassBType.info.orThrow.inlineInfo.methodInfos.get(methodSignature) match {
case Some(methodInlineInfo) =>
val receiverType = classBTypeFromParsedClassfile(call.owner)
// (1) Special case for trait super accessors. trait T { def f = 1 } generates a static
// method t$ which calls `invokespecial T.f`. Even if `f` is not final, this call will
// always resolve to `T.f`. This is a (very) special case. Otherwise, `invokespecial`
// is only used for private methods, constructors and super calls.
//
// (2) A non-final method can be safe to inline if the receiver type is a final subclass. Example:
// class A { @inline def f = 1 }; object B extends A; B.f // can be inlined
//
// TODO: (2) doesn't cover the following example:
// trait TravLike { def map = ... }
// sealed trait List extends TravLike { ... } // assume map is not overridden
// final case class :: / final case object Nil
// (l: List).map // can be inlined
// we need to know that
// - the receiver is sealed
// - what are the children of the receiver
// - all children are final
// - none of the children overrides map
//
// TODO: type analysis can render more calls statically resolved. Example:
// new A.f // can be inlined, the receiver type is known to be exactly A.
val isStaticallyResolved: Boolean = {
isStaticCallsite(call) ||
(call.getOpcode == Opcodes.INVOKESPECIAL && receiverType == callsiteClass) || // (1)
methodInlineInfo.effectivelyFinal ||
receiverType.info.orThrow.inlineInfo.isEffectivelyFinal // (2)
}
val warning = calleeDeclarationClassBType.info.orThrow.inlineInfo.warning.map(
MethodInlineInfoIncomplete(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, _))
CallsiteInfo(
isStaticallyResolved = isStaticallyResolved,
sourceFilePath = calleeSourceFilePath,
annotatedInline = methodInlineInfo.annotatedInline,
annotatedNoInline = methodInlineInfo.annotatedNoInline,
samParamTypes = samParamTypes(calleeMethodNode, paramTps.get, receiverType),
warning = warning)
case None =>
val warning = MethodInlineInfoMissing(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, calleeDeclarationClassBType.info.orThrow.inlineInfo.warning)
CallsiteInfo(false, None, false, false, IntMap.empty, Some(warning))
}
} catch {
case Invalid(noInfo: NoClassBTypeInfo) =>
val warning = MethodInlineInfoError(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, noInfo)
CallsiteInfo(false, None, false, false, IntMap.empty, Some(warning))
}
}
/**
* A callsite in the call graph.
*
* @param callsiteInstruction The invocation instruction
* @param callsiteMethod The method containing the callsite
* @param callsiteClass The class containing the callsite
* @param callee The callee, as it appears in the invocation instruction. For virtual
* calls, an override of the callee might be invoked. Also, the callee
* can be abstract. Contains a warning message if the callee MethodNode
* cannot be found in the bytecode repository.
* @param argInfos Information about the invocation receiver and arguments
* @param callsiteStackHeight The stack height at the callsite, required by the inliner
* @param callsitePosition The source position of the callsite, used for inliner warnings.
*/
final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType,
callee: Either[OptimizerWarning, Callee], argInfos: IntMap[ArgInfo],
callsiteStackHeight: Int, receiverKnownNotNull: Boolean, callsitePosition: Position,
annotatedInline: Boolean, annotatedNoInline: Boolean) {
// an annotation at the callsite takes precedence over an annotation at the definition site
def isInlineAnnotated = annotatedInline || (callee.get.annotatedInline && !annotatedNoInline)
def isNoInlineAnnotated = annotatedNoInline || (callee.get.annotatedNoInline && !annotatedInline)
override def toString =
"Invocation of" +
s" ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}" +
s"@${callsiteMethod.instructions.indexOf(callsiteInstruction)}" +
s" in ${callsiteClass.internalName}.${callsiteMethod.name}${callsiteMethod.desc}"
}
/**
* Information about invocation arguments, obtained through data flow analysis of the callsite method.
*/
sealed trait ArgInfo
case object FunctionLiteral extends ArgInfo
final case class ForwardedParam(index: Int) extends ArgInfo
case object StaticallyKnownArray extends ArgInfo
// final case class ArgTypeInfo(argType: BType, isPrecise: Boolean, knownNotNull: Boolean) extends ArgInfo
// can be extended, e.g., with constant types
/**
* A callee in the call graph.
*
* @param callee The callee, as it appears in the invocation instruction. For
* virtual calls, an override of the callee might be invoked. Also,
* the callee can be abstract.
* @param calleeDeclarationClass The class in which the callee is declared
* @param isStaticallyResolved True if the callee cannot be overridden
* @param annotatedInline True if the callee is annotated @inline
* @param annotatedNoInline True if the callee is annotated @noinline
* @param samParamTypes A map from parameter positions to SAM parameter types
* @param calleeInfoWarning An inliner warning if some information was not available while
* gathering the information about this callee.
*/
final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType,
isStaticallyResolved: Boolean, sourceFilePath: Option[String],
annotatedInline: Boolean, annotatedNoInline: Boolean,
samParamTypes: IntMap[ClassBType],
calleeInfoWarning: Option[CalleeInfoWarning]) {
override def toString = s"Callee($calleeDeclarationClass.${callee.name})"
def canInlineFromSource = inlinerHeuristics.canInlineFromSource(sourceFilePath, calleeDeclarationClass.internalName)
def isAbstract = isAbstractMethod(callee)
def isSpecialMethod = isConstructor(callee) || isNativeMethod(callee) || hasCallerSensitiveAnnotation(callee)
def safeToInline = isStaticallyResolved && canInlineFromSource && !isAbstract && !isSpecialMethod
}
/**
* Metadata about a closure instantiation, stored in the call graph
*
* @param lambdaMetaFactoryCall the InvokeDynamic instruction
* @param ownerMethod the method where the closure is allocated
* @param ownerClass the class containing the above method
* @param capturedArgInfos information about captured arguments. Used for updating the call
* graph when re-writing a closure invocation to the body method.
*/
final case class ClosureInstantiation(lambdaMetaFactoryCall: LambdaMetaFactoryCall, ownerMethod: MethodNode, ownerClass: ClassBType, capturedArgInfos: IntMap[ArgInfo]) {
override def toString = s"ClosureInstantiation($lambdaMetaFactoryCall, ${ownerMethod.name + ownerMethod.desc}, $ownerClass)"
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy