Skip to content

Improve implementation of local lazy vals and synchronized blocks #5354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma
val Throwable_Type: Type = defn.ThrowableType
val Object_isInstanceOf: Symbol = defn.Any_isInstanceOf
val Object_asInstanceOf: Symbol = defn.Any_asInstanceOf
val Object_synchronized: Symbol = defn.Object_synchronized
val Object_equals: Symbol = defn.Any_equals
val ArrayClass: Symbol = defn.ArrayClass
val UnitClass: Symbol = defn.UnitClass
Expand Down
21 changes: 18 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -897,10 +897,25 @@ class Definitions {
// ----- Symbol sets ---------------------------------------------------

lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0)
val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun[Array[Symbol]](implicit ctx => AbstractFunctionType.map(_.symbol.asClass))
val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(implicit ctx => AbstractFunctionType.map(_.symbol.asClass))
def AbstractFunctionClass(n: Int)(implicit ctx: Context): Symbol = AbstractFunctionClassPerRun()(ctx)(n)
private lazy val ImplementedFunctionType = mkArityArray("scala.Function", MaxImplementedFunctionArity, 0)
def FunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun[Array[Symbol]](implicit ctx => ImplementedFunctionType.map(_.symbol.asClass))
def FunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(implicit ctx => ImplementedFunctionType.map(_.symbol.asClass))

val LazyHolder: PerRun[Map[Symbol, Symbol]] = new PerRun({ implicit ctx =>
def holderImpl(holderType: String) = ctx.requiredClass("scala.runtime." + holderType)
Map[Symbol, Symbol](
IntClass -> holderImpl("LazyInt"),
LongClass -> holderImpl("LazyLong"),
BooleanClass -> holderImpl("LazyBoolean"),
FloatClass -> holderImpl("LazyFloat"),
DoubleClass -> holderImpl("LazyDouble"),
ByteClass -> holderImpl("LazyByte"),
CharClass -> holderImpl("LazyChar"),
ShortClass -> holderImpl("LazyShort")
)
.withDefaultValue(holderImpl("LazyRef"))
})

lazy val TupleType: Array[TypeRef] = mkArityArray("scala.Tuple", MaxTupleArity, 1)

Expand Down Expand Up @@ -1059,7 +1074,7 @@ class Definitions {
lazy val NoInitClasses: Set[Symbol] = NotRuntimeClasses + FunctionXXLClass

def isPolymorphicAfterErasure(sym: Symbol): Boolean =
(sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf)
(sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf) || (sym eq Object_synchronized)

def isTupleType(tp: Type)(implicit ctx: Context): Boolean = {
val arity = tp.dealias.argInfos.length
Expand Down
93 changes: 50 additions & 43 deletions compiler/src/dotty/tools/dotc/transform/LazyVals.scala
Original file line number Diff line number Diff line change
Expand Up @@ -129,50 +129,56 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer {
Thicket(field, getter)
}

/** Replace a local lazy val inside a method,
* with a LazyHolder from
* dotty.runtime(eg dotty.runtime.LazyInt)
*/
/** Desugar a local `lazy val x: Int = <RHS>` into:
*
* ```
* val x$lzy = new scala.runtime.LazyInt()
*
* def x$lzycompute(): Int = x$lzy.synchronized {
* if (x$lzy.initialized()) x$lzy.value()
* else x$lzy.initialize(<RHS>)
* // TODO: Implement Unit-typed lazy val optimization described below
* // for a Unit-typed lazy val, this becomes `{ rhs ; x$lzy.initialize() }`
* // to avoid passing around BoxedUnit
* }
*
* def x(): Int = if (x$lzy.initialized()) x$lzy.value() else x$lzycompute()
* ```
*/
def transformLocalDef(x: ValOrDefDef)(implicit ctx: Context): Thicket = {
val valueInitter = x.rhs
val xname = x.name.asTermName
val holderName = LazyLocalName.fresh(xname)
val initName = LazyLocalInitName.fresh(xname)
val tpe = x.tpe.widen.resultType.widen

val holderType =
if (tpe isRef defn.IntClass) "LazyInt"
else if (tpe isRef defn.LongClass) "LazyLong"
else if (tpe isRef defn.BooleanClass) "LazyBoolean"
else if (tpe isRef defn.FloatClass) "LazyFloat"
else if (tpe isRef defn.DoubleClass) "LazyDouble"
else if (tpe isRef defn.ByteClass) "LazyByte"
else if (tpe isRef defn.CharClass) "LazyChar"
else if (tpe isRef defn.ShortClass) "LazyShort"
else "LazyRef"


val holderImpl = ctx.requiredClass("dotty.runtime." + holderType)

val holderSymbol = ctx.newSymbol(x.symbol.owner, holderName, containerFlags, holderImpl.typeRef, coord = x.pos)
val initSymbol = ctx.newSymbol(x.symbol.owner, initName, initFlags, MethodType(Nil, tpe), coord = x.pos)
val result = ref(holderSymbol).select(lazyNme.value).withPos(x.pos)
val flag = ref(holderSymbol).select(lazyNme.initialized)
val initer = valueInitter.changeOwnerAfter(x.symbol, initSymbol, this)
val initBody =
adaptToType(
ref(holderSymbol).select(defn.Object_synchronized).appliedTo(
adaptToType(mkNonThreadSafeDef(result, flag, initer, nullables = Nil), defn.ObjectType)),
tpe)
val initTree = DefDef(initSymbol, initBody)
val holderTree = ValDef(holderSymbol, New(holderImpl.typeRef, List()))
val methodBody = tpd.If(flag.ensureApplied,
result.ensureApplied,
ref(initSymbol).ensureApplied).ensureConforms(tpe)

val methodTree = DefDef(x.symbol.asTerm, methodBody)
ctx.debuglog(s"found a lazy val ${x.show},\nrewrote with ${holderTree.show}")
Thicket(holderTree, initTree, methodTree)
val xname = x.name.asTermName
val tpe = x.tpe.widen.resultType.widen

// val x$lzy = new scala.runtime.LazyInt()
val holderName = LazyLocalName.fresh(xname)
val holderImpl = defn.LazyHolder()(ctx)(tpe.typeSymbol)
val holderSymbol = ctx.newSymbol(x.symbol.owner, holderName, containerFlags, holderImpl.typeRef, coord = x.pos)
val holderTree = ValDef(holderSymbol, New(holderImpl.typeRef, Nil))

val holderRef = ref(holderSymbol)
val getValue = holderRef.select(lazyNme.value).ensureApplied.withPos(x.pos)
val initialized = holderRef.select(lazyNme.initialized).ensureApplied

// def x$lzycompute(): Int = x$lzy.synchronized {
// if (x$lzy.initialized()) x$lzy.value()
// else x$lzy.initialize(<RHS>)
// }
val initName = LazyLocalInitName.fresh(xname)
val initSymbol = ctx.newSymbol(x.symbol.owner, initName, initFlags, MethodType(Nil, tpe), coord = x.pos)
val rhs = x.rhs.changeOwnerAfter(x.symbol, initSymbol, this)
val initialize = holderRef.select(lazyNme.initialize).appliedTo(rhs)
val initBody = holderRef
.select(defn.Object_synchronized)
.appliedToType(tpe)
.appliedTo(If(initialized, getValue, initialize).ensureConforms(tpe))
val initTree = DefDef(initSymbol, initBody)

// def x(): Int = if (x$lzy.initialized()) x$lzy.value() else x$lzycompute()
val accessorBody = If(initialized, getValue, ref(initSymbol).ensureApplied).ensureConforms(tpe)
val accessor = DefDef(x.symbol.asTerm, accessorBody)

ctx.debuglog(s"found a lazy val ${x.show},\nrewrote with ${holderTree.show}")
Thicket(holderTree, initTree, accessor)
}


Expand Down Expand Up @@ -458,6 +464,7 @@ object LazyVals {
val result: TermName = "result".toTermName
val value: TermName = "value".toTermName
val initialized: TermName = "initialized".toTermName
val initialize: TermName = "initialize".toTermName
val retry: TermName = "retry".toTermName
}
}
Expand Down
46 changes: 46 additions & 0 deletions compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -500,4 +500,50 @@ class TestBCode extends DottyBytecodeTest {
liftReceiver)
}
}

