All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scala.tools.nsc.backend.opt.Inliners.scala Maven / Gradle / Ivy

There is a newer version: 2.11.2
Show newest version
/* NSC -- new Scala compiler
 * Copyright 2005-2013 LAMP/EPFL
 * @author  Iulian Dragos
 */


package scala.tools.nsc
package backend.opt

import scala.collection.mutable
import scala.tools.nsc.symtab._
import scala.reflect.internal.util.NoSourceFile

/**
 * Inliner balances two competing goals:
 *   (a) aggressive inlining of:
 *       (a.1) the apply methods of anonymous closures, so that their anon-classes can be eliminated;
 *       (a.2) higher-order-methods defined in an external library, e.g. `Range.foreach()` among many others.
 *   (b) circumventing the barrier to inter-library inlining that private accesses in the callee impose.
 *
 * Summing up the discussion in SI-5442 and SI-5891,
 * the current implementation achieves to a large degree both goals above, and
 * overcomes a problem exhibited by previous versions:
 *
 *   (1) Problem: Attempting to access a private member `p` at runtime resulting in an `IllegalAccessError`,
 *                where `p` is defined in a library L, and is accessed from a library C (for Client),
 *                where C was compiled against L', an optimized version of L where the inliner made `p` public at the bytecode level.
 *                The only such members are fields, either synthetic or isParamAccessor, and thus having a dollar sign in their name
 *                (the accesibility of methods and constructors isn't touched by the inliner).
 *
 * Thus we add one more goal to our list:
 *   (c) Compile C (either optimized or not) against any of L or L',
 *       so that it runs with either L or L' (in particular, compile against L' and run with L).
 *
 * The chosen strategy is described in some detail in the comments for `accessRequirements()` and `potentiallyPublicized()`.
 * Documentation at http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/2011Q4/Inliner.pdf
 *
 *  @author Iulian Dragos
 */
abstract class Inliners extends SubComponent {
  import global._
  import icodes._
  import icodes.opcodes._
  import definitions.{
    NullClass, NothingClass, ObjectClass,
    PredefModule, RuntimePackage, ScalaInlineClass, ScalaNoInlineClass,
    isFunctionType, isByNameParamType
  }

  val phaseName = "inliner"

  /** Debug - for timing the inliner. */
  private def timed[T](s: String, body: => T): T = {
    val t1 = System.currentTimeMillis()
    val res = body
    val t2 = System.currentTimeMillis()
    val ms = (t2 - t1).toInt
    if (ms >= MAX_INLINE_MILLIS)
      println("%s: %d milliseconds".format(s, ms))

    res
  }

  /** Look up implementation of method 'sym in 'clazz'.
   */
  def lookupImplFor(sym: Symbol, clazz: Symbol): Symbol = {
    // TODO: verify that clazz.superClass is equivalent here to clazz.tpe.parents(0).typeSymbol (.tpe vs .info)
    def needsLookup = (
         (clazz != NoSymbol)
      && (clazz != sym.owner)
      && !sym.isEffectivelyFinal
      && clazz.isEffectivelyFinal
    )
    def lookup(clazz: Symbol): Symbol = {
      // println("\t\tlooking up " + meth + " in " + clazz.fullName + " meth.owner = " + meth.owner)
      assert(clazz != NoSymbol, "Walked up past Object.superClass looking for " + sym +
                                ", most likely this reveals the TFA at fault (receiver and callee don't match).")
      if (sym.owner == clazz || isBottomType(clazz)) sym
      else sym.overridingSymbol(clazz) match {
        case NoSymbol  => if (sym.owner.isTrait) sym else lookup(clazz.superClass)
        case imp       => imp
      }
    }
    if (needsLookup) {
      val concreteMethod = lookup(clazz)
      debuglog("\tlooked up method: " + concreteMethod.fullName)

      concreteMethod
    }
    else sym
  }

  /* A warning threshold */
  private final val MAX_INLINE_MILLIS = 2000

  /** The maximum size in basic blocks of methods considered for inlining. */
  final val MAX_INLINE_SIZE = 16

  /** Maximum loop iterations. */
  final val MAX_INLINE_RETRY = 15

  /** Small method size (in blocks) */
  val SMALL_METHOD_SIZE = 1

  /** Create a new phase */
  override def newPhase(p: Phase) = new InliningPhase(p)

  /** The Inlining phase.
   */
  class InliningPhase(prev: Phase) extends ICodePhase(prev) {
    def name = phaseName
    val inliner = new Inliner

    object iclassOrdering extends Ordering[IClass] {
      def compare(a: IClass, b: IClass) = {
        val sourceNamesComparison = (a.cunit.toString() compare b.cunit.toString())
        if(sourceNamesComparison != 0) sourceNamesComparison
        else {
          val namesComparison = (a.toString() compare b.toString())
          if(namesComparison != 0) namesComparison
          else {
            a.symbol.id compare b.symbol.id
          }
        }
      }
    }
    val queue = new mutable.PriorityQueue[IClass]()(iclassOrdering)

    override def apply(c: IClass) { queue += c }

    override def run() {
      knownLacksInline.clear()
      knownHasInline.clear()
      try {
        super.run()
        for(c <- queue) { inliner analyzeClass c }
      } finally {
        inliner.clearCaches()
        knownLacksInline.clear()
        knownHasInline.clear()
      }
    }
  }

  def isBottomType(sym: Symbol) = sym == NullClass || sym == NothingClass

  /** Is the given class a closure? */
  def isClosureClass(cls: Symbol): Boolean =
    cls.isFinal && cls.isSynthetic && !cls.isModuleClass && cls.isAnonymousFunction

