Skip to content

Commit d589c37

Browse files
authored
Merge pull request #6979 from dotty-staging/test-lazy-scheme
Test a new scheme to implement lazy vals
2 parents 1e328b0 + eb5dbca commit d589c37

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

tests/run/lazy-impl.check

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

tests/run/lazy-impl.scala

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
*
19+
* A lazy val `x: A = rhs` is compiled to the following code scheme:
20+
21+
private @volatile var _x: AnyRef = null
22+
23+
@tailrec def x: A =
24+
_x match
25+
case current: A =>
26+
current
27+
case null =>
28+
if CAS(_x, null, Evaluating) then
29+
var result: A = null
30+
try
31+
result = rhs
32+
if result == null then result = NULL // drop if A is non-nullable
33+
finally
34+
if !CAS(_x, Evaluating, result) then
35+
val lock = _x.asInstanceOf[Waiting]
36+
CAS(_x, lock, result)
37+
lock.release()
38+
x
39+
case Evaluating =>
40+
CAS(x, Evaluating, new Waiting)
41+
x
42+
case current: Waiting =>
43+
_x = current.awaitRelease()
44+
x
45+
case NULL => null // drop if A is non-nullable
46+
47+
* The code makes use of the following runtime class:
48+
49+
class Waiting:
50+
private var done = false
51+
def release(): Unit = synchronized:
52+
done = true
53+
notifyAll()
54+
55+
def awaitRelease(): Unit = synchronized:
56+
while !done do wait()
57+
58+
* Note: The code assumes that the getter result type `A` is disjoint from the type
59+
* of `Evaluating` and the `Waiting` class. If this is not the case (e.g. `A` is AnyRef),
60+
* then the conditions in the match have to be re-ordered so that case `_x: A` becomes
61+
* the final default case.
62+
*
63+
* Cost analysis:
64+
*
65+
* - 2 CAS on contention-free initialization
66+
* - 0 or 1 CAS on first read in thread other than initializer thread, depending on
67+
* whether cache has updated
68+
* - no synchronization operations on reads after the first one
69+
* - If there is contention, we see in addition
70+
* - for the initializing thread: another CAS and a synchronized notifyAll
71+
* - for a reading thread: 0 or 1 CAS and a synchronized wait
72+
*
73+
* Code sizes for getter:
74+
*
75+
* this scheme, if nulls are excluded in type: 86 bytes
76+
* current Dotty scheme: 125 bytes
77+
* Scala 2 scheme: 39 bytes
78+
*
79+
* Advantages of the scheme:
80+
*
81+
* - no slot other than the field itself is needed
82+
* - no locks are shared among lazy val initializations and between lazy val initializations
83+
* and normal code
84+
* - no deadlocks (other than those inherent in user code)
85+
* - synchronized code is executed only if there is contention
86+
* - simpler than current Dotty scheme
87+
*
88+
* Disadvantages:
89+
*
90+
* - lazy vals of primitive types are boxed
91+
*/
92+
import sun.misc.Unsafe._
93+
94+
class C {
95+
def init(name: String) = {
96+
Thread.sleep(10)
97+
println(s"initialize $name"); "result"
98+
}
99+
100+
@volatile private[this] var _x: AnyRef = _
101+
102+
// Expansion of: lazy val x: String = init("x")
103+
104+
def x: String = {
105+
val current = _x
106+
if (current.isInstanceOf[String])
107+
current.asInstanceOf[String]
108+
else
109+
x$lzy
110+
}
111+
112+
def x$lzy: String = {
113+
val current = _x
114+
if (current.isInstanceOf[String])
115+
current.asInstanceOf[String]
116+
else {
117+
val offset = C.x_offset
118+
if (current == null) {
119+
if (LazyRuntime.isUnitialized(this, offset)) {
120+
try LazyRuntime.initialize(this, offset, init("x"))
121+
catch {
122+
case ex: Throwable =>
123+
LazyRuntime.initialize(this, offset, null)
124+
throw ex
125+
}
126+
}
127+
}
128+
else
129+
LazyRuntime.awaitInitialized(this, offset, current)
130+
x$lzy
131+
}
132+
}
133+
134+
// Compare with bytecodes for regular lazy val:
135+
lazy val y = init("y")
136+
}
137+
138+
object C {
139+
import LazyRuntime.fieldOffset
140+
val x_offset = fieldOffset(classOf[C], "_x")
141+
}
142+
143+
object LazyRuntime {
144+
val Evaluating = new LazyControl()
145+
146+
private val unsafe: sun.misc.Unsafe = {
147+
val f: java.lang.reflect.Field = classOf[sun.misc.Unsafe].getDeclaredField("theUnsafe");
148+
f.setAccessible(true)
149+
f.get(null).asInstanceOf[sun.misc.Unsafe]
150+
}
151+
152+
def fieldOffset(cls: Class[_], name: String): Long = {
153+
val fld = cls.getDeclaredField(name)
154+
fld.setAccessible(true)
155+
unsafe.objectFieldOffset(fld)
156+
}
157+
158+
def isUnitialized(base: Object, offset: Long): Boolean =
159+
unsafe.compareAndSwapObject(base, offset, null, Evaluating)
160+
161+
def initialize(base: Object, offset: Long, result: Object): Unit =
162+
if (!unsafe.compareAndSwapObject(base, offset, Evaluating, result)) {
163+
val lock = unsafe.getObject(base, offset).asInstanceOf[Waiting]
164+
unsafe.compareAndSwapObject(base, offset, lock, result)
165+
lock.release()
166+
}
167+
168+
def awaitInitialized(base: Object, offset: Long, current: Object): Unit =
169+
if (current.isInstanceOf[Waiting])
170+
current.asInstanceOf[Waiting].awaitRelease()
171+
else
172+
unsafe.compareAndSwapObject(base, offset, Evaluating, new Waiting)
173+
}
174+
175+
class LazyControl
176+
177+
class Waiting extends LazyControl {
178+
179+
private var done = false
180+
181+
def release(): Unit = synchronized {
182+
done = true
183+
notifyAll()
184+
}
185+
186+
def awaitRelease(): Unit = synchronized {
187+
while (!done) wait()
188+
}
189+
}
190+
191+
object Test {
192+
def main(args: Array[String]) = {
193+
val c = new C()
194+
println(c.x)
195+
println(c.x)
196+
println(c.y)
197+
multi()
198+
}
199+
200+
def multi() = {
201+
val rand = java.util.Random()
202+
val c = new C()
203+
val readers =
204+
for i <- 0 until 1000 yield
205+
new Thread {
206+
override def run() = {
207+
Thread.sleep(rand.nextInt(50))
208+
assert(c.x == "result")
209+
}
210+
}
211+
for (t <- readers) t.start()
212+
for (t <- readers) t.join()
213+
}
214+
}

0 commit comments

Comments
 (0)