Skip to content

Commit 743cb07

Browse files
committed
Lazy access speedup
1 parent 5a48c11 commit 743cb07

File tree

3 files changed

+144
-29
lines changed

3 files changed

+144
-29
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package javaslang.control;
2+
3+
import javaslang.JmhRunner;
4+
import javaslang.Lazy;
5+
import javaslang.collection.Array;
6+
import javaslang.collection.Iterator;
7+
import org.junit.Test;
8+
import org.openjdk.jmh.annotations.*;
9+
import org.openjdk.jmh.infra.Blackhole;
10+
11+
import static javaslang.JmhRunner.Includes.JAVA;
12+
import static javaslang.JmhRunner.Includes.JAVASLANG;
13+
14+
public class LazyBenchmark {
15+
static final Array<Class<?>> CLASSES = Array.of(
16+
Get.class
17+
);
18+
19+
@Test
20+
public void testAsserts() { JmhRunner.runDebugWithAsserts(CLASSES); }
21+
22+
public static void main(String... args) {
23+
JmhRunner.runDebugWithAsserts(CLASSES, JAVA, JAVASLANG);
24+
JmhRunner.runSlowNoAsserts(CLASSES, JAVA, JAVASLANG);
25+
}
26+
27+
@State(Scope.Benchmark)
28+
public static class Base {
29+
final int SIZE = 10;
30+
31+
Integer[] EAGERS;
32+
javaslang.Lazy<Integer>[] INITED_LAZIES;
33+
34+
@Setup
35+
@SuppressWarnings({ "unchecked", "rawtypes" })
36+
public void setup() {
37+
EAGERS = Iterator.range(0, SIZE).toJavaArray(Integer.class);
38+
INITED_LAZIES = Iterator.of(EAGERS).map(i -> {
39+
final Lazy<Integer> lazy = Lazy.of(() -> i);
40+
lazy.get();
41+
return lazy;
42+
}).toJavaList().toArray(new Lazy[0]);
43+
}
44+
}
45+
46+
@Threads(4)
47+
@SuppressWarnings({ "WeakerAccess", "rawtypes" })
48+
public static class Get extends Base {
49+
@State(Scope.Thread)
50+
public static class Initialized {
51+
javaslang.Lazy<Integer>[] LAZIES;
52+
53+
@Setup(Level.Invocation)
54+
@SuppressWarnings("unchecked")
55+
public void initializeMutable(Base state) {
56+
LAZIES = Iterator.of(state.EAGERS).map(i -> Lazy.of(() -> i)).toJavaList().toArray(new Lazy[0]);
57+
}
58+
}
59+
60+
@Benchmark
61+
public void java_eager(Blackhole bh) {
62+
for (int i = 0; i < SIZE; i++) {
63+
bh.consume(EAGERS[i]);
64+
}
65+
}
66+
67+
@Benchmark
68+
public void slang_inited_lazy(Blackhole bh) {
69+
for (int i = 0; i < SIZE; i++) {
70+
assert INITED_LAZIES[i].isEvaluated();
71+
bh.consume(INITED_LAZIES[i].get());
72+
}
73+
}
74+
75+
@Benchmark
76+
public void slang_lazy(Initialized state, Blackhole bh) {
77+
for (int i = 0; i < SIZE; i++) {
78+
assert !state.LAZIES[i].isEvaluated();
79+
bh.consume(state.LAZIES[i].get());
80+
}
81+
}
82+
}
83+
}

javaslang/src/main/java/javaslang/Lazy.java

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77

88
import javaslang.collection.Iterator;
99
import javaslang.collection.List;
10-
import javaslang.collection.*;
10+
import javaslang.collection.Seq;
1111
import javaslang.control.Option;
1212

13-
import java.io.*;
14-
import java.lang.reflect.*;
15-
import java.util.*;
16-
import java.util.function.*;
13+
import java.io.IOException;
14+
import java.io.ObjectOutputStream;
15+
import java.io.Serializable;
16+
import java.lang.reflect.InvocationHandler;
17+
import java.lang.reflect.Proxy;
18+
import java.util.NoSuchElementException;
19+
import java.util.Objects;
20+
import java.util.function.Consumer;
21+
import java.util.function.Function;
22+
import java.util.function.Predicate;
23+
import java.util.function.Supplier;
1724