  /*
      TODO now that Inliner runs faster we could consider additional "monadic methods" (in the limit, all those taking a closure as last arg)
      Any "monadic method" occurring in a given caller C that is not `isMonadicMethod()` will prevent CloseElim from eliminating
      any anonymous-closure-class any whose instances are given as argument to C invocations.
   */
  def isMonadicMethod(sym: Symbol) = {
    nme.unspecializedName(sym.name) match {
      case nme.foreach | nme.filter | nme.withFilter | nme.map | nme.flatMap => true
      case _                                                                 => false
    }
  }

  val knownLacksInline = mutable.Set.empty[Symbol] // cache to avoid multiple inliner.hasInline() calls.
  val knownHasInline   = mutable.Set.empty[Symbol] // as above. Motivated by the need to warn on "inliner failures".

  def hasInline(sym: Symbol)    = {
    if     (knownLacksInline(sym)) false
    else if(knownHasInline(sym))   true
    else {
      val b = (sym hasAnnotation ScalaInlineClass)
      if(b) { knownHasInline   += sym }
      else  { knownLacksInline += sym }

      b
    }
  }

  def hasNoInline(sym: Symbol)  = sym hasAnnotation ScalaNoInlineClass

  /**
   * Simple inliner.
   */
  class Inliner {
    object NonPublicRefs extends Enumeration {
      val Private, Protected, Public = Value

      /** Cache whether a method calls private members. */
      val usesNonPublics = mutable.Map.empty[IMethod, Value]
    }
    import NonPublicRefs._

    /** The current iclass */
    private var currentIClazz: IClass = _
    private def warn(pos: Position, msg: String) = currentIClazz.cunit.inlinerWarning(pos, msg)

    private def ownedName(sym: Symbol): String = afterUncurry {
      val count = (
        if (!sym.isMethod) 1
        else if (sym.owner.isAnonymousFunction) 3
        else 2
      )
      (sym.ownerChain take count filterNot (_.isPackageClass)).reverseMap(_.nameString).mkString(".")
    }
    private def inlineLog(what: String, main: => String, comment: => String) {
      def cstr = comment match {
        case ""   => ""
        case str  => " // " + str
      }
      val width = if (currentIClazz eq null) 40 else currentIClazz.symbol.enclosingPackage.fullName.length + 25
      val fmt = "%8s  %-" + width + "s" + cstr
      log(fmt.format(what, main))
    }
    private def inlineLog(what: String, main: Symbol, comment: => String) {
      inlineLog(what, ownedName(main), comment)
    }

    val recentTFAs = mutable.Map.empty[Symbol, Tuple2[Boolean, analysis.MethodTFA]]

    private def getRecentTFA(incm: IMethod, forceable: Boolean): (Boolean, analysis.MethodTFA) = {

        def containsRETURN(blocks: List[BasicBlock]) = blocks exists { bb => bb.lastInstruction.isInstanceOf[RETURN] }

      val opt = recentTFAs.get(incm.symbol)
      if(opt.isDefined) {
        // FYI val cachedBBs = opt.get._2.in.keySet
        // FYI assert(incm.blocks.toSet == cachedBBs)
        // incm.code.touched plays no role here
        return opt.get
      }

      val hasRETURN = containsRETURN(incm.code.blocksList) || (incm.exh exists { eh => containsRETURN(eh.blocks) })
      var a: analysis.MethodTFA = null
      if(hasRETURN) { a = new analysis.MethodTFA(incm); a.run }

      if(forceable) { recentTFAs.put(incm.symbol, (hasRETURN, a)) }

      (hasRETURN, a)
    }

    def clearCaches() {
      // methods
      NonPublicRefs.usesNonPublics.clear()
      recentTFAs.clear
      tfa.knownUnsafe.clear()
      tfa.knownSafe.clear()
      tfa.knownNever.clear()
      // basic blocks
      tfa.preCandidates.clear()
      tfa.relevantBBs.clear()
      // callsites
      tfa.remainingCALLs.clear()
      tfa.isOnWatchlist.clear()
    }

    object imethodOrdering extends Ordering[IMethod] {
      def compare(a: IMethod, b: IMethod) = {
        val namesComparison = (a.toString() compare b.toString())
        if(namesComparison != 0) namesComparison
        else {
          a.symbol.id compare b.symbol.id
        }
      }
    }

    def analyzeClass(cls: IClass): Unit =
      if (settings.inline.value) {
        inlineLog("class", s"${cls.symbol.decodedName}", s"analyzing ${cls.methods.size} methods in $cls")

        this.currentIClazz = cls
        val ms = cls.methods sorted imethodOrdering
        ms foreach { im =>
          if (hasInline(im.symbol)) {
            inlineLog("skip", im.symbol, "no inlining into @inline methods")
          }
          else if(im.hasCode && !im.symbol.isBridge) {
            analyzeMethod(im)
          }
        }
      }

    val tfa   = new analysis.MTFAGrowable()
    tfa.stat  = global.opt.printStats
    val staleOut      = new mutable.ListBuffer[BasicBlock]
    val splicedBlocks = mutable.Set.empty[BasicBlock]
    val staleIn       = mutable.Set.empty[BasicBlock]

