From 2b3a5a03f524895befc29a02981e4bb01477cff5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 1 Aug 2019 19:39:07 +0200 Subject: [PATCH 1/5] Test a new scheme to implement lazy vals --- tests/run/lazy-impl.check | 5 + tests/run/lazy-impl.scala | 240 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 tests/run/lazy-impl.check create mode 100644 tests/run/lazy-impl.scala diff --git a/tests/run/lazy-impl.check b/tests/run/lazy-impl.check new file mode 100644 index 000000000000..5686cffaf18b --- /dev/null +++ b/tests/run/lazy-impl.check @@ -0,0 +1,5 @@ +initialize x +result +result +initialize y +result diff --git a/tests/run/lazy-impl.scala b/tests/run/lazy-impl.scala new file mode 100644 index 000000000000..57a969ab68df --- /dev/null +++ b/tests/run/lazy-impl.scala @@ -0,0 +1,240 @@ +/** A demonstrator for a new algorithm to handle lazy vals. The idea is that + * we use the field slot itself for all synchronization; there are no separate bitmaps + * or locks. The type of a field is always Object. The field goes through the following + * state changes: + * + * null -> Evaluating -+--------------+-> Initialized + * | | + * +--> Waiting --+ + * + * The states of a field are characterized as follows: + * + * x == null Nobody has evaluated the variable yet + * x == Evaluating A thread has started evaluating + * x: Waiting A thread has started evaluating and other threads are waiting + * for the result + * otherwise Variable is initialized + * + * Note 1: This assumes that fields cannot have `null` as normal value. Once we have + * nullability checking, this should be the standard case. We can still accommodate + * fields that can be null by representing `null` with a special value (say `NULL`) + * and storing `NULL` instead of `null` in the field. The necessary tweaks are added + * as comment lines to the code below. + * + * A lazy val `x: A = rhs` is compiled to the following code scheme: + * + * private var _x: AnyRef = null + * def x: A = + * while !_x.isInstanceOf[A] do + * _x match + * case null => + * if CAS(_x, null, Evaluating) then + * var result = rhs + * // if result == null then result == NULL + * if !CAS(x, Evaluating, result) then + * val lock = _x.asInstanceOf[Waiting] + * _x = result + * lock.release(result) + * // case NULL => + * // return null + * case current: Waiting => + * _x = current.awaitRelease() + * case _ => + * CAS(x, Evaluating, new Waiting) + * // end while + * current.asInstanceOf[A] + * + * def x: A = + * _x match + * case current: A => + * current + * case null => + * if CAS(_x, null, Evaluating) then + * var result = rhs + * // if result == null then result == NULL + * if !CAS(x, Evaluating, result) then + * val lock = _x.asInstanceOf[Waiting] + * _x = result + * lock.release(result) + * x + * case Evaluating => + * CAS(x, Evaluating, new Waiting) + * x + * case current: Waiting => + * _x = current.awaitRelease() + * x + * // case NULL => + * // null + * + + * The code makes use of the following runtime class: + * + * class Waiting: + * + * private var done = false + * private var result: AnyRef = _ + * + * def release(result: AnyRef): Unit = synchronized: + * this.result = result + * done = true + * notifyAll() + * + * def awaitRelease(): AnyRef = synchronized: + * if !done then wait() + * result + * + * Note 2: The code assumes that the getter result type `A` is disjoint from the type + * of `Evaluating` and the `Waiting` class. If this is not the case (e.g. `A` is AnyRef), + * then the conditions in the match have to be re-ordered so that case `_x: A` becomes + * the final default case. + * + * Cost analysis: + * + * - 2 CAS on contention-free initialization + * - 0 or 1 CAS on first read in thread other than initializer thread, depending on + * whether cache has updated + * - no synchronization operations on reads after the first one + * - If there is contention, we see in addition + * - for the initializing thread: a volatile write and a synchronized notifyAll + * - for a reading thread: 0 or 1 CAS and a synchronized wait + * + * Code sizes for getter: + * + * this scheme, if nulls are excluded in type: 72 bytes + * current Dotty scheme: 131 bytes + * Scala 2 scheme: 39 bytes + 1 exception handler + * + * Advantages of the scheme: + * + * - no slot other than the field itself is needed + * - no locks are shared among lazy val initializations and between lazy val initializations + * and normal code + * - no deadlocks (other than those inherent in user code) + * - synchronized code is executed only if there is contention + * - simpler that current Dotty scheme + * + * Disadvantages: + * + * - does not work for local lazy vals (but maybe these could be unsynchronized anyway?) + * - lazy vals of primitive types are boxed + */ +import sun.misc.Unsafe._ + +class C { + def init(name: String) = { + Thread.sleep(10) + println(s"initialize $name"); "result" + } + + private[this] var _x: AnyRef = null + + // Expansion of: lazy val x: String = init + + def x: String = { + val current = _x + if (current.isInstanceOf[String]) + current.asInstanceOf[String] + else + x$lzy_compute + } + + def x$lzy_compute: String = { + val current = _x + if (current.isInstanceOf[String]) + current.asInstanceOf[String] + else { + val offset = C.x_offset + if (current == null) { + if (LazyRuntime.isUnitialized(this, offset)) + LazyRuntime.initialize(this, offset, init("x")) + } + else + LazyRuntime.awaitInitialized(this, offset, current) + x$lzy_compute + } + } + + // Compare with bytecodes for regular lazy val: + lazy val y = init("y") +} + +object C { + import LazyRuntime.fieldOffset + val x_offset = fieldOffset(classOf[C], "_x") +} + +object LazyRuntime { + val Evaluating = new LazyControl() + + private val unsafe: sun.misc.Unsafe = { + val f: java.lang.reflect.Field = classOf[sun.misc.Unsafe].getDeclaredField("theUnsafe"); + f.setAccessible(true) + f.get(null).asInstanceOf[sun.misc.Unsafe] + } + + def fieldOffset(cls: Class[_], name: String): Long = { + val fld = cls.getDeclaredField(name) + fld.setAccessible(true) + unsafe.objectFieldOffset(fld) + } + + def isUnitialized(base: Object, offset: Long): Boolean = + unsafe.compareAndSwapObject(base, offset, null, Evaluating) + + def initialize(base: Object, offset: Long, result: Object): Unit = + if (!unsafe.compareAndSwapObject(base, offset, Evaluating, result)) { + val lock = unsafe.getObject(base, offset).asInstanceOf[Waiting] + unsafe.putObject(base, offset, result) + lock.release(result) + } + + def awaitInitialized(base: Object, offset: Long, current: Object): Unit = + if (current.isInstanceOf[Waiting]) + unsafe.putObject(base, offset, current.asInstanceOf[Waiting].awaitRelease()) + else + unsafe.compareAndSwapObject(base, offset, Evaluating, new Waiting) +} + +class LazyControl + +class Waiting extends LazyControl { + + private var done = false + private var result: AnyRef = _ + + def release(result: AnyRef) = synchronized { + this.result = result + done = true + notifyAll() + } + + def awaitRelease(): AnyRef = synchronized { + if (!done) wait() + result + } +} + +object Test { + def main(args: Array[String]) = { + val c = new C() + println(c.x) + println(c.x) + println(c.y) + multi() + } + + def multi() = { + val rand = java.util.Random() + val c = new C() + val readers = + for i <- 0 until 1000 yield + new Thread { + override def run() = { + Thread.sleep(rand.nextInt(50)) + assert(c.x == "result") + } + } + for (t <- readers) t.start() + for (t <- readers) t.join() + } +} From 48786319c82bdfdb9a33af7d64aa77b4ae94931f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 1 Aug 2019 19:54:21 +0200 Subject: [PATCH 2/5] Fix typos --- tests/run/lazy-impl.scala | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/tests/run/lazy-impl.scala b/tests/run/lazy-impl.scala index 57a969ab68df..3e553477e712 100644 --- a/tests/run/lazy-impl.scala +++ b/tests/run/lazy-impl.scala @@ -25,26 +25,6 @@ * * private var _x: AnyRef = null * def x: A = - * while !_x.isInstanceOf[A] do - * _x match - * case null => - * if CAS(_x, null, Evaluating) then - * var result = rhs - * // if result == null then result == NULL - * if !CAS(x, Evaluating, result) then - * val lock = _x.asInstanceOf[Waiting] - * _x = result - * lock.release(result) - * // case NULL => - * // return null - * case current: Waiting => - * _x = current.awaitRelease() - * case _ => - * CAS(x, Evaluating, new Waiting) - * // end while - * current.asInstanceOf[A] - * - * def x: A = * _x match * case current: A => * current @@ -95,12 +75,12 @@ * whether cache has updated * - no synchronization operations on reads after the first one * - If there is contention, we see in addition - * - for the initializing thread: a volatile write and a synchronized notifyAll + * - for the initializing thread: a synchronized notifyAll * - for a reading thread: 0 or 1 CAS and a synchronized wait * * Code sizes for getter: * - * this scheme, if nulls are excluded in type: 72 bytes + * this scheme, if nulls are excluded in type: 72 bytes * current Dotty scheme: 131 bytes * Scala 2 scheme: 39 bytes + 1 exception handler * @@ -111,7 +91,7 @@ * and normal code * - no deadlocks (other than those inherent in user code) * - synchronized code is executed only if there is contention - * - simpler that current Dotty scheme + * - simpler than current Dotty scheme * * Disadvantages: * From ce062c887535c852fb6409317444234ab1097575 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 1 Aug 2019 21:20:33 +0200 Subject: [PATCH 3/5] Update check file --- tests/run/lazy-impl.check | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run/lazy-impl.check b/tests/run/lazy-impl.check index 5686cffaf18b..20d8363c6fee 100644 --- a/tests/run/lazy-impl.check +++ b/tests/run/lazy-impl.check @@ -3,3 +3,4 @@ result result initialize y result +initialize x From a873a86a38b29696b388fc986859edccf6dfa738 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 2 Aug 2019 14:37:55 +0200 Subject: [PATCH 4/5] Handle spurious wakeups --- tests/run/lazy-impl.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/run/lazy-impl.scala b/tests/run/lazy-impl.scala index 3e553477e712..e28cd9e3c73f 100644 --- a/tests/run/lazy-impl.scala +++ b/tests/run/lazy-impl.scala @@ -32,7 +32,7 @@ * if CAS(_x, null, Evaluating) then * var result = rhs * // if result == null then result == NULL - * if !CAS(x, Evaluating, result) then + * if !CAS(_x, Evaluating, result) then * val lock = _x.asInstanceOf[Waiting] * _x = result * lock.release(result) @@ -60,7 +60,7 @@ * notifyAll() * * def awaitRelease(): AnyRef = synchronized: - * if !done then wait() + * while !done do wait() * result * * Note 2: The code assumes that the getter result type `A` is disjoint from the type @@ -189,7 +189,7 @@ class Waiting extends LazyControl { } def awaitRelease(): AnyRef = synchronized { - if (!done) wait() + while (!done) wait() result } } From eb5dbca5b7c46b0f5f3b0b9aedf96610949f1e19 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 7 Aug 2019 17:54:53 +0200 Subject: [PATCH 5/5] Revised implementation --- tests/run/lazy-impl.scala | 124 ++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/tests/run/lazy-impl.scala b/tests/run/lazy-impl.scala index e28cd9e3c73f..675bf204222b 100644 --- a/tests/run/lazy-impl.scala +++ b/tests/run/lazy-impl.scala @@ -15,55 +15,47 @@ * for the result * otherwise Variable is initialized * - * Note 1: This assumes that fields cannot have `null` as normal value. Once we have - * nullability checking, this should be the standard case. We can still accommodate - * fields that can be null by representing `null` with a special value (say `NULL`) - * and storing `NULL` instead of `null` in the field. The necessary tweaks are added - * as comment lines to the code below. * * A lazy val `x: A = rhs` is compiled to the following code scheme: - * - * private var _x: AnyRef = null - * def x: A = - * _x match - * case current: A => - * current - * case null => - * if CAS(_x, null, Evaluating) then - * var result = rhs - * // if result == null then result == NULL - * if !CAS(_x, Evaluating, result) then - * val lock = _x.asInstanceOf[Waiting] - * _x = result - * lock.release(result) - * x - * case Evaluating => - * CAS(x, Evaluating, new Waiting) - * x - * case current: Waiting => - * _x = current.awaitRelease() - * x - * // case NULL => - * // null - * + + private @volatile var _x: AnyRef = null + + @tailrec def x: A = + _x match + case current: A => + current + case null => + if CAS(_x, null, Evaluating) then + var result: A = null + try + result = rhs + if result == null then result = NULL // drop if A is non-nullable + finally + if !CAS(_x, Evaluating, result) then + val lock = _x.asInstanceOf[Waiting] + CAS(_x, lock, result) + lock.release() + x + case Evaluating => + CAS(x, Evaluating, new Waiting) + x + case current: Waiting => + _x = current.awaitRelease() + x + case NULL => null // drop if A is non-nullable * The code makes use of the following runtime class: - * - * class Waiting: - * - * private var done = false - * private var result: AnyRef = _ - * - * def release(result: AnyRef): Unit = synchronized: - * this.result = result - * done = true - * notifyAll() - * - * def awaitRelease(): AnyRef = synchronized: - * while !done do wait() - * result - * - * Note 2: The code assumes that the getter result type `A` is disjoint from the type + + class Waiting: + private var done = false + def release(): Unit = synchronized: + done = true + notifyAll() + + def awaitRelease(): Unit = synchronized: + while !done do wait() + + * Note: The code assumes that the getter result type `A` is disjoint from the type * of `Evaluating` and the `Waiting` class. If this is not the case (e.g. `A` is AnyRef), * then the conditions in the match have to be re-ordered so that case `_x: A` becomes * the final default case. @@ -75,14 +67,14 @@ * whether cache has updated * - no synchronization operations on reads after the first one * - If there is contention, we see in addition - * - for the initializing thread: a synchronized notifyAll + * - for the initializing thread: another CAS and a synchronized notifyAll * - for a reading thread: 0 or 1 CAS and a synchronized wait * * Code sizes for getter: * - * this scheme, if nulls are excluded in type: 72 bytes - * current Dotty scheme: 131 bytes - * Scala 2 scheme: 39 bytes + 1 exception handler + * this scheme, if nulls are excluded in type: 86 bytes + * current Dotty scheme: 125 bytes + * Scala 2 scheme: 39 bytes * * Advantages of the scheme: * @@ -95,7 +87,6 @@ * * Disadvantages: * - * - does not work for local lazy vals (but maybe these could be unsynchronized anyway?) * - lazy vals of primitive types are boxed */ import sun.misc.Unsafe._ @@ -106,31 +97,37 @@ class C { println(s"initialize $name"); "result" } - private[this] var _x: AnyRef = null + @volatile private[this] var _x: AnyRef = _ - // Expansion of: lazy val x: String = init + // Expansion of: lazy val x: String = init("x") def x: String = { val current = _x if (current.isInstanceOf[String]) current.asInstanceOf[String] else - x$lzy_compute + x$lzy } - def x$lzy_compute: String = { + def x$lzy: String = { val current = _x if (current.isInstanceOf[String]) current.asInstanceOf[String] else { val offset = C.x_offset if (current == null) { - if (LazyRuntime.isUnitialized(this, offset)) - LazyRuntime.initialize(this, offset, init("x")) + if (LazyRuntime.isUnitialized(this, offset)) { + try LazyRuntime.initialize(this, offset, init("x")) + catch { + case ex: Throwable => + LazyRuntime.initialize(this, offset, null) + throw ex + } + } } else LazyRuntime.awaitInitialized(this, offset, current) - x$lzy_compute + x$lzy } } @@ -164,13 +161,13 @@ object LazyRuntime { def initialize(base: Object, offset: Long, result: Object): Unit = if (!unsafe.compareAndSwapObject(base, offset, Evaluating, result)) { val lock = unsafe.getObject(base, offset).asInstanceOf[Waiting] - unsafe.putObject(base, offset, result) - lock.release(result) + unsafe.compareAndSwapObject(base, offset, lock, result) + lock.release() } def awaitInitialized(base: Object, offset: Long, current: Object): Unit = if (current.isInstanceOf[Waiting]) - unsafe.putObject(base, offset, current.asInstanceOf[Waiting].awaitRelease()) + current.asInstanceOf[Waiting].awaitRelease() else unsafe.compareAndSwapObject(base, offset, Evaluating, new Waiting) } @@ -180,17 +177,14 @@ class LazyControl class Waiting extends LazyControl { private var done = false - private var result: AnyRef = _ - def release(result: AnyRef) = synchronized { - this.result = result + def release(): Unit = synchronized { done = true notifyAll() } - def awaitRelease(): AnyRef = synchronized { + def awaitRelease(): Unit = synchronized { while (!done) wait() - result } }