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

scala.tools.nsc.backend.jvm.analysis.AliasingAnalyzer.scala Maven / Gradle / Ivy

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 analysis

import scala.annotation.switch
import scala.collection.AbstractIterator
import scala.collection.mutable
import scala.tools.asm.Opcodes
import scala.tools.asm.tree._
import scala.tools.asm.tree.analysis._
import scala.tools.nsc.backend.jvm.BTypes.InternalName
import scala.tools.nsc.backend.jvm.analysis.AliasSet.SmallBitSet
import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._

/**
 * A subclass of Frame that tracks aliasing of values stored in local variables and on the stack.
 *
 * Note: an analysis tracking aliases is roughly 5x slower than a usual analysis (assuming a simple
 * value domain with a fast merge function). For example, nullness analysis is roughly 5x slower
 * than a BasicValue analysis.
 *
 * See the doc of package object `analysis` for some notes on the performance of alias analysis.
 */
class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLocals, nStack) {
  import Opcodes._

  // Auxiliary constructor required for implementing `AliasingAnalyzer.newFrame`
  def this(src: Frame[_ <: V]) = {
    this(src.getLocals, src.getMaxStackSize)
    init(src)
  }

  override def toString: String = super.toString + " - " + aliases.toList.filter(s => s != null && s.size > 1).map(_.toString).distinct.mkString(",")

  /**
   * For every value the set of values that are aliases of it.
   *
   * Invariants:
   *  - If `aliases(i) == null` then i has no aliases. This is equivalent to having
   *    `aliases(i) == SingletonSet(i)`.
   *  - If `aliases(i) != null` then `aliases(i) contains i`.
   *  - If `aliases(i) contains j` then `aliases(i) eq aliases(j)`, i.e., they are references to the
   *    same (mutable) AliasSet.
   */
  val aliases: Array[AliasSet] = new Array[AliasSet](getLocals + getMaxStackSize)

  /**
   * The set of aliased values for a given entry in the `values` array.
   */
  def aliasesOf(entry: Int): AliasSet = {
    if (aliases(entry) != null) aliases(entry)
    else {
      val init = new AliasSet(new AliasSet.SmallBitSet(entry, -1, -1, -1), 1)
      aliases(entry) = init
      init
    }
  }

  /**
   * Define a new alias. For example, an assignment
   *   b = a
   * adds b to the set of aliases of a.
   */
  private def newAlias(assignee: Int, source: Int): Unit = {
    removeAlias(assignee)
    val sourceAliases = aliasesOf(source)
    sourceAliases += assignee
    aliases(assignee) = sourceAliases
  }

  /**
   * Remove an alias. For example, an assignment
   *   a = someUnknownValue()
   * removes a from its former alias set.
   * As another example, stack values are removed from their alias sets when being consumed.
   */
  private def removeAlias(assignee: Int): Unit = {
    if (aliases(assignee) != null) {
      aliases(assignee) -= assignee
      aliases(assignee) = null
    }
  }

  /**
   * Define the alias set for a given value.
   */
  private def setAliasSet(assignee: Int, set: AliasSet): Unit = {
    if (aliases(assignee) != null) {
      aliases(assignee) -= assignee
    }
    aliases(assignee) = set
  }