    /**
     * A transformation local to the body of the IMethod received as argument.
     * An linining decision consists in replacing a callsite with the body of the callee.
     * Please notice that, because `analyzeMethod()` itself may modify a method body,
     * the particular callee bodies that end up being inlined depend on the particular order in which methods are visited
     * (no topological sorting over the call-graph is attempted).
     *
     * Making an inlining decision requires type-flow information for both caller and callee.
     * Regarding the caller, such information is needed only for basic blocks containing inlining candidates
     * (and their transitive predecessors). This observation leads to using a custom type-flow analysis (MTFAGrowable)
     * that can be re-inited, i.e. that reuses lattice elements (type-flow information computed in a previous iteration)
     * as starting point for faster convergence in a new iteration.
     *
     * The mechanics of inlining are iterative for a given invocation of `analyzeMethod(m)`,
     * and are affected by inlinings from previous iterations
     * (ie, "heuristic" rules are based on statistics tracked for that purpose):
     *
     *   (1) before the iterations proper start, so-called preinlining is performed.
     *       Those callsites whose (receiver, concreteMethod) are both known statically
     *       can be analyzed for inlining before computing a type-flow. Details in `preInline()`
     *
     *   (2) the first iteration computes type-flow information for basic blocks containing inlining candidates
     *       (and their transitive predecessors), so called `relevantBBs` basic blocks.
     *       The ensuing analysis of each candidate (performed by `analyzeInc()`)
     *       may result in a CFG isomorphic to that of the callee being inserted in place of the callsite
     *       (i.e. a CALL_METHOD instruction is replaced with a single-entry single-exit CFG,
     *        a substitution we call "successful inlining").
     *
     *   (3) following iterations have `relevantBBs` updated to focus on the inlined basic blocks and their successors only.
     *       Details in `MTFAGrowable.reinit()`
     * */
    def analyzeMethod(m: IMethod): Unit = {
      // m.normalize
      if (settings.debug.value)
        inlineLog("caller", ownedName(m.symbol), "in " + m.symbol.owner.fullName)

      var sizeBeforeInlining  = m.code.blockCount
      var instrBeforeInlining = m.code.instructionCount
      var retry = false
      var count = 0

      // fresh name counter
      val fresh = mutable.HashMap.empty[String, Int] withDefaultValue 0
      // how many times have we already inlined this method here?
      val inlinedMethodCount = mutable.HashMap.empty[Symbol, Int] withDefaultValue 0
      val caller = new IMethodInfo(m)
      def analyzeMessage = s"Analyzing ${caller.length} blocks of $m for inlining sites."

      def preInline(isFirstRound: Boolean): Int = {
        val inputBlocks = caller.m.linearizedBlocks()
        val callsites: Function1[BasicBlock, List[opcodes.CALL_METHOD]] = {
          if(isFirstRound) tfa.conclusives else tfa.knownBeforehand
        }
        inlineWithoutTFA(inputBlocks, callsites)
      }

      /**
       *  Inline straightforward callsites (those that can be inlined without a TFA).
       *
       *  To perform inlining, all we need to know is listed as formal params in `analyzeInc()`:
       *    - callsite and block containing it
       *    - actual (ie runtime) class of the receiver
       *    - actual (ie runtime) method being invoked
       *    - stack length just before the callsite (to check whether enough arguments have been pushed).
       *  The assert below lists the conditions under which "no TFA is needed"
       *  (the statically known receiver and method are both final, thus, at runtime they can't be any others than those).
       *
       */
      def inlineWithoutTFA(inputBlocks: Traversable[BasicBlock], callsites: Function1[BasicBlock, List[opcodes.CALL_METHOD]]): Int = {
        var inlineCount = 0
        import scala.util.control.Breaks._
        for(x <- inputBlocks; easyCake = callsites(x); if easyCake.nonEmpty) {
          breakable {
            for(ocm <- easyCake) {
              assert(ocm.method.isEffectivelyFinal && ocm.method.owner.isEffectivelyFinal)
              if(analyzeInc(ocm, x, ocm.method.owner, -1, ocm.method)) {
                inlineCount += 1
                break
              }
            }
          }
        }

        inlineCount
      }

      /**
       *  Decides whether it's feasible and desirable to inline the body of the method given by `concreteMethod`
       *  at the program point given by `i` (a callsite). The boolean result indicates whether inlining was performed.
       *
       */
      def analyzeInc(i: CALL_METHOD, bb: BasicBlock, receiver: Symbol, stackLength: Int, concreteMethod: Symbol): Boolean = {
        assert(bb.toList contains i, "Candidate callsite does not belong to BasicBlock.")
        val shouldWarn = hasInline(i.method)

        def warnNoInline(reason: String): Boolean = {
          def msg = "Could not inline required method %s because %s.".format(i.method.originalName.decode, reason)
          if (settings.debug.value)
            inlineLog("fail", i.method.fullName, reason)
          if (shouldWarn)
            warn(i.pos, msg)

          false
        }

        var isAvailable = icodes available concreteMethod.enclClass

        if (!isAvailable && shouldLoadImplFor(concreteMethod, receiver)) {
          // Until r22824 this line was:
          //   icodes.icode(concreteMethod.enclClass, true)
          //
          // Changing it to
          //   icodes.load(concreteMethod.enclClass)
          // was the proximate cause for SI-3882:
          //   error: Illegal index: 0 overlaps List((variable par1,LONG))
          //   error: Illegal index: 0 overlaps List((variable par1,LONG))
          isAvailable = icodes.load(concreteMethod.enclClass)
        }

        def isCandidate = (
             isClosureClass(receiver)
          || concreteMethod.isEffectivelyFinal
          || receiver.isEffectivelyFinal
        )

        def isApply     = concreteMethod.name == nme.apply

        def isCountable = !(
             isClosureClass(receiver)
          || isApply
          || isMonadicMethod(concreteMethod)
          || receiver.enclosingPackage == definitions.RuntimePackage
        )   // only count non-closures

        debuglog("Treating " + i
              + "\n\treceiver: " + receiver
              + "\n\ticodes.available: " + isAvailable
              + "\n\tconcreteMethod.isEffectivelyFinal: " + concreteMethod.isEffectivelyFinal)

        if (!isCandidate) warnNoInline("it can be overridden")
        else if (!isAvailable) warnNoInline("bytecode unavailable")
        else lookupIMethod(concreteMethod, receiver) filter (callee => callee.hasCode || warnNoInline("callee has no code")) exists { callee =>
          val inc   = new IMethodInfo(callee)
          val pair  = new CallerCalleeInfo(caller, inc, fresh, inlinedMethodCount)

          if (inc.hasHandlers && (stackLength == -1)) {
            // no inlining is done, yet don't warn about it, stackLength == -1 indicates we're trying to inlineWithoutTFA.
            // Shortly, a TFA will be computed and an error message reported if indeed inlining not possible.
            false
          }
          else {
            val isSafe = pair isStampedForInlining stackLength match {
              case DontInlineHere(msg)                       => warnNoInline(msg)
              case NeverSafeToInline                         => false
              case InlineableAtThisCaller                    => true
              case inl @ FeasibleInline(_, _) if !inl.isSafe => false
              case FeasibleInline(required, toPublicize)     =>
                for (f <- toPublicize) {
                  inlineLog("access", f, "making public")
                  f setFlag Flags.notPRIVATE
                  f setFlag Flags.notPROTECTED
                }
                // only add to `knownSafe` after all `toPublicize` fields actually made public.
                if (required == NonPublicRefs.Public)
                  tfa.knownSafe += inc.sym

                true
            }
            isSafe && {
               retry   = true
               if (isCountable) count += 1
               pair.doInline(bb, i)
               if (!pair.isInlineForced || inc.isMonadic) caller.inlinedCalls += 1
               inlinedMethodCount(inc.sym) += 1

               // Remove the caller from the cache (this inlining might have changed its calls-private relation).
               usesNonPublics -= m
               recentTFAs     -= m.symbol
               true
            }
          }
        }
      }

      /* Pre-inlining consists in invoking the usual inlining subroutine with (receiver class, concrete method) pairs as input
       * where both method and receiver are final, which implies that the receiver computed via TFA will always match `concreteMethod.owner`.
       *
       * As with any invocation of `analyzeInc()` the inlining outcome is based on heuristics which favor inlining an isMonadicMethod before other methods.
       * That's why preInline() is invoked twice: any inlinings downplayed by the heuristics during the first round get an opportunity to rank higher during the second.
       *
       * As a whole, both `preInline()` invocations amount to priming the inlining process,
       * so that the first TFA that is run afterwards is able to gain more information as compared to a cold-start.
       */
      val totalPreInlines = {
        val firstRound = preInline(true)
        if(firstRound == 0) 0 else (firstRound + preInline(false))
      }
      staleOut.clear()
      splicedBlocks.clear()
      staleIn.clear()

      do {
        retry = false
        debuglog(analyzeMessage)

        /* it's important not to inline in unreachable basic blocks. linearizedBlocks() returns only reachable ones. */
        tfa.callerLin = caller.m.linearizedBlocks()
           /* TODO Do we really want to inline inside exception handlers?
           *  Seems counterproductive (the larger the method the less likely it will be JITed).
            * The alternative would be `linearizer.linearizeAt(caller.m, caller.m.startBlock)`.
            * And, we would cut down on TFA iterations, too.
            * See also comment on the same topic in TypeFlowAnalysis. */

        tfa.reinit(m, staleOut.toList, splicedBlocks, staleIn)
        tfa.run

        staleOut.clear()
        splicedBlocks.clear()
        staleIn.clear()

        import scala.util.control.Breaks._
        for(bb <- tfa.callerLin; if tfa.preCandidates(bb)) {
          val cms = bb.toList collect { case cm : CALL_METHOD => cm }
          breakable {
            for (cm <- cms; if tfa.remainingCALLs.isDefinedAt(cm)) {
              val analysis.CallsiteInfo(_, receiver, stackLength, concreteMethod) = tfa.remainingCALLs(cm)
              if (analyzeInc(cm, bb, receiver, stackLength, concreteMethod)) {
                break
              }
            }
          }
        }

        /* As part of inlining, some instructions are moved to a new block.
         *     In detail: the instructions moved to a new block originally appeared after a (by now inlined) callsite.
         *     Their new home is an `afterBlock` created by `doInline()` to that effect.
         *     Each block in staleIn is one such `afterBlock`.
         *
         * Some of those instructions may be CALL_METHOD possibly tracked in `remainingCALLs`
         * (with an entry still noting the old containing block). However, that causes no problem:
         *
         *   (1) such callsites won't be analyzed for inlining by `analyzeInc()` (*in this iteration*)
         *       because of the `break` that abandons the original basic block where it was contained.
         *
         *   (2) Additionally, its new containing block won't be visited either (*in this iteration*)
         *       because the new blocks don't show up in the linearization computed before inlinings started:
         *       `for(bb <- tfa.callerLin; if tfa.preCandidates(bb)) {`
         *
         * For a next iteration, the new home of any instructions that have moved
         * will be tracked properly in `remainingCALLs` after `MTFAGrowable.reinit()` puts on radar their new homes.
         *
         */
        if(retry) {
          for(afterBlock <- staleIn) {
            val justCALLsAfter = afterBlock.toList collect { case c : opcodes.CALL_METHOD => c }
            for(ia <- justCALLsAfter) { tfa.remainingCALLs.remove(ia) }
          }
        }

        /*
        if(splicedBlocks.nonEmpty) { // TODO explore (saves time but leads to slightly different inlining decisions)
          // opportunistically perform straightforward inlinings before the next typeflow round
          val savedRetry = retry
          val savedStaleOut = staleOut.toSet; staleOut.clear()
          val savedStaleIn  = staleIn.toSet ; staleIn.clear()
          val howmany = inlineWithoutTFA(splicedBlocks, tfa.knownBeforehand)
          splicedBlocks ++= staleIn
          staleOut.clear(); staleOut ++= savedStaleOut;
          staleIn.clear();  staleIn  ++= savedStaleIn;
          retry = savedRetry
        }
        */

        if (tfa.stat)
          log(m.symbol.fullName + " iterations: " + tfa.iterations + " (size: " + caller.length + ")")
      }
      while (retry && count < MAX_INLINE_RETRY)

      for(inlFail <- tfa.warnIfInlineFails) {
        warn(inlFail.pos, "At the end of the day, could not inline @inline-marked method " + inlFail.method.originalName.decode)
      }

      m.normalize
      if (sizeBeforeInlining > 0) {
        val instrAfterInlining = m.code.instructionCount
        val prefix = if ((instrAfterInlining > 2 * instrBeforeInlining) && (instrAfterInlining > 200)) "!!" else ""
        val inlinings = caller.inlinedCalls
        if (inlinings > 0) {
          val s1      = s"instructions $instrBeforeInlining -> $instrAfterInlining"
          val s2      = if (sizeBeforeInlining == m.code.blockCount) "" else s", blocks $sizeBeforeInlining -> ${m.code.blockCount}"
          val callees = inlinedMethodCount.toList map { case (k, v) => k.fullNameString + ( if (v == 1) "" else "/" + v ) }

          inlineLog("inlined", m.symbol.fullName, callees.sorted.mkString(inlinings + " inlined: ", ", ", ""))
          inlineLog("<>", m.symbol.fullName, s"${m.symbol.nameString}: $s1$s2")
        }
      }
    }