/** Test that the size of the lazy val initialiazer is under a certain threshold
*
* - Fix to #5340 reduced the size from 39 instructions to 34
* - Fix to #505 reduced the size from 34 instructions to 32
*/
@Test def i5340 = {
val source =
"""class Test {
| def test = {
| lazy val x = 1
| x
| }
|}
""".stripMargin

checkBCode(source) { dir =>
val clsIn = dir.lookupName("Test.class", directory = false).input
val clsNode = loadClassNode(clsIn)
val method = getMethod(clsNode, "x$lzyINIT1$1")
assertEquals(32, instructionsFromMethod(method).size)
}
}

/** Test that synchronize blocks don't box */
@Test def i505 = {
val source =
"""class Test {
| def test: Int = synchronized(1)
|}
""".stripMargin

checkBCode(source) { dir =>
val clsIn = dir.lookupName("Test.class", directory = false).input
val clsNode = loadClassNode(clsIn)
val method = getMethod(clsNode, "test")

val doBox = instructionsFromMethod(method).exists {
case Invoke(_, _, name, _, _) =>
name == "boxToInteger" || name == "unboxToInt"
case _ =>
false
}
assertFalse(doBox)
}
}
}
44 changes: 0 additions & 44 deletions library/src/dotty/runtime/LazyHolders.scala

This file was deleted.

15 changes: 15 additions & 0 deletions tests/run/i505.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
object Test {
def main(args: Array[String]): Unit = {
val a: Int = synchronized(1)
val b: Long = synchronized(1L)
val c: Boolean = synchronized(true)
val d: Float = synchronized(1f)
val e: Double = synchronized(1.0)
val f: Byte = synchronized(1.toByte)
val g: Char = synchronized('1')
val h: Short = synchronized(1.toShort)
val i: String = synchronized("Hello")
val j: List[Int] = synchronized(List(1))
synchronized(())
}
}
8 changes: 8 additions & 0 deletions tests/run/i5340.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Int
Long
Float
Double
Byte
Char
Short
String
23 changes: 23 additions & 0 deletions tests/run/i5340.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
object Test {
def main(args: Array[String]): Unit = {
lazy val a: Int = { println("Int"); 1 }
lazy val b: Long = { println("Long"); 1L }
lazy val c: Float = { println("Float"); 1F }
lazy val d: Double = { println("Double"); 1.0 }
lazy val e: Byte = { println("Byte"); 1 }
lazy val f: Char = { println("Char"); '1' }
lazy val g: Short = { println("Short"); 1 }
lazy val h: String = { println("String"); "1" }
lazy val i: Unit = { println("Unit"); () }

a
b
c
d
e
f
g
h
// i // FIXME: See #5350
}
}