  override def execute(insn: AbstractInsnNode, interpreter: Interpreter[V]): Unit = {
    // Make the extension methods easier to use (otherwise we have to repeat `this`.stackTop)
    def stackTop: Int = this.stackTop
    def peekStack(n: Int): V = this.peekStack(n)

    val prodCons = InstructionStackEffect.forAsmAnalysis(insn, this) // needs to be called before super.execute, see its doc
    val consumed = InstructionStackEffect.cons(prodCons)
    val produced = InstructionStackEffect.prod(prodCons)

    super.execute(insn, interpreter)

    (insn.getOpcode: @switch) match {
      case ILOAD | LLOAD | FLOAD | DLOAD | ALOAD =>
        newAlias(assignee = stackTop, source = insn.asInstanceOf[VarInsnNode].`var`)

      case IINC =>
        removeAlias(insn.asInstanceOf[IincInsnNode].`var`)
      
      case DUP =>
        val top = stackTop
        newAlias(assignee = top, source = top - 1)

      case DUP_X1 =>
        val top = stackTop
        newAlias(assignee = top,     source = top - 1)
        newAlias(assignee = top - 1, source = top - 2)
        newAlias(assignee = top - 2, source = top)

      case DUP_X2 =>
        // Check if the second element on the stack is size 2
        // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.dup_x2
        val isSize2 = peekStack(1).getSize == 2
        val top = stackTop
        newAlias(assignee = top,     source = top - 1)
        newAlias(assignee = top - 1, source = top - 2)
        if (isSize2) {
          // Size 2 values on the stack only take one slot in the `values` array
          newAlias(assignee = top - 2, source = top)
        } else {
          newAlias(assignee = top - 2, source = top - 3)
          newAlias(assignee = top - 3, source = top)
        }

      case DUP2 =>
        val isSize2 = peekStack(0).getSize == 2
        val top = stackTop
        if (isSize2) {
          newAlias(assignee = top, source = top - 1)
        } else {
          newAlias(assignee = top - 1, source = top - 3)
          newAlias(assignee = top,     source = top - 2)
        }

      case DUP2_X1 =>
        val isSize2 = peekStack(0).getSize == 2
        val top = stackTop
        if (isSize2) {
          newAlias(assignee = top,     source = top - 1)
          newAlias(assignee = top - 1, source = top - 2)
          newAlias(assignee = top - 2, source = top)
        } else {
          newAlias(assignee = top,     source = top - 2)
          newAlias(assignee = top - 1, source = top - 3)
          newAlias(assignee = top - 2, source = top - 4)
          newAlias(assignee = top - 4, source = top)
          newAlias(assignee = top - 5, source = top - 1)
        }

      case DUP2_X2 =>
        val top = stackTop
        // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.dup2_x2
        val v1isSize2 = peekStack(0).getSize == 2
        if (v1isSize2) {
          newAlias(assignee = top,     source = top - 1)
          newAlias(assignee = top - 1, source = top - 2)
          val v2isSize2 = peekStack(1).getSize == 2
          if (v2isSize2) {
            // Form 4
            newAlias(assignee = top - 2, source = top)
          } else {
            // Form 2
            newAlias(assignee = top - 2, source = top - 3)
            newAlias(assignee = top - 3, source = top)
          }
        } else {
          newAlias(assignee = top,     source = top - 2)
          newAlias(assignee = top - 1, source = top - 3)
          newAlias(assignee = top - 2, source = top - 4)
          val v3isSize2 = peekStack(2).getSize == 2
          if (v3isSize2) {
            // Form 3
            newAlias(assignee = top - 3, source = top)
            newAlias(assignee = top - 4, source = top - 1)
          } else {
            // Form 1
            newAlias(assignee = top - 3, source = top - 5)
            newAlias(assignee = top - 4, source = top)
            newAlias(assignee = top - 5, source = top - 1)
          }
        }

      case SWAP =>
        // could be written more elegantly with higher-order combinators, but thinking of performance
        val top = stackTop

        def moveNextToTop(): Unit = {
          val nextAliases = aliases(top - 1)
          aliases(top) = nextAliases
          nextAliases -= (top - 1)
          nextAliases += top
        }

        if (aliases(top) != null) {
          val topAliases = aliases(top)
          if (aliases(top - 1) != null) moveNextToTop()
          else aliases(top) = null
          // move top to next
          aliases(top - 1) = topAliases
          topAliases -= top
          topAliases += (top - 1)
        } else {
          if (aliases(top - 1) != null) {
            moveNextToTop()
            aliases(top - 1) = null
          }
        }

      case opcode =>
        (opcode: @switch) match {
          case ISTORE | LSTORE | FSTORE | DSTORE | ASTORE =>
            // not a separate case: we re-use the code below that removes the consumed stack value from alias sets
            val stackTopBefore = stackTop - produced + consumed
            val local = insn.asInstanceOf[VarInsnNode].`var`
            newAlias(assignee = local, source = stackTopBefore)
            // if the value written is size 2, it overwrites the subsequent slot, which is then no
            // longer an alias of anything. see the corresponding case in `Frame.execute`.
            if (getLocal(local).getSize == 2)
              removeAlias(local + 1)

            // if the value at the preceding index is size 2, it is no longer valid, so we remove its
            // aliasing. see corresponding case in `Frame.execute`
            if (local > 0) {
              val precedingValue = getLocal(local - 1)
              if (precedingValue != null && precedingValue.getSize == 2)
                removeAlias(local - 1)
            }

          case _ =>
        }

        // Remove consumed stack values from aliasing sets.
        // Example: iadd
        //  - before: local1, local2, stack1, consumed1, consumed2
        //  - after:  local1, local2, stack1, produced1             // stackTop = 3
        val firstConsumed = stackTop - produced + 1                 // firstConsumed = 3
        for (i <- 0 until consumed)
          removeAlias(firstConsumed + i)                            // remove aliases for 3 and 4
    }
  }