    private def isHigherOrderMethod(sym: Symbol) = (
         sym.isMethod
      && beforeExplicitOuter(sym.info.paramTypes exists isFunctionType) // was "at erasurePhase.prev"
    )

    /** Should method 'sym' being called in 'receiver' be loaded from disk? */
    def shouldLoadImplFor(sym: Symbol, receiver: Symbol): Boolean = {
      def alwaysLoad    = (receiver.enclosingPackage == RuntimePackage) || (receiver == PredefModule.moduleClass)
      def loadCondition = sym.isEffectivelyFinal && isMonadicMethod(sym) && isHigherOrderMethod(sym)

      val res = hasInline(sym) || alwaysLoad || loadCondition
      debuglog("shouldLoadImplFor: " + receiver + "." + sym + ": " + res)
      res
    }

    class IMethodInfo(val m: IMethod) {
      override def toString = m.toString

      val sym           = m.symbol
      val name          = sym.name
      def owner         = sym.owner
      def paramTypes    = sym.info.paramTypes
      def minimumStack  = paramTypes.length + 1

      def isBridge      = sym.isBridge
      val isInClosure   = isClosureClass(owner)
      val isHigherOrder = isHigherOrderMethod(sym)
      def isMonadic     = isMonadicMethod(sym)

      def handlers      = m.exh
      def blocks        = m.blocks
      def locals        = m.locals
      def length        = blocks.length
      def openBlocks    = blocks filterNot (_.closed)
      def instructions  = m.code.instructions
      // def linearized    = linearizer linearize m

