diff options
author | Lukas Rytz <lukas.rytz@typesafe.com> | 2016-01-25 13:02:09 +0100 |
---|---|---|
committer | Lukas Rytz <lukas.rytz@typesafe.com> | 2016-01-25 13:02:09 +0100 |
commit | 90ee1b871236402f8543bf424a4f38d909598b3a (patch) | |
tree | 6340addb2c5fcbf661a2fabaeffc245e1edd128d | |
parent | 1081e718f8f8e174dbf615e42b157e187d3d3886 (diff) | |
parent | 3dccf0ee22303e1c192ed3e40391df48d811f3f6 (diff) | |
download | scala-90ee1b871236402f8543bf424a4f38d909598b3a.tar.gz scala-90ee1b871236402f8543bf424a4f38d909598b3a.tar.bz2 scala-90ee1b871236402f8543bf424a4f38d909598b3a.zip |
Merge pull request #4858 from lrytz/opt/elimBoxes
Eliminate non-escaping boxes, tuples and refs
36 files changed, 3833 insertions, 858 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala index 1cf547a5ac..32f8c7826f 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala @@ -142,12 +142,12 @@ object AsmUtils { * Run ASM's CheckClassAdapter over a class. Returns None if no problem is found, otherwise * Some(msg) with the verifier's error message. */ - def checkClass(classNode: ClassNode): Option[String] = { + def checkClass(classNode: ClassNode, dumpNonErroneous: Boolean = false): Option[String] = { val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) classNode.accept(cw) val sw = new StringWriter() val pw = new PrintWriter(sw) - CheckClassAdapter.verify(new ClassReader(cw.toByteArray), false, pw) + CheckClassAdapter.verify(new ClassReader(cw.toByteArray), dumpNonErroneous, pw) val res = sw.toString if (res.isEmpty) None else Some(res) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index 790469d874..59d584c370 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -640,7 +640,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { case Apply(fun @ _, List(expr)) if currentRun.runDefinitions.isBox(fun.symbol) => val nativeKind = tpeTK(expr) genLoad(expr, nativeKind) - val MethodNameAndType(mname, methodType) = asmBoxTo(nativeKind) + val MethodNameAndType(mname, methodType) = srBoxesRuntimeBoxToMethods(nativeKind) bc.invokestatic(srBoxesRunTimeRef.internalName, mname, methodType.descriptor, app.pos) generatedType = boxResultType(fun.symbol) // was typeToBType(fun.symbol.tpe.resultType) @@ -648,7 +648,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { genLoad(expr) val boxType = unboxResultType(fun.symbol) // was typeToBType(fun.symbol.owner.linkedClassOfClass.tpe) generatedType = boxType - val MethodNameAndType(mname, methodType) = asmUnboxTo(boxType) + val MethodNameAndType(mname, methodType) = srBoxesRuntimeUnboxToMethods(boxType) bc.invokestatic(srBoxesRunTimeRef.internalName, mname, methodType.descriptor, app.pos) case app @ Apply(fun, args) => diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index ab52bf72d8..c3f6399901 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -1113,7 +1113,7 @@ abstract class BTypes { */ /** - * Just a named pair, used in CoreBTypes.asmBoxTo/asmUnboxTo. + * Just a named pair, used in CoreBTypes.srBoxesRuntimeBoxToMethods/srBoxesRuntimeUnboxToMethods. */ final case class MethodNameAndType(name: String, methodType: MethodBType) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 53304d137f..9bfa7dae27 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -106,7 +106,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { assert(classSym != NoSymbol, "Cannot create ClassBType from NoSymbol") assert(classSym.isClass, s"Cannot create ClassBType from non-class symbol $classSym") assertClassNotArrayNotPrimitive(classSym) - assert(!primitiveTypeMap.contains(classSym) || isCompilingPrimitive, s"Cannot create ClassBType for primitive class symbol $classSym") + assert(!primitiveTypeToBType.contains(classSym) || isCompilingPrimitive, s"Cannot create ClassBType for primitive class symbol $classSym") if (classSym == NothingClass) srNothingRef else if (classSym == NullClass) srNullRef else { @@ -152,7 +152,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def primitiveOrClassToBType(sym: Symbol): BType = { assertClassNotArray(sym) assert(!sym.isImplClass, sym) - primitiveTypeMap.getOrElse(sym, classBTypeFromSymbol(sym)) + primitiveTypeToBType.getOrElse(sym, classBTypeFromSymbol(sym)) } /** @@ -214,7 +214,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def assertClassNotArrayNotPrimitive(sym: Symbol): Unit = { assertClassNotArray(sym) - assert(!primitiveTypeMap.contains(sym) || isCompilingPrimitive, sym) + assert(!primitiveTypeToBType.contains(sym) || isCompilingPrimitive, sym) } def implementedInterfaces(classSym: Symbol): List[Symbol] = { @@ -331,7 +331,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { superClassSym == ObjectClass else // A ClassBType for a primitive class (scala.Boolean et al) is only created when compiling these classes. - ((superClassSym != NoSymbol) && !superClassSym.isInterface) || (isCompilingPrimitive && primitiveTypeMap.contains(classSym)), + ((superClassSym != NoSymbol) && !superClassSym.isInterface) || (isCompilingPrimitive && primitiveTypeToBType.contains(classSym)), s"Bad superClass for $classSym: $superClassSym" ) val superClass = if (superClassSym == NoSymbol) None diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala index 05cc484135..13f4738d3d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala @@ -287,7 +287,7 @@ object BackendReporting { s"Failed to get the type of a method of class symbol $classFullName due to SI-9111." case ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass) => - s"Failed to build the inline information: $missingClass." + s"Failed to build the inline information: $missingClass" case UnknownScalaInlineInfoVersion(internalName, version) => s"Cannot read ScalaInlineInfo version $version in classfile $internalName. Use a more recent compiler." diff --git a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala index 028eea9da2..8bb71a386f 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala @@ -1,7 +1,7 @@ package scala.tools.nsc package backend.jvm -import scala.annotation.switch +import scala.tools.nsc.backend.jvm.BTypes.InternalName /** * Core BTypes and some other definitions. The initialization of these definitions requires access @@ -29,14 +29,14 @@ import scala.annotation.switch class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { import bTypes._ import global._ - import rootMirror.{requiredClass, requiredModule, getClassIfDefined} + import rootMirror.{requiredClass, requiredModule, getRequiredClass, getClassIfDefined} import definitions._ /** * Maps primitive types to their corresponding PrimitiveBType. The map is defined lexically above * the first use of `classBTypeFromSymbol` because that method looks at the map. */ - lazy val primitiveTypeMap: Map[Symbol, PrimitiveBType] = Map( + lazy val primitiveTypeToBType: Map[Symbol, PrimitiveBType] = Map( UnitClass -> UNIT, BooleanClass -> BOOL, CharClass -> CHAR, @@ -45,34 +45,22 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { IntClass -> INT, LongClass -> LONG, FloatClass -> FLOAT, - DoubleClass -> DOUBLE - ) - - private lazy val BOXED_UNIT : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.Void]) - private lazy val BOXED_BOOLEAN : ClassBType = classBTypeFromSymbol(BoxedBooleanClass) - private lazy val BOXED_BYTE : ClassBType = classBTypeFromSymbol(BoxedByteClass) - private lazy val BOXED_SHORT : ClassBType = classBTypeFromSymbol(BoxedShortClass) - private lazy val BOXED_CHAR : ClassBType = classBTypeFromSymbol(BoxedCharacterClass) - private lazy val BOXED_INT : ClassBType = classBTypeFromSymbol(BoxedIntClass) - private lazy val BOXED_LONG : ClassBType = classBTypeFromSymbol(BoxedLongClass) - private lazy val BOXED_FLOAT : ClassBType = classBTypeFromSymbol(BoxedFloatClass) - private lazy val BOXED_DOUBLE : ClassBType = classBTypeFromSymbol(BoxedDoubleClass) + DoubleClass -> DOUBLE) /** * Map from primitive types to their boxed class type. Useful when pushing class literals onto the * operand stack (ldc instruction taking a class literal), see genConstant. */ lazy val boxedClassOfPrimitive: Map[PrimitiveBType, ClassBType] = Map( - UNIT -> BOXED_UNIT, - BOOL -> BOXED_BOOLEAN, - BYTE -> BOXED_BYTE, - SHORT -> BOXED_SHORT, - CHAR -> BOXED_CHAR, - INT -> BOXED_INT, - LONG -> BOXED_LONG, - FLOAT -> BOXED_FLOAT, - DOUBLE -> BOXED_DOUBLE - ) + UNIT -> classBTypeFromSymbol(requiredClass[java.lang.Void]), + BOOL -> classBTypeFromSymbol(BoxedBooleanClass), + BYTE -> classBTypeFromSymbol(BoxedByteClass), + SHORT -> classBTypeFromSymbol(BoxedShortClass), + CHAR -> classBTypeFromSymbol(BoxedCharacterClass), + INT -> classBTypeFromSymbol(BoxedIntClass), + LONG -> classBTypeFromSymbol(BoxedLongClass), + FLOAT -> classBTypeFromSymbol(BoxedFloatClass), + DOUBLE -> classBTypeFromSymbol(BoxedDoubleClass)) lazy val boxedClasses: Set[ClassBType] = boxedClassOfPrimitive.values.toSet @@ -82,7 +70,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { */ lazy val boxResultType: Map[Symbol, ClassBType] = { for ((valueClassSym, boxMethodSym) <- currentRun.runDefinitions.boxMethod) - yield boxMethodSym -> boxedClassOfPrimitive(primitiveTypeMap(valueClassSym)) + yield boxMethodSym -> boxedClassOfPrimitive(primitiveTypeToBType(valueClassSym)) } /** @@ -90,7 +78,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { * For example, the method symbol for `Byte.unbox()`) is mapped to the PrimitiveBType BYTE. */ lazy val unboxResultType: Map[Symbol, PrimitiveBType] = { for ((valueClassSym, unboxMethodSym) <- currentRun.runDefinitions.unboxMethod) - yield unboxMethodSym -> primitiveTypeMap(valueClassSym) + yield unboxMethodSym -> primitiveTypeToBType(valueClassSym) } /* @@ -106,6 +94,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { lazy val ObjectRef : ClassBType = classBTypeFromSymbol(ObjectClass) lazy val StringRef : ClassBType = classBTypeFromSymbol(StringClass) + lazy val PredefRef : ClassBType = classBTypeFromSymbol(PredefModule.moduleClass) lazy val jlStringBuilderRef : ClassBType = classBTypeFromSymbol(StringBuilderClass) lazy val jlThrowableRef : ClassBType = classBTypeFromSymbol(ThrowableClass) lazy val jlCloneableRef : ClassBType = classBTypeFromSymbol(JavaCloneableClass) // java/lang/Cloneable @@ -116,58 +105,138 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { lazy val sbScalaBeanInfoRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.beans.ScalaBeanInfo]) lazy val jliSerializedLambdaRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda]) lazy val jliMethodHandlesRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodHandles]) - lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(exitingPickler(rootMirror.getRequiredClass("java.lang.invoke.MethodHandles.Lookup"))) // didn't find a reliable non-stringly-typed way that works for inner classes in the backend + lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(exitingPickler(getRequiredClass("java.lang.invoke.MethodHandles.Lookup"))) // didn't find a reliable non-stringly-typed way that works for inner classes in the backend lazy val jliMethodTypeRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodType]) lazy val jliCallSiteRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.CallSite]) lazy val jliLambdaMetafactoryRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.LambdaMetafactory]) lazy val srLambdaDeserializerRef : ClassBType = classBTypeFromSymbol(requiredModule[scala.runtime.LambdaDeserializer.type].moduleClass) lazy val srBoxesRunTimeRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime]) + lazy val srBoxedUnitRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxedUnit]) - lazy val hashMethodSym: Symbol = getMember(ScalaRunTimeModule, nme.hash_) + private def methodNameAndType(cls: Symbol, name: Name, static: Boolean = false, filterOverload: Symbol => Boolean = _ => true): MethodNameAndType = { + val holder = if (static) cls.companionModule.moduleClass else cls + val method = holder.info.member(name).suchThat(filterOverload) + assert(!method.isOverloaded, method) + MethodNameAndType(name.toString, methodBTypeFromSymbol(method)) + } - // TODO @lry avoiding going through through missingHook for every line in the REPL: https://github.com/scala/scala/commit/8d962ed4ddd310cc784121c426a2e3f56a112540 - lazy val AndroidParcelableInterface : Symbol = getClassIfDefined("android.os.Parcelable") - lazy val AndroidCreatorClass : Symbol = getClassIfDefined("android.os.Parcelable$Creator") + private def srBoxesRuntimeMethods(getName: (String, String) => String): Map[BType, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val bType = primitiveTypeToBType(primitive) + val name = newTermName(getName(primitive.name.toString, boxedClass(primitive).name.toString)) + (bType, methodNameAndType(BoxesRunTimeClass, name)) + })(collection.breakOut) + } - lazy val BeanInfoAttr: Symbol = requiredClass[scala.beans.BeanInfo] + // Z -> MethodNameAndType(boxToBoolean,(Z)Ljava/lang/Boolean;) + lazy val srBoxesRuntimeBoxToMethods: Map[BType, MethodNameAndType] = srBoxesRuntimeMethods((primitive, boxed) => "boxTo" + boxed) - /* The Object => String overload. */ - lazy val String_valueOf: Symbol = { - getMember(StringModule, nme.valueOf) filter (sym => sym.info.paramTypes match { - case List(pt) => pt.typeSymbol == ObjectClass - case _ => false - }) + // Z -> MethodNameAndType(unboxToBoolean,(Ljava/lang/Object;)Z) + lazy val srBoxesRuntimeUnboxToMethods: Map[BType, MethodNameAndType] = srBoxesRuntimeMethods((primitive, boxed) => "unboxTo" + primitive) + + def singleParamOfClass(cls: Symbol) = (s: Symbol) => s.paramss match { + case List(List(param)) => param.info.typeSymbol == cls + case _ => false } - /** - * Methods in scala.runtime.BoxesRuntime - */ - lazy val asmBoxTo : Map[BType, MethodNameAndType] = Map( - BOOL -> MethodNameAndType("boxToBoolean", MethodBType(List(BOOL), BOXED_BOOLEAN)), - BYTE -> MethodNameAndType("boxToByte", MethodBType(List(BYTE), BOXED_BYTE)), - CHAR -> MethodNameAndType("boxToCharacter", MethodBType(List(CHAR), BOXED_CHAR)), - SHORT -> MethodNameAndType("boxToShort", MethodBType(List(SHORT), BOXED_SHORT)), - INT -> MethodNameAndType("boxToInteger", MethodBType(List(INT), BOXED_INT)), - LONG -> MethodNameAndType("boxToLong", MethodBType(List(LONG), BOXED_LONG)), - FLOAT -> MethodNameAndType("boxToFloat", MethodBType(List(FLOAT), BOXED_FLOAT)), - DOUBLE -> MethodNameAndType("boxToDouble", MethodBType(List(DOUBLE), BOXED_DOUBLE)) - ) - - lazy val asmUnboxTo: Map[BType, MethodNameAndType] = Map( - BOOL -> MethodNameAndType("unboxToBoolean", MethodBType(List(ObjectRef), BOOL)), - BYTE -> MethodNameAndType("unboxToByte", MethodBType(List(ObjectRef), BYTE)), - CHAR -> MethodNameAndType("unboxToChar", MethodBType(List(ObjectRef), CHAR)), - SHORT -> MethodNameAndType("unboxToShort", MethodBType(List(ObjectRef), SHORT)), - INT -> MethodNameAndType("unboxToInt", MethodBType(List(ObjectRef), INT)), - LONG -> MethodNameAndType("unboxToLong", MethodBType(List(ObjectRef), LONG)), - FLOAT -> MethodNameAndType("unboxToFloat", MethodBType(List(ObjectRef), FLOAT)), - DOUBLE -> MethodNameAndType("unboxToDouble", MethodBType(List(ObjectRef), DOUBLE)) - ) + // java/lang/Boolean -> MethodNameAndType(valueOf,(Z)Ljava/lang/Boolean;) + lazy val javaBoxMethods: Map[InternalName, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + val method = methodNameAndType(boxed, newTermName("valueOf"), static = true, filterOverload = singleParamOfClass(primitive)) + (classBTypeFromSymbol(boxed).internalName, method) + })(collection.breakOut) + } + + // java/lang/Boolean -> MethodNameAndType(booleanValue,()Z) + lazy val javaUnboxMethods: Map[InternalName, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + val name = primitive.name.toString.toLowerCase + "Value" + (classBTypeFromSymbol(boxed).internalName, methodNameAndType(boxed, newTermName(name))) + })(collection.breakOut) + } + + private def predefBoxingMethods(getName: (String, String) => String): Map[String, MethodBType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + val name = getName(primitive.name.toString, boxed.name.toString) + (name, methodNameAndType(PredefModule.moduleClass, newTermName(name)).methodType) + })(collection.breakOut) + } + + // boolean2Boolean -> (Z)Ljava/lang/Boolean; + lazy val predefAutoBoxMethods: Map[String, MethodBType] = predefBoxingMethods((primitive, boxed) => primitive.toLowerCase + "2" + boxed) + + // Boolean2boolean -> (Ljava/lang/Boolean;)Z + lazy val predefAutoUnboxMethods: Map[String, MethodBType] = predefBoxingMethods((primitive, boxed) => boxed + "2" + primitive.toLowerCase) + + private def staticRefMethods(name: Name): Map[InternalName, MethodNameAndType] = { + allRefClasses.map(refClass => + (classBTypeFromSymbol(refClass).internalName, methodNameAndType(refClass, name, static = true)))(collection.breakOut) + } + + // scala/runtime/BooleanRef -> MethodNameAndType(create,(Z)Lscala/runtime/BooleanRef;) + lazy val srRefCreateMethods: Map[InternalName, MethodNameAndType] = staticRefMethods(nme.create) + + // scala/runtime/BooleanRef -> MethodNameAndType(zero,()Lscala/runtime/BooleanRef;) + lazy val srRefZeroMethods: Map[InternalName, MethodNameAndType] = staticRefMethods(nme.zero) + + // java/lang/Boolean -> MethodNameAndType(<init>,(Z)V) + lazy val primitiveBoxConstructors: Map[InternalName, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + (classBTypeFromSymbol(boxed).internalName, methodNameAndType(boxed, nme.CONSTRUCTOR, filterOverload = singleParamOfClass(primitive))) + })(collection.breakOut) + } + + private def nonOverloadedConstructors(classes: Iterable[Symbol]): Map[InternalName, MethodNameAndType] = { + classes.map(cls => (classBTypeFromSymbol(cls).internalName, methodNameAndType(cls, nme.CONSTRUCTOR)))(collection.breakOut) + } + + // scala/runtime/BooleanRef -> MethodNameAndType(<init>,(Z)V) + lazy val srRefConstructors: Map[InternalName, MethodNameAndType] = nonOverloadedConstructors(allRefClasses) + + private def specializedSubclasses(cls: Symbol): List[Symbol] = { + exitingSpecialize(cls.info) // the `transformInfo` method of specialization adds specialized subclasses to the `specializedClass` map + specializeTypes.specializedClass.collect({ + case ((`cls`, _), specCls) => specCls + }).toList + } + + // scala/Tuple3 -> MethodNameAndType(<init>,(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V) + // scala/Tuple2$mcZC$sp -> MethodNameAndType(<init>,(ZC)V) + lazy val tupleClassConstructors: Map[InternalName, MethodNameAndType] = { + val tupleClassSymbols = TupleClass.seq ++ specializedSubclasses(TupleClass(1)) ++ specializedSubclasses(TupleClass(2)) + nonOverloadedConstructors(tupleClassSymbols) + } + + // enumeration of specialized classes is temporary, while we still use the java-defined JFunctionN. + // once we switch to ordinary FunctionN, we can use specializedSubclasses just like for tuples. + private def functionClasses(base: String): Set[Symbol] = { + def primitives = Iterator("B", "S", "I", "J", "C", "F", "D", "Z", "V") + def ijfd = Iterator("I", "J", "F", "D") + def ijfdzv = Iterator("I", "J", "F", "D", "Z", "V") + def ijd = Iterator("I", "J", "D") + val classNames = Set.empty[String] ++ { + (0 to 22).map(base + _) + } ++ { + primitives.map(base + "0$mc" + _ + "$sp") // Function0 + } ++ { + // return type specializations appear first in the name string (alphabetical sorting) + for (r <- ijfdzv; a <- ijfd) yield base + "1$mc" + r + a + "$sp" // Function1 + } ++ { + for (r <- ijfdzv; a <- ijd; b <- ijd) yield base + "2$mc" + r + a + b + "$sp" // Function2 + } + classNames map getRequiredClass + } + + lazy val srJFunctionRefs: Set[InternalName] = functionClasses("scala.runtime.java8.JFunction").map(classBTypeFromSymbol(_).internalName) lazy val typeOfArrayOp: Map[Int, BType] = { import scalaPrimitives._ Map( - (List(ZARRAY_LENGTH, ZARRAY_GET, ZARRAY_SET) map (_ -> BOOL)) ++ + (List(ZARRAY_LENGTH, ZARRAY_GET, ZARRAY_SET) map (_ -> BOOL)) ++ (List(BARRAY_LENGTH, BARRAY_GET, BARRAY_SET) map (_ -> BYTE)) ++ (List(SARRAY_LENGTH, SARRAY_GET, SARRAY_SET) map (_ -> SHORT)) ++ (List(CARRAY_LENGTH, CARRAY_GET, CARRAY_SET) map (_ -> CHAR)) ++ @@ -178,6 +247,22 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { (List(OARRAY_LENGTH, OARRAY_GET, OARRAY_SET) map (_ -> ObjectRef)) : _* ) } + + lazy val hashMethodSym: Symbol = getMember(ScalaRunTimeModule, nme.hash_) + + // TODO @lry avoiding going through through missingHook for every line in the REPL: https://github.com/scala/scala/commit/8d962ed4ddd310cc784121c426a2e3f56a112540 + lazy val AndroidParcelableInterface : Symbol = getClassIfDefined("android.os.Parcelable") + lazy val AndroidCreatorClass : Symbol = getClassIfDefined("android.os.Parcelable$Creator") + + lazy val BeanInfoAttr: Symbol = requiredClass[scala.beans.BeanInfo] + + /* The Object => String overload. */ + lazy val String_valueOf: Symbol = { + getMember(StringModule, nme.valueOf) filter (sym => sym.info.paramTypes match { + case List(pt) => pt.typeSymbol == ObjectClass + case _ => false + }) + } } /** @@ -193,10 +278,14 @@ trait CoreBTypesProxyGlobalIndependent[BTS <: BTypes] { import bTypes._ def boxedClasses: Set[ClassBType] + def boxedClassOfPrimitive: Map[PrimitiveBType, ClassBType] - def ObjectRef : ClassBType def srNothingRef : ClassBType def srNullRef : ClassBType + + def ObjectRef : ClassBType + def StringRef : ClassBType + def PredefRef : ClassBType def jlCloneableRef : ClassBType def jiSerializableRef : ClassBType def juHashMapRef : ClassBType @@ -205,6 +294,26 @@ trait CoreBTypesProxyGlobalIndependent[BTS <: BTypes] { def jliMethodHandlesRef : ClassBType def jliMethodHandlesLookupRef : ClassBType def srLambdaDeserializerRef : ClassBType + def srBoxesRunTimeRef : ClassBType + def srBoxedUnitRef : ClassBType + + def srBoxesRuntimeBoxToMethods : Map[BType, MethodNameAndType] + def srBoxesRuntimeUnboxToMethods : Map[BType, MethodNameAndType] + + def javaBoxMethods : Map[InternalName, MethodNameAndType] + def javaUnboxMethods : Map[InternalName, MethodNameAndType] + + def predefAutoBoxMethods : Map[String, MethodBType] + def predefAutoUnboxMethods : Map[String, MethodBType] + + def srRefCreateMethods : Map[InternalName, MethodNameAndType] + def srRefZeroMethods : Map[InternalName, MethodNameAndType] + + def primitiveBoxConstructors : Map[InternalName, MethodNameAndType] + def srRefConstructors : Map[InternalName, MethodNameAndType] + def tupleClassConstructors : Map[InternalName, MethodNameAndType] + + def srJFunctionRefs: Set[InternalName] } /** @@ -219,20 +328,21 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: _coreBTypes = coreBTypes.asInstanceOf[CoreBTypes[bTypes.type]] } - def primitiveTypeMap: Map[Symbol, PrimitiveBType] = _coreBTypes.primitiveTypeMap + def primitiveTypeToBType: Map[Symbol, PrimitiveBType] = _coreBTypes.primitiveTypeToBType def boxedClasses: Set[ClassBType] = _coreBTypes.boxedClasses - def boxedClassOfPrimitive: Map[PrimitiveBType, ClassBType] = _coreBTypes.boxedClassOfPrimitive def boxResultType: Map[Symbol, ClassBType] = _coreBTypes.boxResultType def unboxResultType: Map[Symbol, PrimitiveBType] = _coreBTypes.unboxResultType + def srNothingRef : ClassBType = _coreBTypes.srNothingRef + def srNullRef : ClassBType = _coreBTypes.srNullRef + def ObjectRef : ClassBType = _coreBTypes.ObjectRef def StringRef : ClassBType = _coreBTypes.StringRef + def PredefRef : ClassBType = _coreBTypes.PredefRef def jlStringBuilderRef : ClassBType = _coreBTypes.jlStringBuilderRef - def srNothingRef : ClassBType = _coreBTypes.srNothingRef - def srNullRef : ClassBType = _coreBTypes.srNullRef def jlThrowableRef : ClassBType = _coreBTypes.jlThrowableRef def jlCloneableRef : ClassBType = _coreBTypes.jlCloneableRef def jiSerializableRef : ClassBType = _coreBTypes.jiSerializableRef @@ -248,6 +358,29 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: def jliLambdaMetafactoryRef : ClassBType = _coreBTypes.jliLambdaMetafactoryRef def srLambdaDeserializerRef : ClassBType = _coreBTypes.srLambdaDeserializerRef def srBoxesRunTimeRef : ClassBType = _coreBTypes.srBoxesRunTimeRef + def srBoxedUnitRef : ClassBType = _coreBTypes.srBoxedUnitRef + + def srBoxesRuntimeBoxToMethods : Map[BType, MethodNameAndType] = _coreBTypes.srBoxesRuntimeBoxToMethods + def srBoxesRuntimeUnboxToMethods : Map[BType, MethodNameAndType] = _coreBTypes.srBoxesRuntimeUnboxToMethods + + def javaBoxMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.javaBoxMethods + def javaUnboxMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.javaUnboxMethods + + def predefAutoBoxMethods : Map[String, MethodBType] = _coreBTypes.predefAutoBoxMethods + def predefAutoUnboxMethods : Map[String, MethodBType] = _coreBTypes.predefAutoUnboxMethods + + def srRefCreateMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.srRefCreateMethods + def srRefZeroMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.srRefZeroMethods + + def primitiveBoxConstructors : Map[InternalName, MethodNameAndType] = _coreBTypes.primitiveBoxConstructors + def srRefConstructors : Map[InternalName, MethodNameAndType] = _coreBTypes.srRefConstructors + def tupleClassConstructors : Map[InternalName, MethodNameAndType] = _coreBTypes.tupleClassConstructors + + def srJFunctionRefs: Set[InternalName] = _coreBTypes.srJFunctionRefs + + def typeOfArrayOp: Map[Int, BType] = _coreBTypes.typeOfArrayOp + + // Some symbols. These references should probably be moved to Definitions. def hashMethodSym: Symbol = _coreBTypes.hashMethodSym @@ -257,9 +390,4 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: def BeanInfoAttr: Symbol = _coreBTypes.BeanInfoAttr def String_valueOf: Symbol = _coreBTypes.String_valueOf - - def asmBoxTo : Map[BType, MethodNameAndType] = _coreBTypes.asmBoxTo - def asmUnboxTo: Map[BType, MethodNameAndType] = _coreBTypes.asmUnboxTo - - def typeOfArrayOp: Map[Int, BType] = _coreBTypes.typeOfArrayOp } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala index 35e4db33bc..b0ec37db97 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala @@ -237,7 +237,7 @@ abstract class GenBCode extends BCodeSyncAndTry { } if (settings.YoptInlinerEnabled) bTypes.inliner.runInliner() - if (settings.YoptClosureElimination) + if (settings.YoptClosureInvocations) closureOptimizer.rewriteClosureApplyInvocations() } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala index 596ee55290..086946e4e3 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala @@ -28,6 +28,8 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc 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. * @@ -99,7 +101,7 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc super.execute(insn, interpreter) (insn.getOpcode: @switch) match { - case ALOAD => + case ILOAD | LLOAD | FLOAD | DLOAD | ALOAD => newAlias(assignee = stackTop, source = insn.asInstanceOf[VarInsnNode].`var`) case DUP => @@ -212,23 +214,26 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc } case opcode => - if (opcode == 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) - } + (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. @@ -296,18 +301,25 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc if (!knownOk(i)) { val thisAliases = this.aliases(i) val otherAliases = aliasingOther.aliases(i) - if (thisAliases != null && otherAliases != null) { - // 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) + 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) + } } } } @@ -413,7 +425,7 @@ abstract class IntIterator extends Iterator[Int] { class AliasSet(var set: Object /*SmallBitSet | Array[Long]*/, var size: Int) { import AliasSet._ - override def toString: String = set.toString + 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 diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index b02bc7c96e..e8630c65d9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -3,10 +3,13 @@ package backend.jvm package analysis import scala.annotation.switch -import scala.tools.asm.{Opcodes, Handle, Type, Label} +import scala.tools.asm.{Handle, Type, Label} +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis.{Frame, BasicInterpreter, Analyzer, Value} +import GenBCode._ import scala.tools.nsc.backend.jvm.BTypes._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ import java.lang.invoke.LambdaMetafactory import scala.collection.mutable @@ -23,6 +26,8 @@ import scala.collection.convert.decorateAsScala._ */ class BackendUtils[BT <: BTypes](val btypes: BT) { import btypes._ + import btypes.coreBTypes._ + import callGraph.ClosureInstantiation /** * A wrapper to make ASM's Analyzer a bit easier to use. @@ -47,6 +52,7 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { private val basicValueSizeLimit = 9000l * 1000l * 1000l private val sourceValueSizeLimit = 8000l * 950l * 950l + def sizeOKForAliasing(method: MethodNode): Boolean = size(method) < nullnessSizeLimit def sizeOKForNullness(method: MethodNode): Boolean = size(method) < nullnessSizeLimit def sizeOKForBasicValue(method: MethodNode): Boolean = size(method) < basicValueSizeLimit def sizeOKForSourceValue(method: MethodNode): Boolean = size(method) < sourceValueSizeLimit @@ -54,6 +60,8 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { class ProdConsAnalyzer(val methodNode: MethodNode, classInternalName: InternalName) extends AsmAnalyzer(methodNode, classInternalName, new Analyzer(new InitialProducerSourceInterpreter)) with ProdConsAnalyzerImpl + class NonLubbingTypeFlowAnalyzer(val methodNode: MethodNode, classInternalName: InternalName) extends AsmAnalyzer(methodNode, classInternalName, new Analyzer(new NonLubbingTypeFlowInterpreter)) + /** * Add: * private static java.util.Map $deserializeLambdaCache$ = null @@ -68,8 +76,6 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { */ def addLambdaDeserialize(classNode: ClassNode): Unit = { val cw = classNode - import scala.tools.asm.Opcodes._ - import btypes.coreBTypes._ // Make sure to reference the ClassBTypes of all types that are used in the code generated // here (e.g. java/util/Map) are initialized. Initializing a ClassBType adds it to the @@ -140,6 +146,137 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { (result, map, hasSerializableClosureInstantiation) } + def getBoxedUnit: FieldInsnNode = new FieldInsnNode(GETSTATIC, srBoxedUnitRef.internalName, "UNIT", srBoxedUnitRef.descriptor) + + private val anonfunAdaptedName = """.*\$anonfun\$\d+\$adapted""".r + def hasAdaptedImplMethod(closureInit: ClosureInstantiation): Boolean = { + isrJFunctionType(Type.getReturnType(closureInit.lambdaMetaFactoryCall.indy.desc).getInternalName) && + anonfunAdaptedName.pattern.matcher(closureInit.lambdaMetaFactoryCall.implMethod.getName).matches + } + + private def primitiveAsmTypeToBType(primitiveType: Type): PrimitiveBType = (primitiveType.getSort: @switch) match { + case Type.BOOLEAN => BOOL + case Type.BYTE => BYTE + case Type.CHAR => CHAR + case Type.SHORT => SHORT + case Type.INT => INT + case Type.LONG => LONG + case Type.FLOAT => FLOAT + case Type.DOUBLE => DOUBLE + case _ => null + } + + def isScalaBox(insn: MethodInsnNode): Boolean = { + insn.owner == srBoxesRunTimeRef.internalName && { + val args = Type.getArgumentTypes(insn.desc) + args.length == 1 && (srBoxesRuntimeBoxToMethods.get(primitiveAsmTypeToBType(args(0))) match { + case Some(MethodNameAndType(name, tp)) => name == insn.name && tp.descriptor == insn.desc + case _ => false + }) + } + } + + def getScalaBox(primitiveType: Type): MethodInsnNode = { + val bType = primitiveAsmTypeToBType(primitiveType) + val MethodNameAndType(name, methodBType) = srBoxesRuntimeBoxToMethods(bType) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + def isScalaUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == srBoxesRunTimeRef.internalName && (srBoxesRuntimeUnboxToMethods.get(primitiveAsmTypeToBType(Type.getReturnType(insn.desc))) match { + case Some(MethodNameAndType(name, tp)) => name == insn.name && tp.descriptor == insn.desc + case _ => false + }) + } + + def getScalaUnbox(primitiveType: Type): MethodInsnNode = { + val bType = primitiveAsmTypeToBType(primitiveType) + val MethodNameAndType(name, methodBType) = srBoxesRuntimeUnboxToMethods(bType) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + private def calleeInMap(insn: MethodInsnNode, map: Map[InternalName, MethodNameAndType]): Boolean = map.get(insn.owner) match { + case Some(MethodNameAndType(name, tp)) => insn.name == name && insn.desc == tp.descriptor + case _ => false + } + + def isJavaBox(insn: MethodInsnNode): Boolean = calleeInMap(insn, javaBoxMethods) + def isJavaUnbox(insn: MethodInsnNode): Boolean = calleeInMap(insn, javaUnboxMethods) + + def isPredefAutoBox(insn: MethodInsnNode): Boolean = { + insn.owner == PredefRef.internalName && (predefAutoBoxMethods.get(insn.name) match { + case Some(tp) => insn.desc == tp.descriptor + case _ => false + }) + } + + def isPredefAutoUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == PredefRef.internalName && (predefAutoUnboxMethods.get(insn.name) match { + case Some(tp) => insn.desc == tp.descriptor + case _ => false + }) + } + + def isRefCreate(insn: MethodInsnNode): Boolean = calleeInMap(insn, srRefCreateMethods) + def isRefZero(insn: MethodInsnNode): Boolean = calleeInMap(insn, srRefZeroMethods) + + def runtimeRefClassBoxedType(refClass: InternalName): Type = Type.getArgumentTypes(srRefCreateMethods(refClass).methodType.descriptor)(0) + + def isSideEffectFreeCall(insn: MethodInsnNode): Boolean = { + isScalaBox(insn) || isScalaUnbox(insn) || + isJavaBox(insn) || // not java unbox, it may NPE + isSideEffectFreeConstructorCall(insn) + } + + def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { + isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) + } + + def isModuleLoad(insn: AbstractInsnNode, moduleName: InternalName): Boolean = insn match { + case fi: FieldInsnNode => fi.getOpcode == GETSTATIC && fi.owner == moduleName && fi.name == "MODULE$" && fi.desc == ("L" + moduleName + ";") + case _ => false + } + + def isPredefLoad(insn: AbstractInsnNode) = isModuleLoad(insn, PredefRef.internalName) + + def isPrimitiveBoxConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, primitiveBoxConstructors) + def isRuntimeRefConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, srRefConstructors) + def isTupleConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, tupleClassConstructors) + + // unused objects created by these constructors are eliminated by pushPop + private lazy val sideEffectFreeConstructors: Set[(String, String)] = { + val ownerDesc = (p: (InternalName, MethodNameAndType)) => (p._1, p._2.methodType.descriptor) + primitiveBoxConstructors.map(ownerDesc).toSet ++ + srRefConstructors.map(ownerDesc) ++ + tupleClassConstructors.map(ownerDesc) ++ Set( + (ObjectRef.internalName, MethodBType(Nil, UNIT).descriptor), + (StringRef.internalName, MethodBType(Nil, UNIT).descriptor), + (StringRef.internalName, MethodBType(List(StringRef), UNIT).descriptor), + (StringRef.internalName, MethodBType(List(ArrayBType(CHAR)), UNIT).descriptor)) + } + + def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { + insn.name == INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc)) + } + + private lazy val classesOfSideEffectFreeConstructors = sideEffectFreeConstructors.map(_._1) + + def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode) = { + insn.getOpcode == NEW && { + val ti = insn.asInstanceOf[TypeInsnNode] + classesOfSideEffectFreeConstructors.contains(ti.desc) + } + } + + def isBoxedUnit(insn: AbstractInsnNode) = { + insn.getOpcode == GETSTATIC && { + val fi = insn.asInstanceOf[FieldInsnNode] + fi.owner == srBoxedUnitRef.internalName && fi.name == "UNIT" && fi.desc == srBoxedUnitRef.descriptor + } + } + + def isrJFunctionType(internalName: InternalName): Boolean = srJFunctionRefs(internalName) + /** * Visit the class node and collect all referenced nested classes. */ @@ -274,15 +411,13 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { * Analyzer: its implementation also skips over unreachable code in the same way. */ def computeMaxLocalsMaxStack(method: MethodNode): Unit = { - import Opcodes._ - if (isAbstractMethod(method) || isNativeMethod(method)) { method.maxLocals = 0 method.maxStack = 0 } else if (!maxLocalsMaxStackComputed(method)) { val size = method.instructions.size - var maxLocals = (Type.getArgumentsAndReturnSizes(method.desc) >> 2) - (if (isStaticMethod(method)) 1 else 0) + var maxLocals = parametersSize(method) var maxStack = 0 // queue of instruction indices where analysis should start diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala index 4e81018451..dd19ad594f 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala @@ -27,7 +27,7 @@ object InstructionStackEffect { * This method requires the `frame` to be in the state **before** executing / interpreting the * `insn`. */ - def forAsmAnalysis[V <: Value](insn: AbstractInsnNode, frame: Frame[V]): Int = computeConsProd(insn, frame = frame) + def forAsmAnalysis[V <: Value](insn: AbstractInsnNode, frame: Frame[V]): Int = computeConsProd(insn, forClassfile = false, conservative = false, frame = frame) /** * Returns the maximal possible growth of the stack when executing `insn`. The returned value @@ -41,7 +41,7 @@ object InstructionStackEffect { * allows looking up the sizes of values on the stack. */ def maxStackGrowth(insn: AbstractInsnNode): Int = { - val prodCons = computeConsProd(insn, conservative = true) + val prodCons = computeConsProd(insn, forClassfile = false, conservative = true) prod(prodCons) - cons(prodCons) } @@ -50,7 +50,7 @@ object InstructionStackEffect { * (the `cons` / `prod` extract individual values). The returned values are correct for writing * into a classfile (see doc on the `analysis` package object). */ - def forClassfile(insn: AbstractInsnNode): Int = computeConsProd(insn, forClassfile = true) + def forClassfile(insn: AbstractInsnNode): Int = computeConsProd(insn, forClassfile = true, conservative = false) private def invokeConsProd(methodDesc: String, insn: AbstractInsnNode, forClassfile: Boolean): Int = { val consumesReceiver = insn.getOpcode != INVOKESTATIC && insn.getOpcode != INVOKEDYNAMIC @@ -71,7 +71,7 @@ object InstructionStackEffect { d == "J" || d == "D" } - private def computeConsProd[V <: Value](insn: AbstractInsnNode, forClassfile: Boolean = false, conservative: Boolean = false, frame: Frame[V] = null): Int = { + private def computeConsProd[V <: Value](insn: AbstractInsnNode, forClassfile: Boolean, conservative: Boolean, frame: Frame[V] = null): Int = { // not used if `forClassfile || conservative`: in these cases, `frame` is allowed to be `null` def peekStack(n: Int): V = frame.peekStack(n) @@ -335,5 +335,4 @@ object InstructionStackEffect { IFNONNULL => t(1, 0) } } - } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala index f9ac12eb4d..30e73f8ac2 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala @@ -63,7 +63,7 @@ object NullnessValue { def unknown(insn: AbstractInsnNode) = if (BytecodeUtils.instructionResultSize(insn) == 2) UnknownValue2 else UnknownValue1 } -final class NullnessInterpreter extends Interpreter[NullnessValue](Opcodes.ASM5) { +final class NullnessInterpreter(bTypes: BTypes) extends Interpreter[NullnessValue](Opcodes.ASM5) { def newValue(tp: Type): NullnessValue = { // ASM loves giving semantics to null. The behavior here is the same as in SourceInterpreter, // which is provided by the framework. @@ -113,13 +113,13 @@ final class NullnessInterpreter extends Interpreter[NullnessValue](Opcodes.ASM5) def ternaryOperation(insn: AbstractInsnNode, value1: NullnessValue, value2: NullnessValue, value3: NullnessValue): NullnessValue = UnknownValue1 - def naryOperation(insn: AbstractInsnNode, values: util.List[_ <: NullnessValue]): NullnessValue = (insn.getOpcode: @switch) match { - case Opcodes.MULTIANEWARRAY => + def naryOperation(insn: AbstractInsnNode, values: util.List[_ <: NullnessValue]): NullnessValue = insn match { + case mi: MethodInsnNode if bTypes.backendUtils.isNonNullMethodInvocation(mi) => NotNullValue case _ => - // TODO: use a list of methods that are known to return non-null values - NullnessValue.unknown(insn) + if (insn.getOpcode == Opcodes.MULTIANEWARRAY) NotNullValue + else NullnessValue.unknown(insn) } def returnOperation(insn: AbstractInsnNode, value: NullnessValue, expected: NullnessValue): Unit = () @@ -197,7 +197,7 @@ class NullnessFrame(nLocals: Int, nStack: Int) extends AliasingFrame[NullnessVal * This class is required to override the `newFrame` methods, which makes makes sure the analyzer * uses NullnessFrames. */ -class NullnessAnalyzer extends Analyzer[NullnessValue](new NullnessInterpreter) { +class NullnessAnalyzer(bTypes: BTypes) extends Analyzer[NullnessValue](new NullnessInterpreter(bTypes)) { override def newFrame(nLocals: Int, nStack: Int): NullnessFrame = new NullnessFrame(nLocals, nStack) override def newFrame(src: Frame[_ <: NullnessValue]): NullnessFrame = new NullnessFrame(src) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala index c933341492..894799bf36 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala @@ -94,8 +94,13 @@ trait ProdConsAnalyzerImpl { inputValues(insn).iterator.flatMap(v => v.insns.asScala).toSet } - def consumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = - _consumersOfOutputsFrom.get(insn).map(v => v.indices.flatMap(v.apply)(collection.breakOut): Set[AbstractInsnNode]).getOrElse(Set.empty) + def consumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = insn match { + case _: UninitializedLocalProducer => Set.empty + case ParameterProducer(local) => consumersOfValueAt(methodNode.instructions.getFirst, local) + case ExceptionProducer(handlerLabel, handlerFrame) => consumersOfValueAt(handlerLabel, handlerFrame.stackTop) + case _ => + _consumersOfOutputsFrom.get(insn).map(v => v.indices.flatMap(v.apply)(collection.breakOut): Set[AbstractInsnNode]).getOrElse(Set.empty) + } /** * Returns the potential initial producer instructions of a value in the frame of `insn`. @@ -151,13 +156,19 @@ trait ProdConsAnalyzerImpl { inputValueSlots(insn).flatMap(slot => initialProducersForValueAt(insn, slot)).toSet } - def ultimateConsumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = { - lazy val next = insn.getNext - outputValueSlots(insn).flatMap(slot => ultimateConsumersOfValueAt(next, slot)).toSet + def ultimateConsumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = insn match { + case _: UninitializedLocalProducer => Set.empty + case _ => + lazy val next = insn match { + case _: ParameterProducer => methodNode.instructions.getFirst + case ExceptionProducer(handlerLabel, _) => handlerLabel + case _ => insn.getNext + } + outputValueSlots(insn).flatMap(slot => ultimateConsumersOfValueAt(next, slot)).toSet } private def isCopyOperation(insn: AbstractInsnNode): Boolean = { - isVarInstruction(insn) || { + isLoadOrStore(insn) || { (insn.getOpcode: @switch) match { case DUP | DUP_X1 | DUP_X2 | DUP2 | DUP2_X1 | DUP2_X2 | SWAP | CHECKCAST => true case _ => false @@ -378,7 +389,7 @@ trait ProdConsAnalyzerImpl { private def outputValueSlots(insn: AbstractInsnNode): Seq[Int] = insn match { case ParameterProducer(local) => Seq(local) case UninitializedLocalProducer(local) => Seq(local) - case ExceptionProducer(frame) => Seq(frame.stackTop) + case ExceptionProducer(_, frame) => Seq(frame.stackTop) case _ => if (insn.getOpcode == -1) return Seq.empty if (isStore(insn)) { @@ -443,9 +454,9 @@ abstract class InitialProducer extends AbstractInsnNode(-1) { override def accept(cv: MethodVisitor): Unit = throw new UnsupportedOperationException } -case class ParameterProducer(local: Int) extends InitialProducer -case class UninitializedLocalProducer(local: Int) extends InitialProducer -case class ExceptionProducer[V <: Value](handlerFrame: Frame[V]) extends InitialProducer +case class ParameterProducer(local: Int) extends InitialProducer +case class UninitializedLocalProducer(local: Int) extends InitialProducer +case class ExceptionProducer[V <: Value](handlerLabel: LabelNode, handlerFrame: Frame[V]) extends InitialProducer class InitialProducerSourceInterpreter extends SourceInterpreter { override def newParameterValue(isInstanceMethod: Boolean, local: Int, tp: Type): SourceValue = { @@ -457,6 +468,6 @@ class InitialProducerSourceInterpreter extends SourceInterpreter { } override def newExceptionValue(tryCatchBlockNode: TryCatchBlockNode, handlerFrame: Frame[_ <: Value], exceptionType: Type): SourceValue = { - new SourceValue(1, ExceptionProducer(handlerFrame)) + new SourceValue(1, ExceptionProducer(tryCatchBlockNode.handler, handlerFrame)) } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala new file mode 100644 index 0000000000..bcf9978c16 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala @@ -0,0 +1,36 @@ +package scala.tools.nsc +package backend.jvm +package analysis + +import scala.tools.asm.Type +import scala.tools.asm.tree.analysis.{BasicValue, BasicInterpreter} + +abstract class TypeFlowInterpreter extends BasicInterpreter { + override def newValue(tp: Type) = { + if (tp == null) super.newValue(tp) + else if (isRef(tp)) new BasicValue(tp) + else super.newValue(tp) + } + + def isRef(tp: Type) = tp != null && (tp.getSort match { + case Type.OBJECT | Type.ARRAY => true + case _ => false + }) + + def refLub(a: BasicValue, b: BasicValue): BasicValue + + override def merge(a: BasicValue, b: BasicValue): BasicValue = { + if (a == b) a + else if (isRef(a.getType) && isRef(b.getType)) refLub(a, b) + else BasicValue.UNINITIALIZED_VALUE + } +} + +/** + * A [[TypeFlowInterpreter]] which collapses LUBs of non-equal reference types to Object. + * This could be made more precise by looking up ClassBTypes for the two reference types and using + * the `jvmWiseLUB` method. + */ +class NonLubbingTypeFlowInterpreter extends TypeFlowInterpreter { + def refLub(a: BasicValue, b: BasicValue): BasicValue = BasicValue.REFERENCE_VALUE // java/lang/Object +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala new file mode 100644 index 0000000000..4ecaf17a10 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala @@ -0,0 +1,907 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.annotation.tailrec +import scala.tools.asm.Type +import scala.tools.asm.Opcodes._ +import scala.tools.asm.tree._ +import scala.collection.mutable +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ + +class BoxUnbox[BT <: BTypes](val btypes: BT) { + import btypes._ + import backendUtils._ + + /** + * Eliminate box-unbox paris within `method`. Such appear commonly after closure elimination: + * + * def t2 = { + * val f = (b: Byte, i: Int) => i + b // no specialized variant for this function type + * f(1, 2) // invokes the generic `apply` + * } + * + * The closure optimizer re-writes the `apply` call to `anonfun$adapted` method, which takes + * boxed arguments. After inlining this method, we get + * + * def t2 = { + * val a = boxByte(1) + * val b = boxInteger(2) + * val r = boxInteger(anonfun$(unboxByte(a), unboxInt(b))) + * unboxInt(r) + * } + * + * All these box/unbox operations are eliminated here. + * + * Implementation: for every box operation, find all consumers of the boxed value, then all + * producers of these consumers, repeat until reaching a fixpoint. If this results in a set of + * boxing and unboxing operations, the box can be eliminated. + * + * There are two methods for eliminating boxes: + * M1: If there is a single boxing operation, the boxed value(s) are stored into new local + * variable(s) at the allocation site. Accesses to the boxed value are re-written to reads / + * writes of these locals. Advantages: + * - supports mutable boxes (IntRef and friends) + * - supports eliminating unbox operations even if the box object needs to be created + * because it escapes (see E4) + * - works by keeping the unboxed value(s) in locals AND the box in its original form + * - only for immutable boxes: modifications to the escaped box cannot be applied to + * the local variable(s) holding the boxed value(s). + * Restriction: + * - does not work if there are multiple boxing operations (see E1) + * + * M2: If there are multiple boxing operations, the boxing operations are simply eliminated, + * leaving the unboxed value(s) on the stack. Store / load operations that previously + * acted on the box are adapted to handle the boxed type(s). If the box contains multiple + * values (or a size-2 value, which doesn't fit into locals that were used for the box), + * new local slots are used for store / load operations. Restrictions: + * - does not support re-writing writes to (mutable) boxes (see E2) + * - does not support re-writing reads of boxes that also escape (see E3) + * + * + * E1: M1 only works if there's a single boxing operation. + * def e1(b: Boolean) = { + * val i: Integer = box(10) // 10 is stored into a new local, box operation and i removed + * val j: Integer = box(20) // 20 is stored into a new local, box operation adn j removed + * val r = if (b) i else j // loads and stores of the box are eliminated, r no longer exists + * unbox(r) // cannot rewrite: we don't know which local to load + * } + * Note: the example has no write and the box does not escape, so M2 works here. + * + * E2: mutable boxes with multiple boxing operations cannot be eliminated. + * M1: see E1 + * M2: cannot replace an `IntRef` on the stack by an `Int` value on the stack, an Int on the + * stack cannot be modified. + * + * def e2(b: Boolean) = { + * val r1 = new IntRef(0) + * val r2 = new IntRef(1) + * val modRef = if (b) r1 else r2 + * modRef.elem += 10 // M1: cannot rewrite: which local to write? same as E1. + * (if (b) r1 else r2).elem += 10 // M2: cannot change an Int on the stack + * (r1.elem, r2.elem) + * } + * + * + * E3: escaping boxes with multiple boxing operations cannot be rewritten. + * M1: see E1. + * M2: at *, instead of an Integer, an Int is on the stack, but the escape method expects an + * Integer. We cannot just create a box at this point: if there are multiple escapes (or + * an escape is executed more than once), the difference could be observed (reference + * equality). + * + * def e3(b: Boolean) = { + * val i: Integer = box(1) + * val j: Integer = box(2) + * escape(if (b) i else j) // * + * unbox(if (b) i else j) + * } + * + * + * E4: M1 supports rewriting unbox operations of immutable boxes that escape + * def e4 = { + * val i: Integer = box(10) // 10 is stored into a new local, loaded as argument for the box call + * escape(i) // not changed, still loads the local i holding the box + * unbox(i) // rewritten to a pop (of the box) and a load of the local variable + * } + * + * + * E4 seems to be a bit of a corner case, but it's necessary to unblock box eliminations with + * mutual dependencies. Example: + * + * val ((a, b), c) = ((1, 2), 3) + * a + b + c + * + * generates (after a few cleanups) the following (pseudo-bytecode, ignoring primitive boxing, specialization): + * + * load 1, load 2, new Tuple2 // stack: Tuple2 + * load 3 // stack: Tuple2; Int + * val local1 = new Tuple2 + * val local2 = local1._1.asInstanceOf[Tuple2] + * val c = local1._2.asInstanceOf[Int] + * if (local2 == null) throw new MatchError(local1) + * val a = local2._1 + * val b = local2._2 + * a + b + c + * + * In order to eliminate the tuples, we first need to eliminate the outer tuple (stored in local1) + * - single box operation, so we use M1 + * - there are three consumers of the outer tuple: `local1._1`, `local1._2` and + * `new MatchError(local1)`. in the last one, the tuple escapes. + * - note that the MatchError creation is dead code: local2 is never null. However, our nullness + * analysis cannot identify this: it does not track nullness through tuple stores and loads. + * - if we re-write the non-escaping consumers of the outer tuple, but keep the tuple allocation + * and the escaping consumer, we get the follwoing: + * + * load 1, load 2 + * val newLocal1 = new Tuple2; load newLocal1 // stack: Tuple2 + * val newLocal2 = 3; load newLocal2 // stack: Tuple2; Int + * val local1 = new Tuple2 + * val local2 = newLocal1 + * val c = newLocal2 + * if (local2 == null) throw new MatchError(local1) + * val a = local2._1 + * val b = local2._2 + * a + b + c + * + * At this point, the nullness analysis sees that `local2 == null` is false, dead code elimination + * removes the `throw new MatchError(local1)`. After eliminating the allocation of the outer tuple, + * the inner tuple (stored in newLocal1) can also be eliminated. + * + * + * Special case for tuples wrt specialization: a tuple getter may box or unbox the value stored + * in the tuple: calling `_1` on a `Tuple2$mcII$sp` boxes the primitive Int stored in the tuple. + * Similarly, calling `_1$mcI$sp` on a non-specialized `Tuple2` unboxes the Integer in the tuple. + * When eliminating such getters, we have to introduce appropriate box / unbox calls. + * + * + * TODO: add new calls (box / unbox) to the call graph (not urgent) + * TODO: update the call graph because stack heights change (not urgent). + * this may also affect other optimizations, we ignored the issue so far. check how stack + * heights stored in the call graph are used. + * Note: these tasks are not urgent because the call graph is not currently used during / after + * method-local optimizations, only before to perform inlining and closure rewriting. + */ + def boxUnboxElimination(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + val toDelete = mutable.Set.empty[AbstractInsnNode] + + val knownHandled = mutable.Set.empty[AbstractInsnNode] + + lazy val prodCons = new ProdConsAnalyzer(method, owner) + + var nextLocal = method.maxLocals + def getLocal(size: Int) = { + val r = nextLocal + nextLocal += size + r + } + + var maxStackGrowth = 0 + + /** Mehtod M1 for eliminating box-unbox pairs (see doc comment in the beginning of this file) */ + def replaceBoxOperationsSingleCreation(creation: BoxCreation, finalCons: Set[BoxConsumer], boxKind: BoxKind, keepBox: Boolean): Unit = { + /** + * If the box is eliminated, all copy operations (loads, stores, others) of the box need to + * be removed. This method returns all copy operations that should be removed. + * + * Returns `None` in case some exotic copy operation is found that cannot be removed + * (DUP2_X1 and friends - these are never emitted by scalac). In this case, the box cannot + * be eliminated. + */ + def copyOpsToEliminate: Option[Set[AbstractInsnNode]] = { + var elidableCopyOps = Set.empty[AbstractInsnNode] + var replaceOK = true + val copyOps = new CopyOpsIterator(Set(creation), finalCons, prodCons) + while (replaceOK && copyOps.hasNext) copyOps.next() match { + case vi: VarInsnNode => + elidableCopyOps += vi + + case copyOp if copyOp.getOpcode == DUP => + elidableCopyOps += copyOp + + case _ => + replaceOK = false + } + if (replaceOK) Some(elidableCopyOps) else None + } + + val canRewrite = keepBox || (copyOpsToEliminate match { + case Some(copyOps) => + toDelete ++= copyOps + true + + case _ => false + }) + + if (canRewrite) { + val localSlots: Vector[(Int, Type)] = boxKind.boxedTypes.map(tp => (getLocal(tp.getSize), tp))(collection.breakOut) + + // store boxed value(s) into localSlots + val storeOps = localSlots.toList reverseMap { case (slot, tp) => + new VarInsnNode(tp.getOpcode(ISTORE), slot) + } + val storeInitialValues = creation.loadInitialValues match { + case Some(ops) => ops ::: storeOps + case None => storeOps + } + if (keepBox) { + val loadOps: List[VarInsnNode] = localSlots.map({ case (slot, tp) => + new VarInsnNode(tp.getOpcode(ILOAD), slot) + })(collection.breakOut) + toInsertBefore(creation.valuesConsumer) = storeInitialValues ::: loadOps + } else { + toReplace(creation.valuesConsumer) = storeInitialValues + toDelete ++= creation.allInsns - creation.valuesConsumer + } + + // rewrite consumers + finalCons foreach { + case write: StaticSetterOrInstanceWrite => + assert(!keepBox, s"cannot eliminate box write if the box remains (and escapes): $write") + val (slot, tp) = localSlots(boxKind.extractedValueIndex(write)) + val storeOp = new VarInsnNode(tp.getOpcode(ISTORE), slot) + toReplace(write.consumer) = List(storeOp) + + case c: EscapingConsumer => + assert(keepBox, s"found escaping consumer, but box is eliminated: $c") + + case extraction => + val (slot, tp) = localSlots(boxKind.extractedValueIndex(extraction)) + val loadOps = new VarInsnNode(tp.getOpcode(ILOAD), slot) :: extraction.postExtractionAdaptationOps(tp) + if (keepBox) toReplace(extraction.consumer) = getPop(1) :: loadOps + else toReplace(extraction.consumer) = loadOps + toDelete ++= extraction.allInsns - extraction.consumer + } + } + } + + /** Method M2 for eliminating box-unbox pairs (see doc comment in the beginning of this file) */ + def replaceBoxOperationsMultipleCreations(allCreations: Set[BoxCreation], allConsumers: Set[BoxConsumer], boxKind: BoxKind): Unit = { + /** + * If a single-value size-1 box is eliminated, local variables slots holding the box are + * reused to hold the unboxed value. In case there's an entry for that local variable in the + * method's local variables table (debug info), adapt the type. + * + * If there are multiple entries for a local variable that's changing types, then all + * entries for that variable are deleted - it's not obvious how to find the correct entry. + * Note that scalac never re-uses local variable slots for non-overlapping locals. Also note + * that all locals that are newly created during the optimizer don't have an entry either. + * + * Finally, note that variables that become unused are removed later from the table by + * removeUnusedLocalVariableNodes in LocalOpt. + * + * Unlike modifications that affect the method's instructions (which uses toReplace etc), + * we can directly modify the local variable table - it does not affect the frames of the + * ProdCons analysis. + */ + def updateLocalVariableTypes(reTypedLocals: Map[Int, Type]): Unit = { + lazy val localsByIndex = method.localVariables.asScala.groupBy(_.index) + for ((index, tp) <- reTypedLocals) localsByIndex.get(index).map(_.toList) match { + case Some(List(local)) => + local.desc = tp.getDescriptor + case Some(locals) => + locals foreach method.localVariables.remove + case _ => + } + } + + /** Remove box creations - leave the boxed value(s) on the stack instead. */ + def replaceCreationOps(): Unit = { + for (creation <- allCreations) creation.loadInitialValues match { + case None => + toDelete ++= creation.allInsns + + case Some(ops) => + toReplace(creation.valuesConsumer) = ops + toDelete ++= (creation.allInsns - creation.valuesConsumer) + } + } + + /** + * Replace a value extraction operation. For a single-value box, the extraction operation can + * just be removed. An extraction from a multi-value box is replaced by POP operations for the + * non-used values, and an xSTORE / xLOAD for the extracted value. Example: tuple3._2 becomes + * POP; xSTORE n; POP; xLOAD n. + */ + def replaceExtractionOps(): Unit = { + if (boxKind.boxedTypes.lengthCompare(1) == 0) { + // fast path for single-value boxes + allConsumers.foreach(extraction => extraction.postExtractionAdaptationOps(boxKind.boxedTypes.head) match { + case Nil => + toDelete ++= extraction.allInsns + case ops => + toReplace(extraction.consumer) = ops + toDelete ++= extraction.allInsns - extraction.consumer + }) + } else { + for (extraction <- allConsumers) { + val valueIndex = boxKind.extractedValueIndex(extraction) + val replacementOps = if (valueIndex == 0) { + val pops = boxKind.boxedTypes.tail.map(t => getPop(t.getSize)) + pops ::: extraction.postExtractionAdaptationOps(boxKind.boxedTypes.head) + } else { + var loadOps: List[AbstractInsnNode] = null + val consumeStack = boxKind.boxedTypes.zipWithIndex reverseMap { + case (tp, i) => + if (i == valueIndex) { + val resultSlot = getLocal(tp.getSize) + loadOps = new VarInsnNode(tp.getOpcode(ILOAD), resultSlot) :: extraction.postExtractionAdaptationOps(tp) + new VarInsnNode(tp.getOpcode(ISTORE), resultSlot) + } else { + getPop(tp.getSize) + } + } + consumeStack ::: loadOps + } + toReplace(extraction.consumer) = replacementOps + toDelete ++= extraction.allInsns - extraction.consumer + } + } + } + + checkCopyOpReplacements(allCreations, allConsumers, boxKind.boxedTypes, nextLocal, prodCons) match { + case Some((replacements, nextCopyOpLocal, reTypedLocals)) => + toReplace ++= replacements + updateLocalVariableTypes(reTypedLocals) + nextLocal = nextCopyOpLocal + replaceCreationOps() + replaceExtractionOps() + // Conservative (safe) value for stack growth. In every frame that initially has a multi-value + // box on the stack, the stack now contains all of the individual values. So for every eliminated + // box, the maxStack may be up to N-1 slots larger. + maxStackGrowth += boxKind.boxedTypes.length - 1 + + case None => + } + } + + val it = method.instructions.iterator + while (it.hasNext) { + val insn = it.next() + if (!knownHandled(insn)) BoxKind.valueCreationKind(insn, prodCons) match { + case Some((boxCreation, boxKind)) => + allCreationsConsumers(boxCreation, boxKind, prodCons) match { + case Some((allCreations, allConsumers)) => + val (escapingConsumers, boxConsumers) = allConsumers.partition(_.isEscaping) + if (boxConsumers.nonEmpty) { + for (c <- allCreations) knownHandled ++= c.allInsns + for (e <- allConsumers) knownHandled ++= e.allInsns + + val hasEscaping = escapingConsumers.nonEmpty + val hasWrite = allConsumers.exists(_.isWrite) + if (!hasEscaping && !hasWrite) { + // M2 -- see doc comment in the beginning of this file + // If both M1 and M2 can be applied, we prefer M2 because it doesn't introduce new locals. + replaceBoxOperationsMultipleCreations(allCreations, allConsumers, boxKind) + } else if (allCreations.size == 1 && (!hasEscaping || !boxKind.isMutable)) { + // M1 -- see doc comment in the beginning of this file + replaceBoxOperationsSingleCreation(allCreations.head, allConsumers, boxKind, keepBox = hasEscaping) + } + } + + case None => + } + + case None => + } + } + + def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { + case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) + case _ => + } + + for ((location, ops) <- toInsertBefore; op <- ops) + method.instructions.insertBefore(location, op) + + for ((oldOp, newOps) <- toReplace) { + for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp) + method.instructions.remove(oldOp) + removeFromCallGraph(oldOp) + } + + for (op <- toDelete) { + method.instructions.remove(op) + removeFromCallGraph(op) + } + + method.maxLocals = nextLocal + method.maxStack += maxStackGrowth + toInsertBefore.nonEmpty || toReplace.nonEmpty || toDelete.nonEmpty + } + } + + /** + * Given a box creations operation + * - find all ultimate consumers for the produced value. then: + * - for all consumed values, find all producer operations. check that all are box creations + * - recurse until reaching a fixpoint + * + * Returns a set of box creations and a set of box consumers. Note that the box consumers may + * contain [[EscapingConsumer]]s, even if there are multiple box creation operations. The callee + * will handle this case (and not attempt to eliminate the box). + */ + def allCreationsConsumers(initialCreation: BoxCreation, boxKind: BoxKind, prodCons: ProdConsAnalyzer): Option[(Set[BoxCreation], Set[BoxConsumer])] = { + var creations = Set(initialCreation) + var consumers = Set.empty[BoxConsumer] + + def addCreations(boxConsumer: BoxConsumer): Boolean = { + val newProds = boxConsumer.boxProducers(prodCons).filterNot(prod => creations.exists(_.producer == prod)) + newProds.forall(prod => boxKind.checkBoxCreation(prod, prodCons) match { + case Some(boxCreation) => + creations += boxCreation + addBoxConsumers(boxCreation) + + case _ => false + }) + } + + def addBoxConsumers(creation: BoxCreation): Boolean = { + val newCons = creation.boxConsumers(prodCons, ultimate = true).filterNot(cons => consumers.exists(_.consumer == cons)) + newCons.forall(cons => boxKind.checkBoxConsumer(cons, prodCons) match { + case Some(boxConsumer) => + consumers += boxConsumer + addCreations(boxConsumer) + + case _ => + creations.size <= 1 && { + // If there's a single box creation, the box operations can still be rewritten + consumers += EscapingConsumer(cons) + true + } + }) + } + + if (addBoxConsumers(initialCreation)) Some((creations, consumers)) + else None + } + + /** + * Takes two sets `initialProds` and `finalCons` such that all boxes produced by the first set + * are only consumed by an operation in the second set. + * + * Returns a map that replaces copy operations (ALOAD / ASTORE) between the producers and + * consumers with corresponding copy operations for the values stored in the box. The returned + * `Int` value returns the next free local variable slot. + * + * Examples: + * - for an Integer box, an ASTORE x is simply replaced by ISTORE x + * - for a pair of two references, an ASTORE x is replaced by `ASTORE x1; ASTORE x2` where x1 + * and x2 are fresh locals + * + * Not all copy operations can be supported: DUP only works for single-value boxes, the more + * exotic copy operations (DUP2_X2) are not supported (note that Scalac never emits them). If a + * copy operation cannot be replaced, this method returns `None`. + */ + def checkCopyOpReplacements(initialProds: Set[BoxCreation], finalCons: Set[BoxConsumer], valueTypes: List[Type], nextLocal: Int, prodCons: ProdConsAnalyzer): Option[(Map[AbstractInsnNode, List[AbstractInsnNode]], Int, Map[Int, Type])] = { + var replacements = Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + var reTypedLocals = Map.empty[Int, Type] + + var nextCopyOpLocal = nextLocal + val newLocalsMap: mutable.LongMap[List[(Type, Int)]] = mutable.LongMap.empty + def newLocals(index: Int) = newLocalsMap.getOrElseUpdate(index, valueTypes match { + case List(t) if t.getSize == 1 => + reTypedLocals += index -> t + List((t, index)) + case _ => valueTypes.map(t => { + val newIndex = nextCopyOpLocal + nextCopyOpLocal += t.getSize + (t, newIndex) + }) + }) + + var replaceOK = true + val copyOps = new CopyOpsIterator(initialProds, finalCons, prodCons) + while (replaceOK && copyOps.hasNext) copyOps.next() match { + case vi: VarInsnNode => + val isLoad = vi.getOpcode == ALOAD + val typedVarOp = (tp: (Type, Int)) => { + val opc = tp._1.getOpcode(if (isLoad) ILOAD else ISTORE) + new VarInsnNode(opc, tp._2) + } + val locs = newLocals(vi.`var`) + replacements += vi -> (if (isLoad) locs.map(typedVarOp) else locs.reverseMap(typedVarOp)) + + case copyOp => + if (copyOp.getOpcode == DUP && valueTypes.lengthCompare(1) == 0) { + if (valueTypes.head.getSize == 2) + replacements += copyOp -> List(new InsnNode(DUP2)) + } else { + replaceOK = false + } + } + if (replaceOK) Some((replacements, nextCopyOpLocal, reTypedLocals)) else None + } + + /** + * For a set of box creation operations and a corresponding set of box consumer operations, + * this iterator returns all copy operations (load, store, dup) that are in between. + */ + class CopyOpsIterator(initialCreations: Set[BoxCreation], finalCons: Set[BoxConsumer], prodCons: ProdConsAnalyzer) extends Iterator[AbstractInsnNode] { + private var queue = mutable.Queue.empty[AbstractInsnNode] ++ initialCreations.iterator.flatMap(_.boxConsumers(prodCons, ultimate = false)) + + // a single copy operation can consume multiple producers: val a = if (b) box(1) else box(2). + // the `ASTORE a` has two producers (the two box operations). we need to handle it only once. + private val visited = mutable.Set.empty[AbstractInsnNode] + + private val boxConsumingOps = finalCons.map(_.consumer) + + @tailrec private def advanceToNextCopyOp(): Unit = { + if (queue.nonEmpty) { + val h = queue.front + if (visited(h) || boxConsumingOps(h)) { + queue.dequeue() + advanceToNextCopyOp() + } + } + } + + def hasNext: Boolean = { + advanceToNextCopyOp() + queue.nonEmpty + } + + def next(): AbstractInsnNode = { + advanceToNextCopyOp() + val r = queue.dequeue() + visited += r + queue ++= prodCons.consumersOfOutputsFrom(r) + r + } + } + + trait BoxKind { + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] + def boxedTypes: List[Type] + def extractedValueIndex(extraction: BoxConsumer): Int + def isMutable: Boolean + } + + object BoxKind { + def valueCreationKind(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[(BoxCreation, BoxKind)] = { + PrimitiveBox.checkPrimitiveBox(insn, None, prodCons) orElse + Ref.checkRefCreation(insn, None, prodCons) orElse + Tuple.checkTupleCreation(insn, None, prodCons) + } + + /** + * Check if `newOp` is part of a standard object construction pattern in which: + * + * NEW T + * DUP + * [load constructor args] + * INVOKESPECIAL T.init + * + * The method ensures that the entire construction pattern is closed in itself, without any + * branches going in or out. This is checked by looking at producers / consumers: + * - `DUP` is the only consumer of `NEW`, and vice versa + * - `DUP` the only producer for the receiver of the constructor call + * - The set of consumers of `DUP` without the constructor call is the same as + * the set of consumers of the value on the stack top after the constructor call + */ + def checkInstanceCreation(newOp: TypeInsnNode, prodCons: ProdConsAnalyzer): Option[(InsnNode, MethodInsnNode)] = { + val newCons = prodCons.consumersOfOutputsFrom(newOp) + if (newCons.size == 1 && newCons.head.getOpcode == DUP) { + val dupOp = newCons.head.asInstanceOf[InsnNode] + if (prodCons.producersForInputsOf(dupOp) == Set(newOp)) { + val dupCons = prodCons.consumersOfOutputsFrom(dupOp) + val initCalls = dupCons collect { + case mi: MethodInsnNode if mi.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME && mi.owner == newOp.desc => mi + } + if (initCalls.size == 1) { + val initCall = initCalls.head + val numArgs = Type.getArgumentTypes(initCall.desc).length + val receiverProds = prodCons.producersForValueAt(initCall, prodCons.frameAt(initCall).stackTop - numArgs) + if (receiverProds == Set(dupOp)) { + val dupConsWithoutInit = dupCons - initCall + val afterInit = initCall.getNext + val stackTopAfterInit = prodCons.frameAt(afterInit).stackTop + val initializedInstanceCons = prodCons.consumersOfValueAt(afterInit, stackTopAfterInit) + if (initializedInstanceCons == dupConsWithoutInit && prodCons.producersForValueAt(afterInit, stackTopAfterInit) == Set(dupOp)) { + return Some((dupOp, initCall)) + } + } + } + } + } + None + } + + /** + * If `mi` is an invocation of a method on Predef, check if the receiver is a GETSTATIC of + * Predef.MODULE$ and return it. + */ + def checkReceiverPredefLoad(mi: MethodInsnNode, prodCons: ProdConsAnalyzer): Option[AbstractInsnNode] = { + val numArgs = Type.getArgumentTypes(mi.desc).length + val receiverProds = prodCons.producersForValueAt(mi, prodCons.frameAt(mi).stackTop - numArgs) + if (receiverProds.size == 1) { + val prod = receiverProds.head + if (isPredefLoad(prod) && prodCons.consumersOfOutputsFrom(prod) == Set(mi)) return Some(prod) + } + None + } + } + + case class PrimitiveBox(boxedType: Type, boxClass: InternalName) extends BoxKind { + import PrimitiveBox._ + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkPrimitiveBox(insn, Some(this), prodCons).map(_._1) + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkPrimitiveUnbox(insn, this, prodCons) + def boxedTypes: List[Type] = List(boxedType) + def extractedValueIndex(extraction: BoxConsumer): Int = 0 + def isMutable = false + } + + object PrimitiveBox { + private def boxedType(mi: MethodInsnNode) = Type.getArgumentTypes(mi.desc)(0) + + private def boxClass(mi: MethodInsnNode) = { + if (mi.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME) mi.owner + else Type.getReturnType(mi.desc).getInternalName + } + + def checkPrimitiveBox(insn: AbstractInsnNode, expectedKind: Option[PrimitiveBox], prodCons: ProdConsAnalyzer): Option[(BoxCreation, PrimitiveBox)] = { + // mi is either a box factory or a box constructor invocation + def checkKind(mi: MethodInsnNode) = expectedKind match { + case Some(kind) => if (kind.boxClass == boxClass(mi)) expectedKind else None + case None => Some(PrimitiveBox(boxedType(mi), boxClass(mi))) + } + + insn match { + case mi: MethodInsnNode => + if (isScalaBox(mi) || isJavaBox(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) + else if (isPredefAutoBox(mi)) + for (predefLoad <- BoxKind.checkReceiverPredefLoad(mi, prodCons); kind <- checkKind(mi)) + yield (ModuleFactory(predefLoad, mi), kind) + else None + + case ti: TypeInsnNode if ti.getOpcode == NEW => + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isPrimitiveBoxConstructor(initCall); kind <- checkKind(initCall)) + yield (InstanceCreation(ti, dupOp, initCall), kind) + + case _ => None + } + } + + def checkPrimitiveUnbox(insn: AbstractInsnNode, kind: PrimitiveBox, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = { + def typeOK(mi: MethodInsnNode) = kind.boxedType == Type.getReturnType(mi.desc) + insn match { + case mi: MethodInsnNode => + if ((isScalaUnbox(mi) || isJavaUnbox(mi)) && typeOK(mi)) Some(StaticGetterOrInstanceRead(mi)) + else if (isPredefAutoUnbox(mi) && typeOK(mi)) BoxKind.checkReceiverPredefLoad(mi, prodCons).map(ModuleGetter(_, mi)) + else None + + case _ => None + } + } + } + + case class Ref(boxedType: Type, refClass: InternalName) extends BoxKind { + import Ref._ + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkRefCreation(insn, Some(this), prodCons).map(_._1) + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkRefConsumer(insn, this, prodCons) + def boxedTypes: List[Type] = List(boxedType) + def extractedValueIndex(extraction: BoxConsumer): Int = 0 + def isMutable = true + } + + object Ref { + private def boxedType(mi: MethodInsnNode): Type = runtimeRefClassBoxedType(mi.owner) + private def refClass(mi: MethodInsnNode): InternalName = mi.owner + private def loadZeroValue(refZeroCall: MethodInsnNode): List[AbstractInsnNode] = List(loadZeroForTypeSort(runtimeRefClassBoxedType(refZeroCall.owner).getSort)) + + def checkRefCreation(insn: AbstractInsnNode, expectedKind: Option[Ref], prodCons: ProdConsAnalyzer): Option[(BoxCreation, Ref)] = { + def checkKind(mi: MethodInsnNode): Option[Ref] = expectedKind match { + case Some(kind) => if (kind.refClass == refClass(mi)) expectedKind else None + case None => Some(Ref(boxedType(mi), refClass(mi))) + } + + insn match { + case mi: MethodInsnNode => + if (isRefCreate(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) + else if (isRefZero(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = Some(loadZeroValue(mi))), _)) + else None + + case ti: TypeInsnNode if ti.getOpcode == NEW => + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isRuntimeRefConstructor(initCall); kind <- checkKind(initCall)) + yield (InstanceCreation(ti, dupOp, initCall), kind) + + case _ => None + } + } + + def checkRefConsumer(insn: AbstractInsnNode, kind: Ref, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = insn match { + case fi: FieldInsnNode if fi.owner == kind.refClass && fi.name == "elem" => + if (fi.getOpcode == GETFIELD) Some(StaticGetterOrInstanceRead(fi)) + else if (fi.getOpcode == PUTFIELD) Some(StaticSetterOrInstanceWrite(fi)) + else None + + case _ => None + } + } + + case class Tuple(boxedTypes: List[Type], tupleClass: InternalName) extends BoxKind { + import Tuple._ + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkTupleCreation(insn, Some(this), prodCons).map(_._1) + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkTupleExtraction(insn, this, prodCons) + def extractedValueIndex(extraction: BoxConsumer): Int = extraction match { + case StaticGetterOrInstanceRead(mi: MethodInsnNode) => tupleGetterIndex(mi.name) + case PrimitiveBoxingGetter(mi) => tupleGetterIndex(mi.name) + case PrimitiveUnboxingGetter(mi, _) => tupleGetterIndex(mi.name) + case _ => throw new AssertionError(s"Expected tuple getter, found $extraction") + } + def isMutable = false + } + + object Tuple { + private def boxedTypes(mi: MethodInsnNode): List[Type] = Type.getArgumentTypes(mi.desc).toList + private def tupleClass(mi: MethodInsnNode): InternalName = mi.owner + + def checkTupleCreation(insn: AbstractInsnNode, expectedKind: Option[Tuple], prodCons: ProdConsAnalyzer): Option[(BoxCreation, Tuple)] = { + def checkKind(mi: MethodInsnNode): Option[Tuple] = expectedKind match { + case Some(kind) => if (kind.tupleClass == tupleClass(mi)) expectedKind else None + case None => Some(Tuple(boxedTypes(mi), tupleClass(mi))) + } + + insn match { + // no need to check for TupleN.apply: the compiler transforms case companion apply calls to constructor invocations + case ti: TypeInsnNode if ti.getOpcode == NEW => + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isTupleConstructor(initCall); kind <- checkKind(initCall)) + yield (InstanceCreation(ti, dupOp, initCall), kind) + + case _ => None + } + } + + private val specializedTupleClassR = "scala/Tuple[12]\\$mc[IJDCZ]{1,2}\\$sp".r + private def isSpecializedTupleClass(tupleClass: InternalName) = specializedTupleClassR.pattern.matcher(tupleClass).matches + + private val specializedTupleGetterR = "_[12]\\$mc[IJDCZ]\\$sp".r + private def isSpecializedTupleGetter(mi: MethodInsnNode) = specializedTupleGetterR.pattern.matcher(mi.name)matches + + private val tupleGetterR = "_\\d\\d?".r + private def isTupleGetter(mi: MethodInsnNode) = tupleGetterR.pattern.matcher(mi.name).matches + + def checkTupleExtraction(insn: AbstractInsnNode, kind: Tuple, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = { + val expectedTupleClass = kind.tupleClass + insn match { + case mi: MethodInsnNode => + val tupleClass = mi.owner + if (isSpecializedTupleClass(expectedTupleClass)) { + val typeOK = tupleClass == expectedTupleClass || tupleClass == expectedTupleClass.substring(0, expectedTupleClass.indexOf('$')) + if (typeOK) { + if (isSpecializedTupleGetter(mi)) return Some(StaticGetterOrInstanceRead(mi)) + else if (isTupleGetter(mi)) return Some(PrimitiveBoxingGetter(mi)) + } + } else if (expectedTupleClass == tupleClass) { + if (isSpecializedTupleGetter(mi)) return Some(PrimitiveUnboxingGetter(mi, Type.getReturnType(mi.desc))) + else if (isTupleGetter(mi)) return Some(StaticGetterOrInstanceRead(mi)) + } + + case _ => + } + None + } + + private val getterIndexPattern = "_(\\d{1,2}).*".r + def tupleGetterIndex(getterName: String) = getterName match { case getterIndexPattern(i) => i.toInt - 1 } + } + + // TODO: add more + // case class ValueClass(valueClass: Type, valueType: Type) extends BoxKind + + sealed trait BoxCreation { + // to support box creation operations that don't consume an initial value from the stack, e.g., IntRef.zero + val loadInitialValues: Option[List[AbstractInsnNode]] + + /** + * The instruction that produces the box value; for instance creations, the `NEW` operation. + */ + def producer: AbstractInsnNode + + /** + * The instruction that consumes the boxed values; for instance creations, the `init` call. + */ + def valuesConsumer: MethodInsnNode = this match { + case StaticFactory(call, _) => call + case ModuleFactory(_, call) => call + case InstanceCreation(_, _, initCall) => initCall + } + + def allInsns: Set[AbstractInsnNode] = this match { + case StaticFactory(c, _) => Set(c) + case ModuleFactory(m, c) => Set(m, c) + case InstanceCreation(n, d, i) => Set(n, d, i) + } + + /** + * The consumers of the box produced by this box creation. If `ultimate` is true, then the + * final consumers are returned (e.g., an unbox operation), otherwise direct consumers (e.g., + * a store operation). + */ + def boxConsumers(prodCons: ProdConsAnalyzer, ultimate: Boolean): Set[AbstractInsnNode] = { + val startInsn = this match { + // for the non-transitive case (ultimate == false), it's important to start at the `dupOp`, + // not the `newOp` - look at the BoxCreation as a black box, get its consumers. + case InstanceCreation(_, dupOp, _) => dupOp + case _ => producer + } + val cons = if (ultimate) prodCons.ultimateConsumersOfOutputsFrom(startInsn) else prodCons.consumersOfOutputsFrom(startInsn) + this match { + case InstanceCreation(_, _, initCall) => cons - initCall + case _ => cons + } + } + } + + case class StaticFactory(producer: MethodInsnNode, loadInitialValues: Option[List[AbstractInsnNode]]) extends BoxCreation + case class ModuleFactory(moduleLoad: AbstractInsnNode, producer: MethodInsnNode) extends BoxCreation { + val loadInitialValues: Option[List[AbstractInsnNode]] = None + } + case class InstanceCreation(newOp: TypeInsnNode, dupOp: InsnNode, initCall: MethodInsnNode) extends BoxCreation { + def producer = newOp + val loadInitialValues: Option[List[AbstractInsnNode]] = None + } + + sealed trait BoxConsumer { + val consumer: AbstractInsnNode + + def allInsns: Set[AbstractInsnNode] = this match { + case ModuleGetter(m, c) => Set(m, c) + case _ => Set(consumer) + } + + /** + * The initial producers of the box value consumed by this box consumer + */ + def boxProducers(prodCons: ProdConsAnalyzer): Set[AbstractInsnNode] = { + val stackTop = prodCons.frameAt(consumer).stackTop + val slot = if (isWrite) stackTop - 1 else stackTop + prodCons.initialProducersForValueAt(consumer, slot) + } + + def isEscaping = this match { + case _: EscapingConsumer => true + case _ => false + } + + def isWrite = this match { + case _: StaticSetterOrInstanceWrite => true + case _ => false + } + + /** + * If this box consumer extracts a boxed value and applies a conversion, this method returns + * equivalent conversion operations. For example, invoking `_1$mcI$sp` on a non-specialized + * `Tuple2` extracts the Integer value and unboxes it. + */ + def postExtractionAdaptationOps(typeOfExtractedValue: Type): List[AbstractInsnNode] = this match { + case PrimitiveBoxingGetter(_) => List(getScalaBox(typeOfExtractedValue)) + case PrimitiveUnboxingGetter(_, unboxedPrimitive) => List(getScalaUnbox(unboxedPrimitive)) + case _ => Nil + } + } + + /** Static extractor (BoxesRunTime.unboxToInt) or GETFIELD or getter invocation */ + case class StaticGetterOrInstanceRead(consumer: AbstractInsnNode) extends BoxConsumer + /** A getter that boxes the returned value, e.g., `Tuple2$mcII$sp._1` */ + case class PrimitiveBoxingGetter(consumer: MethodInsnNode) extends BoxConsumer + /** A getter that unboxes the returned value, e.g., `Tuple2._1$mcI$sp` */ + case class PrimitiveUnboxingGetter(consumer: MethodInsnNode, unboxedPrimitive: Type) extends BoxConsumer + /** An extractor method in a Scala module, e.g., `Predef.Integer2int` */ + case class ModuleGetter(moduleLoad: AbstractInsnNode, consumer: MethodInsnNode) extends BoxConsumer + /** PUTFIELD or setter invocation */ + case class StaticSetterOrInstanceWrite(consumer: AbstractInsnNode) extends BoxConsumer + /** An unknown box consumer */ + case class EscapingConsumer(consumer: AbstractInsnNode) extends BoxConsumer +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index e543a8c3e0..ff36f36589 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -12,10 +12,13 @@ import scala.collection.mutable import scala.reflect.internal.util.Collections._ import scala.tools.asm.commons.CodeSizeEvaluator import scala.tools.asm.tree.analysis._ -import scala.tools.asm.{MethodWriter, ClassWriter, Label, Opcodes, Type} +import scala.tools.asm.{Label, Type} +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import GenBCode._ import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.analysis.InstructionStackEffect object BytecodeUtils { @@ -27,7 +30,7 @@ object BytecodeUtils { object Goto { def unapply(instruction: AbstractInsnNode): Option[JumpInsnNode] = { - if (instruction.getOpcode == Opcodes.GOTO) Some(instruction.asInstanceOf[JumpInsnNode]) + if (instruction.getOpcode == GOTO) Some(instruction.asInstanceOf[JumpInsnNode]) else None } } @@ -47,8 +50,9 @@ object BytecodeUtils { } object VarInstruction { - def unapply(instruction: AbstractInsnNode): Option[VarInsnNode] = { - if (isVarInstruction(instruction)) Some(instruction.asInstanceOf[VarInsnNode]) + def unapply(instruction: AbstractInsnNode): Option[(AbstractInsnNode, Int)] = { + if (isLoadStoreOrRet(instruction)) Some((instruction, instruction.asInstanceOf[VarInsnNode].`var`)) + else if (instruction.getOpcode == IINC) Some((instruction, instruction.asInstanceOf[IincInsnNode].`var`)) else None } @@ -57,30 +61,32 @@ object BytecodeUtils { def isJumpNonJsr(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode // JSR is deprecated in classfile version 50, disallowed in 51. historically, it was used to implement finally. - op == Opcodes.GOTO || isConditionalJump(instruction) + op == GOTO || isConditionalJump(instruction) } def isConditionalJump(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - (op >= Opcodes.IFEQ && op <= Opcodes.IF_ACMPNE) || op == Opcodes.IFNULL || op == Opcodes.IFNONNULL + (op >= IFEQ && op <= IF_ACMPNE) || op == IFNULL || op == IFNONNULL } def isReturn(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - op >= Opcodes.IRETURN && op <= Opcodes.RETURN + op >= IRETURN && op <= RETURN } def isLoad(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - op >= Opcodes.ILOAD && op <= Opcodes.ALOAD + op >= ILOAD && op <= ALOAD } def isStore(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - op >= Opcodes.ISTORE && op <= Opcodes.ASTORE + op >= ISTORE && op <= ASTORE } - def isVarInstruction(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) + def isLoadStoreOrRet(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) || instruction.getOpcode == RET + + def isLoadOrStore(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) def isExecutable(instruction: AbstractInsnNode): Boolean = instruction.getOpcode >= 0 @@ -88,29 +94,34 @@ object BytecodeUtils { methodNode.name == INSTANCE_CONSTRUCTOR_NAME || methodNode.name == CLASS_CONSTRUCTOR_NAME } - def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_STATIC) != 0 + def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_STATIC) != 0 - def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_ABSTRACT) != 0 + def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_ABSTRACT) != 0 - def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_SYNCHRONIZED) != 0 + def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_SYNCHRONIZED) != 0 - def isNativeMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_NATIVE) != 0 + def isNativeMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_NATIVE) != 0 def hasCallerSensitiveAnnotation(methodNode: MethodNode) = methodNode.visibleAnnotations != null && methodNode.visibleAnnotations.asScala.exists(_.desc == "Lsun/reflect/CallerSensitive;") - def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & Opcodes.ACC_FINAL) != 0 + def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & ACC_FINAL) != 0 - def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0 + def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (ACC_FINAL | ACC_PRIVATE | ACC_STATIC)) != 0 - def isStrictfpMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_STRICT) != 0 + def isStrictfpMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_STRICT) != 0 def isReference(t: Type) = t.getSort == Type.OBJECT || t.getSort == Type.ARRAY - def nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { - var result = instruction - do { result = result.getNext } - while (result != null && !isExecutable(result) && !alsoKeep(result)) - Option(result) + @tailrec def nextExecutableInstruction(insn: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { + val next = insn.getNext + if (next == null || isExecutable(next) || alsoKeep(next)) Option(next) + else nextExecutableInstruction(next, alsoKeep) + } + + @tailrec def nextExecutableInstructionOrLabel(insn: AbstractInsnNode): Option[AbstractInsnNode] = { + val next = insn.getNext + if (next == null || isExecutable(next) || next.isInstanceOf[LabelNode]) Option(next) + else nextExecutableInstructionOrLabel(next) } def sameTargetExecutableInstruction(a: JumpInsnNode, b: JumpInsnNode): Boolean = { @@ -124,14 +135,14 @@ object BytecodeUtils { def removeJumpAndAdjustStack(method: MethodNode, jump: JumpInsnNode) { val instructions = method.instructions val op = jump.getOpcode - if ((op >= Opcodes.IFEQ && op <= Opcodes.IFGE) || op == Opcodes.IFNULL || op == Opcodes.IFNONNULL) { + if ((op >= IFEQ && op <= IFLE) || op == IFNULL || op == IFNONNULL) { instructions.insert(jump, getPop(1)) - } else if ((op >= Opcodes.IF_ICMPEQ && op <= Opcodes.IF_ICMPLE) || op == Opcodes.IF_ACMPEQ || op == Opcodes.IF_ACMPNE) { + } else if ((op >= IF_ICMPEQ && op <= IF_ICMPLE) || op == IF_ACMPEQ || op == IF_ACMPNE) { instructions.insert(jump, getPop(1)) instructions.insert(jump, getPop(1)) } else { // we can't remove JSR: its execution does not only jump, it also adds a return address to the stack - assert(jump.getOpcode == Opcodes.GOTO) + assert(jump.getOpcode == GOTO) } instructions.remove(jump) } @@ -148,42 +159,61 @@ object BytecodeUtils { } def negateJumpOpcode(jumpOpcode: Int): Int = (jumpOpcode: @switch) match { - case Opcodes.IFEQ => Opcodes.IFNE - case Opcodes.IFNE => Opcodes.IFEQ + case IFEQ => IFNE + case IFNE => IFEQ - case Opcodes.IFLT => Opcodes.IFGE - case Opcodes.IFGE => Opcodes.IFLT + case IFLT => IFGE + case IFGE => IFLT - case Opcodes.IFGT => Opcodes.IFLE - case Opcodes.IFLE => Opcodes.IFGT + case IFGT => IFLE + case IFLE => IFGT - case Opcodes.IF_ICMPEQ => Opcodes.IF_ICMPNE - case Opcodes.IF_ICMPNE => Opcodes.IF_ICMPEQ + case IF_ICMPEQ => IF_ICMPNE + case IF_ICMPNE => IF_ICMPEQ - case Opcodes.IF_ICMPLT => Opcodes.IF_ICMPGE - case Opcodes.IF_ICMPGE => Opcodes.IF_ICMPLT + case IF_ICMPLT => IF_ICMPGE + case IF_ICMPGE => IF_ICMPLT - case Opcodes.IF_ICMPGT => Opcodes.IF_ICMPLE - case Opcodes.IF_ICMPLE => Opcodes.IF_ICMPGT + case IF_ICMPGT => IF_ICMPLE + case IF_ICMPLE => IF_ICMPGT - case Opcodes.IF_ACMPEQ => Opcodes.IF_ACMPNE - case Opcodes.IF_ACMPNE => Opcodes.IF_ACMPEQ + case IF_ACMPEQ => IF_ACMPNE + case IF_ACMPNE => IF_ACMPEQ - case Opcodes.IFNULL => Opcodes.IFNONNULL - case Opcodes.IFNONNULL => Opcodes.IFNULL + case IFNULL => IFNONNULL + case IFNONNULL => IFNULL } def isSize2LoadOrStore(opcode: Int): Boolean = (opcode: @switch) match { - case Opcodes.LLOAD | Opcodes.DLOAD | Opcodes.LSTORE | Opcodes.DSTORE => true + case LLOAD | DLOAD | LSTORE | DSTORE => true case _ => false } def getPop(size: Int): InsnNode = { - val op = if (size == 1) Opcodes.POP else Opcodes.POP2 + val op = if (size == 1) POP else POP2 new InsnNode(op) } - def instructionResultSize(instruction: AbstractInsnNode) = InstructionResultSize(instruction) + def instructionResultSize(insn: AbstractInsnNode) = InstructionStackEffect.prod(InstructionStackEffect.forClassfile(insn)) + + def loadZeroForTypeSort(sort: Int) = (sort: @switch) match { + case Type.BOOLEAN | + Type.BYTE | + Type.CHAR | + Type.SHORT | + Type.INT => new InsnNode(ICONST_0) + case Type.LONG => new InsnNode(LCONST_0) + case Type.FLOAT => new InsnNode(FCONST_0) + case Type.DOUBLE => new InsnNode(DCONST_0) + case Type.OBJECT => new InsnNode(ACONST_NULL) + } + + /** + * The number of local variable slots used for parameters and for the `this` reference. + */ + def parametersSize(methodNode: MethodNode): Int = { + (Type.getArgumentsAndReturnSizes(methodNode.desc) >> 2) - (if (isStaticMethod(methodNode)) 1 else 0) + } def labelReferences(method: MethodNode): Map[LabelNode, Set[AnyRef]] = { val res = mutable.Map.empty[LabelNode, Set[AnyRef]] @@ -311,10 +341,10 @@ object BytecodeUtils { */ def fixLoadedNothingOrNullValue(loadedType: Type, loadInstr: AbstractInsnNode, methodNode: MethodNode, bTypes: BTypes): Unit = { if (loadedType == bTypes.coreBTypes.srNothingRef.toASMType) { - methodNode.instructions.insert(loadInstr, new InsnNode(Opcodes.ATHROW)) + methodNode.instructions.insert(loadInstr, new InsnNode(ATHROW)) } else if (loadedType == bTypes.coreBTypes.srNullRef.toASMType) { - methodNode.instructions.insert(loadInstr, new InsnNode(Opcodes.ACONST_NULL)) - methodNode.instructions.insert(loadInstr, new InsnNode(Opcodes.POP)) + methodNode.instructions.insert(loadInstr, new InsnNode(ACONST_NULL)) + methodNode.instructions.insert(loadInstr, new InsnNode(POP)) } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index b192e1b46a..863bb2d10a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -103,7 +103,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { val analyzer = { if (compilerSettings.YoptNullnessTracking && AsmAnalyzer.sizeOKForNullness(methodNode)) { - Some(new AsmAnalyzer(methodNode, definingClass.internalName, new NullnessAnalyzer)) + Some(new AsmAnalyzer(methodNode, definingClass.internalName, new NullnessAnalyzer(btypes))) } else if (AsmAnalyzer.sizeOKForBasicValue(methodNode)) { Some(new AsmAnalyzer(methodNode, definingClass.internalName)) } else None @@ -461,7 +461,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { // When re-writing the closure callsite to the implMethod, we have to insert a cast. // // The check below ensures that - // (1) the implMethod type has the expected singature (captured types plus argument types + // (1) the implMethod type has the expected signature (captured types plus argument types // from instantiatedMethodType) // (2) the receiver of the implMethod matches the first captured type // (3) all parameters that are not the same in samMethodType and instantiatedMethodType diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index 4203a93f2e..f98a08a6a9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -8,10 +8,10 @@ package backend.jvm package opt import scala.annotation.switch -import scala.collection.immutable +import scala.collection.{mutable, immutable} import scala.collection.immutable.IntMap import scala.reflect.internal.util.NoPosition -import scala.tools.asm.{Type, Opcodes} +import scala.tools.asm.{Handle, Type, Opcodes} import scala.tools.asm.tree._ import scala.tools.nsc.backend.jvm.BTypes.InternalName import BytecodeUtils._ @@ -23,7 +23,9 @@ import scala.collection.convert.decorateAsScala._ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { import btypes._ import callGraph._ + import coreBTypes._ import backendUtils._ + import ClosureOptimizer._ /** * If a closure is allocated and invoked within the same method, re-write the invocation to the @@ -71,34 +73,58 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { } } - // For each closure instantiation, a list of callsites of the closure that can be re-written - // If a callsite cannot be rewritten, for example because the lambda body method is not accessible, - // a warning is returned instead. - val callsitesToRewrite: List[(ClosureInstantiation, List[Either[RewriteClosureApplyToClosureBodyFailed, (MethodInsnNode, Int)]])] = { - closureInstantiations.iterator.flatMap({ - case (methodNode, closureInits) => - // A lazy val to ensure the analysis only runs if necessary (the value is passed by name to `closureCallsites`) - // We don't need to worry about the method being too large for running an analysis: large - // methods are not added to the call graph / closureInstantiations map. - lazy val prodCons = new ProdConsAnalyzer(methodNode, closureInits.valuesIterator.next().ownerClass.internalName) - val sortedInits = immutable.TreeSet.empty ++ closureInits.values - sortedInits.iterator.map(init => (init, closureCallsites(init, prodCons))).filter(_._2.nonEmpty) - }).toList // mapping to a list (not a map) to keep the sorting + // Use collections that keep insertion order, ensures order of closure rewrites (bytecode stability) + val toRewrite = mutable.LinkedHashMap.empty[ClosureInstantiation, mutable.ArrayBuffer[(MethodInsnNode, Int)]] + def addRewrite(init: ClosureInstantiation, invocation: MethodInsnNode, stackHeight: Int) = { + val calls = toRewrite.getOrElseUpdate(init, mutable.ArrayBuffer.empty[(MethodInsnNode, Int)]) + calls += ((invocation, stackHeight)) } - // Rewrite all closure callsites (or issue inliner warnings for those that cannot be rewritten) - for ((closureInit, callsites) <- callsitesToRewrite) { - // Local variables that hold the captured values and the closure invocation arguments. - // They are lazy vals to ensure that locals for captured values are only allocated if there's - // actually a callsite to rewrite (an not only warnings to be issued). - lazy val (localsForCapturedValues, argumentLocalsList) = localsForClosureRewrite(closureInit) - for (callsite <- callsites) callsite match { - case Left(warning) => - backendReporting.inlinerWarning(warning.pos, warning.toString) - - case Right((invocation, stackHeight)) => - rewriteClosureApplyInvocation(closureInit, invocation, stackHeight, localsForCapturedValues, argumentLocalsList) - } + // For each closure instantiation find callsites of the closure and add them to the toRewrite + // buffer (cannot change a method's bytecode while still looking for further invocations to + // rewrite, the frame indices of the ProdCons analysis would get out of date). If a callsite + // cannot be rewritten, for example because the lambda body method is not accessible, issue a + // warning. The `toList` in the next line prevents modifying closureInstantiations while + // iterating it: minimalRemoveUnreachableCode (called in the loop) removes elements. + for (method <- closureInstantiations.keysIterator.toList if AsmAnalyzer.sizeOKForBasicValue(method)) closureInstantiations.get(method) match { + case Some(closureInitsBeforeDCE) if closureInitsBeforeDCE.nonEmpty => + val ownerClass = closureInitsBeforeDCE.head._2.ownerClass.internalName + + // Advanced ProdCons queries (initialProducersForValueAt) expect no unreachable code. + localOpt.minimalRemoveUnreachableCode(method, ownerClass) + + if (AsmAnalyzer.sizeOKForSourceValue(method)) closureInstantiations.get(method) match { + case Some(closureInits) => + // A lazy val to ensure the analysis only runs if necessary (the value is passed by name to `closureCallsites`) + lazy val prodCons = new ProdConsAnalyzer(method, ownerClass) + + // sorting for bytecode stability (e.g. indices of local vars created during the rewrite) + val sortedInits = immutable.TreeSet.empty ++ closureInits.values + + for (init <- sortedInits) { + val callsites = closureCallsites(init, prodCons) + if (callsites.nonEmpty) { + callsites foreach { + case Left(warning) => + backendReporting.inlinerWarning(warning.pos, warning.toString) + + case Right((invocation, stackHeight)) => + addRewrite(init, invocation, stackHeight) + } + } + } + + case _ => + } + + case _ => + } + + toRewrite foreach { + case (closureInit, invocations) => + // Local variables that hold the captured values and the closure invocation arguments. + val (localsForCapturedValues, argumentLocalsList) = localsForClosureRewrite(closureInit) + for ((invocation, stackHeight) <- invocations) rewriteClosureApplyInvocation(closureInit, invocation, stackHeight, localsForCapturedValues, argumentLocalsList) } } @@ -118,20 +144,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val argTypes = closureInit.lambdaMetaFactoryCall.samMethodType.getArgumentTypes val firstArgLocal = ownerMethod.maxLocals - // The comment in the unapply method of `LambdaMetaFactoryCall` explains why we have to introduce - // casts for arguments that have different types in samMethodType and instantiatedMethodType. - val castLoadTypes = { - val instantiatedMethodType = closureInit.lambdaMetaFactoryCall.instantiatedMethodType - (argTypes, instantiatedMethodType.getArgumentTypes).zipped map { - case (samArgType, instantiatedArgType) if samArgType != instantiatedArgType => - // the LambdaMetaFactoryCall extractor ensures that the two types are reference types, - // so we don't end up casting primitive values. - Some(instantiatedArgType) - case _ => - None - } - } - val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes, castLoadTypes) + val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes) ownerMethod.maxLocals = firstArgLocal + argLocals.size (captureLocals, argLocals) @@ -169,6 +182,28 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { }).toList } + /** + * Check whether `invocation` invokes the SAM of the IndyLambda `closureInit`. + * + * In addition to a perfect match, we also identify cases where a generic FunctionN is created + * but the invocation is to a specialized variant apply$sp... Vice-versa, we also allow the + * case where a specialized FunctionN$sp.. is created but the generic apply is invoked. In + * these cases, the translation will introduce the necessary box / unbox invocations. Example: + * + * val f: Int => Any = (x: Int) => 1 + * f(10) + * + * The IndyLambda creates a specialized `JFunction1$mcII$sp`, whose SAM is `apply$mcII$sp(I)I`. + * The invocation calls `apply(Object)Object`: the method name and type don't match. + * We identify these cases, insert the necessary unbox operation for the arguments, and invoke + * the `$anonfun(I)I` method. + * + * Tests in InlinerTest.optimizeSpecializedClosures. In that test, methods t4/t4a/t5/t8 show + * examples where the parameters have to be unboxed because generic `apply` is called, but the + * lambda body method takes primitive types. + * The opposite case is in t9: a the specialized `apply$sp..` is invoked, but the lambda body + * method takes boxed arguments, so we have to insert boxing operations. + */ private def isSamInvocation(invocation: MethodInsnNode, closureInit: ClosureInstantiation, prodCons: => ProdConsAnalyzer): Boolean = { val indy = closureInit.lambdaMetaFactoryCall.indy if (invocation.getOpcode == INVOKESTATIC) false @@ -183,11 +218,85 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { receiverProducers.size == 1 && receiverProducers.head == indy } - invocation.name == indy.name && { - val indySamMethodDesc = closureInit.lambdaMetaFactoryCall.samMethodType.getDescriptor - indySamMethodDesc == invocation.desc - } && - closureIsReceiver // most expensive check last + def isSpecializedVersion(specName: String, nonSpecName: String) = specName.startsWith(nonSpecName) && specializationSuffix.pattern.matcher(specName.substring(nonSpecName.length)).matches + + def sameOrSpecializedType(specTp: Type, nonSpecTp: Type) = { + specTp == nonSpecTp || { + val specDesc = specTp.getDescriptor + val nonSpecDesc = nonSpecTp.getDescriptor + specDesc.length == 1 && primitives.contains(specDesc) && nonSpecDesc == ObjectRef.descriptor + } + } + + def specializedDescMatches(specMethodDesc: String, nonSpecMethodDesc: String) = { + val specArgs = Type.getArgumentTypes(specMethodDesc) + val nonSpecArgs = Type.getArgumentTypes(nonSpecMethodDesc) + specArgs.corresponds(nonSpecArgs)(sameOrSpecializedType) && sameOrSpecializedType(Type.getReturnType(specMethodDesc), Type.getReturnType(nonSpecMethodDesc)) + } + + def nameAndDescMatch = { + val aName = invocation.name + val bName = indy.name + val aDesc = invocation.desc + val bDesc = closureInit.lambdaMetaFactoryCall.samMethodType.getDescriptor + if (aName == bName) aDesc == bDesc + else if (isSpecializedVersion(aName, bName)) specializedDescMatches(aDesc, bDesc) + else if (isSpecializedVersion(bName, aName)) specializedDescMatches(bDesc, aDesc) + else false + } + + nameAndDescMatch && closureIsReceiver // most expensive check last + } + } + + private def isPrimitiveType(asmType: Type) = { + val sort = asmType.getSort + Type.VOID <= sort && sort <= Type.DOUBLE + } + + /** + * The argument types of the lambda body method may differ in two ways from the argument types of + * the closure member method that is invoked (and replaced by a call to the body). + * - The lambda body method may have more specific types than the invoked closure member, see + * comment in [[LambdaMetaFactoryCall.unapply]]. + * - The invoked closure member might be a specialized variant of the SAM or vice-versa, see + * comment method [[isSamInvocation]]. + */ + private def adaptStoredArguments(closureInit: ClosureInstantiation, invocation: MethodInsnNode): Int => Option[AbstractInsnNode] = { + val invokeDesc = invocation.desc + // The lambda body method has additional parameters for captured values. Here we need to consider + // only those parameters of the body method that correspond to lambda parameters. This happens + // to be exactly LMF.instantiatedMethodType. In fact, `LambdaMetaFactoryCall.unapply` ensures + // that the body method signature is exactly (capturedParams + instantiatedMethodType). + val lambdaBodyMethodDescWithoutCaptures = closureInit.lambdaMetaFactoryCall.instantiatedMethodType.getDescriptor + if (invokeDesc == lambdaBodyMethodDescWithoutCaptures) { + _ => None + } else { + val invokeArgTypes = Type.getArgumentTypes(invokeDesc) + val implMethodArgTypes = Type.getArgumentTypes(lambdaBodyMethodDescWithoutCaptures) + val res = new Array[Option[AbstractInsnNode]](invokeArgTypes.length) + for (i <- invokeArgTypes.indices) { + if (invokeArgTypes(i) == implMethodArgTypes(i)) { + res(i) = None + } else if (isPrimitiveType(implMethodArgTypes(i)) && invokeArgTypes(i).getDescriptor == ObjectRef.descriptor) { + res(i) = Some(getScalaUnbox(implMethodArgTypes(i))) + } else if (isPrimitiveType(invokeArgTypes(i)) && implMethodArgTypes(i).getDescriptor == ObjectRef.descriptor) { + res(i) = Some(getScalaBox(invokeArgTypes(i))) + } else { + assert(!isPrimitiveType(invokeArgTypes(i)), invokeArgTypes(i)) + assert(!isPrimitiveType(implMethodArgTypes(i)), implMethodArgTypes(i)) + // The comment in the unapply method of `LambdaMetaFactoryCall` explains why we have to introduce + // casts for arguments that have different types in samMethodType and instantiatedMethodType. + // + // Note: + // - invokeArgTypes is the same as the argument types in the IndyLambda's samMethodType, + // this is ensured by the `isSamInvocation` filter in this file + // - implMethodArgTypes is the same as the arg types in the IndyLambda's instantiatedMethodType, + // this is ensured by the unapply method in LambdaMetaFactoryCall (file CallGraph) + res(i) = Some(new TypeInsnNode(CHECKCAST, implMethodArgTypes(i).getInternalName)) + } + } + res } } @@ -196,7 +305,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val lambdaBodyHandle = closureInit.lambdaMetaFactoryCall.implMethod // store arguments - insertStoreOps(invocation, ownerMethod, argumentLocalsList) + insertStoreOps(invocation, ownerMethod, argumentLocalsList, adaptStoredArguments(closureInit, invocation)) // drop the closure from the stack ownerMethod.instructions.insertBefore(invocation, new InsnNode(POP)) @@ -228,8 +337,22 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val bodyInvocation = new MethodInsnNode(bodyOpcode, lambdaBodyHandle.getOwner, lambdaBodyHandle.getName, lambdaBodyHandle.getDesc, isInterface) ownerMethod.instructions.insertBefore(invocation, bodyInvocation) - val returnType = Type.getReturnType(lambdaBodyHandle.getDesc) - fixLoadedNothingOrNullValue(returnType, bodyInvocation, ownerMethod, btypes) // see comment of that method + val bodyReturnType = Type.getReturnType(lambdaBodyHandle.getDesc) + val invocationReturnType = Type.getReturnType(invocation.desc) + if (isPrimitiveType(invocationReturnType) && bodyReturnType.getDescriptor == ObjectRef.descriptor) { + val op = + if (invocationReturnType.getSort == Type.VOID) getPop(1) + else getScalaUnbox(invocationReturnType) + ownerMethod.instructions.insertBefore(invocation, op) + } else if (isPrimitiveType(bodyReturnType) && invocationReturnType.getDescriptor == ObjectRef.descriptor) { + val op = + if (bodyReturnType.getSort == Type.VOID) getBoxedUnit + else getScalaBox(bodyReturnType) + ownerMethod.instructions.insertBefore(invocation, op) + } else { + // see comment of that method + fixLoadedNothingOrNullValue(bodyReturnType, bodyInvocation, ownerMethod, btypes) + } ownerMethod.instructions.remove(invocation) @@ -277,6 +400,9 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { // Rewriting a closure invocation may render code unreachable. For example, the body method of // (x: T) => ??? has return type Nothing$, and an ATHROW is added (see fixLoadedNothingOrNullValue). unreachableCodeEliminated -= ownerMethod + + if (hasAdaptedImplMethod(closureInit) && inliner.canInlineBody(bodyMethodCallsite).isEmpty) + inliner.inlineCallsite(bodyMethodCallsite) } /** @@ -293,13 +419,10 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { // local. On the other hand, further optimizations (copy propagation, remove unused locals) will // clean it up. - // Captured variables don't need to be cast when loaded at the callsite (castLoadTypes are None). - // This is checked in `isClosureInstantiation`: the types of the captured variables in the indy - // instruction match exactly the corresponding parameter types in the body method. - val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes, castLoadTypes = _ => None) + val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes) closureInit.ownerMethod.maxLocals = firstCaptureLocal + localsForCaptures.size - insertStoreOps(indy, closureInit.ownerMethod, localsForCaptures) + insertStoreOps(indy, closureInit.ownerMethod, localsForCaptures, _ => None) insertLoadOps(indy, closureInit.ownerMethod, localsForCaptures) localsForCaptures @@ -311,8 +434,16 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * * The lowest stack value is stored in the head of the locals list, so the last local is stored first. */ - private def insertStoreOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList) = - insertLocalValueOps(before, methodNode, localsList, store = true) + private def insertStoreOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList, beforeStore: Int => Option[AbstractInsnNode]) = { + // The first instruction needs to store into the last local of the `localsList`. + // To avoid reversing the list, we use `insert(previous)`. + val previous = before.getPrevious + def ins(op: AbstractInsnNode) = methodNode.instructions.insert(previous, op) + for ((l, i) <- localsList.locals.zipWithIndex) { + ins(new VarInsnNode(l.storeOpcode, l.local)) + beforeStore(i) foreach ins + } + } /** * Insert load operations in front of the `before` instruction to copy the local values denoted @@ -320,20 +451,10 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * * The head of the locals list will be the lowest value on the stack, so the first local is loaded first. */ - private def insertLoadOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList) = - insertLocalValueOps(before, methodNode, localsList, store = false) - - private def insertLocalValueOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList, store: Boolean): Unit = { - // If `store` is true, the first instruction needs to store into the last local of the `localsList`. - // Load instructions on the other hand are emitted in the order of the list. - // To avoid reversing the list, we use `insert(previousInstr)` for stores and `insertBefore(before)` for loads. - lazy val previous = before.getPrevious + private def insertLoadOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList) = { for (l <- localsList.locals) { - val varOp = new VarInsnNode(if (store) l.storeOpcode else l.loadOpcode, l.local) - if (store) methodNode.instructions.insert(previous, varOp) - else methodNode.instructions.insertBefore(before, varOp) - if (!store) for (castType <- l.castLoadedValue) - methodNode.instructions.insert(varOp, new TypeInsnNode(CHECKCAST, castType.getInternalName)) + val op = new VarInsnNode(l.loadOpcode, l.local) + methodNode.instructions.insertBefore(before, op) } } @@ -355,12 +476,12 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * Local(6, refOpOffset) :: * Nil */ - def fromTypes(firstLocal: Int, types: Array[Type], castLoadTypes: Int => Option[Type]): LocalsList = { + def fromTypes(firstLocal: Int, types: Array[Type]): LocalsList = { var sizeTwoOffset = 0 val locals: List[Local] = types.indices.map(i => { // The ASM method `type.getOpcode` returns the opcode for operating on a value of `type`. val offset = types(i).getOpcode(ILOAD) - ILOAD - val local = Local(firstLocal + i + sizeTwoOffset, offset, castLoadTypes(i)) + val local = Local(firstLocal + i + sizeTwoOffset, offset) if (local.size == 2) sizeTwoOffset += 1 local })(collection.breakOut) @@ -374,10 +495,15 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * The xLOAD / xSTORE opcodes are in the following sequence: I, L, F, D, A, so the offset for * a local variable holding a reference (`A`) is 4. See also method `getOpcode` in [[scala.tools.asm.Type]]. */ - case class Local(local: Int, opcodeOffset: Int, castLoadedValue: Option[Type]) { + case class Local(local: Int, opcodeOffset: Int) { def size = if (loadOpcode == LLOAD || loadOpcode == DLOAD) 2 else 1 def loadOpcode = ILOAD + opcodeOffset def storeOpcode = ISTORE + opcodeOffset } } + +object ClosureOptimizer { + val primitives = "BSIJCFDZV" + val specializationSuffix = s"(\\$$mc[$primitives]+\\$$sp)".r +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala new file mode 100644 index 0000000000..f91530903d --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala @@ -0,0 +1,641 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.annotation.{switch, tailrec} +import scala.tools.asm.tree.analysis.BasicInterpreter +import scala.tools.asm.Type +import scala.tools.asm.Opcodes._ +import scala.tools.asm.tree._ +import scala.collection.mutable +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ + +class CopyProp[BT <: BTypes](val btypes: BT) { + import btypes._ + import backendUtils._ + + + /** + * For every `xLOAD n`, find all local variable slots that are aliases of `n` using an + * AliasingAnalyzer and change the instruction to `xLOAD m` where `m` is the smallest alias. + * This leaves behind potentially stale `xSTORE n` instructions, which are then eliminated + * by [[eliminateStaleStores]]. + */ + def copyPropagation(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForAliasing(method) && { + var changed = false + val numParams = parametersSize(method) + lazy val aliasAnalysis = new AsmAnalyzer(method, owner, new AliasingAnalyzer(new BasicInterpreter)) + + // Remember locals that are used in a `LOAD` instruction. Assume a program has two LOADs: + // + // ... + // LOAD 3 // aliases of 3 here: <3> + // ... + // LOAD 1 // aliases of 1 here: <1, 3> + // + // In this example, we should change the second load from 1 to 3, which might render the + // local variable 1 unused. + val knownUsed = new Array[Boolean](method.maxLocals) + + def usedOrMinAlias(it: IntIterator, init: Int): Int = { + if (knownUsed(init)) init + else { + var r = init + while (it.hasNext) { + val n = it.next() + // knownUsed.lenght is the number of locals, `n` may be a stack slot + if (n < knownUsed.length && knownUsed(n)) return n + if (n < r) r = n + } + r + } + } + + val it = method.instructions.iterator + while (it.hasNext) it.next() match { + case vi: VarInsnNode if vi.`var` >= numParams && isLoad(vi) => + val aliases = aliasAnalysis.frameAt(vi).asInstanceOf[AliasingFrame[_]].aliasesOf(vi.`var`) + if (aliases.size > 1) { + val alias = usedOrMinAlias(aliases.iterator, vi.`var`) + if (alias != -1) { + changed = true + vi.`var` = alias + } + } + knownUsed(vi.`var`) = true + + case _ => + } + + changed + } + } + + /** + * Eliminate `xSTORE` instructions that have no consumer. If the instruction can be completely + * eliminated, it is replaced by a POP. The [[eliminatePushPop]] cleans up unnecessary POPs. + * + * Note that an `ASOTRE` can not always be eliminated: it removes a reference to the object that + * is currently stored in that local, which potentially frees it for GC (SI-5313). Therefore + * we replace such stores by `POP; ACONST_NULL; ASTORE x`. + */ + def eliminateStaleStores(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + lazy val prodCons = new ProdConsAnalyzer(method, owner) + def hasNoCons(varIns: AbstractInsnNode, slot: Int) = prodCons.consumersOfValueAt(varIns.getNext, slot).isEmpty + + // insns to delete: IINC that have no consumer + val toDelete = mutable.ArrayBuffer.empty[IincInsnNode] + + // xSTORE insns to be replaced by POP or POP2 + val storesToDrop = mutable.ArrayBuffer.empty[VarInsnNode] + + // ASTORE insn that have no consumer. + // - if the local is not live, the store is replaced by POP + // - otherwise, pop the argument value and store NULL instead. Unless the boolean field is + // `true`: then the store argument is already known to be ACONST_NULL. + val toNullOut = mutable.ArrayBuffer.empty[(VarInsnNode, Boolean)] + + // `true` for variables that are known to be live + val liveVars = new Array[Boolean](method.maxLocals) + + val it = method.instructions.iterator + while (it.hasNext) it.next() match { + case vi: VarInsnNode if isStore(vi) && hasNoCons(vi, vi.`var`) => + val canElim = vi.getOpcode != ASTORE || { + val currentFieldValueProds = prodCons.initialProducersForValueAt(vi, vi.`var`) + currentFieldValueProds.size == 1 && (currentFieldValueProds.head match { + case ParameterProducer(0) => !isStaticMethod(method) // current field value is `this`, which won't be gc'd anyway + case _: UninitializedLocalProducer => true // field is not yet initialized, so current value cannot leak + case _ => false + }) + } + if (canElim) storesToDrop += vi + else { + val prods = prodCons.producersForValueAt(vi, prodCons.frameAt(vi).stackTop) + val isStoreNull = prods.size == 1 && prods.head.getOpcode == ACONST_NULL + toNullOut += ((vi, isStoreNull)) + } + + case ii: IincInsnNode if hasNoCons(ii, ii.`var`) => + toDelete += ii + + case vi: VarInsnNode => + liveVars(vi.`var`) = true + + case ii: IincInsnNode => + liveVars(ii.`var`) = true + + case _ => + } + + def replaceByPop(vi: VarInsnNode): Unit = { + val size = if (isSize2LoadOrStore(vi.getOpcode)) 2 else 1 + method.instructions.set(vi, getPop(size)) + } + + toDelete foreach method.instructions.remove + + storesToDrop foreach replaceByPop + + for ((vi, isStoreNull) <- toNullOut) { + if (!liveVars(vi.`var`)) replaceByPop(vi) // can drop `ASTORE x` where x has only dead stores + else { + if (!isStoreNull) { + val prev = vi.getPrevious + method.instructions.insert(prev, new InsnNode(ACONST_NULL)) + method.instructions.insert(prev, getPop(1)) + } + } + } + + toDelete.nonEmpty || storesToDrop.nonEmpty || toNullOut.nonEmpty + } + } + + /** + * When a POP instruction has a single producer, remove the POP and eliminate the producer by + * bubbling up the POPs. For example, given + * ILOAD 1; ILOAD 2; IADD; POP + * we first eliminate the POP, then the IADD, then its inputs, so the entire sequence goes away. + * If a producer cannot be eliminated (need to keep side-effects), a POP is inserted. + * + * A special case eliminates the creation of unused objects with side-effect-free constructors: + * NEW scala/Tuple1; DUP; ALOAD 0; INVOKESPECIAL scala/Tuple1.<init>; POP + * The POP has a signle producer (the DUP), it's easy to eliminate these two. A special case + * is needed to eliminate the INVOKESPECIAL and NEW. + */ + def eliminatePushPop(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + // A queue of instructions producing a value that has to be eliminated. If possible, the + // instruction (and its inputs) will be removed, otherwise a POP is inserted after + val queue = mutable.Queue.empty[ProducedValue] + // Contains constructor invocations for values that can be eliminated if unused. + val sideEffectFreeConstructorCalls = mutable.ArrayBuffer.empty[MethodInsnNode] + + // instructions to remove (we don't change the bytecode while analyzing it. this allows + // running the ProdConsAnalyzer only once.) + val toRemove = mutable.Set.empty[AbstractInsnNode] + // instructions to insert before some instruction + val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[InsnNode]] + // an instruction to insert after some instruction + val toInsertAfter = mutable.Map.empty[AbstractInsnNode, AbstractInsnNode] + + lazy val prodCons = new ProdConsAnalyzer(method, owner) + + /** + * Returns the producers for the stack value `inputSlot` consumed by `cons`, if the consumer + * instruction is the only consumer for all of these producers. + * + * If a producer has multiple consumers, or the value is the caught exception in a catch + * block, this method returns Set.empty. + */ + def producersIfSingleConsumer(cons: AbstractInsnNode, inputSlot: Int): Set[AbstractInsnNode] = { + /** + * True if the values produced by `prod` are all the same. Most instructions produce a single + * value. DUP and DUP2 (with a size-2 input) produce two equivalent values. However, there + * are some exotic instructions that produce multiple non-equal values (DUP_X1, SWAP, ...). + * + * Assume we have `DUP_X2; POP`. In order to remove the `POP` we need to change the DUP_X2 + * into something else, which is not straightforward. + * + * Since scalac never emits any of those exotic bytecodes, we don't optimize them. + */ + def producerHasSingleOutput(prod: AbstractInsnNode): Boolean = prod match { + case _: ExceptionProducer[_] | _: UninitializedLocalProducer => + // POP of an exception in a catch block cannot be removed. For an uninitialized local, + // there should not be a consumer. We are conservative and include it here, so the + // producer would not be removed. + false + + case _: ParameterProducer => + true + + case _ => (prod.getOpcode: @switch) match { + case DUP => true + case DUP2 => prodCons.frameAt(prod).peekStack(0).getSize == 2 + case _ => InstructionStackEffect.prod(InstructionStackEffect.forAsmAnalysis(prod, prodCons.frameAt(prod))) == 1 + } + } + + val prods = prodCons.producersForValueAt(cons, inputSlot) + val singleConsumer = prods forall { prod => + producerHasSingleOutput(prod) && { + // for DUP / DUP2, we only consider the value that is actually consumed by cons + val conss = prodCons.consumersOfValueAt(prod.getNext, inputSlot) + conss.size == 1 && conss.head == cons + } + } + if (singleConsumer) prods else Set.empty + } + + /** + * For a POP instruction that is the single consumer of its producers, remove the POP and + * enqueue the producers. + */ + def handleInitialPop(pop: AbstractInsnNode): Unit = { + val prods = producersIfSingleConsumer(pop, prodCons.frameAt(pop).stackTop) + if (prods.nonEmpty) { + toRemove += pop + val size = if (pop.getOpcode == POP2) 2 else 1 + queue ++= prods.map(ProducedValue(_, size)) + } + } + + /** + * Traverse the method in its initial state and collect all POP instructions and side-effect + * free constructor invocations that can be eliminated. + */ + def collectInitialPopsAndPureConstrs(): Unit = { + val it = method.instructions.iterator + while (it.hasNext) { + val insn = it.next() + (insn.getOpcode: @switch) match { + case POP | POP2 => + handleInitialPop(insn) + + case INVOKESPECIAL => + val mi = insn.asInstanceOf[MethodInsnNode] + if (isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi + + case _ => + } + } + } + + /** + * Eliminate the `numArgs` inputs of the instruction `prod` (which was eliminated). Fo + * each input value + * - if the `prod` instruction is the single consumer, enqueue the producers of the input + * - otherwise, insert a POP instruction to POP the input value + */ + def handleInputs(prod: AbstractInsnNode, numArgs: Int): Unit = { + val frame = prodCons.frameAt(prod) + val pops = mutable.ListBuffer.empty[InsnNode] + @tailrec def handle(stackOffset: Int): Unit = { + if (stackOffset >= 0) { + val prods = producersIfSingleConsumer(prod, frame.stackTop - stackOffset) + val nSize = frame.peekStack(stackOffset).getSize + if (prods.isEmpty) pops append getPop(nSize) + else queue ++= prods.map(ProducedValue(_, nSize)) + handle(stackOffset - 1) + } + } + handle(numArgs - 1) // handle stack offsets (numArgs - 1) to 0 + if (pops.nonEmpty) toInsertBefore(prod) = pops.toList + } + + /** + * Eliminate the closure value produced by `indy`. If the SAM type is known to construct + * without side-effects (e.g. scala/runtime/java8/JFunctionN), the `indy` and its inputs + * are eliminated, otherwise a POP is inserted. + */ + def handleClosureInst(indy: InvokeDynamicInsnNode): Unit = { + if (isrJFunctionType(Type.getReturnType(indy.desc).getInternalName)) { + toRemove += indy + callGraph.removeClosureInstantiation(indy, method) + handleInputs(indy, Type.getArgumentTypes(indy.desc).length) + } else { + toInsertAfter(indy) = getPop(1) + } + } + + def runQueue(): Unit = while (queue.nonEmpty) { + val ProducedValue(prod, size) = queue.dequeue() + + def prodString = s"Producer ${AsmUtils textify prod}@${method.instructions.indexOf(prod)}\n${AsmUtils textify method}" + def popAfterProd(): Unit = toInsertAfter(prod) = getPop(size) + + (prod.getOpcode: @switch) match { + case ACONST_NULL | ICONST_M1 | ICONST_0 | ICONST_1 | ICONST_2 | ICONST_3 | ICONST_4 | ICONST_5 | LCONST_0 | LCONST_1 | FCONST_0 | FCONST_1 | FCONST_2 | DCONST_0 | DCONST_1 | + BIPUSH | SIPUSH | ILOAD | LLOAD | FLOAD | DLOAD | ALOAD=> + toRemove += prod + + case opc @ (DUP | DUP2) => + assert(opc != 2 || size == 2, s"DUP2 for two size-1 values; $prodString") // ensured in method `producerHasSingleOutput` + if (toRemove(prod)) + // the DUP is already scheduled for removal because one of its consumers is a POP. + // now the second consumer is also a POP, so we need to eliminate the DUP's input. + handleInputs(prod, 1) + else + toRemove += prod + + case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => + // these are excluded in method `producerHasSingleOutput` + assert(false, s"Cannot eliminate value pushed by an instruction with multiple output values; $prodString") + + case IDIV | LDIV | IREM | LREM => + popAfterProd() // keep potential division by zero + + case IADD | LADD | FADD | DADD | ISUB | LSUB | FSUB | DSUB | IMUL | LMUL | FMUL | DMUL | FDIV | DDIV | FREM | DREM | + LSHL | LSHR | LUSHR | + IAND | IOR | IXOR | LAND | LOR | LXOR | + LCMP | FCMPL | FCMPG | DCMPL | DCMPG => + toRemove += prod + handleInputs(prod, 2) + + case INEG | LNEG | FNEG | DNEG | + I2L | I2F | I2D | L2I | L2F | L2D | F2I | F2L | F2D | D2I | D2L | D2F | I2B | I2C | I2S => + toRemove += prod + handleInputs(prod, 1) + + case GETFIELD | GETSTATIC => + // TODO eliminate side-effect free module loads (https://github.com/scala/scala-dev/issues/16) + if (isBoxedUnit(prod)) toRemove += prod + else popAfterProd() // keep potential class initialization (static field) or NPE (instance field) + + case INVOKEVIRTUAL | INVOKESPECIAL | INVOKESTATIC | INVOKEINTERFACE => + val methodInsn = prod.asInstanceOf[MethodInsnNode] + if (isSideEffectFreeCall(methodInsn)) { + toRemove += prod + callGraph.removeCallsite(methodInsn, method) + val receiver = if (methodInsn.getOpcode == INVOKESTATIC) 0 else 1 + handleInputs(prod, Type.getArgumentTypes(methodInsn.desc).length + receiver) + } else + popAfterProd() + + case INVOKEDYNAMIC => + prod match { + case callGraph.LambdaMetaFactoryCall(indy, _, _, _) => handleClosureInst(indy) + case _ => popAfterProd() + } + + case NEW => + if (isNewForSideEffectFreeConstructor(prod)) toRemove += prod + else popAfterProd() + + case LDC => prod.asInstanceOf[LdcInsnNode].cst match { + case _: java.lang.Integer | _: java.lang.Float | _: java.lang.Long | _: java.lang.Double | _: String => + toRemove += prod + + case _ => + // don't remove class literals, method types, method handles: keep a potential NoClassDefFoundError + popAfterProd() + } + + case MULTIANEWARRAY => + toRemove += prod + handleInputs(prod, prod.asInstanceOf[MultiANewArrayInsnNode].dims) + + case _ => + popAfterProd() + } + } + + // there are two cases when we can eliminate a constructor call: + // - NEW T; INVOKESPECIAL T.<init> -- there's no DUP, the new object is consumed only by the constructor) + // - NEW T; DUP; INVOKESPECIAL T.<init>, where the DUP will be removed + def eliminateUnusedPureConstructorCalls(): Boolean = { + var changed = false + + def removeConstructorCall(mi: MethodInsnNode): Unit = { + toRemove += mi + callGraph.removeCallsite(mi, method) + sideEffectFreeConstructorCalls -= mi + changed = true + } + + for (mi <- sideEffectFreeConstructorCalls.toList) { // toList to allow removing elements while traversing + val frame = prodCons.frameAt(mi) + val stackTop = frame.stackTop + val numArgs = Type.getArgumentTypes(mi.desc).length + val receiverProds = producersIfSingleConsumer(mi, stackTop - numArgs) + if (receiverProds.size == 1) { + val receiverProd = receiverProds.head + if (receiverProd.getOpcode == NEW) { + removeConstructorCall(mi) + handleInputs(mi, numArgs + 1) // removes the producers of args and receiver + } else if (receiverProd.getOpcode == DUP && toRemove.contains(receiverProd)) { + val dupProds = producersIfSingleConsumer(receiverProd, prodCons.frameAt(receiverProd).stackTop) + if (dupProds.size == 1 && dupProds.head.getOpcode == NEW) { + removeConstructorCall(mi) + handleInputs(mi, numArgs) // removes the producers of args. the producer of the receiver is DUP and already in toRemove. + queue += ProducedValue(dupProds.head, 1) // removes the NEW (which is NOT the producer of the receiver!) + } + } + } + } + changed + } + + collectInitialPopsAndPureConstrs() + + // eliminating producers enables eliminating unused constructor calls (when a DUP gets removed). + // vice-versa, eliminating a constructor call adds producers of constructor parameters to the queue. + // so the two run in a loop. + runQueue() + while (eliminateUnusedPureConstructorCalls()) + runQueue() + + var changed = false + toInsertAfter foreach { + case (target, insn) => + nextExecutableInstructionOrLabel(target) match { + // `insn` is of type `InsnNode`, so we only need to check the Opcode when comparing to another instruction + case Some(next) if next.getOpcode == insn.getOpcode && toRemove(next) => + // Inserting and removing a POP at the same place should not enable `changed`. This happens + // when a POP directly follows a producer that cannot be eliminated, e.g. INVOKESTATIC A.m ()I; POP + // The POP is initially added to `toRemove`, and the `INVOKESTATIC` producer is added to the queue. + // Because the producer cannot be elided, a POP is added to `toInsertAfter`. + toRemove -= next + + case _ => + changed = true + method.instructions.insert(target, insn) + } + } + toInsertBefore foreach { + case (target, insns) => + changed = true + insns.foreach(method.instructions.insertBefore(target, _)) + } + toRemove foreach { insn => + changed = true + method.instructions.remove(insn) + } + changed + } + } + + case class ProducedValue(producer: AbstractInsnNode, size: Int) { + override def toString = s"<${AsmUtils textify producer}>" + } + + /** + * Remove `xSTORE n; xLOAD n` paris if + * - the local variable n is not used anywhere else in the method (1), and + * - there are no executable instructions and no live labels (jump targets) between the two (2) + * + * Note: store-load pairs that cannot be eliminated could be replaced by `DUP; xSTORE n`, but + * that's just cosmetic and doesn't help for anything. + * + * (1) This could be made more precise by running a prodCons analysis and checking that the load + * is the only user of the store. Then we could eliminate the pair even if the variable is live + * (except for ASTORE, SI-5313). Not needing an analyzer is more efficient, and catches most + * cases. + * + * (2) The implementation uses a conservative estimation for liveness (if some instruction uses + * local n, then n is considered live in the entire method). In return, it doesn't need to run an + * Analyzer on the method, making it more efficient. + * + * This method also removes `ACONST_NULL; ASTORE n` if the local n is not live. This pattern is + * introduced by [[eliminateStaleStores]]. + * + * The implementation is a little tricky to support the following case: + * ISTORE 1; ISTORE 2; ILOAD 2; ACONST_NULL; ASTORE 3; ILOAD 1 + * The outer store-load pair can be removed if two the inner pairs can be. + */ + def eliminateStoreLoad(method: MethodNode): Boolean = { + val removePairs = mutable.Set.empty[RemovePair] + val liveVars = new Array[Boolean](method.maxLocals) + val liveLabels = mutable.Set.empty[LabelNode] + + def mkRemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]): RemovePair = { + val r = RemovePair(store, other, depends) + removePairs += r + r + } + + def registerLiveVarsLabels(insn: AbstractInsnNode): Unit = insn match { + case vi: VarInsnNode => liveVars(vi.`var`) = true + case ii: IincInsnNode => liveVars(ii.`var`) = true + case j: JumpInsnNode => liveLabels += j.label + case s: TableSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala + case s: LookupSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala + case _ => + } + + val pairStartStack = new mutable.Stack[(AbstractInsnNode, mutable.ListBuffer[RemovePairDependency])] + + def push(insn: AbstractInsnNode) = { + pairStartStack push ((insn, mutable.ListBuffer.empty)) + } + + def addDepends(dependency: RemovePairDependency) = if (pairStartStack.nonEmpty) { + val (_, depends) = pairStartStack.top + depends += dependency + } + + def completesStackTop(load: AbstractInsnNode) = isLoad(load) && pairStartStack.nonEmpty && { + pairStartStack.top match { + case (store: VarInsnNode, _) => store.`var` == load.asInstanceOf[VarInsnNode].`var` + case _ => false + } + } + + /** + * Try to pair `insn` with its correspondant on the stack + * - if the stack top is a store and `insn` is a corresponding load, create a pair + * - otherwise, check the two top stack values for `null; store`. if it matches, create + * a pair and continue pairing `insn` on the remaining stack + * - otherwise, empty the stack and mark the local variables in it live + */ + def tryToPairInstruction(insn: AbstractInsnNode): Unit = { + @tailrec def emptyStack(): Unit = if (pairStartStack.nonEmpty) { + registerLiveVarsLabels(pairStartStack.pop()._1) + emptyStack() + } + + @tailrec def tryPairing(): Unit = { + if (completesStackTop(insn)) { + val (store: VarInsnNode, depends) = pairStartStack.pop() + addDepends(mkRemovePair(store, insn, depends.toList)) + } else if (pairStartStack.nonEmpty) { + val (top, topDepends) = pairStartStack.pop() + if (pairStartStack.nonEmpty) { + (pairStartStack.top, top) match { + case ((ldNull: InsnNode, depends), store: VarInsnNode) if ldNull.getOpcode == ACONST_NULL && store.getOpcode == ASTORE => + pairStartStack.pop() + addDepends(mkRemovePair(store, ldNull, depends.toList)) + // example: store; (null; store;) (store; load;) load + // s1^ ^^^^^p1^^^^^ // p1 is added to s1's depends + // then: store; (null; store;) load + // s2^ ^^^^p2^^^^^ // p1 and p2 are added to s2's depends + topDepends foreach addDepends + tryPairing() + + case _ => + // empty the stack - a non-matching insn was found, cannot create any pairs to remove + registerLiveVarsLabels(insn) + registerLiveVarsLabels(top) + emptyStack() + } + } else { + // stack only has one element + registerLiveVarsLabels(insn) + registerLiveVarsLabels(top) + } + } else { + // stack is empty already + registerLiveVarsLabels(insn) + } + } + + tryPairing() + } + + + var insn = method.instructions.getFirst + + @tailrec def advanceToNextExecutableOrLabel(): Unit = { + insn = insn.getNext + if (insn != null && !isExecutable(insn) && !insn.isInstanceOf[LabelNode]) advanceToNextExecutableOrLabel() + } + + while (insn != null) { + insn match { + case _ if insn.getOpcode == ACONST_NULL => push(insn) + case vi: VarInsnNode if isStore(vi) => push(insn) + case label: LabelNode if pairStartStack.nonEmpty => addDepends(LabelNotLive(label)) + case _ => tryToPairInstruction(insn) + } + advanceToNextExecutableOrLabel() + } + + // elide RemovePairs that depend on live labels or other RemovePair that have to be elided. + // example: store 1; store 2; label x; load 2; load 1 + // if x is live, the inner pair has to be elided, causing the outer pair to be elided too. + + var doneEliding = false + + def elide(removePair: RemovePair) = { + doneEliding = false + liveVars(removePair.store.`var`) = true + removePairs -= removePair + } + + while (!doneEliding) { + doneEliding = true + for (removePair <- removePairs.toList) { + val slot = removePair.store.`var` + if (liveVars(slot)) elide(removePair) + else removePair.depends foreach { + case LabelNotLive(label) => if (liveLabels(label)) elide(removePair) + case other: RemovePair => if (!removePairs(other)) elide(removePair) + } + } + } + + for (removePair <- removePairs) { + method.instructions.remove(removePair.store) + method.instructions.remove(removePair.other) + } + + removePairs.nonEmpty + } +} + +trait RemovePairDependency +case class RemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]) extends RemovePairDependency { + override def toString = s"<${AsmUtils textify store},${AsmUtils textify other}> [$depends]" +} +case class LabelNotLive(label: LabelNode) extends RemovePairDependency 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 449a56fdd1..f247e884ae 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -26,14 +26,6 @@ class Inliner[BT <: BTypes](val btypes: BT) { import inlinerHeuristics._ import backendUtils._ - def eliminateUnreachableCodeAndUpdateCallGraph(methodNode: MethodNode, definingClass: InternalName): Unit = { - localOpt.minimalRemoveUnreachableCode(methodNode, definingClass) foreach { - case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, methodNode) - case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, methodNode) - case _ => - } - } - def runInliner(): Unit = { rewriteFinalTraitMethodInvocations() @@ -78,11 +70,13 @@ class Inliner[BT <: BTypes](val btypes: BT) { } } + // Rewriting final trait method callsites to the implementation class enables inlining. def rewriteFinalTraitMethodInvocations(): Unit = { - // Rewriting final trait method callsites to the implementation class enables inlining. - // We cannot just iterate over the values of the `callsites` map because the rewrite changes the - // map. Therefore we first copy the values to a list. - callsites.valuesIterator.flatMap(_.valuesIterator).toList.foreach(rewriteFinalTraitMethodInvocation) + // Collect callsites to rewrite before actually rewriting anything. This prevents changing the + // `callsties` map while iterating it. + val toRewrite = mutable.ArrayBuffer.empty[Callsite] + for (css <- callsites.valuesIterator; cs <- css.valuesIterator if doRewriteTraitCallsite(cs)) toRewrite += cs + toRewrite foreach rewriteFinalTraitMethodInvocation } /** @@ -101,79 +95,82 @@ class Inliner[BT <: BTypes](val btypes: BT) { * (the receiver type is the interface, so the method is abstract). */ def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = { - if (doRewriteTraitCallsite(callsite)) { - val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, samParamTypes, infoWarning)) = callsite.callee + // The analyzer used below needs to have a non-null frame for the callsite instruction + localOpt.minimalRemoveUnreachableCode(callsite.callsiteMethod, callsite.callsiteClass.internalName) - val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) + // If the callsite was eliminated by DCE, do nothing. + if (!callGraph.containsCallsite(callsite)) return - val implClassInternalName = calleeDeclarationClass.internalName + "$class" + val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, samParamTypes, infoWarning)) = callsite.callee - val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match { - case Some(internalName) => classBTypeFromParsedClassfile(internalName) - case None => calleeDeclarationClass - }) + val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) - def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = { - byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1) - } + val implClassInternalName = calleeDeclarationClass.internalName + "$class" - // The rewrite reading the implementation class and the implementation method from the bytecode - // repository. If either of the two fails, the rewrite is not performed. - val res = for { - selfParamType <- selfParamTypeV - implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*) - implClassMethod <- implClassMethodV(implMethodDescriptor) - implClassBType = classBTypeFromParsedClassfile(implClassInternalName) - selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType) - } yield { - - // The self parameter type may be incompatible with the trait type. - // trait T { self: S => def foo = 1 } - // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write - // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a - // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the - // receiver value and add a cast to the self type after each. - if (!selfTypeOk) { - // We don't need to worry about the method being too large for running an analysis. - // Callsites of large methods are not added to the call graph. - localOpt.minimalRemoveUnreachableCode(callsite.callsiteMethod, callsite.callsiteClass.internalName) - val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new Analyzer(new SourceInterpreter)) - val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(traitMethodArgumentTypes.length) - for (i <- receiverValue.insns.asScala) { - val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName) - callsite.callsiteMethod.instructions.insert(i, cast) - } - } + val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match { + case Some(internalName) => classBTypeFromParsedClassfile(internalName) + case None => calleeDeclarationClass + }) - val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false) - callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) - callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = { + byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1) + } - callGraph.removeCallsite(callsite.callsiteInstruction, callsite.callsiteMethod) - val staticCallSamParamTypes = { - if (selfParamType.info.get.inlineInfo.sam.isEmpty) samParamTypes - 0 - else samParamTypes.updated(0, selfParamType) + // The rewrite reading the implementation class and the implementation method from the bytecode + // repository. If either of the two fails, the rewrite is not performed. + val res = for { + selfParamType <- selfParamTypeV + implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*) + implClassMethod <- implClassMethodV(implMethodDescriptor) + implClassBType = classBTypeFromParsedClassfile(implClassInternalName) + selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType) + } yield { + + // The self parameter type may be incompatible with the trait type. + // trait T { self: S => def foo = 1 } + // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write + // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a + // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the + // receiver value and add a cast to the self type after each. + if (!selfTypeOk) { + // We don't need to worry about the method being too large for running an analysis. + // Callsites of large methods are not added to the call graph. + val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new Analyzer(new SourceInterpreter)) + val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(traitMethodArgumentTypes.length) + for (i <- receiverValue.insns.asScala) { + val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName) + callsite.callsiteMethod.instructions.insert(i, cast) } - val staticCallsite = callsite.copy( - callsiteInstruction = newCallsiteInstruction, - callee = Right(Callee( - callee = implClassMethod, - calleeDeclarationClass = implClassBType, - safeToInline = true, - safeToRewrite = false, - annotatedInline = annotatedInline, - annotatedNoInline = annotatedNoInline, - samParamTypes = staticCallSamParamTypes, - calleeInfoWarning = infoWarning)) - ) - callGraph.addCallsite(staticCallsite) } - for (warning <- res.left) { - val Right(callee) = callsite.callee - val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning))) - callGraph.addCallsite(callsite.copy(callee = Right(newCallee))) + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false) + callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) + callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + + callGraph.removeCallsite(callsite.callsiteInstruction, callsite.callsiteMethod) + val staticCallSamParamTypes = { + if (selfParamType.info.get.inlineInfo.sam.isEmpty) samParamTypes - 0 + else samParamTypes.updated(0, selfParamType) } + val staticCallsite = callsite.copy( + callsiteInstruction = newCallsiteInstruction, + callee = Right(Callee( + callee = implClassMethod, + calleeDeclarationClass = implClassBType, + safeToInline = true, + safeToRewrite = false, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline, + samParamTypes = staticCallSamParamTypes, + calleeInfoWarning = infoWarning)) + ) + callGraph.addCallsite(staticCallsite) + } + + for (warning <- res.left) { + val Right(callee) = callsite.callee + val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning))) + callGraph.addCallsite(callsite.copy(callee = Right(newCallee))) } } @@ -311,21 +308,9 @@ class Inliner[BT <: BTypes](val btypes: BT) { def inline(request: InlineRequest): List[CannotInlineWarning] = canInlineBody(request.callsite) match { case Some(w) => List(w) case None => - // Inlining a method can create unreachable code. Example: - // def f = throw e - // def g = f; println() // println is unreachable after inlining f - // If we have an inline request for a call to g, and f has been already inlined into g, we - // need to run DCE before inlining g. - val Right(callee) = request.callsite.callee - eliminateUnreachableCodeAndUpdateCallGraph(callee.callee, callee.calleeDeclarationClass.internalName) - // Skip over DCE'd callsites - if (callGraph.containsCallsite(request.callsite)) { - inlineCallsite(request.callsite) - val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite)) - postRequests flatMap inline - } else { - Nil - } + inlineCallsite(request.callsite) + val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite)) + postRequests flatMap inline } /** @@ -334,8 +319,6 @@ class Inliner[BT <: BTypes](val btypes: BT) { * Preconditions: * - The callsite can safely be inlined (canInlineBody is true) * - The maxLocals and maxStack values of the callsite method are correctly computed - * - The callsite method contains no unreachable basic blocks, i.e., running an Analyzer does - * not produce any `null` frames * * @return A map associating instruction nodes of the callee with the corresponding cloned * instruction in the callsite method. @@ -345,6 +328,17 @@ class Inliner[BT <: BTypes](val btypes: BT) { val Right(callsiteCallee) = callsite.callee import callsiteCallee.{callee, calleeDeclarationClass} + // Inlining requires the callee not to have unreachable code, the analyzer used below should not + // return any `null` frames. Note that inlining a method can create unreachable code. Example: + // def f = throw e + // def g = f; println() // println is unreachable after inlining f + // If we have an inline request for a call to g, and f has been already inlined into g, we + // need to run DCE on g's body before inlining g. + localOpt.minimalRemoveUnreachableCode(callee, calleeDeclarationClass.internalName) + + // If the callsite was eliminated by DCE, do nothing. + if (!callGraph.containsCallsite(callsite)) return + // New labels for the cloned instructions val labelsMap = cloneLabels(callee) val (clonedInstructions, instructionMap, hasSerializableClosureInstantiation) = cloneInstructions(callee, labelsMap) @@ -521,7 +515,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { // Remove the elided invocation from the call graph callGraph.removeCallsite(callsiteInstruction, callsiteMethod) - // Inlining a method body can render some code unreachable, see example above (in runInliner). + // Inlining a method body can render some code unreachable, see example above in this method. unreachableCodeEliminated -= callsiteMethod } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala deleted file mode 100644 index 79e44a8503..0000000000 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala +++ /dev/null @@ -1,236 +0,0 @@ -package scala.tools.nsc.backend.jvm.opt - -import scala.annotation.switch -import scala.tools.asm.{Handle, Type, Opcodes} -import scala.tools.asm.tree._ - -object InstructionResultSize { - import Opcodes._ - def apply(instruction: AbstractInsnNode): Int = (instruction.getOpcode: @switch) match { - // The order of opcodes is (almost) the same as in Opcodes.java - case ACONST_NULL => 1 - - case ICONST_M1 | - ICONST_0 | - ICONST_1 | - ICONST_2 | - ICONST_3 | - ICONST_4 | - ICONST_5 => 1 - - case LCONST_0 | - LCONST_1 => 2 - - case FCONST_0 | - FCONST_1 | - FCONST_2 => 1 - - case DCONST_0 | - DCONST_1 => 2 - - case BIPUSH | - SIPUSH => 1 - - case LDC => - instruction.asInstanceOf[LdcInsnNode].cst match { - case _: java.lang.Long | - _: java.lang.Double => 2 - - case _ => 1 - } - - case ILOAD | - FLOAD | - ALOAD => 1 - - case LLOAD | - DLOAD => 2 - - case IALOAD | - FALOAD | - AALOAD | - BALOAD | - CALOAD | - SALOAD => 1 - - case LALOAD | - DALOAD => 2 - - case ISTORE | - LSTORE | - FSTORE | - DSTORE | - ASTORE => 0 - - case IASTORE | - LASTORE | - FASTORE | - DASTORE | - AASTORE | - BASTORE | - CASTORE | - SASTORE => 0 - - case POP | - POP2 => 0 - - case DUP | - DUP_X1 | - DUP_X2 | - DUP2 | - DUP2_X1 | - DUP2_X2 | - SWAP => throw new IllegalArgumentException("Can't compute the size of DUP/SWAP without knowing what's on stack top") - - case IADD | - FADD => 1 - - case LADD | - DADD => 2 - - case ISUB | - FSUB => 1 - - case LSUB | - DSUB => 2 - - case IMUL | - FMUL => 1 - - case LMUL | - DMUL => 2 - - case IDIV | - FDIV => 1 - - case LDIV | - DDIV => 2 - - case IREM | - FREM => 1 - - case LREM | - DREM => 2 - - case INEG | - FNEG => 1 - - case LNEG | - DNEG => 2 - - case ISHL | - ISHR => 1 - - case LSHL | - LSHR => 2 - - case IUSHR => 1 - - case LUSHR => 2 - - case IAND | - IOR | - IXOR => 1 - - case LAND | - LOR | - LXOR => 2 - - case IINC => 1 - - case I2F | - L2I | - L2F | - F2I | - D2I | - D2F | - I2B | - I2C | - I2S => 1 - - case I2L | - I2D | - L2D | - F2L | - F2D | - D2L => 2 - - case LCMP | - FCMPL | - FCMPG | - DCMPL | - DCMPG => 1 - - case IFEQ | - IFNE | - IFLT | - IFGE | - IFGT | - IFLE => 0 - - case IF_ICMPEQ | - IF_ICMPNE | - IF_ICMPLT | - IF_ICMPGE | - IF_ICMPGT | - IF_ICMPLE | - IF_ACMPEQ | - IF_ACMPNE => 0 - - case GOTO => 0 - - case JSR => throw new IllegalArgumentException("Subroutines are not supported.") - - case RET => 0 - - case TABLESWITCH | - LOOKUPSWITCH => 0 - - case IRETURN | - FRETURN | - ARETURN => 1 - - case LRETURN | - DRETURN => 2 - - case RETURN => 0 - - case GETSTATIC => Type.getType(instruction.asInstanceOf[FieldInsnNode].desc).getSize - - case PUTSTATIC => 0 - - case GETFIELD => Type.getType(instruction.asInstanceOf[FieldInsnNode].desc).getSize - - case PUTFIELD => 0 - - case INVOKEVIRTUAL | - INVOKESPECIAL | - INVOKESTATIC | - INVOKEINTERFACE => - val desc = instruction.asInstanceOf[MethodInsnNode].desc - Type.getReturnType(desc).getSize - - case INVOKEDYNAMIC => - val desc = instruction.asInstanceOf[InvokeDynamicInsnNode].desc - Type.getReturnType(desc).getSize - - case NEW => 1 - - case NEWARRAY | - ANEWARRAY | - ARRAYLENGTH => 1 - - case ATHROW => 0 - - case CHECKCAST | - INSTANCEOF => 1 - - case MONITORENTER | - MONITOREXIT => 0 - - case MULTIANEWARRAY => 1 - - case IFNULL | - IFNONNULL => 0 - } -} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index a80b3d0487..c1fdb7eb59 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -7,49 +7,146 @@ package scala.tools.nsc package backend.jvm package opt -import scala.annotation.switch -import scala.tools.asm.{Type, ClassWriter, MethodWriter, Opcodes} +import scala.annotation.{tailrec, switch} +import scala.tools.asm.Type +import scala.tools.asm.tree.analysis.Frame +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ +import scala.collection.{mutable, immutable} import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.analysis._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ /** - * Optimizations within a single method. + * Optimizations within a single method. Certain optimizations enable others, for example removing + * unreachable code can render a `try` block empty and enable removeEmptyExceptionHandlers. The + * latter in turn enables more unreachable code to be eliminated (the `catch` block), so there is + * a cyclic dependency. Optimizations that depend on each other are therefore executed in a loop + * until reaching a fixpoint. * - * unreachable code - * - removes instructions of basic blocks to which no branch instruction points - * + enables eliminating some exception handlers and local variable descriptors - * > eliminating them is required for correctness, as explained in `removeUnreachableCode` + * The optimizations marked UPSTREAM enable optimizations that were already executed, so they cause + * another iteration in the fixpoint loop. * - * empty exception handlers - * - removes exception handlers whose try block is empty - * + eliminating a handler where the try block is empty and reachable will turn the catch block - * unreachable. in this case "unreachable code" is invoked recursively until reaching a fixpoint. - * > for try blocks that are unreachable, "unreachable code" removes also the instructions of the - * catch block, and the recursive invocation is not necessary. + * nullness optimizations: rewrite null-checking branches to GOTO if nullness is known + * + enables downstream + * - unreachable code (null / non-null branch becomes unreachable) + * - box-unbox elimination (may render an escaping consumer of a box unreachable) + * - stale stores (aload x is replaced by aconst_null if it's known null) + * - simplify jumps (replaces conditional jumps by goto, so may enable goto chains) * - * simplify jumps - * - various simplifications, see doc comments of individual optimizations - * + changing or eliminating jumps may render some code unreachable, therefore "simplify jumps" is - * executed in a loop with "unreachable code" + * unreachable code / DCE (removes instructions of basic blocks to which there is no branch) + * + enables downstream: + * - stale stores (loads may be eliminated, removing consumers of a store) + * - empty handlers (try blocks may become empty) + * - simplify jumps (goto l; [dead code]; l: ..) => remove goto + * - stale local variable descriptors + * - (not box-unbox, which is implemented using prod-cons, so it doesn't consider dead code) * - * empty local variable descriptors - * - removes entries from the local variable table where the variable is not actually used - * + enables eliminating labels that the entry points to (if they are not otherwise referenced) + * note that eliminating empty handlers and stale local variable descriptors is required for + * correctness, see the comment in the body of `methodOptimizations`. * - * empty line numbers - * - eliminates line number nodes that describe no executable instructions - * + enables eliminating the label of the line number node (if it's not otherwise referenced) + * box-unbox elimination (eliminates box-unbox pairs withing the same method) + * + enables UPSTREAM: + * - nullness optimizations (a box extraction operation (unknown nullness) may be rewritten to + * a read of a non-null local. example in doc comment of box-unbox implementation) + * - further box-unbox elimination (e.g. an Integer stored in a Tuple; eliminating the tuple may + * enable eliminating the Integer) + * + enables downstream: + * - copy propagation (new locals are introduced, may be aliases of existing) + * - stale stores (multi-value boxes where not all values are used) + * - redundant casts (`("a", "b")._1`: the generic `_1` method returns `Object`, a cast + * to String is added. The cast is redundant after eliminating the tuple.) + * - empty local variable descriptors (local variables that were holding the box may become unused) * - * stale labels - * - eliminate labels that are not referenced, merge sequences of label definitions. + * copy propagation (replaces LOAD n to the LOAD m for the smallest m that is an alias of n) + * + enables downstrem: + * - stale stores (a stored value may not be loaded anymore) + * - store-load pairs (a load n may now be right after a store n) + * + NOTE: copy propagation is only executed once, in the first fixpoint loop iteration. none of + * the other optimizations enables further copy prop. we still run it as part of the loop + * because it requires unreachable code to be eliminated. + * + * stale stores (replace STORE by POP) + * + enables downstream: + * - push-pop (the new pop may be the single consumer for an instruction) + * + * redundant casts: eliminates casts that are statically known to succeed (uses type propagation) + * + enables UPSTREAM: + * - box-unbox elimination (a removed checkcast may be a box consumer) + * + enables downstream: + * - push-pop for closure allocation elimination (every indyLambda is followed by a checkcast, see SI-9540) + * + * push-pop (when a POP is the only consumer of a value, remove the POP and its producer) + * + enables UPSTREAM: + * - stale stores (if a LOAD is removed, a corresponding STORE may become stale) + * - box-unbox elimination (push-pop may eliminate a closure allocation, rendering a captured + * box non-escaping) + * + enables downstream: + * - store-load pairs (a variable may become non-live) + * - stale handlers (push-pop removes code) + * - simplify jumps (push-pop removes code) + * + * store-load pairs (remove `STORE x; LOAD x` if x is otherwise not used in the method) + * + enables downstream: + * - empty handlers (code is removes, a try block may become empty + * - simplify jumps (code is removed, a goto may become redundant for example) + * - stale local variable descriptors + * + * empty handlers (removes exception handlers whose try block is empty) + * + enables UPSTREAM: + * - unreachable code (catch block becomes unreachable) + * - box-unbox (a box may be escape in an operation in a dead handler) + * + enables downstream: + * - simplify jumps + * + * simplify jumps (various, like `GOTO l; l: ...`, see doc comments of individual optimizations) + * + enables UPSTREAM + * - unreachable code (`GOTO a; a: GOTO b; b: ...`, the first jump is changed to `GOTO b`, the second becomes unreachable) + * - store-load pairs (a `GOTO l; l: ...` is removed between store and load) + * - push-pop (`IFNULL l; l: ...` is replaced by `POP`) + * + * + * The following cleanup optimizations don't enable any upstream optimizations, so they can be + * executed once at the end, when the above optimizations reach a fixpoint. + * + * + * empty local variable descriptors (removes unused variables from the local variable table) + * + enables downstream: + * - stale labels (labels that the entry points to, if not otherwise referenced) + * + * empty line numbers (eliminates line number nodes that describe no executable instructions) + * + enables downstream: + * - stale labels (label of the line number node, if not otherwise referenced) + * + * stale labels (eliminate labels that are not referenced, merge sequences of label definitions) + * + * + * Note on a method's maxLocals / maxStack: the backend only uses those values for running + * Analyzers. The values can be conservative approximations: if an optimization removes code and + * the maximal stack size is now smaller, the larger maxStack value will still work fine for + * running an Analyzer (just that frames allocate more space than required). The correct max + * values written to the bytecode are re-computed during classfile serialization. + * To keep things simpler, we don't update the max values in every optimization: + * - we do it in `removeUnreachableCodeImpl`, because it's quite straightforward + * - maxLocals is updated in `compactLocalVariables`, which runs at the end of method optimizations + * + * + * Note on updating the call graph: whenever an optimization eliminates a callsite or a closure + * instantiation, we eliminate the corresponding entry from the call graph. */ class LocalOpt[BT <: BTypes](val btypes: BT) { import LocalOptImpls._ import btypes._ + import coreBTypes._ import backendUtils._ + val boxUnbox = new BoxUnbox(btypes) + import boxUnbox._ + + val copyProp = new CopyProp(btypes) + import copyProp._ + /** * Remove unreachable code from a method. * @@ -59,28 +156,30 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { * * @return A set containing the eliminated instructions */ - def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Set[AbstractInsnNode] = { - if (method.instructions.size == 0) return Set.empty // fast path for abstract methods - if (unreachableCodeEliminated(method)) return Set.empty // we know there is no unreachable code - if (!AsmAnalyzer.sizeOKForBasicValue(method)) return Set.empty // the method is too large for running an analyzer + def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = { + // In principle, for the inliner, a single removeUnreachableCodeImpl would be enough. But that + // would potentially leave behind stale handlers (empty try block) which is not legal in the + // classfile. So we run both removeUnreachableCodeImpl and removeEmptyExceptionHandlers. + if (method.instructions.size == 0) return false // fast path for abstract methods + if (unreachableCodeEliminated(method)) return false // we know there is no unreachable code + if (!AsmAnalyzer.sizeOKForBasicValue(method)) return false // the method is too large for running an analyzer // For correctness, after removing unreachable code, we have to eliminate empty exception // handlers, see scaladoc of def methodOptimizations. Removing an live handler may render more // code unreachable and therefore requires running another round. - def removalRound(): Set[AbstractInsnNode] = { - val (removedInstructions, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) - val removedRecursively = if (removedInstructions.nonEmpty) { + def removalRound(): Boolean = { + val (insnsRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) + if (insnsRemoved) { val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start)) if (liveHandlerRemoved) removalRound() - else Set.empty - } else Set.empty - removedInstructions ++ removedRecursively + } + insnsRemoved } - val removedInstructions = removalRound() - if (removedInstructions.nonEmpty) removeUnusedLocalVariableNodes(method)() + val changed = removalRound() + if (changed) removeUnusedLocalVariableNodes(method)() unreachableCodeEliminated += method - removedInstructions + changed } /** @@ -97,15 +196,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { } /** - * Remove unreachable code from a method. - * - * We rely on dead code elimination provided by the ASM framework, as described in the ASM User - * Guide (http://asm.ow2.org/index.html), Section 8.2.1. It runs a data flow analysis, which only - * computes Frame information for reachable instructions. Instructions for which no Frame data is - * available after the analysis are unreachable. - * - * Also simplifies branching instructions, removes unused local variable descriptors, empty - * exception handlers, unnecessary label declarations and empty line number nodes. + * Run method-level optimizations, see comment on class [[LocalOpt]]. * * Returns `true` if the bytecode of `method` was changed. */ @@ -138,37 +229,150 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar // for local variables in dead blocks. Maybe that's a bug in the ASM framework. - def canRunDCE = AsmAnalyzer.sizeOKForBasicValue(method) - def removalRound(): Boolean = { - // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (compilerSettings.YoptUnreachableCode && canRunDCE) { - val (removedInstructions, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) - val removedHandlers = removeEmptyExceptionHandlers(method) - (removedInstructions.nonEmpty, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) - } else { - (false, false, false) + var currentTrace: String = null + val doTrace = compilerSettings.YoptTrace.isSetByUser && compilerSettings.YoptTrace.value == ownerClassName + "." + method.name + def traceIfChanged(optName: String): Unit = if (doTrace) { + val after = AsmUtils.textify(method) + if (currentTrace != after) { + println(s"after $optName") + println(after) } - - val jumpsChanged = if (compilerSettings.YoptSimplifyJumps) simplifyJumps(method) else false - - // Eliminating live handlers and simplifying jump instructions may render more code - // unreachable, so we need to run another round. - if (liveHandlerRemoved || jumpsChanged) removalRound() - - codeRemoved || handlersRemoved || jumpsChanged + currentTrace = after } - val codeHandlersOrJumpsChanged = removalRound() + /** + * Runs the optimizations that depend on each other in a loop until reaching a fixpoint. See + * comment in class [[LocalOpt]]. + * + * Returns a pair of booleans (codeChanged, requireEliminateUnusedLocals). + */ + def removalRound( + requestNullness: Boolean, + requestDCE: Boolean, + requestBoxUnbox: Boolean, + requestStaleStores: Boolean, + requestPushPop: Boolean, + requestStoreLoad: Boolean, + firstIteration: Boolean, + maxRecursion: Int = 10): (Boolean, Boolean) = { + if (maxRecursion == 0) return (false, false) + + traceIfChanged("beforeMethodOpt") + + // NULLNESS OPTIMIZATIONS + val runNullness = compilerSettings.YoptNullnessTracking && requestNullness + val nullnessOptChanged = runNullness && nullnessOptimizations(method, ownerClassName) + traceIfChanged("nullness") + + // UNREACHABLE CODE + // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores, + // boxUnboxElimination) require not having unreachable instructions (null frames). + val runDCE = (compilerSettings.YoptUnreachableCode && (requestDCE || nullnessOptChanged)) || + compilerSettings.YoptBoxUnbox || + compilerSettings.YoptCopyPropagation + val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) + traceIfChanged("dce") + + // BOX-UNBOX + val runBoxUnbox = compilerSettings.YoptBoxUnbox && (requestBoxUnbox || nullnessOptChanged) + val boxUnboxChanged = runBoxUnbox && boxUnboxElimination(method, ownerClassName) + traceIfChanged("boxUnbox") + + // COPY PROPAGATION + val runCopyProp = compilerSettings.YoptCopyPropagation && (firstIteration || boxUnboxChanged) + val copyPropChanged = runCopyProp && copyPropagation(method, ownerClassName) + traceIfChanged("copyProp") + + // STALE STORES + val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || nullnessOptChanged || codeRemoved || boxUnboxChanged || copyPropChanged) + val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName) + traceIfChanged("staleStores") + + // REDUNDANT CASTS + val runRedundantCasts = compilerSettings.YoptRedundantCasts && (firstIteration || boxUnboxChanged) + val castRemoved = runRedundantCasts && eliminateRedundantCasts(method, ownerClassName) + traceIfChanged("redundantCasts") + + // PUSH-POP + val runPushPop = compilerSettings.YoptCopyPropagation && (requestPushPop || firstIteration || storesRemoved || castRemoved) + val pushPopRemoved = runPushPop && eliminatePushPop(method, ownerClassName) + traceIfChanged("pushPop") + + // STORE-LOAD PAIRS + val runStoreLoad = compilerSettings.YoptCopyPropagation && (requestStoreLoad || boxUnboxChanged || copyPropChanged || pushPopRemoved) + val storeLoadRemoved = runStoreLoad && eliminateStoreLoad(method) + traceIfChanged("storeLoadPairs") + + // STALE HANDLERS + val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] + val handlersRemoved = removedHandlers.nonEmpty + val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start)) + traceIfChanged("staleHandlers") + + // SIMPLIFY JUMPS + // almost all of the above optimizations enable simplifying more jumps, so we just run it in every iteration + val runSimplifyJumps = compilerSettings.YoptSimplifyJumps + val jumpsChanged = runSimplifyJumps && simplifyJumps(method) + traceIfChanged("simplifyJumps") + + // See doc comment in the beginning of this file (optimizations marked UPSTREAM) + val runNullnessAgain = boxUnboxChanged + val runDCEAgain = liveHandlerRemoved || jumpsChanged + val runBoxUnboxAgain = boxUnboxChanged || castRemoved || pushPopRemoved || liveHandlerRemoved + val runStaleStoresAgain = pushPopRemoved + val runPushPopAgain = jumpsChanged + val runStoreLoadAgain = jumpsChanged + val runAgain = runNullnessAgain || runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runPushPopAgain || runStoreLoadAgain + + val downstreamRequireEliminateUnusedLocals = runAgain && removalRound( + requestNullness = runNullnessAgain, + requestDCE = runDCEAgain, + requestBoxUnbox = runBoxUnboxAgain, + requestStaleStores = runStaleStoresAgain, + requestPushPop = runPushPopAgain, + requestStoreLoad = runStoreLoadAgain, + firstIteration = false, + maxRecursion = maxRecursion - 1)._2 + + val requireEliminateUnusedLocals = downstreamRequireEliminateUnusedLocals || + nullnessOptChanged || // nullness opt may eliminate stores / loads, rendering a local unused + codeRemoved || // see comment in method `methodOptimizations` + boxUnboxChanged || // box-unbox renders locals (holding boxes) unused + storesRemoved || + storeLoadRemoved || + handlersRemoved + + val codeChanged = nullnessOptChanged || codeRemoved || boxUnboxChanged || castRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged + (codeChanged, requireEliminateUnusedLocals) + } - // (*) Removing stale local variable descriptors is required for correctness of unreachable-code + val (nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged, requireEliminateUnusedLocals) = if (AsmAnalyzer.sizeOKForBasicValue(method)) { + // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE + // here is more thorough than `minimalRemoveUnreachableCode` that run before inlining. + val r = removalRound( + requestNullness = true, + requestDCE = true, + requestBoxUnbox = true, + requestStaleStores = true, + requestPushPop = true, + requestStoreLoad = true, + firstIteration = true) + if (compilerSettings.YoptUnreachableCode) unreachableCodeEliminated += method + r + } else (false, false) + + // (*) Removing stale local variable descriptors is required for correctness, see comment in `methodOptimizations` val localsRemoved = if (compilerSettings.YoptCompactLocals) compactLocalVariables(method) // also removes unused - else if (compilerSettings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) + else if (requireEliminateUnusedLocals) removeUnusedLocalVariableNodes(method)() // (*) else false + traceIfChanged("localVariables") - val lineNumbersRemoved = if (compilerSettings.YoptEmptyLineNumbers) removeEmptyLineNumbers(method) else false + val lineNumbersRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLineNumbers(method) else false + traceIfChanged("lineNumbers") - val labelsRemoved = if (compilerSettings.YoptEmptyLabels) removeEmptyLabelNodes(method) else false + val labelsRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLabelNodes(method) else false + traceIfChanged("labels") // assert that local variable annotations are empty (we don't emit them) - otherwise we'd have // to eliminate those covering an empty range, similar to removeUnusedLocalVariableNodes. @@ -176,9 +380,101 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) - unreachableCodeEliminated += method + nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + } + + /** + * Apply various optimizations based on nullness analysis information. + * - IFNULL / IFNONNULL are rewritten to GOTO if nullness is known + * - IF_ACMPEQ / IF_ACMPNE are rewritten to GOTO if the both references are known null, or if + * one is known null and the other known not-null + * - ALOAD is replaced by ACONST_NULL if the local is known to hold null + * - ASTORE of null is removed if the local is known to hold null + * - INSTANCEOF of null is replaced by `ICONST_0` + * - scala.runtime.BoxesRunTime.unboxToX(null) is rewritten to a zero-value load + */ + def nullnessOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { + AsmAnalyzer.sizeOKForNullness(method) && { + lazy val nullnessAnalyzer = new AsmAnalyzer(method, ownerClassName, new NullnessAnalyzer(btypes)) + + // When running nullness optimizations the method may still have unreachable code. Analyzer + // frames of unreachable instructions are `null`. + def frameAt(insn: AbstractInsnNode): Option[Frame[NullnessValue]] = Option(nullnessAnalyzer.frameAt(insn)) + + def nullness(insn: AbstractInsnNode, slot: Int): Option[NullnessValue] = { + frameAt(insn).map(_.getValue(slot)) + } - codeHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + def isNull(insn: AbstractInsnNode, slot: Int) = nullness(insn, slot).contains(NullValue) + + // cannot change instructions while iterating, it gets the analysis out of synch (indexed by instructions) + val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + + val it = method.instructions.iterator() + while (it.hasNext) it.next() match { + case vi: VarInsnNode if isNull(vi, vi.`var`) => + if (vi.getOpcode == ALOAD) + toReplace(vi) = List(new InsnNode(ACONST_NULL)) + else if (vi.getOpcode == ASTORE) + for (frame <- frameAt(vi) if frame.peekStack(0) == NullValue) + toReplace(vi) = List(getPop(1)) + + case ji: JumpInsnNode => + val isIfNull = ji.getOpcode == IFNULL + val isIfNonNull = ji.getOpcode == IFNONNULL + if (isIfNull || isIfNonNull) for (frame <- frameAt(ji)) { + val nullness = frame.peekStack(0) + val taken = nullness == NullValue && isIfNull || nullness == NotNullValue && isIfNonNull + val avoided = nullness == NotNullValue && isIfNull || nullness == NullValue && isIfNonNull + if (taken || avoided) { + val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil + toReplace(ji) = getPop(1) :: jump + } + } else { + val isIfEq = ji.getOpcode == IF_ACMPEQ + val isIfNe = ji.getOpcode == IF_ACMPNE + if (isIfEq || isIfNe) for (frame <- frameAt(ji)) { + val aNullness = frame.peekStack(1) + val bNullness = frame.peekStack(0) + val eq = aNullness == NullValue && bNullness == NullValue + val ne = aNullness == NullValue && bNullness == NotNullValue || aNullness == NotNullValue && bNullness == NullValue + val taken = isIfEq && eq || isIfNe && ne + val avoided = isIfEq && ne || isIfNe && eq + if (taken || avoided) { + val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil + toReplace(ji) = getPop(1) :: getPop(1) :: jump + } + } + } + + case ti: TypeInsnNode => + if (ti.getOpcode == INSTANCEOF) for (frame <- frameAt(ti) if frame.peekStack(0) == NullValue) { + toReplace(ti) = List(getPop(1), new InsnNode(ICONST_0)) + } + + case mi: MethodInsnNode => + if (isScalaUnbox(mi)) for (frame <- frameAt(mi) if frame.peekStack(0) == NullValue) { + toReplace(mi) = List( + getPop(1), + loadZeroForTypeSort(Type.getReturnType(mi.desc).getSort)) + } + + case _ => + } + + def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { + case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) + case _ => + } + + for ((oldOp, newOps) <- toReplace) { + for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp) + method.instructions.remove(oldOp) + removeFromCallGraph(oldOp) + } + + toReplace.nonEmpty + } } /** @@ -186,14 +482,14 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { * * @return A set containing eliminated instructions, and a set containing all live label nodes. */ - def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Set[AbstractInsnNode], Set[LabelNode]) = { + def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Boolean, Set[LabelNode]) = { val a = new AsmAnalyzer(method, ownerClassName) val frames = a.analyzer.getFrames var i = 0 var liveLabels = Set.empty[LabelNode] - var removedInstructions = Set.empty[AbstractInsnNode] - var maxLocals = (Type.getArgumentsAndReturnSizes(method.desc) >> 2) - (if (BytecodeUtils.isStaticMethod(method)) 1 else 0) + var changed = false + var maxLocals = parametersSize(method) var maxStack = 0 val itr = method.instructions.iterator() while (itr.hasNext) { @@ -214,18 +510,63 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { maxLocals = math.max(maxLocals, i.`var` + 1) case _ => - if (!isLive || insn.getOpcode == Opcodes.NOP) { + if (!isLive || insn.getOpcode == NOP) { // Instruction iterators allow removing during iteration. // Removing is O(1): instructions are doubly linked list elements. itr.remove() - removedInstructions += insn + changed = true + insn match { + case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, method) + case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, method) + case _ => + } } } i += 1 } method.maxLocals = maxLocals method.maxStack = maxStack - (removedInstructions, liveLabels) + (changed, liveLabels) + } + + /** + * Eliminate `CHECKCAST` instructions that are statically known to succeed. This is safe if the + * tested object is null: `null.asInstanceOf` always succeeds. + * + * The type of the tested object is determined using a NonLubbingTypeFlowAnalyzer. Note that this + * analysis collapses LUBs of non-equal references types to Object for simplicity. Example: + * given `B <: A <: Object`, the cast in `(if (..) new B else new A).asInstanceOf[A]` would not + * be eliminated. + * + * Note: we cannot replace `INSTANCEOF` tests by only looking at the types, `null.isInstanceOf` + * always returns false, so we'd also need nullness information. + */ + def eliminateRedundantCasts(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForBasicValue(method) && { + def isSubType(aRefDesc: String, bClass: InternalName): Boolean = aRefDesc == bClass || bClass == ObjectRef.internalName || { + (bTypeForDescriptorOrInternalNameFromClassfile(aRefDesc) conformsTo classBTypeFromParsedClassfile(bClass)).getOrElse(false) + } + + lazy val typeAnalyzer = new NonLubbingTypeFlowAnalyzer(method, owner) + + // cannot remove instructions while iterating, it gets the analysis out of synch (indexed by instructions) + val toRemove = mutable.Set.empty[TypeInsnNode] + + val it = method.instructions.iterator() + while (it.hasNext) it.next() match { + case ti: TypeInsnNode if ti.getOpcode == CHECKCAST => + val frame = typeAnalyzer.frameAt(ti) + val valueTp = frame.getValue(frame.stackTop) + if (valueTp.isReference && isSubType(valueTp.getType.getDescriptor, ti.desc)) { + toRemove += ti + } + + case _ => + } + + toRemove foreach method.instructions.remove + toRemove.nonEmpty + } } } @@ -245,16 +586,16 @@ object LocalOptImpls { def removeEmptyExceptionHandlers(method: MethodNode): Set[TryCatchBlockNode] = { /** True if there exists code between start and end. */ def containsExecutableCode(start: AbstractInsnNode, end: LabelNode): Boolean = { - start != end && ((start.getOpcode : @switch) match { + start != end && ((start.getOpcode: @switch) match { // FrameNode, LabelNode and LineNumberNode have opcode == -1. - case -1 | Opcodes.GOTO => containsExecutableCode(start.getNext, end) + case -1 | GOTO => containsExecutableCode(start.getNext, end) case _ => true }) } var removedHandlers = Set.empty[TryCatchBlockNode] val handlersIter = method.tryCatchBlocks.iterator() - while(handlersIter.hasNext) { + while (handlersIter.hasNext) { val handler = handlersIter.next() if (!containsExecutableCode(handler.start, handler.end)) { removedHandlers += handler @@ -273,9 +614,10 @@ object LocalOptImpls { * same type or name. */ def removeUnusedLocalVariableNodes(method: MethodNode)(firstLocalIndex: Int = parametersSize(method), renumber: Int => Int = identity): Boolean = { - def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = { + @tailrec def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = { start != end && (start match { case v: VarInsnNode if v.`var` == varIndex => true + case i: IincInsnNode if i.`var` == varIndex => true case _ => variableIsUsed(start.getNext, end, varIndex) }) } @@ -295,17 +637,6 @@ object LocalOptImpls { } /** - * The number of local variable slots used for parameters and for the `this` reference. - */ - private def parametersSize(method: MethodNode): Int = { - // Double / long fields occupy two slots, so we sum up the sizes. Since getSize returns 0 for - // void, we have to add `max 1`. - val paramsSize = scala.tools.asm.Type.getArgumentTypes(method.desc).iterator.map(_.getSize max 1).sum - val thisSize = if ((method.access & Opcodes.ACC_STATIC) == 0) 1 else 0 - paramsSize + thisSize - } - - /** * Compact the local variable slots used in the method's implementation. This prevents having * unused slots for example after eliminating unreachable code. * @@ -320,8 +651,8 @@ object LocalOptImpls { val renumber = collection.mutable.ArrayBuffer.empty[Int] // Add the index of the local variable used by `varIns` to the `renumber` array. - def addVar(varIns: VarInsnNode): Unit = { - val index = varIns.`var` + def addVar(varIns: AbstractInsnNode, slot: Int): Unit = { + val index = slot val isWide = isSize2LoadOrStore(varIns.getOpcode) // Ensure the length of `renumber`. Unused variable indices are mapped to -1. @@ -339,7 +670,7 @@ object LocalOptImpls { val firstLocalIndex = parametersSize(method) for (i <- 0 until firstLocalIndex) renumber += i // parameters and `this` are always used. method.instructions.iterator().asScala foreach { - case VarInstruction(varIns) => addVar(varIns) + case VarInstruction(varIns, slot) => addVar(varIns, slot) case _ => } @@ -360,10 +691,12 @@ object LocalOptImpls { // update variable instructions according to the renumber table method.maxLocals = nextIndex method.instructions.iterator().asScala.foreach { - case VarInstruction(varIns) => - val oldIndex = varIns.`var` - if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) - varIns.`var` = renumber(varIns.`var`) + case VarInstruction(varIns, slot) => + val oldIndex = slot + if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) varIns match { + case vi: VarInsnNode => vi.`var` = renumber(slot) + case ii: IincInsnNode => ii.`var` = renumber(slot) + } case _ => } true @@ -551,7 +884,7 @@ object LocalOptImpls { private def simplifyBranchOverGoto(method: MethodNode, instruction: AbstractInsnNode): Option[JumpInsnNode] = instruction match { case ConditionalJump(jump) => // don't skip over labels, see doc comment - nextExecutableInstruction(jump, alsoKeep = _.isInstanceOf[LabelNode]) match { + nextExecutableInstructionOrLabel(jump) match { case Some(Goto(goto)) => if (nextExecutableInstruction(goto, alsoKeep = Set(jump.label)) == Some(jump.label)) { val newJump = new JumpInsnNode(negateJumpOpcode(jump.getOpcode), goto.label) @@ -579,7 +912,7 @@ object LocalOptImpls { case Goto(jump) => nextExecutableInstruction(jump.label) match { case Some(target) => - if (isReturn(target) || target.getOpcode == Opcodes.ATHROW) { + if (isReturn(target) || target.getOpcode == ATHROW) { method.instructions.set(jump, target.clone(null)) true } else false diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 78e8c328b8..20e2c4346f 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -222,52 +222,65 @@ trait ScalaSettings extends AbsScalaSettings val Ydelambdafy = ChoiceSetting ("-Ydelambdafy", "strategy", "Strategy used for translating lambdas into JVM code.", List("inline", "method"), "method") object YoptChoices extends MultiChoiceEnumeration { - val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code, exception handlers protecting no instructions, debug information of eliminated variables.") + val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code, exception handlers guarding no instructions, redundant metadata (debug information, line numbers).") val simplifyJumps = Choice("simplify-jumps", "Simplify branching instructions, eliminate unnecessary ones.") - val emptyLineNumbers = Choice("empty-line-numbers", "Eliminate unnecessary line number information.") - val emptyLabels = Choice("empty-labels", "Eliminate and collapse redundant labels in the bytecode.") val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.") + val copyPropagation = Choice("copy-propagation", "Eliminate redundant local variables and unused values (including closures). Enables unreachable-code.") + val redundantCasts = Choice("redundant-casts", "Eliminate redundant casts using a type propagation analysis.") + val boxUnbox = Choice("box-unbox", "Eliminate box-unbox pairs within the same method (also tuples, xRefs, value class instances). Enables unreachable-code.") val nullnessTracking = Choice("nullness-tracking", "Track nullness / non-nullness of local variables and apply optimizations.") - val closureElimination = Choice("closure-elimination" , "Rewrite closure invocations to the implementation method and eliminate closures.") - val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled.") - val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath.") + val closureInvocations = Choice("closure-invocations" , "Rewrite closure invocations to the implementation method.") + val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled. Enables unreachable-code.") + val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath. Enables unreachable-code.") - val lNone = Choice("l:none", "Don't enable any optimizations.") + // note: unlike the other optimizer levels, "l:none" appears up in the `Yopt.value` set because it's not an expanding option (expandsTo is empty) + val lNone = Choice("l:none", "Disable optimizations. Takes precedence: `-Yopt:l:none,+box-unbox` / `-Yopt:l:none -Yopt:box-unbox` don't enable box-unbox.") private val defaultChoices = List(unreachableCode) - val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) + val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString("", ",", "."), expandsTo = defaultChoices) - private val methodChoices = List(unreachableCode, simplifyJumps, emptyLineNumbers, emptyLabels, compactLocals, nullnessTracking, closureElimination) - val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) + private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, redundantCasts, boxUnbox, nullnessTracking, closureInvocations) + val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString("", ",", "."), expandsTo = methodChoices) private val projectChoices = List(lMethod, inlineProject) - val lProject = Choice("l:project", "Enable cross-method optimizations within the current project: "+ projectChoices.mkString(","), expandsTo = projectChoices) + val lProject = Choice("l:project", "Enable cross-method optimizations within the current project: "+ projectChoices.mkString("", ",", "."), expandsTo = projectChoices) private val classpathChoices = List(lProject, inlineGlobal) - val lClasspath = Choice("l:classpath", "Enable cross-method optimizations across the entire classpath: "+ classpathChoices.mkString(","), expandsTo = classpathChoices) + val lClasspath = Choice("l:classpath", "Enable cross-method optimizations across the entire classpath: "+ classpathChoices.mkString("", ",", "."), expandsTo = classpathChoices) } + // We don't use the `default` parameter of `MultiChoiceSetting`: it specifies the default values + // when `-Yopt` is passed without explicit choices. When `-Yopt` is not explicitly specified, the + // set `Yopt.value` is empty. val Yopt = MultiChoiceSetting( name = "-Yopt", helpArg = "optimization", descr = "Enable optimizations", domain = YoptChoices) - def YoptNone = Yopt.isSetByUser && Yopt.value.isEmpty - def YoptUnreachableCode = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode) - def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) - def YoptEmptyLineNumbers = Yopt.contains(YoptChoices.emptyLineNumbers) - def YoptEmptyLabels = Yopt.contains(YoptChoices.emptyLabels) - def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) - def YoptNullnessTracking = Yopt.contains(YoptChoices.nullnessTracking) - def YoptClosureElimination = Yopt.contains(YoptChoices.closureElimination) - - def YoptInlineProject = Yopt.contains(YoptChoices.inlineProject) - def YoptInlineGlobal = Yopt.contains(YoptChoices.inlineGlobal) + private def optEnabled(choice: YoptChoices.Choice) = { + !Yopt.contains(YoptChoices.lNone) && { + Yopt.contains(choice) || + !Yopt.isSetByUser && YoptChoices.lDefault.expandsTo.contains(choice) + } + } + + def YoptNone = Yopt.contains(YoptChoices.lNone) + def YoptUnreachableCode = optEnabled(YoptChoices.unreachableCode) + def YoptSimplifyJumps = optEnabled(YoptChoices.simplifyJumps) + def YoptCompactLocals = optEnabled(YoptChoices.compactLocals) + def YoptCopyPropagation = optEnabled(YoptChoices.copyPropagation) + def YoptRedundantCasts = optEnabled(YoptChoices.redundantCasts) + def YoptBoxUnbox = optEnabled(YoptChoices.boxUnbox) + def YoptNullnessTracking = optEnabled(YoptChoices.nullnessTracking) + def YoptClosureInvocations = optEnabled(YoptChoices.closureInvocations) + + def YoptInlineProject = optEnabled(YoptChoices.inlineProject) + def YoptInlineGlobal = optEnabled(YoptChoices.inlineGlobal) def YoptInlinerEnabled = YoptInlineProject || YoptInlineGlobal - def YoptBuildCallGraph = YoptInlinerEnabled || YoptClosureElimination - def YoptAddToBytecodeRepository = YoptBuildCallGraph || YoptInlinerEnabled || YoptClosureElimination + def YoptBuildCallGraph = YoptInlinerEnabled || YoptClosureInvocations + def YoptAddToBytecodeRepository = YoptBuildCallGraph || YoptInlinerEnabled || YoptClosureInvocations val YoptInlineHeuristics = ChoiceSetting( name = "-Yopt-inline-heuristics", @@ -306,6 +319,8 @@ trait ScalaSettings extends AbsScalaSettings def YoptWarningNoInlineMissingBytecode = YoptWarnings.contains(YoptWarningsChoices.noInlineMissingBytecode) def YoptWarningNoInlineMissingScalaInlineInfoAttr = YoptWarnings.contains(YoptWarningsChoices.noInlineMissingScalaInlineInfoAttr) + val YoptTrace = StringSetting("-YoptTrace", "package/Class.method", "Trace the optimizer progress for a specific method.", "") + private def removalIn212 = "This flag is scheduled for removal in 2.12. If you have a case where you need this flag then please report a bug." object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup, jvm = Value } diff --git a/src/compiler/scala/tools/nsc/transform/LambdaLift.scala b/src/compiler/scala/tools/nsc/transform/LambdaLift.scala index facb6ed263..f6e2dd68f0 100644 --- a/src/compiler/scala/tools/nsc/transform/LambdaLift.scala +++ b/src/compiler/scala/tools/nsc/transform/LambdaLift.scala @@ -31,11 +31,6 @@ abstract class LambdaLift extends InfoTransform { } } - /** scala.runtime.*Ref classes */ - private lazy val allRefClasses: Set[Symbol] = { - refClass.values.toSet ++ volatileRefClass.values.toSet ++ Set(VolatileObjectRefClass, ObjectRefClass) - } - /** Each scala.runtime.*Ref class has a static method `create(value)` that simply instantiates the Ref to carry that value. */ private lazy val refCreateMethod: Map[Symbol, Symbol] = { mapFrom(allRefClasses.toList)(x => getMemberMethod(x.companionModule, nme.create)) diff --git a/src/partest-extras/scala/tools/partest/ASMConverters.scala b/src/partest-extras/scala/tools/partest/ASMConverters.scala index b4c686473b..d990160ce8 100644 --- a/src/partest-extras/scala/tools/partest/ASMConverters.scala +++ b/src/partest-extras/scala/tools/partest/ASMConverters.scala @@ -38,6 +38,11 @@ object ASMConverters { } def dropNonOp = dropLinesFrames.dropStaleLabels + + def summary: List[Any] = dropNonOp map { + case i: Invoke => i.name + case i => i.opcode + } } sealed abstract class Instruction extends Product { diff --git a/src/reflect/scala/reflect/internal/Definitions.scala b/src/reflect/scala/reflect/internal/Definitions.scala index d167f1485d..ba6c363918 100644 --- a/src/reflect/scala/reflect/internal/Definitions.scala +++ b/src/reflect/scala/reflect/internal/Definitions.scala @@ -94,6 +94,10 @@ trait Definitions extends api.StandardDefinitions { lazy val refClass = classesMap(x => getRequiredClass("scala.runtime." + x + "Ref")) lazy val volatileRefClass = classesMap(x => getRequiredClass("scala.runtime.Volatile" + x + "Ref")) + lazy val allRefClasses: Set[Symbol] = { + refClass.values.toSet ++ volatileRefClass.values.toSet ++ Set(VolatileObjectRefClass, ObjectRefClass) + } + def isNumericSubClass(sub: Symbol, sup: Symbol) = ( (numericWeight contains sub) && (numericWeight contains sup) diff --git a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala index a9b91b5ec3..d6b611a3f4 100644 --- a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala +++ b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala @@ -423,6 +423,7 @@ trait JavaUniverseForce { self: runtime.JavaUniverse => definitions.boxedClass definitions.refClass definitions.volatileRefClass + definitions.allRefClasses definitions.UnitClass definitions.ByteClass definitions.ShortClass diff --git a/test/files/jvm/bytecode-test-example.flags b/test/files/jvm/bytecode-test-example.flags new file mode 100644 index 0000000000..bc22511cff --- /dev/null +++ b/test/files/jvm/bytecode-test-example.flags @@ -0,0 +1 @@ +-Yopt:l:none diff --git a/test/files/run/t7852.flags b/test/files/run/t7852.flags index f6262fd3e0..bc22511cff 100644 --- a/test/files/run/t7852.flags +++ b/test/files/run/t7852.flags @@ -1 +1 @@ --Ynooptimise +-Yopt:l:none diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index 1f2ec274d3..69c3f69380 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -166,6 +166,19 @@ object CodeGenTools { assertTrue(s"\nExpected: $expected\nActual : $actual", actual === expected) } + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } + def getSingleMethod(classNode: ClassNode, name: String): Method = convertMethod(classNode.methods.asScala.toList.find(_.name == name).get) @@ -179,7 +192,7 @@ object CodeGenTools { def findInstr(method: MethodNode, query: String): List[AbstractInsnNode] = { val useNext = query(0) == '+' val instrPart = if (useNext) query.drop(1) else query - val insns = method.instructions.iterator.asScala.find(i => textify(i) contains instrPart).toList + val insns = method.instructions.iterator.asScala.filter(i => textify(i) contains instrPart).toList if (useNext) insns.map(_.getNext) else insns } @@ -195,4 +208,8 @@ object CodeGenTools { implicit class MortalInstruction(val ins: Instruction) extends AnyVal { def dead: (Instruction, Boolean) = (ins, false) } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala index a7d1dc168a..daff6fc223 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala @@ -33,7 +33,7 @@ class NullnessAnalyzerTest extends ClearAfterClass { val noOptCompiler = NullnessAnalyzerTest.noOptCompiler import noOptCompiler.genBCode.bTypes.backendUtils._ - def newNullnessAnalyzer(methodNode: MethodNode, classInternalName: InternalName = "C") = new AsmAnalyzer(methodNode, classInternalName, new NullnessAnalyzer) + def newNullnessAnalyzer(methodNode: MethodNode, classInternalName: InternalName = "C") = new AsmAnalyzer(methodNode, classInternalName, new NullnessAnalyzer(noOptCompiler.genBCode.bTypes)) def testNullness(analyzer: AsmAnalyzer[NullnessValue], method: MethodNode, query: String, index: Int, nullness: NullnessValue): Unit = { for (i <- findInstr(method, query)) { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala new file mode 100644 index 0000000000..11014f5e64 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala @@ -0,0 +1,63 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.analysis.{AliasingFrame, AliasingAnalyzer} + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import BackendReporting._ +import BytecodeUtils._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object AnalyzerTest extends ClearAfterClass.Clearable { + var noOptCompiler = newCompiler(extraArgs = "-Yopt:l:none") + def clear(): Unit = { noOptCompiler = null } +} + +@RunWith(classOf[JUnit4]) +class AnalyzerTest extends ClearAfterClass { + ClearAfterClass.stateToClear = AnalyzerTest + val noOptCompiler = AnalyzerTest.noOptCompiler + + @Test + def aliasingOfPrimitives(): Unit = { + val code = + """class C { + | def f(a: Int, b: Long) = { + | val c = a - b // a is converted with i2l + | val d = c + | val e = a + | // locals: 0 this -- 1 a -- 2-3 b -- 4-5 c -- 6-7 d -- 8 e + | e + d // e is converted with i2l + | } + |} + """.stripMargin + + val List(c) = compileClasses(noOptCompiler)(code) + val a = new AliasingAnalyzer(new BasicInterpreter) + val f = findAsmMethod(c, "f") + a.analyze("C", f) + + val List(_, i2l) = findInstr(f, "I2L") + val aliasesAtI2l = a.frameAt(i2l, f).asInstanceOf[AliasingFrame[_]].aliases + assertEquals(aliasesAtI2l(1).iterator.toList, List(1, 8, 9)) // a, e and stack top + assertEquals(aliasesAtI2l(4).iterator.toList, List(4, 6)) + + val List(add) = findInstr(f, "LADD") + val aliasesAtAdd = a.frameAt(add, f).asInstanceOf[AliasingFrame[_]].aliases + assertEquals(aliasesAtAdd(1).iterator.toList, List(1, 8)) // after i2l the value on the stack is no longer an alias + assertEquals(aliasesAtAdd(4).iterator.toList, List(4, 6, 10)) // c, d and stack top + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala index 735bbefbbb..b314643fb1 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -49,7 +49,7 @@ class ClosureOptimizerTest extends ClearAfterClass { """.stripMargin val List(c) = compileClasses(compiler)(code) - val t = c.methods.asScala.toList.find(_.name == "t").get + val t = findAsmMethod(c, "t") val List(bodyCall) = findInstr(t, "INVOKESTATIC C.C$$$anonfun$1 ()Lscala/runtime/Nothing$") assert(bodyCall.getNext.getOpcode == ATHROW) } @@ -65,9 +65,45 @@ class ClosureOptimizerTest extends ClearAfterClass { """.stripMargin val List(c) = compileClasses(compiler)(code) - val t = c.methods.asScala.toList.find(_.name == "t").get + val t = findAsmMethod(c, "t") val List(bodyCall) = findInstr(t, "INVOKESTATIC C.C$$$anonfun$1 ()Lscala/runtime/Null$") assert(bodyCall.getNext.getOpcode == POP) assert(bodyCall.getNext.getNext.getOpcode == ACONST_NULL) } + + @Test + def makeLMFCastExplicit(): Unit = { + val code = + """class C { + | def t(l: List[String]) = { + | val fun: String => String = s => s + | fun(l.head) + | } + |} + """.stripMargin + val List(c) = compileClasses(compiler)(code) + assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, + List(VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/collection/immutable/List", "head", "()Ljava/lang/Object;", false), + TypeOp(CHECKCAST, "java/lang/String"), Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(Ljava/lang/String;)Ljava/lang/String;", false), + Op(ARETURN))) + } + + @Test + def closureOptWithUnreachableCode(): Unit = { + // this example used to crash the ProdCons analysis in the closure optimizer - ProdCons + // expects no unreachable code. + val code = + """class C { + | @inline final def m = throw new Error("") + | def t = { + | val f = (x: Int) => x + 1 + | m + | f(10) // unreachable after inlining m + | } + |} + """.stripMargin + val List(c) = compileClasses(compiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.summary, + List(NEW, DUP, LDC, "<init>", ATHROW)) + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala index 5c9bd1c188..44b6c7ca9e 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -22,7 +22,6 @@ object InlinerSeparateCompilationTest { @RunWith(classOf[JUnit4]) class InlinerSeparateCompilationTest { import InlinerSeparateCompilationTest._ - import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} @Test def inlnieMixedinMember(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 1e01627969..192a036d5b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -23,8 +23,9 @@ import scala.collection.convert.decorateAsScala._ import scala.tools.testing.ClearAfterClass object InlinerTest extends ClearAfterClass.Clearable { - val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings" + val args = "-Yopt:l:classpath -Yopt-warnings" var compiler = newCompiler(extraArgs = args) + var inlineOnlyCompiler = newCompiler(extraArgs = "-Yopt:inline-project") // allows inspecting the caches after a compilation run def notPerRun: List[Clearable] = List( @@ -34,37 +35,20 @@ object InlinerTest extends ClearAfterClass.Clearable { compiler.genBCode.bTypes.callGraph.callsites) notPerRun foreach compiler.perRunCaches.unrecordCache - def clear(): Unit = { compiler = null } - - implicit class listStringLines[T](val l: List[T]) extends AnyVal { - def stringLines = l.mkString("\n") - } - - def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) - def assertNoInvoke(ins: List[Instruction]): Unit = { - assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) - } - - def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) - def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { - assert(l.exists { - case Invoke(_, `receiver`, `method`, _, _) => true - case _ => false - }, l.stringLines) - } + def clear(): Unit = { compiler = null; inlineOnlyCompiler = null } } @RunWith(classOf[JUnit4]) class InlinerTest extends ClearAfterClass { ClearAfterClass.stateToClear = InlinerTest - import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} - val compiler = InlinerTest.compiler import compiler.genBCode.bTypes._ import compiler.genBCode.bTypes.backendUtils._ import inlinerHeuristics._ + val inlineOnlyCompiler = InlinerTest.inlineOnlyCompiler + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { InlinerTest.notPerRun.foreach(_.clear()) compileClasses(compiler)(scalaCode, javaCode, allowMessage) @@ -148,15 +132,22 @@ class InlinerTest extends ClearAfterClass { // See also discussion around ATHROW in BCodeBodyBuilder val g = inlineTest(code) - val expectedInlined = List( - VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this - Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ??? - assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined) + val invokeQQQ = List( + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), + Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) + + val gBeforeLocalOpt = VarOp(ALOAD, 0) :: VarOp(ASTORE, 1) :: invokeQQQ ::: List( + VarOp(ASTORE, 2), + Jump(GOTO, Label(11)), + Label(11), + VarOp(ALOAD, 2), + Op(ATHROW)) + + assertSameCode(convertMethod(g).instructions.dropNonOp, gBeforeLocalOpt) compiler.genBCode.bTypes.localOpt.methodOptimizations(g, "C") - assertSameCode(convertMethod(g).instructions.dropNonOp, - expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW))) + assertSameCode(convertMethod(g).instructions.dropNonOp, invokeQQQ :+ Op(ATHROW)) } @Test @@ -396,7 +387,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val List(c) = compile(code) + // use a compiler without local optimizations (cleanups) + val List(c) = compileClasses(inlineOnlyCompiler)(code) val ms @ List(f1, f2, g1, g2) = c.methods.asScala.filter(_.name.length == 2).toList // stack height at callsite of f1 is 1, so max of g1 after inlining is max of f1 + 1 @@ -959,7 +951,7 @@ class InlinerTest extends ClearAfterClass { val List(c) = compile(code) val t = getSingleMethod(c, "t").instructions assertNoInvoke(t) - assert(2 == t.collect({case Ldc(_, "hai!") => }).size) // twice the body of f + assert(1 == t.collect({case Ldc(_, "hai!") => }).size) // push-pop eliminates the first LDC("hai!") assert(1 == t.collect({case Jump(IFNONNULL, _) => }).size) // one single null check } @@ -985,17 +977,17 @@ class InlinerTest extends ClearAfterClass { val List(c, _, _) = compile(code) val t1 = getSingleMethod(c, "t1") - assert(t1.instructions exists { - case _: InvokeDynamic => true - case _ => false + assert(t1.instructions forall { // indy is eliminated by push-pop + case _: InvokeDynamic => false + case _ => true }) // the indy call is inlined into t, and the closure elimination rewrites the closure invocation to the body method assertInvoke(t1, "C", "C$$$anonfun$2") val t2 = getSingleMethod(c, "t2") - assert(t2.instructions exists { - case _: InvokeDynamic => true - case _ => false + assert(t2.instructions forall { // indy is eliminated by push-pop + case _: InvokeDynamic => false + case _ => true }) assertInvoke(t2, "M$", "M$$$anonfun$1") } @@ -1215,4 +1207,265 @@ class InlinerTest extends ClearAfterClass { assertNoInvoke(getSingleMethod(c, "g")) assertNoInvoke(getSingleMethod(d, "t")) } + + @Test + def optimizeSpecializedClosures(): Unit = { + val code = + """class ValKl(val x: Int) extends AnyVal + | + |class C { + | def t1 = { + | // IndyLambda: SAM type is JFunction1$mcII$sp, SAM is apply$mcII$sp(I)I, body method is $anonfun(I)I + | val f = (x: Int) => x + 1 + | // invocation of apply$mcII$sp(I)I, matches the SAM in IndyLambda. no boxing / unboxing needed. + | f(10) + | // opt: re-write the invocation to the body method + | } + | + | @inline final def m1a(f: Long => Int) = f(1l) + | def t1a = m1a(l => l.toInt) // after inlining m1a, we have the same situation as in t1 + | + | def t2 = { + | // there is no specialized variant of Function2 for this combination of types, so the IndyLambda has to create a generic Function2. + | // IndyLambda: SAM type is JFunction2, SAM is apply(ObjectObject)Object, body method is $anonfun$adapted(ObjectObject)Object + | val f = (b: Byte, i: Int) => i + b + | // invocation of apply(ObjectOjbect)Object, matches SAM in IndyLambda. arguments are boxed, result unboxed. + | f(1, 2) + | // opt: re-wrtie to $anonfun$adapted + | // inline that call, then we get box-unbox pairs (can be eliminated) and a call to $anonfun(BI)I + | } + | + | def t3 = { + | // similar to t2: for functions with value class parameters, IndyLambda always uses the generic Function version. + | // IndyLambda: SAM type is JFunction1, SAM is apply(Object)Object, body method is $anonfun$adapted(Object)Object + | val f = (a: ValKl) => a + | // invocation of apply(Object)Object, ValKl instance is created, result extracted + | f(new ValKl(1)) + | // opt: re-write to $anonfun$adapted. + | // inline that call, then we get value class instantiation-extraction pairs and a call to $anonfun(I)I + | } + | + | def t4 = { + | // IndyLambda: SAM type is JFunction1$mcII$sp, SAM is apply$mcII$sp(I)I, body method is $anonfun(I)I + | val f: Int => Any = (x: Int) => 1 + | // invocation of apply(Object)Object, argument is boxed. method name and type doesn't match IndyLambda. + | f(10) + | // opt: rewriting to the body method requires inserting an unbox operation for the argument, and a box operation for the result + | // that produces a box-unbox pair and a call to $anonfun(I)I + | } + | + | + | @inline final def m4a[T, U, V](f: (T, U) => V, x: T, y: U) = f(x, y) // invocation to generic apply(ObjectObject)Object + | def t4a = m4a((x: Int, y: Double) => 1l + x + y.toLong, 1, 2d) // IndyLambda uses specilized JFunction2$mcJID$sp. after inlining m4a, similar to t4. + | + | def t5 = { + | // no specialization for the comibnation of primitives + | // IndyLambda: SAM type is JFunction2, SAM is generic apply, body method is $anonfun$adapted + | val f: (Int, Byte) => Any = (x: Int, b: Byte) => 1 + | // invocation of generic apply. + | f(10, 3) + | // opt: re-write to $anonfun$adapted, inline that method. generates box-unbox pairs and a call to $anonfun(IB)I + | } + | + | def t5a = m4a((x: Int, y: Byte) => 1, 12, 31.toByte) // similar to t5 after inlining m4a + | + | // m6$mIVc$sp invokes apply$mcVI$sp + | @inline final def m6[@specialized(Int) T, @specialized(Unit) U](f: T => U, x: T): Unit = f(x) + | // IndyLambda: JFunction1$mcVI$sp, SAM is apply$mcVI$sp, body method $anonfun(I)V + | // invokes m6$mIVc$sp (Lscala/Function1;I)V + | def t6 = m6((x: Int) => (), 10) + | // opt: after inlining m6, the closure method invocation (apply$mcVI$sp) matches the IndyLambda, the call can be rewritten, no boxing + | + | // m7 invokes apply + | @inline final def m7[@specialized(Boolean) T, @specialized(Int) U](f: T => U, x: T): Unit = f(x) + | // IndyLambda: JFunction1, SAM is apply(Object)Object, body method is $anonfun$adapted(Obj)Obj + | // `true` is boxed before passing to m7 + | def t7 = m7((x: Boolean) => (), true) + | // opt: after inlining m7, the apply call is re-written to $anonfun$adapted, which is then inlined. + | // we get a box-unbox pair and a call to $anonfun(Z)V + | + | + | // invokes the generic apply(ObjObj)Obj + | @inline final def m8[T, U, V](f: (T, U) => V, x: T, y: U) = f(x, y) + | // IndyLambda: JFunction2$mcJID$sp, SAM is apply$mcJID$sp, body method $anonfun(ID)J + | // boxes the int and double arguments and calls m8, unboxToLong the result + | def t8 = m8((x: Int, y: Double) => 1l + x + y.toLong, 1, 2d) + | // opt: after inlining m8, rewrite to the body method $anonfun(ID)J, which requires inserting unbox operations for the params, box for the result + | // the box-unbox pairs can then be optimized away + | + | // m9$mVc$sp invokes apply$mcVI$sp + | @inline final def m9[@specialized(Unit) U](f: Int => U): Unit = f(1) + | // IndyLambda: JFunction1, SAM is apply(Obj)Obj, body method $anonfun$adapted(Ojb)Obj + | // invocation of m9$mVc$sp + | def t9 = m9(println) + | // opt: after inlining m9, rewrite to $anonfun$adapted(Ojb)Obj, which requires inserting a box operation for the parameter. + | // then we inline $adapted, which has signature (Obj)V. the `BoxedUnit.UNIT` from the body of $anonfun$adapted is eliminated by push-pop + | + | def t9a = (1 to 10) foreach println // similar to t9 + | + | def intCons(i: Int): Unit = () + | // IndyLambda: JFunction1$mcVI$sp, SAM is apply$mcVI$sp, body method $anonfun(I)V + | def t10 = m9(intCons) + | // after inlining m9, rewrite the apply$mcVI$sp call to the body method, no adaptations required + | + | def t10a = (1 to 10) foreach intCons // similar to t10 + |} + """.stripMargin + val List(c, _, _) = compile(code) + + assertEquals(getSingleMethod(c, "t1").instructions.summary, + List(BIPUSH, "C$$$anonfun$1", IRETURN)) + + assertEquals(getSingleMethod(c, "t1a").instructions.summary, + List(LCONST_1, "C$$$anonfun$2", IRETURN)) + + assertEquals(getSingleMethod(c, "t2").instructions.summary, List( + ICONST_1, ICONST_2, "C$$$anonfun$3",IRETURN)) + + // val a = new ValKl(n); new ValKl(anonfun(a.x)).x + // value class instantiation-extraction should be optimized by boxing elim + assertEquals(getSingleMethod(c, "t3").instructions.summary, List( + NEW, DUP, ICONST_1, "<init>", ASTORE, + NEW, DUP, ALOAD, "x", + "C$$$anonfun$4", + "<init>", + "x", IRETURN)) + + assertEquals(getSingleMethod(c, "t4").instructions.summary, List( + BIPUSH, "C$$$anonfun$5", "boxToInteger", ARETURN)) + + assertEquals(getSingleMethod(c, "t4a").instructions.summary, List( + ICONST_1, LDC, "C$$$anonfun$6", LRETURN)) + + assertEquals(getSingleMethod(c, "t5").instructions.summary, List( + BIPUSH, ICONST_3, "C$$$anonfun$7", "boxToInteger", ARETURN)) + + assertEquals(getSingleMethod(c, "t5a").instructions.summary, List( + BIPUSH, BIPUSH, I2B, "C$$$anonfun$8", IRETURN)) + + assertEquals(getSingleMethod(c, "t6").instructions.summary, List( + BIPUSH, "C$$$anonfun$9", RETURN)) + + assertEquals(getSingleMethod(c, "t7").instructions.summary, List( + ICONST_1, "C$$$anonfun$10", RETURN)) + + assertEquals(getSingleMethod(c, "t8").instructions.summary, List( + ICONST_1, LDC, "C$$$anonfun$11", LRETURN)) + + assertEquals(getSingleMethod(c, "t9").instructions.summary, List( + ICONST_1, "boxToInteger", "C$$$anonfun$12", RETURN)) + + // t9a inlines Range.foreach, which is quite a bit of code, so just testing the core + assertInvoke(getSingleMethod(c, "t9a"), "C", "C$$$anonfun$13") + assert(getSingleMethod(c, "t9a").instructions.summary.contains("boxToInteger")) + + assertEquals(getSingleMethod(c, "t10").instructions.summary, List( + ICONST_1, ISTORE, + ALOAD, ILOAD, + "C$$$anonfun$14", RETURN)) + + // t10a inlines Range.foreach + assertInvoke(getSingleMethod(c, "t10a"), "C", "C$$$anonfun$15") + assert(!getSingleMethod(c, "t10a").instructions.summary.contains("boxToInteger")) + } + + @Test + def refElimination(): Unit = { + val code = + """class C { + | def t1 = { + | var i = 0 + | @inline def inner() = i += 1 + | inner() + | i + | } + | + | final def m(f: Int => Unit) = f(10) + | def t2 = { + | var x = -1 // IntRef not yet eliminated: closure elimination does not + | m(i => if (i == 10) x = 1) // yet inline the anonfun method, need to improve the heuristsics + | x + | } + |} + """.stripMargin + val List(c) = compile(code) + assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(Op(ICONST_0), Op(ICONST_1), Op(IADD), Op(IRETURN))) + assertEquals(getSingleMethod(c, "t2").instructions collect { case i: Invoke => i.owner +"."+ i.name }, List( + "scala/runtime/IntRef.create", "C.C$$$anonfun$1")) + } + + @Test + def tupleElimination(): Unit = { + val code = + """class C { + | @inline final def tpl[A, B](a: A, b: B) = (a, b) + | @inline final def t_1[A, B](t: (A, B)) = t._1 + | @inline final def t_2[A, B](t: (A, B)) = t._2 + | + | def t1 = { + | val t = (3, 4) // specialized tuple + | t_1(t) + t_2(t) // invocations to generic _1 / _2, box operation inserted when eliminated + | } + | + | def t2 = { + | val t = tpl(1, 2) // generic Tuple2[Integer, Integer] created + | t._1 + t._2 // invokes the specialized _1$mcI$sp, eliminating requires adding an unbox operation + | } + | + | @inline final def m = (1, 3) + | def t3 = { + | val (a, b) = m + | a - b + | } + | + | def t4 = { + | val ((a, b), (c, d)) = (m, m) + | a + b + c + d + | } + | + | def t5 = m match { + | case (1, y) => y + | case (x, y) => x * y + | } + |} + """.stripMargin + val List(c) = compile(code) + assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(Op(ICONST_3), Op(ICONST_4), Op(IADD), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(Op(ICONST_1), Op(ICONST_2), Op(IADD), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t3").instructions.dropNonOp, List(Op(ICONST_1), Op(ICONST_3), Op(ISUB), Op(IRETURN))) + assertNoInvoke(getSingleMethod(c, "t4")) + assertNoInvoke(getSingleMethod(c, "t5")) + } + + @Test + def redundantCasts(): Unit = { + + // we go through the hoop of inlining the casts because erasure eliminates `asInstanceOf` calls + // that are statically known to succeed. For example the following cast is removed by erasure: + // `(if (b) c else d).asInstanceOf[C]` + + val code = + """class C { + | @inline final def asO(a: Any) = a.asInstanceOf[Object] + | @inline final def asC(a: Any) = a.asInstanceOf[C] + | @inline final def asD(a: Any) = a.asInstanceOf[D] + | + | def t1(c: C) = asC(c) // eliminated + | def t2(c: C) = asO(c) // eliminated + | def t3(c: Object) = asC(c) // not elimianted + | def t4(c: C, d: D, b: Boolean) = asC(if (b) c else d) // not eliminated: lub of two non-equal reference types approximated with Object + | def t5(c: C, d: D, b: Boolean) = asO(if (b) c else d) + | def t6(c: C, cs: Array[C], b: Boolean) = asO(if (b) c else cs) + |} + |class D extends C + """.stripMargin + val List(c, _) = compile(code) + def casts(m: String) = getSingleMethod(c, m).instructions collect { case TypeOp(CHECKCAST, tp) => tp } + assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t3").instructions.dropNonOp, List(VarOp(ALOAD, 1), TypeOp(CHECKCAST, "C"), Op(ARETURN))) + assertEquals(casts("t4"), List("C")) + assertEquals(casts("t5"), Nil) + assertEquals(casts("t6"), Nil) + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala deleted file mode 100644 index 8d910629ca..0000000000 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ /dev/null @@ -1,92 +0,0 @@ -package scala.tools.nsc -package backend.jvm -package opt - -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.Test -import scala.tools.asm.Opcodes._ -import org.junit.Assert._ - -import scala.tools.testing.AssertUtil._ - -import CodeGenTools._ -import scala.tools.partest.ASMConverters -import ASMConverters._ -import scala.tools.testing.ClearAfterClass - -object MethodLevelOpts extends ClearAfterClass.Clearable { - var methodOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") - def clear(): Unit = { methodOptCompiler = null } -} - -@RunWith(classOf[JUnit4]) -class MethodLevelOpts extends ClearAfterClass { - ClearAfterClass.stateToClear = MethodLevelOpts - - val methodOptCompiler = MethodLevelOpts.methodOptCompiler - - def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) - - @Test - def eliminateEmptyTry(): Unit = { - val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" - val warn = "a pure expression does nothing in statement position" - assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) - } - - @Test - def cannotEliminateLoadBoxedUnit(): Unit = { - // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. - val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" - val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 1) - assertSameCode(m.instructions.take(3), List(Label(0), LineNumber(1, Label(0)), Field(GETSTATIC, "scala/runtime/BoxedUnit", "UNIT", "Lscala/runtime/BoxedUnit;"))) - } - - @Test - def inlineThrowInCatchNotTry(): Unit = { - // the try block does not contain the `ATHROW` instruction, but in the catch block, `ATHROW` is inlined - val code = "def f(e: Exception) = throw { try e catch { case _: Throwable => e } }" - val m = singleMethod(methodOptCompiler)(code) - assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) - assertSameCode(m.instructions, - wrapInDefault(VarOp(ALOAD, 1), Label(3), Op(ATHROW), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), VarOp(ALOAD, 1), Op(ATHROW)) - ) - } - - @Test - def inlineReturnInCatchNotTry(): Unit = { - val code = "def f: Int = return { try 1 catch { case _: Throwable => 2 } }" - // cannot inline the IRETURN into the try block (because RETURN may throw IllegalMonitorState) - val m = singleMethod(methodOptCompiler)(code) - assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) - assertSameCode(m.instructions, - wrapInDefault(Op(ICONST_1), Label(3), Op(IRETURN), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), Op(ICONST_2), Op(IRETURN))) - } - - @Test - def simplifyJumpsInTryCatchFinally(): Unit = { - val code = - """def f: Int = - | try { - | return 1 - | } catch { - | case _: Throwable => - | return 2 - | } finally { - | return 2 - | // dead - | val x = try 10 catch { case _: Throwable => 11 } - | println(x) - | } - """.stripMargin - val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 2) - assertSameCode(m.instructions.dropNonOp, // drop line numbers and labels that are only used by line numbers - - // one single label left :-) - List(Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(20)), Op(POP), Op(ICONST_2), VarOp(ISTORE, 2), Jump(GOTO, Label(20)), VarOp(ASTORE, 3), Op(ICONST_2), Op(IRETURN), Label(20), Op(ICONST_2), Op(IRETURN)) - ) - } -} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala new file mode 100644 index 0000000000..46e1cebfc4 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -0,0 +1,562 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.nsc.backend.jvm.AsmUtils._ +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object MethodLevelOptsTest extends ClearAfterClass.Clearable { + var methodOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") + def clear(): Unit = { methodOptCompiler = null } +} + +@RunWith(classOf[JUnit4]) +class MethodLevelOptsTest extends ClearAfterClass { + ClearAfterClass.stateToClear = MethodLevelOptsTest + + val methodOptCompiler = MethodLevelOptsTest.methodOptCompiler + + def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) + + @Test + def eliminateEmptyTry(): Unit = { + val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" + val warn = "a pure expression does nothing in statement position" + assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) + } + + @Test + def eliminateLoadBoxedUnit(): Unit = { + // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. + val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" + val m = singleMethod(methodOptCompiler)(code) + assertTrue(m.handlers.length == 0) + assertSameCode(m.instructions.dropNonOp, + List(Op(ICONST_1), Op(IRETURN))) + } + + @Test + def inlineThrowInCatchNotTry(): Unit = { + // the try block does not contain the `ATHROW` instruction, but in the catch block, `ATHROW` is inlined + val code = "def f(e: Exception) = throw { try e catch { case _: Throwable => e } }" + val m = singleMethod(methodOptCompiler)(code) + assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) + assertSameCode(m.instructions, + wrapInDefault(VarOp(ALOAD, 1), Label(3), Op(ATHROW), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), VarOp(ALOAD, 1), Op(ATHROW)) + ) + } + + @Test + def inlineReturnInCatchNotTry(): Unit = { + val code = "def f: Int = return { try 1 catch { case _: Throwable => 2 } }" + // cannot inline the IRETURN into the try block (because RETURN may throw IllegalMonitorState) + val m = singleMethod(methodOptCompiler)(code) + assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) + assertSameCode(m.instructions, + wrapInDefault(Op(ICONST_1), Label(3), Op(IRETURN), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), Op(ICONST_2), Op(IRETURN))) + } + + @Test + def simplifyJumpsInTryCatchFinally(): Unit = { + val code = + """def f: Int = + | try { + | return 1 + | } catch { + | case _: Throwable => + | return 2 + | } finally { + | return 3 + | // dead + | val x = try 10 catch { case _: Throwable => 11 } + | println(x) + | } + """.stripMargin + val m = singleMethod(methodOptCompiler)(code) + assertTrue(m.handlers.isEmpty) + assertSameCode(m.instructions.dropNonOp, List(Op(ICONST_3), Op(IRETURN))) + } + + @Test + def nullStoreLoadElim(): Unit = { + // point of this test: we have two cleanups + // - remove `ACONST_NULL; ASTORE x` if x is otherwise not live + // - remove `ASTORE x; ALOAD x` if x is otherwise not live + // in the example below, we have `ACONST_NULL; ASTORE x; ALOAD x`. in this case the store-load + // should be removed (even though it looks like a null-store at first). + val code = + """class C { + | def t = { + | val x = null + | x.toString + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, + List(Op(ACONST_NULL), Invoke(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false), Op(ARETURN))) + } + + @Test + def deadStoreReferenceElim(): Unit = { + val code = + """class C { + | def t = { + | var a = "a" // assign to non-initialized, directly removed by dead store + | a = "b" // assign to initialized, replaced by null-store, which is then removed: the var is not live, the uses are null-store or store-load + | a = "c" + | a // store-load pair will be eliminated + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t").instructions.dropNonOp, + List(Ldc(LDC, "c"), Op(ARETURN))) + } + + @Test + def deadStoreReferenceKeepNull(): Unit = { + val code = + """class C { + | def t = { + | var a = "el" // this store is live, used in the println. + | println(a) + | a = "met" // since it's an ASTORE to a live variable, cannot elim the store (SI-5313), but store null instead. + | // so we get `LDC met; POP; ACONST_NULL; ASTORE 1`. the `LDC met; POP` is eliminated by push-pop. + | a = "zit" // this store is live, so we get `LDC zit; ASOTRE 1; ALOAD 1; ARETURN`. + | // we cannot eliminated the store-load sequence, because the local is live (again SI-5313). + | a + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + + assertEquals( + getSingleMethod(c, "t").instructions.dropNonOp, + List( + Ldc(LDC, "el"), VarOp(ASTORE, 1), + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/Predef$", "println", "(Ljava/lang/Object;)V", false), + Op(ACONST_NULL), VarOp(ASTORE, 1), + Ldc(LDC, "zit"), VarOp(ASTORE, 1), VarOp(ALOAD, 1), Op(ARETURN))) + } + + @Test + def elimUnusedTupleObjectStringBox(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = (x, y) // Tuple2$mcII$sp + | val b = (a, y) // Tuple2 + | val c = (new Object, "krik", new String) // unused java/lang/Object, java/lang/String allocation and string constant is also eliminated + | val d = new java.lang.Integer(x) + | val e = new String(new Array[Char](23)) // array allocation not eliminated, as it may throw (negative size, SI-8601) + | val f = new scala.runtime.IntRef(11) + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(IntOp(BIPUSH, 23), IntOp(NEWARRAY, 5), Op(POP), VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def noElimImpureConstructor(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = new java.lang.Integer("nono") + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(TypeOp(NEW, "java/lang/Integer"), Ldc(LDC, "nono"), Invoke(INVOKESPECIAL, "java/lang/Integer", "<init>", "(Ljava/lang/String;)V", false), + VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def elimUnusedBoxUnbox(): Unit = { + val code = + """class C { + | def t(a: Long): Int = { + | val t = 3 + a + | val u = a + t + | val v: Any = u // scala/runtime/BoxesRunTime.boxToLong + | + | val w = (v, a) // a Tuple2 (not specialized because first value is Any) + | // so calls scala/runtime/BoxesRunTime.boxToLong on the second value + | + | val x = v.asInstanceOf[Long] // scala/runtime/BoxesRunTime.unboxToLong + | + | val z = (java.lang.Long.valueOf(a), t) // java box call on the left, scala/runtime/BoxesRunTime.boxToLong on the right + | + | 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(Op(ICONST_0), Op(IRETURN))) + } + + @Test + def elimUnusedClosure(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val f = (a: Int) => a + x + y + | val g = (b: Int) => b - x + | val h = (s: String) => println(s) + | f(30) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals( + getSingleMethod(c, "t").instructions.dropNonOp, + List( + IntOp(BIPUSH, 30), VarOp(ISTORE, 3), // no constant propagation, so we keep the store (and load below) of a const + VarOp(ILOAD, 1), + VarOp(ILOAD, 2), + VarOp(ILOAD, 3), + Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(III)I", false), Op(IRETURN))) + } + + @Test + def rewriteSpecializedClosureCall(): Unit = { + val code = + """class C { + | def t = { + | val f1 = (x: Int) => println(x) // int-unit specialization + | val f2 = (x: Int, y: Long) => x == y // int-long-boolean + | f1(1) + | f2(3, 4) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + val t = getSingleMethod(c, "t") + assert(!t.instructions.exists(_.opcode == INVOKEDYNAMIC), t) + } + + @Test + def boxUnboxPrimitive(): Unit = { + val code = + """class C { + | def t1 = { + | val a: Any = runtime.BoxesRunTime.boxToInteger(1) + | runtime.BoxesRunTime.unboxToInt(a) + 1 + | } + | + | // two box and two unbox operations + | def t2(b: Boolean) = { + | val a = if (b) (3l: Any) else 2l + | a.asInstanceOf[Long] + 1 + a.asInstanceOf[Long] + | } + | + | def t3(i: Integer): Int = i.asInstanceOf[Int] + | + | def t4(l: Long): Any = l + | + | def t5(i: Int): Int = { + | val b = Integer.valueOf(i) + | val c: Integer = i + | b.asInstanceOf[Int] + c.intValue + | } + | + | def t6: Long = { + | val y = new java.lang.Boolean(true) + | val i: Integer = if (y) new Integer(10) else 13 + | val j: java.lang.Long = 3l + | j + i + | } + | + | def t7: Int = { + | val a: Any = 3 + | a.asInstanceOf[Int] + a.asInstanceOf[Int] + | } + | + | def t8 = null.asInstanceOf[Int] + | + | def t9: Int = { + | val a = Integer.valueOf(10) + | val b = runtime.BoxesRunTime.unboxToInt(a) + | a + b + | } + | + | @noinline def escape(a: Any) = () + | + | // example E4 in BoxUnbox doc comment + | def t10: Int = { + | val a = Integer.valueOf(10) // int 10 is stored into local + | escape(a) + | a // no unbox, 10 is read from local + | } + | + | // the boxes here cannot be eliminated. see doc comment in BoxUnbox, example E1. + | def t11(b: Boolean): Int = { + | val i = Integer.valueOf(10) + | val j = Integer.valueOf(41) + | escape(i) // force rewrite method M1 (see doc in BoxUnbox) + | val res: Integer = if (b) i else j + | res.toInt // cannot be re-written to a local variable read - we don't know which local to read + | } + | + | // both boxes have a single unboxing consumer, and the escape. note that the escape does + | // NOT put the two boxes into the same set of rewrite operations: we can rewrite both + | // boxes with their unbox individually. in both cases the box also escapes, so method + | // M1 will keep the box around. + | def t12(b: Boolean): Int = { + | val i = Integer.valueOf(10) + | val j = Integer.valueOf(32) + | escape(if (b) i else j) // force method M1. the escape here is a consumer for both boxes + | if (b) i.toInt else j.toInt // both boxes (i, j) have their own unboxing consumer + | } + |} + """.stripMargin + + val List(c) = compileClasses(methodOptCompiler)(code) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "scala/runtime/BoxesRunTime", "unboxToInt") + assertInvoke(getSingleMethod(c, "t4"), "scala/runtime/BoxesRunTime", "boxToLong") + assertNoInvoke(getSingleMethod(c, "t5")) + assertNoInvoke(getSingleMethod(c, "t6")) + assertNoInvoke(getSingleMethod(c, "t7")) + assertEquals(getSingleMethod(c, "t8").instructions.summary, List(ICONST_0, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t9")) + // t10: no invocation of unbox + assertEquals(getSingleMethod(c, "t10").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("java/lang/Integer", "valueOf"), + ("C", "escape"))) + + assertEquals(getSingleMethod(c, "t11").instructions.summary, List( + BIPUSH, "valueOf", ASTORE /*2*/, + BIPUSH, "valueOf", ASTORE /*3*/, + ALOAD /*0*/, ALOAD /*2*/, "escape", + ILOAD /*1*/, IFEQ /*L1*/, ALOAD /*2*/, GOTO /*L2*/, /*Label L1*/ -1, ALOAD /*3*/, /*Label L2*/ -1, + ASTORE /*4*/, GETSTATIC /*Predef*/, ALOAD /*4*/, "Integer2int", IRETURN)) + + // no unbox invocations + assertEquals(getSingleMethod(c, "t12").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("java/lang/Integer", "valueOf"), + ("java/lang/Integer", "valueOf"), + ("C", "escape"))) + } + + @Test + def refEliminiation(): Unit = { + val code = + """class C { + | import runtime._ + | @noinline def escape(a: Any) = () + | + | def t1 = { // box eliminated + | val r = new IntRef(0) + | r.elem + | } + | + | def t2(b: Boolean) = { + | val r1 = IntRef.zero() // both eliminated + | val r2 = IntRef.create(1) + | val res: IntRef = if (b) r1 else r2 + | res.elem + | } + | + | def t3 = { + | val r = LongRef.create(10l) // eliminated + | r.elem += 3 + | r.elem + | } + | + | def t4(b: Boolean) = { + | val x = BooleanRef.create(false) // eliminated + | if (b) x.elem = true + | if (x.elem) "a" else "b" + | } + | + | def t5 = { + | val r = IntRef.create(10) // not eliminated: the box might be modified in the escape + | escape(r) + | r.elem + | } + | + | def t6(b: Boolean) = { + | val r1 = IntRef.zero() + | val r2 = IntRef.create(1) + | r1.elem = 39 + | val res: IntRef = if (b) r1 else r2 + | res.elem // boxes remain: can't rewrite this read, don't know which local + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t1").instructions.summary, List(ICONST_0, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t2")) + assertEquals(getSingleMethod(c, "t3").instructions.summary, List(LDC, LDC, LADD, LRETURN)) + assertNoInvoke(getSingleMethod(c, "t4")) + assertEquals(getSingleMethod(c, "t5").instructions collect { case Field(_, owner, name, _) => s"$owner.$name" }, + List("scala/runtime/IntRef.elem")) + assertEquals(getSingleMethod(c, "t6").instructions collect { case Field(op, owner, name, _) => s"$op $owner.$name" }, + List(s"$PUTFIELD scala/runtime/IntRef.elem", s"$GETFIELD scala/runtime/IntRef.elem")) + } + + @Test + def tupleElimination(): Unit = { + val code = + """class C { + | def t1(b: Boolean) = { + | val t = ("hi", "fish") + | if (b) t._1 else t._2 + | } + | + | def t2 = { + | val t = (1, 3) // specialized tuple + | t._1 + t._2 // specialized accessors (_1$mcII$sp) + | } + | + | def t3 = { + | // boxed before tuple creation, a non-specialized tuple is created + | val t = (new Integer(3), Integer.valueOf(4)) + | t._1 + t._2 // invokes the generic `_1` / `_2` getters, both values unboxed by Integer2int + | } + | + | def t4: Any = { + | val t = (3, 3) // specialized tuple is created, ints are not boxed + | (t: Tuple2[Any, Any])._1 // when eliminating the _1 call, need to insert a boxing operation + | } + | + | // the inverse of t4 also happens: an Tuple[Integer] where _1$mcI$sp is invoked. In this + | // case, an unbox operation needs to be added when eliminating the extraction. The only + | // way I found to test this is with an inlined generic method, see InlinerTest.tupleElimination. + | def tpl[A, B](a: A, b: B) = (a, b) + | def t5: Int = tpl(1, 2)._1 // invokes _1$mcI$sp + | + | def t6 = { + | val (a, b) = (1, 2) + | a - b + | } + | + | def t7 = { + | // this example is more tricky to handle than it looks, see doc comment in BoxUnbox. + | val ((a, b), c) = ((1, 2), 3) + | a + b + c + | } + | + | def t8 = { + | val ((a, b), (c, d)) = ((1, 2), (3, Integer.valueOf(10))) + | a + b + c + d + | } + | + | def t9(a: Int, b: Int) = (a, b) match { // tuple is optimized away + | case (x, y) if x == y => 0 + | case (x, y) => x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertEquals(getSingleMethod(c, "t2").instructions.summary, List(ICONST_1, ICONST_3, IADD, IRETURN)) + assertEquals(getSingleMethod(c, "t3").instructions.summary, List(ICONST_3, ICONST_4, IADD, IRETURN)) + assertEquals(getSingleMethod(c, "t4").instructions.summary, List(ICONST_3, "boxToInteger", ARETURN)) + assertEquals(getSingleMethod(c, "t5").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("scala/runtime/BoxesRunTime", "boxToInteger"), + ("scala/runtime/BoxesRunTime", "boxToInteger"), + ("C", "tpl"), + ("scala/Tuple2", "_1$mcI$sp"))) + assertEquals(getSingleMethod(c, "t6").instructions.summary, List(ICONST_1, ICONST_2, ISUB, IRETURN)) + assertEquals(getSingleMethod(c, "t7").instructions.summary, List( + ICONST_1, ICONST_2, ISTORE, ISTORE, + ICONST_3, ISTORE, + ILOAD, ILOAD, IADD, ILOAD, IADD, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t8")) + assertNoInvoke(getSingleMethod(c, "t9")) + } + + @Test + def nullnessOpts(): Unit = { + val code = + """class C { + | def t1 = { + | val a = new C + | if (a == null) + | println() // eliminated + | a + | } + | + | def t2 = null.asInstanceOf[Long] // replaced by zero value + | + | def t3 = { + | val t = (1, 3) + | val a = null + | if (t ne a) t._1 + | else throw new Error() + | } + | + | def t4 = { + | val i = Integer.valueOf(1) + | val a = null + | if (i eq a) throw new Error() + | else i.toInt + | } + | + | def t5 = { + | val i = runtime.DoubleRef.zero() + | if (i == null) throw new Error() + | else i.elem + | } + | + | def t6 = { + | var a = null + | var i = null + | a = i // eliminated (store of null to variable that is already null) + | a // replaced by ACONST_NULL (load of variable that is known null) + | } + | + | def t7 = { + | val a = null + | a.isInstanceOf[String] // eliminated, replaced by 0 (null.isInstanceOf is always false) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t1").instructions.summary, List(NEW, DUP, "<init>", ARETURN)) + assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(Op(LCONST_0), Op(LRETURN))) + assertSameCode(getSingleMethod(c, "t3").instructions.dropNonOp, List(Op(ICONST_1), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t4").instructions.dropNonOp, List(Op(ICONST_1), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t5").instructions.dropNonOp, List(Op(DCONST_0), Op(DRETURN))) + assertSameCode(getSingleMethod(c, "t6").instructions.dropNonOp, List(Op(ACONST_NULL), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t7").instructions.dropNonOp, List(Op(ICONST_0), Op(IRETURN))) + } + + @Test + def elimRedundantNullCheck(): Unit = { + val code = + """class C { + | def t(x: Object) = { + | val bool = x == null + | if (x != null) 1 else 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t").instructions.dropNonOp, + List(VarOp(ALOAD, 1), Jump(IFNULL, Label(6)), Op(ICONST_1), Op(IRETURN), Label(6), Op(ICONST_0), Op(IRETURN))) + } +} |