  /**
   * When entering an exception handler, all values are dropped from the stack (and the exception
   * value is pushed). The ASM analyzer invokes `firstHandlerInstructionFrame.clearStack()`. To
   * ensure consistent aliasing sets, we need to remove the dropped values from aliasing sets.
   */
  override def clearStack(): Unit = {
    var i = getLocals
    val end = i + getStackSize
    while (i < end) {
      removeAlias(i)
      i += 1
    }
    super.clearStack()
  }

  /**
   * Merge the AliasingFrame `other` into this AliasingFrame.
   *
   * Aliases that are common in both frames are kept. Example:
   *
   * var x, y = null
   * if (...) {
   *   x = a
   *   y = a     // (x, y, a) are aliases
   * } else {
   *   x = a
   *   y = b     // (x, a) and (y, b)
   * }
   * [...]       // (x, a) -- merge of ((x, y, a)) and ((x, a), (y, b))
   */
  override def merge(other: Frame[_ <: V], interpreter: Interpreter[V]): Boolean = {
    // merge is the main performance hot spot of a data flow analysis.

    // in nullness analysis, super.merge (which actually merges the nullness values) takes 20% of
    // the overall analysis time.
    val valuesChanged = super.merge(other, interpreter)

    // in nullness analysis, merging the alias sets takes ~55% of the analysis time. therefore, this
    // code has been heavily optimized. most of the time is spent in the `hasNext` method of the
    // andNotIterator, see its comment.

    var aliasesChanged = false
    val aliasingOther = other.asInstanceOf[AliasingFrame[_]]

    val numValues = getLocals + getStackSize
    // assume (a, b) are aliases both in this frame, and the other frame. when merging the alias set
    // for a, we already see that a and b will be aliases in the final result. so we can skip over
    // merging the alias set for b. in this case, while merging the sets for a, knownOk(b) will be
    // set to `true`.
    val knownOk = new Array[Boolean](numValues)
    var i = 0
    while (i < numValues) {
      if (!knownOk(i)) {
        val thisAliases = this.aliases(i)
        val otherAliases = aliasingOther.aliases(i)
        if (thisAliases != null) {
          if (otherAliases == null) {
            if (thisAliases.size > 1) {
              aliasesChanged = true
              removeAlias(i)
            }
          } else {
            // The iterator yields elements that are in `thisAliases` but not in `otherAliases`.
            // As a side-effect, for every index `i` that is in both alias sets, the iterator sets
            // `knownOk(i) = true`: the alias sets for these values don't need to be merged again.
            val thisNotOtherIt = AliasSet.andNotIterator(thisAliases, otherAliases, knownOk)
            if (thisNotOtherIt.hasNext) {
              aliasesChanged = true
              val newSet = AliasSet.empty
              while (thisNotOtherIt.hasNext) {
                val next = thisNotOtherIt.next()
                newSet += next
                setAliasSet(next, newSet)
              }
            }
          }
        }
      }
      i += 1
    }

    valuesChanged || aliasesChanged
  }

  private def min(s: SmallBitSet) = {
    var r = s.a
    if (             s.b < r) r = s.b
    if (s.c != -1 && s.c < r) r = s.c
    if (s.d != -1 && s.d < r) r = s.d
    r
  }