      def isSmall         = (length <= SMALL_METHOD_SIZE) && blocks(0).length < 10
      def isLarge         = length > MAX_INLINE_SIZE
      def isRecursive     = m.recursive
      def hasHandlers     = handlers.nonEmpty || m.bytecodeHasEHs
      def hasClosureParam = paramTypes exists (tp => isByNameParamType(tp) || isFunctionType(tp))

      def isSynchronized         = sym.hasFlag(Flags.SYNCHRONIZED)
      def hasNonFinalizerHandler = handlers exists {
        case _: Finalizer => true
        case _            => false
      }

      // the number of inlined calls in 'm', used by 'isScoreOK'
      var inlinedCalls = 0

      def addLocals(ls: List[Local])  = m.locals ++= ls
      def addLocal(l: Local)          = addLocals(List(l))
      def addHandlers(exhs: List[ExceptionHandler]) = m.exh = exhs ::: m.exh

      /**
       * This method inspects the callee's instructions, finding out the most restrictive accessibility implied by them.
       *
       * Rather than giving up upon encountering an access to a private field `p`, it provisorily admits `p` as "can-be-made-public", provided:
       *   - `p` is being compiled as part of this compilation run, and
       *   - `p` is synthetic or param-accessor.
       *
       * This method is side-effect free, in particular it lets the invoker decide
       * whether the accessibility of the `toBecomePublic` fields should be changed or not.
       */
      def accessRequirements: AccessReq = {

        var toBecomePublic: List[Symbol] = Nil

        def check(sym: Symbol, cond: Boolean) =
          if (cond) Private
          else if (sym.isProtected) Protected
          else Public

        def canMakePublic(f: Symbol): Boolean =
          (m.sourceFile ne NoSourceFile) &&
          (f.isSynthetic || f.isParamAccessor) &&
          { toBecomePublic = f :: toBecomePublic; true }

        /* A safety check to consider as private, for the purposes of inlining, a public field that:
         *   (1) is defined in an external library, and
         *   (2) can be presumed synthetic (due to a dollar sign in its name).
         * Such field was made public by `doMakePublic()` and we don't want to rely on that,
         * because under other compilation conditions (ie no -optimize) that won't be the case anymore.
         *
         * This allows aggressive intra-library inlining (making public if needed)
         * that does not break inter-library scenarios (see comment for `Inliners`).
         *
         * TODO handle more robustly the case of a trait var changed at the source-level from public to private[this]
         *      (eg by having ICodeReader use unpickler, see SI-5442).

         DISABLED

        def potentiallyPublicized(f: Symbol): Boolean = {
          (m.sourceFile eq NoSourceFile) && f.name.containsChar('$')
        }
        */

        def checkField(f: Symbol)   = check(f, f.isPrivate && !canMakePublic(f))
        def checkSuper(n: Symbol)   = check(n, n.isPrivate || !n.isClassConstructor)
        def checkMethod(n: Symbol)  = check(n, n.isPrivate)

        def getAccess(i: Instruction) = i match {
          case CALL_METHOD(n, SuperCall(_)) => checkSuper(n)
          case CALL_METHOD(n, _)            => checkMethod(n)
          case LOAD_FIELD(f, _)             => checkField(f)
          case STORE_FIELD(f, _)            => checkField(f)
          case _                            => Public
        }

        var seen = Public
        val iter = instructions.iterator
        while((seen ne Private) && iter.hasNext) {
          val i = iter.next()
          getAccess(i) match {
            case Private    =>
              inlineLog("access", s"instruction $i requires private access", "pos=" + i.pos)
              toBecomePublic = Nil
              seen = Private
            case Protected  => seen = Protected
            case _          => ()
          }
        }

        AccessReq(seen, toBecomePublic)
      }

    }

