-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Test a new scheme to implement lazy vals #6979
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
initialize x | ||
result | ||
result | ||
initialize y | ||
result | ||
initialize x |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
/** 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 | ||
* | ||
* | ||
* A lazy val `x: A = rhs` is compiled to the following code scheme: | ||
|
||
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 | ||
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. | ||
* | ||
* 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: 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: 86 bytes | ||
* current Dotty scheme: 125 bytes | ||
* Scala 2 scheme: 39 bytes | ||
* | ||
* 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 than current Dotty scheme | ||
* | ||
* Disadvantages: | ||
* | ||
* - 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" | ||
} | ||
|
||
@volatile private[this] var _x: AnyRef = _ | ||
|
||
// Expansion of: lazy val x: String = init("x") | ||
|
||
def x: String = { | ||
val current = _x | ||
if (current.isInstanceOf[String]) | ||
current.asInstanceOf[String] | ||
else | ||
x$lzy | ||
} | ||
|
||
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)) { | ||
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 | ||
} | ||
} | ||
|
||
// 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.compareAndSwapObject(base, offset, lock, result) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need a CAS here: unsafe.putObjectVolatile(base, offset, result) |
||
lock.release() | ||
} | ||
|
||
def awaitInitialized(base: Object, offset: Long, current: Object): Unit = | ||
if (current.isInstanceOf[Waiting]) | ||
current.asInstanceOf[Waiting].awaitRelease() | ||
else | ||
unsafe.compareAndSwapObject(base, offset, Evaluating, new Waiting) | ||
} | ||
|
||
class LazyControl | ||
|
||
class Waiting extends LazyControl { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is equivalent to a CountDownLatch initialised with a |
||
|
||
private var done = false | ||
|
||
def release(): Unit = synchronized { | ||
done = true | ||
notifyAll() | ||
} | ||
|
||
def awaitRelease(): Unit = synchronized { | ||
while (!done) wait() | ||
} | ||
} | ||
|
||
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() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently
Tailrec
runs beforeLazyVals
. We need to write the loop manually:Note the
while(<EmptyTree>)
. We special case it in the compiler. It means infinite loop and types toNothing
: https://github.com/lampepfl/dotty/blob/963719ed2679f3d4c8188ec068e53941b181ef76/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala#L529-L530There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know. That's trap one could fall into easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want to rethrow the original exception in case the second initialize fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the second initialize can fail. There's no user code exectuted for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@odersky Good point. I guess in the cases it fails it is something like a SoE or similar VME.