  override def init(src: Frame[_ <: V]): Frame[V] = {
    super.init(src) // very quick (just an arraycopy)
    System.arraycopy(src.asInstanceOf[AliasingFrame[_]].aliases, 0, aliases, 0, aliases.length) // also quick

    val newSets = mutable.HashMap.empty[AliasSet, AliasSet]

    // the rest of this method (cloning alias sets) is the second performance˙hotspot (next to
    // AliasingFrame.merge). for nullness, it takes ~20% of the analysis time.
    // the difficulty here is that we have to clone the alias sets correctly. if two values a, b are
    // aliases, then aliases(a) eq aliases(b). we need to make sure to use the same clone for the
    // two values.

    var i = 0
    while (i < aliases.length) {
      val set = aliases(i)
      if (set != null) {
        // size cannot be 0 - alias sets are always at least singletons.
        // for sets of size 1-4, don't use the `newSets` map - lookup / update is slow
        if (set.size == 1) {
          aliases(i) = null
        } else if (set.size <= 4) {
          val small = set.set.asInstanceOf[AliasSet.SmallBitSet]
          val firstOfSet = i == min(small)
          if (firstOfSet) {
            val newSet = set.clone()
            aliases(small.a) = newSet
            aliases(small.b) = newSet
            if (small.c != -1) aliases(small.c) = newSet
            if (small.d != -1) aliases(small.d) = newSet
          }
        } else {
          // the actual hot spot is the hash map operations here: this is where almost all of the 20%
          // mentioned above is spent.
          // i also benchmarked an alternative implementation: keep an array of booleans for indexes
          // that already contain the cloned set. iterate through all elements of the cloned set and
          // assign the cloned set. this approach is 50% slower than using a hash map.
          if (newSets contains set) aliases(i) = newSets(set)
          else {
            val newSet = set.clone()
            newSets(set) = newSet
            aliases(i) = newSet
          }
        }
      }
      i += 1
    }
    this
  }
}

/**
 * An analyzer that uses AliasingFrames instead of bare Frames. This can be used when an analysis
 * needs to track aliases, but doesn't require a more specific Frame subclass.
 */
class AliasingAnalyzer[V <: Value](interpreter: Interpreter[V]) extends Analyzer[V](interpreter) {
  override def newFrame(nLocals: Int, nStack: Int): AliasingFrame[V] = new AliasingFrame(nLocals, nStack)
  override def newFrame(src: Frame[_ <: V]): AliasingFrame[V] = new AliasingFrame(src)
}

// Marker trait for AsmAnalyzers that use AliasingFrame
trait AliasingAsmAnalyzerMarker

class BasicAliasingAnalyzer(methodNode: MethodNode, classInternalName: InternalName)
  extends AsmAnalyzer[BasicValue](methodNode, classInternalName, new AliasingAnalyzer(new BasicInterpreter))
    with AliasingAsmAnalyzerMarker

/**
 * An iterator over Int (required to prevent boxing the result of next).
 */
abstract class IntIterator extends AbstractIterator[Int] {
  def hasNext: Boolean
  def next(): Int
}

/**
 * An efficient mutable bit set.
 *
 * @param set  Either a SmallBitSet or an Array[Long]
 * @param size The size of the set, useful for performance of certain operations
 */
class AliasSet(var set: Object /*SmallBitSet | Array[Long]*/, var size: Int) {
  import AliasSet._

  override def toString: String = iterator.toSet.mkString("<", ",", ">")

  /**
   * An iterator for the elements of this bit set. Note that only one iterator can be used at a
   * time. Also make sure not to change the underlying AliasSet during iteration.
   */
  def iterator: IntIterator = andNotIterator(this, empty, null)