1825
/**
1926
* Represents a lazy evaluated value. Compared to a Supplier, Lazy is memoizing, i.e. it evaluates only once and
@@ -44,9 +51,7 @@ public final class Lazy<T> implements Value<T>, Supplier<T>, Serializable {
4451

4552
// read http://javarevisited.blogspot.de/2014/05/double-checked-locking-on-singleton-in-java.html
4653
private transient volatile Supplier<? extends T> supplier;
47-
48-
// does not need to be volatile, visibility piggy-backs on volatile read of `supplier`
49-
private T value;
54+
private T value; // will behave as a volatile in reality, because a supplier volatile read will update all fields (see https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile)
5055

5156
// should not be called directly
5257
private Lazy(Supplier<? extends T> supplier) {
@@ -135,16 +140,13 @@ public Option<T> filter(Predicate<? super T> predicate) {
135140
*/
136141
@Override
137142
public T get() {
138-
// using a local var speeds up the double-check idiom by 25%, see Effective Java, Item 71
139-
Supplier<? extends T> tmp = supplier;
140-
if (tmp != null) {
141-
synchronized (this) {
142-
tmp = supplier;
143-
if (tmp != null) {
144-
value = tmp.get();
145-
supplier = null; // free mem
146-
}
147-
}
143+
return (supplier == null) ? value : computeValue();
144+
}
145+
private synchronized T computeValue() {
146+
final Supplier<? extends T> s = supplier;
147+
if (s != null) {
148+
value = s.get();
149+
supplier = null;
148150
}
149151
return value;
150152
}

javaslang/src/test/java/javaslang/LazyTest.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@
55
*/
66
package javaslang;
77

8-
import javaslang.collection.List;
9-
import javaslang.collection.Seq;
8+
import javaslang.collection.*;
109
import javaslang.control.Option;
1110
import javaslang.control.Try;
1211
import org.junit.Test;
1312

1413
import java.util.Objects;
14+
import java.util.concurrent.CompletableFuture;
15+
import java.util.concurrent.atomic.AtomicBoolean;
1516

17+
import static java.util.concurrent.CompletableFuture.runAsync;
1618
import static javaslang.Serializables.deserialize;
1719
import static javaslang.Serializables.serialize;
20+
import static javaslang.collection.Iterator.range;
1821
import static org.assertj.core.api.Assertions.assertThat;
1922

2023
public class LazyTest {
@@ -77,7 +80,7 @@ public void shouldMapOverLazyValue() {
7780
final Lazy<Integer> testee = Lazy.of(() -> 42);
7881
final Lazy<Integer> expected = Lazy.of(() -> 21);
7982

80-
assertThat(testee.map( i -> i / 2)).isEqualTo(expected);
83+
assertThat(testee.map(i -> i / 2)).isEqualTo(expected);
8184
}
8285

8386
@Test
@@ -86,16 +89,16 @@ public void shouldFilterOverLazyValue() {
8689
final Option<Integer> expectedPositive = Option.some(42);
8790
final Option<Integer> expectedNegative = Option.none();
8891

89-
assertThat(testee.filter( i -> i % 2 == 0)).isEqualTo(expectedPositive);
90-
assertThat(testee.filter( i -> i % 2 != 0)).isEqualTo(expectedNegative);
92+
assertThat(testee.filter(i -> i % 2 == 0)).isEqualTo(expectedPositive);
93+
assertThat(testee.filter(i -> i % 2 != 0)).isEqualTo(expectedNegative);
9194
}
9295

9396
@Test
9497
public void shouldTransformLazyValue() {
9598
final Lazy<Integer> testee = Lazy.of(() -> 42);
9699
final Integer expected = 21;
97100

98-
final Integer actual = testee.transform( lazy -> lazy.get() / 2 );
101+
final Integer actual = testee.transform(lazy -> lazy.get() / 2);
99102

100103
assertThat(actual).isEqualTo(expected);
101104
}
@@ -173,9 +176,10 @@ public void shouldSerializeDeserializeNonNil() {
173176

174177
@Test
175178
public void shouldSupportMultithreading() {
176-
final boolean[] lock = new boolean[] { true };
179+
final AtomicBoolean isEvaluated = new AtomicBoolean();
180+
final AtomicBoolean lock = new AtomicBoolean();
177181
final Lazy<Integer> lazy = Lazy.of(() -> {
178-
while (lock[0]) {
182+
while (lock.get()) {
179183
Try.run(() -> Thread.sleep(300));
180184
}
181185
return 1;
@@ -184,22 +188,48 @@ public void shouldSupportMultithreading() {
184188
Try.run(() -> Thread.sleep(100));
185189
new Thread(() -> {
186190
Try.run(() -> Thread.sleep(100));
187-
lock[0] = false;
191+
lock.set(false);
188192
}).start();
189-
assertThat(lazy.isEvaluated()).isFalse();
193+
isEvaluated.compareAndSet(false, lazy.isEvaluated());
190194
lazy.get();
191195
}).start();
196+
assertThat(isEvaluated.get()).isFalse();
192197
assertThat(lazy.get()).isEqualTo(1);
193198
}
194199

200+
@Test
201+
@SuppressWarnings({ "StatementWithEmptyBody", "rawtypes" })
202+
public void shouldBeConsistentFromMultipleThreads() throws Exception {
203+
for (int i = 0; i < 100; i++) {
204+
final AtomicBoolean canProceed = new AtomicBoolean(false);
205+
final Vector<CompletableFuture<Void>> futures = Vector.range(0, 10).map(j -> {
206+
final AtomicBoolean isEvaluated = new AtomicBoolean(false);
207+
final Integer expected = ((j % 2) == 1) ? null : j;
208+
Lazy<Integer> lazy = Lazy.of(() -> {
209+
assertThat(isEvaluated.getAndSet(true)).isFalse();
210+
return expected;
211+
});
212+
return Tuple.of(lazy, expected);
213+
}).flatMap(t -> range(0, 5).map(j -> runAsync(() -> {
214+
while (!canProceed.get()) { /* busy wait */ }
215+
assertThat(t._1.get()).isEqualTo(t._2);
216+
}))
217+
);
218+
219+
final CompletableFuture all = CompletableFuture.allOf(futures.toJavaList().toArray(new CompletableFuture<?>[0]));
220+
canProceed.set(true);
221+
all.join();
222+
}
223+
}
224+
195225
// -- equals
196226

197227
@Test
198228
public void shouldDetectEqualObject() {
199229
assertThat(Lazy.of(() -> 1).equals("")).isFalse();
200230
assertThat(Lazy.of(() -> 1).equals(Lazy.of(() -> 1))).isTrue();
201231
assertThat(Lazy.of(() -> 1).equals(Lazy.of(() -> 2))).isFalse();
202-
Lazy<Integer> same = Lazy.of(() -> 1);
232+
final Lazy<Integer> same = Lazy.of(() -> 1);
203233
assertThat(same.equals(same)).isTrue();
204234
}
205235

0 commit comments

Comments
 (0)