    /**
     * Classifies a pair (caller, callee) into one of four categories:
     *
     *   (a) inlining should be performed, classified in turn into:
     *       (a.1) `InlineableAtThisCaller`: unconditionally at this caller
     *       (a.2) `FeasibleInline`: it only remains for certain access requirements to be met (see `IMethodInfo.accessRequirements()`)
     *
     *   (b) inlining shouldn't be performed, classified in turn into:
     *       (b.1) `DontInlineHere`: indicates that this particular occurrence of the callee at the caller shouldn't be inlined.
     *                - Nothing is said about the outcome for other callers, or for other occurrences of the callee for the same caller.
     *                - In particular inlining might be possible, but heuristics gave a low score for it.
     *       (b.2) `NeverSafeToInline`: the callee can't be inlined anywhere, irrespective of caller.
     *
     * The classification above is computed by `isStampedForInlining()` based on which `analyzeInc()` goes on to:
     *   - either log the reason for failure --- case (b) ---,
     *   - or perform inlining --- case (a) ---.
     */
    sealed abstract class InlineSafetyInfo {
      def isSafe   = false
      def isUnsafe = !isSafe
    }
    case object NeverSafeToInline           extends InlineSafetyInfo
    case object InlineableAtThisCaller      extends InlineSafetyInfo { override def isSafe = true }
    case class  DontInlineHere(msg: String) extends InlineSafetyInfo
    case class  FeasibleInline(accessNeeded: NonPublicRefs.Value,
                               toBecomePublic: List[Symbol]) extends InlineSafetyInfo {
      override def isSafe = true
    }

    case class AccessReq(
      accessNeeded:   NonPublicRefs.Value,
      toBecomePublic: List[Symbol]
    )

    final class CallerCalleeInfo(val caller: IMethodInfo, val inc: IMethodInfo, fresh: mutable.Map[String, Int], inlinedMethodCount: scala.collection.Map[Symbol, Int]) {

      assert(!caller.isBridge && inc.m.hasCode,
             "A guard in Inliner.analyzeClass() should have prevented from getting here.")

      def isLargeSum  = caller.length + inc.length - 1 > SMALL_METHOD_SIZE

      private def freshName(s: String): TermName = {
        fresh(s) += 1
        newTermName(s + fresh(s))
      }

      private def isKnownToInlineSafely: Boolean = { tfa.knownSafe(inc.sym) }

      val isInlineForced    = hasInline(inc.sym)
      val isInlineForbidden = hasNoInline(inc.sym)
      assert(!(isInlineForced && isInlineForbidden), "method ("+inc.m+") marked both @inline and @noinline.")

      /** Inline 'inc' into 'caller' at the given block and instruction.
       *  The instruction must be a CALL_METHOD.
       */
      def doInline(block: BasicBlock, instr: CALL_METHOD) {

        staleOut += block

        tfa.remainingCALLs.remove(instr) // this bookkpeeping is done here and not in MTFAGrowable.reinit due to (1st) convenience and (2nd) necessity.
        tfa.isOnWatchlist.remove(instr)  // ditto
        tfa.warnIfInlineFails.remove(instr)

        val targetPos = instr.pos

        def blockEmit(i: Instruction) = block.emit(i, targetPos)
        def newLocal(baseName: String, kind: TypeKind) =
          new Local(caller.sym.newVariable(freshName(baseName), targetPos) setInfo kind.toType, kind, false)

        val (hasRETURN, a) = getRecentTFA(inc.m, isInlineForced)

        /* The exception handlers that are active at the current block. */
        val activeHandlers = caller.handlers filter (_ covered block)

        /* Map 'original' blocks to the ones inlined in the caller. */
        val inlinedBlock = mutable.Map[BasicBlock, BasicBlock]()

        val varsInScope = mutable.HashSet[Local]() ++= block.varsInScope

        /** Side effects varsInScope when it sees SCOPE_ENTERs. */
        def instrBeforeFilter(i: Instruction): Boolean = {
          i match { case SCOPE_ENTER(l) => varsInScope += l ; case _ => () }
          i ne instr
        }
        val instrBefore = block.toList takeWhile instrBeforeFilter
        val instrAfter  = block.toList drop (instrBefore.length + 1)

        assert(!instrAfter.isEmpty, "CALL_METHOD cannot be the last instruction in block!")

        // store the '$this' into the special local
        val inlinedThis = newLocal("$inlThis", REFERENCE(ObjectClass))

        /** buffer for the returned value */
        val retVal = inc.m.returnType match {
          case UNIT  => null
          case x     => newLocal("$retVal", x)
        }

        val inlinedLocals = mutable.HashMap.empty[Local, Local]

        /** Add a new block in the current context. */
        def newBlock() = {
          val b = caller.m.code.newBlock
          activeHandlers foreach (_ addCoveredBlock b)
          if (retVal ne null) b.varsInScope += retVal
          b.varsInScope += inlinedThis
          b.varsInScope ++= varsInScope
          b
        }

        def translateExh(e: ExceptionHandler) = {
          val handler: ExceptionHandler = e.dup
          handler.covered = handler.covered map inlinedBlock
          handler setStartBlock inlinedBlock(e.startBlock)
          handler
        }

        /** alfa-rename `l` in caller's context. */
        def dupLocal(l: Local): Local = {
          val sym = caller.sym.newVariable(freshName(l.sym.name.toString), l.sym.pos)
          // sym.setInfo(l.sym.tpe)
          val dupped = new Local(sym, l.kind, false)
          inlinedLocals(l) = dupped
          dupped
        }

        val afterBlock = newBlock()

        /** Map from nw.init instructions to their matching NEW call */
        val pending: mutable.Map[Instruction, NEW] = new mutable.HashMap

        /** Map an instruction from the callee to one suitable for the caller. */
        def map(i: Instruction): Instruction = {
          def assertLocal(l: Local) = {
            assert(caller.locals contains l, "Could not find local '" + l + "' in locals, nor in inlinedLocals: " + inlinedLocals)
            i
          }
          def isInlined(l: Local) = inlinedLocals isDefinedAt l

          val newInstr = i match {
            case THIS(clasz)                    => LOAD_LOCAL(inlinedThis)
            case STORE_THIS(_)                  => STORE_LOCAL(inlinedThis)
            case JUMP(whereto)                  => JUMP(inlinedBlock(whereto))
            case CJUMP(succ, fail, cond, kind)  => CJUMP(inlinedBlock(succ), inlinedBlock(fail), cond, kind)
            case CZJUMP(succ, fail, cond, kind) => CZJUMP(inlinedBlock(succ), inlinedBlock(fail), cond, kind)
            case SWITCH(tags, labels)           => SWITCH(tags, labels map inlinedBlock)
            case RETURN(_)                      => JUMP(afterBlock)
            case LOAD_LOCAL(l) if isInlined(l)  => LOAD_LOCAL(inlinedLocals(l))
            case STORE_LOCAL(l) if isInlined(l) => STORE_LOCAL(inlinedLocals(l))
            case LOAD_LOCAL(l)                  => assertLocal(l)
            case STORE_LOCAL(l)                 => assertLocal(l)
            case SCOPE_ENTER(l) if isInlined(l) => SCOPE_ENTER(inlinedLocals(l))
            case SCOPE_EXIT(l) if isInlined(l)  => SCOPE_EXIT(inlinedLocals(l))

            case nw @ NEW(sym) =>
              val r = NEW(sym)
              pending(nw.init) = r
              r

            case CALL_METHOD(meth, Static(true)) if meth.isClassConstructor =>
              CALL_METHOD(meth, Static(true))

            case _ => i.clone()
          }
          // check any pending NEW's
          pending remove i foreach (_.init = newInstr.asInstanceOf[CALL_METHOD])
          newInstr
        }

        caller addLocals (inc.locals map dupLocal)
        caller addLocal inlinedThis

        if (retVal ne null)
          caller addLocal retVal

        inc.m foreachBlock { b =>
          inlinedBlock += (b -> newBlock())
          inlinedBlock(b).varsInScope ++= (b.varsInScope map inlinedLocals)
        }

        // re-emit the instructions before the call
        block.open
        block.clear
        block emit instrBefore

        // store the arguments into special locals
        inc.m.params.reverse foreach (p => blockEmit(STORE_LOCAL(inlinedLocals(p))))
        blockEmit(STORE_LOCAL(inlinedThis))

        // jump to the start block of the callee
        blockEmit(JUMP(inlinedBlock(inc.m.startBlock)))
        block.close

        // duplicate the other blocks in the callee
        val calleeLin = inc.m.linearizedBlocks()
        calleeLin foreach { bb =>
          var info = if(hasRETURN) (a in bb) else null
          def emitInlined(i: Instruction) = inlinedBlock(bb).emit(i, targetPos)
          def emitDrops(toDrop: Int)      = info.stack.types drop toDrop foreach (t => emitInlined(DROP(t)))

          for (i <- bb) {
            i match {
              case RETURN(UNIT) => emitDrops(0)
              case RETURN(kind) =>
                if (info.stack.length > 1) {
                  emitInlined(STORE_LOCAL(retVal))
                  emitDrops(1)
                  emitInlined(LOAD_LOCAL(retVal))
                }
              case _            => ()
            }
            emitInlined(map(i))
            info = if(hasRETURN) a.interpret(info, i) else null
          }
          inlinedBlock(bb).close
        }

        afterBlock emit instrAfter
        afterBlock.close

        staleIn        += afterBlock
        splicedBlocks ++= (calleeLin map inlinedBlock)

        // add exception handlers of the callee
        caller addHandlers (inc.handlers map translateExh)
        assert(pending.isEmpty, "Pending NEW elements: " + pending)
        if (settings.debug.value) icodes.checkValid(caller.m)
      }