  def +=(value: Int): Unit = (set: @unchecked) match {
    case s: SmallBitSet => (size: @switch) match {
      case 0 =>                                                     s.a = value; size = 1
      case 1 => if (value != s.a)                                 { s.b = value; size = 2 }
      case 2 => if (value != s.a && value != s.b)                 { s.c = value; size = 3 }
      case 3 => if (value != s.a && value != s.b && value != s.c) { s.d = value; size = 4 }
      case 4 =>
        if (value != s.a && value != s.b && value != s.c && value != s.d) {
          this.set = bsEmpty
          this.size = 0
          bsAdd(this, s.a)
          bsAdd(this, s.b)
          bsAdd(this, s.c)
          bsAdd(this, s.d)
          bsAdd(this, value)
        }
    }
    case _: Array[Long] =>
      bsAdd(this, value)
  }

  def -=(value: Int): Unit = (set: @unchecked) match {
    case s: SmallBitSet => (size: @switch) match {
      case 0 =>
      case 1 =>
        if      (value == s.a) { s.a = -1; size = 0 }
      case 2 =>
        if      (value == s.a) { s.a = s.b; s.b = -1; size = 1 }
        else if (value == s.b) {            s.b = -1; size = 1 }
      case 3 =>
        if      (value == s.a) { s.a = s.b; s.b = s.c; s.c = -1; size = 2 }
        else if (value == s.b) {            s.b = s.c; s.c = -1; size = 2 }
        else if (value == s.c) {                       s.c = -1; size = 2 }
      case 4 =>
        if      (value == s.a) { s.a = s.b; s.b = s.c; s.c = s.d; s.d = -1; size = 3 }
        else if (value == s.b) {            s.b = s.c; s.c = s.d; s.d = -1; size = 3 }
        else if (value == s.c) {                       s.c = s.d; s.d = -1; size = 3 }
        else if (value == s.d) {                                  s.d = -1; size = 3 }
    }
    case _: Array[Long] =>
      bsRemove(this, value)
      if (this.size == 4)
        this.set = bsToSmall(this.set.asInstanceOf[Array[Long]])
  }

  override def clone(): AliasSet = {
    val resSet: Object = (set: @unchecked) match {
      case s: SmallBitSet => new SmallBitSet(s.a, s.b, s.c, s.d)
      case bits: Array[Long] => bits.clone()
    }
    new AliasSet(resSet, this.size)
  }
}

object AliasSet {
  def empty = new AliasSet(new SmallBitSet(-1, -1, -1, -1), 0)

  final class SmallBitSet(var a: Int, var b: Int, var c: Int, var d: Int) {
    override def toString = s"($a, $b, $c, $d)"
  }

  def bsEmpty: Array[Long] = new Array[Long](1)

  private def bsEnsureCapacity(set: Array[Long], index: Int): Array[Long] = {
    if (index < set.length) set
    else {
      var newLength = set.length
      while (index >= newLength) newLength *= 2
      val newSet = new Array[Long](newLength)
      Array.copy(set, 0, newSet, 0, set.length)
      newSet
    }
  }

  def bsAdd(set: AliasSet, bit: Int): Unit = {
    val bits = set.set.asInstanceOf[Array[Long]]
    val index = bit >> 6
    val resSet = bsEnsureCapacity(bits, index)
    val before = resSet(index)
    val result = before | (1L << bit)
    if (result != before) {
      resSet(index) = result
      set.set = resSet
      set.size += 1
    }
  }

  def bsRemove(set: AliasSet, bit: Int): Unit = {
    val bits = set.set.asInstanceOf[Array[Long]]
    val index = bit >> 6
    if (index < bits.length) {
      val before = bits(index)
      val result = before & ~(1L << bit)
      if (result != before) {
        bits(index) = result
        set.size -= 1
      }
    }
  }

  def bsContains(set: Array[Long], bit: Int): Boolean = {
    val index = bit >> 6
    bit >= 0 && index < set.length && (set(index) & (1L << bit)) != 0L
  }

//  var sizesHist: Array[Int] = new Array[Int](1000)

  /**
   * Convert a bit array to a SmallBitSet. Requires the bit array to contain exactly four bits.
   */
  def bsToSmall(bits: Array[Long]): SmallBitSet = {
    var a = -1
    var b = -1
    var c = -1
    var i = 0
    val end = bits.length * 64
    while (i < end) {
      if (bsContains(bits, i)) {
        if (a == -1) a = i
        else if (b == -1) b = i
        else if (c == -1) c = i
        else return new SmallBitSet(a, b, c, i)
      }
      i += 1
    }
    null
  }

