Skip to content

Commit 5b112b8

Browse files
committed
Test a new scheme to implement lazy vals
1 parent b25d17c commit 5b112b8

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

tests/run/lazy-impl.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
initialize x
2+
result
3+
result
4+
initialize y
5+
result

tests/run/lazy-impl.scala

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/** A demonstrator for a new algorithm to handle lazy vals. The idea is that
2+
* we use the field slot itself for all synchronization; there are no separate bitmaps
3+
* or locks. The type of a field is always Object. The field goes through the following
4+
* state changes:
5+
*
6+
* null -> Evaluating -+--------------+-> Initialized
7+
* | |
8+
* +--> Waiting --+
9+
*
10+
* The states of a field are characterized as follows:
11+
*
12+
* x == null Nobody has evaluated the variable yet
13+
* x == Evaluating A thread has started evaluating
14+
* x: Waiting A thread has started evaluating and other threads are waiting
15+
* for the result
16+
* otherwise Variable is initialized
17+
*
18+
* Note 1: This assumes that fields cannot have `null` as normal value. Once we have
19+
* nullability checking, this should be the standard case. We can still accommodate
20+
* fields that can be null by representing `null` with a special value (say `NULL`)
21+
* and storing `NULL` instead of `null` in the field. The necessary tweaks are added
22+
* as comment lines to the code below.
23+
*
24+
* A lazy val `x: A = rhs` is compiled to the following code scheme:
25+
*
26+
* private var _x: AnyRef = null
27+
* def x: A =
28+
* while !_x.isInstanceOf[A] do
29+
* _x match
30+
* case null =>
31+
* if CAS(_x, null, Evaluating) then
32+
* var result = rhs
33+
* // if result == null then result == NULL
34+
* if !CAS(x, Evaluating, result) then
35+
* val lock = _x.asInstanceOf[Waiting]
36+
* _x = result
37+
* lock.release(result)
38+
* // case NULL =>
39+
* // return null
40+
* case current: Waiting =>
41+
* _x = current.awaitRelease()
42+
* case _ =>
43+
* CAS(x, Evaluating, new Waiting)
44+
* // end while
45+
* current.asInstanceOf[A]
46+
*
47+
* def x: A =
48+
* _x match
49+
* case current: A =>
50+
* current
51+
* case null =>
52+
* if CAS(_x, null, Evaluating) then
53+
* var result = rhs
54+
* // if result == null then result == NULL
55+
* if !CAS(x, Evaluating, result) then
56+
* val lock = _x.asInstanceOf[Waiting]
57+
* _x = result
58+
* lock.release(result)
59+
* x
60+
* case Evaluating =>
61+
* CAS(x, Evaluating, new Waiting)
62+
* x
63+
* case current: Waiting =>
64+
* _x = current.awaitRelease()
65+
* x
66+
* // case NULL =>
67+
* // null
68+
*
69+
70+
* The code makes use of the following runtime class:
71+
*
72+
* class Waiting:
73+
*
74+
* private var done = false
75+
* private var result: AnyRef = _
76+
*
77+
* def release(result: AnyRef): Unit = synchronized:
78+
* this.result = result
79+
* done = true
80+
* notifyAll()
81+
*
82+
* def awaitRelease(): AnyRef = synchronized:
83+
* if !done then wait()
84+
* result
85+
*
86+
* Note 2: The code assumes that the getter result type `A` is disjoint from the type
87+
* of `Evaluating` and the `Waiting` class. If this is not the case (e.g. `A` is AnyRef),
88+
* then the conditions in the match have to be re-ordered so that case `_x: A` becomes
89+
* the final default case.
90+
*
91+
* Cost analysis:
92+
*
93+
* - 2 CAS on contention-free initialization
94+
* - 0 or 1 CAS on first read in thread other than initializer thread, depending on
95+
* whether cache has updated
96+
* - no synchronization operations on reads after the first one
97+
* - If there is contention, we see in addition
98+
* - for the initializing thread: a volatile write and a synchronized notifyAll
99+
* - for a reading thread: 0 or 1 CAS and a synchronized wait
100+
*
101+
* Code sizes for getter:
102+
*
103+
* this scheme, if nulls are excluded in type: 72 bytes
104+
* current Dotty scheme: 131 bytes
105+
* Scala 2 scheme: 39 bytes + 1 exception handler
106+
*
107+
* Advantages of the scheme:
108+
*
109+
* - no slot other than the field itself is needed
110+
* - no locks are shared among lazy val initializations and between lazy val initializations
111+
* and normal code
112+
* - no deadlocks (other than those inherent in user code)
113+
* - synchronized code is executed only if there is contention
114+
* - simpler that current Dotty scheme
115+
*
116+
* Disadvantages:
117+
*
118+
* - does not work for local lazy vals (but maybe these could be unsynchronized anyway?)
119+
* - lazy vals of primitive types are boxed
120+
*/
121+
import sun.misc.Unsafe._
122+
123+
class C {
124+
def init(name: String) = {
125+
Thread.sleep(10)
126+
println(s"initialize $name"); "result"
127+
}
128+
129+
private[this] var _x: AnyRef = null
130+
131+
// Expansion of: lazy val x: String = init
132+
133+
def x: String = {
134+
val current = _x
135+
if (current.isInstanceOf[String])
136+
current.asInstanceOf[String]
137+
else
138+
x$lzy_compute
139+
}
140+
141+
def x$lzy_compute: String = {
142+
val current = _x
143+
if (current.isInstanceOf[String])
144+
current.asInstanceOf[String]
145+
else {
146+
val offset = C.x_offset
147+
if (current == null) {
148+
if (LazyRuntime.isUnitialized(this, offset))
149+
LazyRuntime.initialize(this, offset, init("x"))
150+
}
151+
else
152+
LazyRuntime.awaitInitialized(this, offset, current)
153+
x$lzy_compute
154+
}
155+
}
156+
157+
// Compare with bytecodes for regular lazy val:
158+
lazy val y = init("y")
159+
}
160+
161+
object C {
162+
import LazyRuntime.fieldOffset
163+
val x_offset = fieldOffset(classOf[C], "_x")
164+
}
165+
166+
object LazyRuntime {
167+
val Evaluating = new LazyControl()
168+
169+
private val unsafe: sun.misc.Unsafe = {
170+
val f: java.lang.reflect.Field = classOf[sun.misc.Unsafe].getDeclaredField("theUnsafe");
171+
f.setAccessible(true)
172+
f.get(null).asInstanceOf[sun.misc.Unsafe]
173+
}
174+
175+
def fieldOffset(cls: Class[_], name: String): Long = {
176+
val fld = cls.getDeclaredField(name)
177+
fld.setAccessible(true)
178+
unsafe.objectFieldOffset(fld)
179+
}
180+
181+
def isUnitialized(base: Object, offset: Long): Boolean =
182+
unsafe.compareAndSwapObject(base, offset, null, Evaluating)
183+
184+
def initialize(base: Object, offset: Long, result: Object): Unit =
185+
if (!unsafe.compareAndSwapObject(base, offset, Evaluating, result)) {
186+
val lock = unsafe.getObject(base, offset).asInstanceOf[Waiting]
187+
unsafe.putObject(base, offset, result)
188+
lock.release(result)
189+
}
190+
191+
def awaitInitialized(base: Object, offset: Long, current: Object): Unit =
192+
if (current.isInstanceOf[Waiting])
193+
unsafe.putObject(base, offset, current.asInstanceOf[Waiting].awaitRelease())
194+
else
195+
unsafe.compareAndSwapObject(base, offset, Evaluating, new Waiting)
196+
}
197+
198+
class LazyControl
199+
200+
class Waiting extends LazyControl {
201+
202+
private var done = false
203+
private var result: AnyRef = _
204+
205+
def release(result: AnyRef) = synchronized {
206+
this.result = result
207+
done = true
208+
notifyAll()
209+
}
210+
211+
def awaitRelease(): AnyRef = synchronized {
212+
if (!done) wait()
213+
result
214+
}
215+
}
216+
217+
object Test {
218+
def main(args: Array[String]) = {
219+
val c = new C()
220+
println(c.x)
221+
println(c.x)
222+
println(c.y)
223+
multi()
224+
}
225+
226+
def multi() = {
227+
val rand = java.util.Random()
228+
val c = new C()
229+
val readers =
230+
for i <- 0 until 1000 yield
231+
new Thread {
232+
override def run() = {
233+
Thread.sleep(rand.nextInt(50))
234+
assert(c.x == "result")
235+
}
236+
}
237+
for (t <- readers) t.start()
238+
for (t <- readers) t.join()
239+
}
240+
}

0 commit comments

Comments
 (0)