      def isStampedForInlining(stackLength: Int): InlineSafetyInfo = {

        if(tfa.blackballed(inc.sym)) { return NeverSafeToInline }

        if(!isKnownToInlineSafely) {

          if(inc.openBlocks.nonEmpty) {
            val msg = ("Encountered " + inc.openBlocks.size + " open block(s) in isSafeToInline: this indicates a bug in the optimizer!\n" +
                       "  caller = " + caller.m + ", callee = " + inc.m)
            warn(inc.sym.pos, msg)
            tfa.knownNever += inc.sym
            return DontInlineHere("Open blocks in " + inc.m)
          }

          val reasonWhyNever: String = {
            var rs: List[String] = Nil
            if(inc.isRecursive)    { rs ::= "is recursive"           }
            if(isInlineForbidden)  { rs ::= "is annotated @noinline" }
            if(inc.isSynchronized) { rs ::= "is synchronized method" }
            if(inc.m.bytecodeHasEHs) { rs ::= "bytecode contains exception handlers / finally clause" } // SI-6188
            if(inc.m.bytecodeHasInvokeDynamic) { rs ::= "bytecode contains invoke dynamic" }
            if(rs.isEmpty) null else rs.mkString("", ", and ", "")
          }

          if(reasonWhyNever != null) {
            tfa.knownNever += inc.sym
            inlineLog("never", inc.sym, reasonWhyNever)
            // next time around NeverSafeToInline is returned, thus skipping (duplicate) msg, this is intended.
            return DontInlineHere(inc.m + " " + reasonWhyNever)
          }

          if(sameSymbols) { // TODO but this also amounts to recursive, ie should lead to adding to tfa.knownNever, right?
            tfa.knownUnsafe += inc.sym;
            return DontInlineHere("sameSymbols (ie caller == callee)")
          }

        }

        /*
         * From here on, two main categories of checks remain, (a) and (b) below:
         *   (a.1) either the scoring heuristics give green light; or
         *   (a.2) forced as candidate due to @inline.
         * After that, safety proper is checked:
         *   (b.1) the callee does not contain calls to private methods when called from another class
         *   (b.2) the callee is not going to be inlined into a position with non-empty stack,
         *         while having a top-level finalizer (see liftedTry problem)
         * As a result of (b), some synthetic private members can be chosen to become public.
         */

        val score    = inlinerScore
        val scoreStr = if (score > 0) "+" + score else "" + score
        val what     = if (score > 0) "ok to" else "don't"
        inlineLog(scoreStr, inc.m.symbol, s"$what inline into ${ownedName(caller.m.symbol)}")

        if (!isInlineForced && score <= 0) {
          // During inlining retry, a previous caller-callee pair that scored low may pass.
          // Thus, adding the callee to tfa.knownUnsafe isn't warranted.
          return DontInlineHere(s"inliner heuristic")
        }

        if(inc.hasHandlers && (stackLength > inc.minimumStack)) {
          return DontInlineHere("callee contains exception handlers / finally clause, and is invoked with non-empty operand stack") // SI-6157
        }

        if(isKnownToInlineSafely) { return InlineableAtThisCaller }

        if(stackLength > inc.minimumStack && inc.hasNonFinalizerHandler) {
          val msg = "method " + inc.sym + " is used on a non-empty stack with finalizer."
          debuglog(msg)
          // FYI: not reason enough to add inc.sym to tfa.knownUnsafe (because at other callsite in this caller, inlining might be ok)
          return DontInlineHere(msg)
        }

        val accReq = inc.accessRequirements
        if(!canAccess(accReq.accessNeeded)) {
          tfa.knownUnsafe += inc.sym
          val msg = "access level required by callee not matched by caller"
          inlineLog("fail", inc.sym, msg)
          return DontInlineHere(msg)
        }

        FeasibleInline(accReq.accessNeeded, accReq.toBecomePublic)

      }

      def canAccess(level: NonPublicRefs.Value) = level match {
        case Private    => caller.owner == inc.owner
        case Protected  => caller.owner.tpe <:< inc.owner.tpe
        case Public     => true
      }
      private def sameSymbols = caller.sym == inc.sym
      private def sameOwner   = caller.owner == inc.owner

      /** Gives green light for inlining (which may still be vetoed later). Heuristics:
       *   - it's bad to make the caller larger (> SMALL_METHOD_SIZE) if it was small
       *   - it's bad to inline large methods
       *   - it's good to inline higher order functions
       *   - it's good to inline closures functions.
       *   - it's bad (useless) to inline inside bridge methods
       */
      def inlinerScore: Int = {
        var score = 0

        // better not inline inside closures, but hope that the closure itself is repeatedly inlined
        if (caller.isInClosure)           score -= 2
        else if (caller.inlinedCalls < 1) score -= 1 // only monadic methods can trigger the first inline

        if (inc.isSmall) score += 1;
        // if (inc.hasClosureParam) score += 2
        if (inc.isLarge) score -= 1;
        if (caller.isSmall && isLargeSum) {
          score -= 1
          debuglog(s"inliner score decreased to $score because small caller $caller would become large")
        }

        if (inc.isMonadic)          score += 3
        else if (inc.isHigherOrder) score += 1

        if (inc.isInClosure)                 score += 2
        if (inlinedMethodCount(inc.sym) > 2) score -= 2
        score
      }
    }

    def lookupIMethod(meth: Symbol, receiver: Symbol): Option[IMethod] = {
      def tryParent(sym: Symbol) = icodes icode sym flatMap (_ lookupMethod meth)

      (receiver.info.baseClasses.iterator map tryParent find (_.isDefined)).flatten
    }
  } /* class Inliner */
} /* class Inliners */




© 2015 - 2024 Weber Informatics LLC | Privacy Policy