  /**
   * An iterator that yields the elements that are in one bit set and not in another (&~).
   */
  private class AndNotIt(setA: AliasSet, setB: AliasSet, thisAndOther: Array[Boolean]) extends IntIterator {
    // values in the first bit set
    private var a, b, c, d = -1
    private var xs: Array[Long] = null

    // values in the second bit set
    private var notA, notB, notC, notD = -1
    private var notXs: Array[Long] = null

    // holds the next value of `x`, `y` or `z` that should be returned. assigned in hasNext
    private var abcdNext = -1

    // counts through elements in the `xs` bit set
    private var i = 0
    // true if the current value of `i` should be returned by this iterator
    private var iValid = false

    (setA.set: @unchecked) match {
      case s: SmallBitSet => a = s.a; b = s.b; c = s.c; d = s.d
      case bits: Array[Long] => xs = bits
    }

    (setB.set: @unchecked) match {
      case s: SmallBitSet => notA = s.a; notB = s.b; notC = s.c; notD = s.d
      case bits: Array[Long] => notXs = bits
    }

    // for each value that exists both in this AND (&) the other bit, `thisAndOther` is set to true.
    // hacky side-effect, used for performance of AliasingFrame.merge.
    private def setThisAndOther(x: Int) = if (thisAndOther != null) thisAndOther(x) = true

    private def checkABCD(x: Int, num: Int): Boolean = {
      // assert(x == a && num == 1 || x == b && num == 2 || ...)
      x != -1 && {
        val otherHasA = x == notA || x == notB  || x == notC || x == notD || (notXs != null && bsContains(notXs, x))
        if (otherHasA) setThisAndOther(x)
        else abcdNext = x
        (num: @switch) match {
          case 1 => a = -1
          case 2 => b = -1
          case 3 => c = -1
          case 4 => d = -1
        }
        !otherHasA
      }
    }

    // main performance hot spot
    private def checkXs = {
      (xs != null) && {
        val end = xs.length * 64

        while (i < end && {
          val index = i >> 6
          if (xs(index) == 0L) { // boom. for nullness, this saves 35% of the overall analysis time.
            i = ((index + 1) << 6) - 1 // -1 required because i is incremented in the loop body
            true
          } else {
            val mask = 1L << i
            // if (mask > xs(index)) we could also advance i to the next value, but that didn't pay off in benchmarks
            val thisHasI = (xs(index) & mask) != 0L
            !thisHasI || {
              val otherHasI = i == notA || i == notB || i == notC || i == notD || (notXs != null && index < notXs.length && (notXs(index) & mask) != 0L)
              if (otherHasI) setThisAndOther(i)
              otherHasI
            }
          }
        }) i += 1

        iValid = i < end
        iValid
      }
    }

    // this is the main hot spot of alias analysis. for nullness, 38% of the overall analysis time
    // is spent here. within hasNext, almost the entire time is spent in `checkXs`.
    //
    def hasNext: Boolean = iValid || abcdNext != -1 || checkABCD(a, 1) || checkABCD(b, 2) || checkABCD(c, 3) || checkABCD(d, 4) || checkXs

    def next(): Int = {
      if (hasNext) {
        if (abcdNext != -1) {
          val r = abcdNext; abcdNext = -1; r
        } else {
          val r = i; i += 1; iValid = false; r
        }
      } else Iterator.empty.next()
    }
  }

//  The number of bits in a bit array. Useful for debugging.
//  def bsSize(bits: Array[Long]) = {
//    var r = 0
//    var i = 0
//    while (i < bits.length) {
//      r += java.lang.Long.bitCount(bits(i))
//      i += 1
//    }
//    r
//  }

  /**
   * An iterator returning the elements in a that are not also in b (a &~ b).
   *
   * If `thisAndOther` is non-null, the iterator sets thisAndOther(i) to true for every value that
   * is both in a and b (&).
   */
  def andNotIterator(a: AliasSet, b: AliasSet, thisAndOther: Array[Boolean]): IntIterator = new AndNotIt(a, b, thisAndOther)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy