From 743cb07b3832672851efb344929f8c4e1dd303da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pap=20L=C5=91rinc?= Date: Mon, 19 Sep 2016 18:17:53 +0300 Subject: [PATCH] `Lazy` access speedup --- .../java/javaslang/control/LazyBenchmark.java | 83 +++++++++++++++++++ javaslang/src/main/java/javaslang/Lazy.java | 38 +++++---- .../src/test/java/javaslang/LazyTest.java | 52 +++++++++--- 3 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 javaslang-benchmark/src/test/java/javaslang/control/LazyBenchmark.java diff --git a/javaslang-benchmark/src/test/java/javaslang/control/LazyBenchmark.java b/javaslang-benchmark/src/test/java/javaslang/control/LazyBenchmark.java new file mode 100644 index 0000000000..aa2dd4f04e --- /dev/null +++ b/javaslang-benchmark/src/test/java/javaslang/control/LazyBenchmark.java @@ -0,0 +1,83 @@ +package javaslang.control; + +import javaslang.JmhRunner; +import javaslang.Lazy; +import javaslang.collection.Array; +import javaslang.collection.Iterator; +import org.junit.Test; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import static javaslang.JmhRunner.Includes.JAVA; +import static javaslang.JmhRunner.Includes.JAVASLANG; + +public class LazyBenchmark { + static final Array> CLASSES = Array.of( + Get.class + ); + + @Test + public void testAsserts() { JmhRunner.runDebugWithAsserts(CLASSES); } + + public static void main(String... args) { + JmhRunner.runDebugWithAsserts(CLASSES, JAVA, JAVASLANG); + JmhRunner.runSlowNoAsserts(CLASSES, JAVA, JAVASLANG); + } + + @State(Scope.Benchmark) + public static class Base { + final int SIZE = 10; + + Integer[] EAGERS; + javaslang.Lazy[] INITED_LAZIES; + + @Setup + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void setup() { + EAGERS = Iterator.range(0, SIZE).toJavaArray(Integer.class); + INITED_LAZIES = Iterator.of(EAGERS).map(i -> { + final Lazy lazy = Lazy.of(() -> i); + lazy.get(); + return lazy; + }).toJavaList().toArray(new Lazy[0]); + } + } + + @Threads(4) + @SuppressWarnings({ "WeakerAccess", "rawtypes" }) + public static class Get extends Base { + @State(Scope.Thread) + public static class Initialized { + javaslang.Lazy[] LAZIES; + + @Setup(Level.Invocation) + @SuppressWarnings("unchecked") + public void initializeMutable(Base state) { + LAZIES = Iterator.of(state.EAGERS).map(i -> Lazy.of(() -> i)).toJavaList().toArray(new Lazy[0]); + } + } + + @Benchmark + public void java_eager(Blackhole bh) { + for (int i = 0; i < SIZE; i++) { + bh.consume(EAGERS[i]); + } + } + + @Benchmark + public void slang_inited_lazy(Blackhole bh) { + for (int i = 0; i < SIZE; i++) { + assert INITED_LAZIES[i].isEvaluated(); + bh.consume(INITED_LAZIES[i].get()); + } + } + + @Benchmark + public void slang_lazy(Initialized state, Blackhole bh) { + for (int i = 0; i < SIZE; i++) { + assert !state.LAZIES[i].isEvaluated(); + bh.consume(state.LAZIES[i].get()); + } + } + } +} \ No newline at end of file diff --git a/javaslang/src/main/java/javaslang/Lazy.java b/javaslang/src/main/java/javaslang/Lazy.java index 322f1ca7c3..c156ff5fe2 100644 --- a/javaslang/src/main/java/javaslang/Lazy.java +++ b/javaslang/src/main/java/javaslang/Lazy.java @@ -7,13 +7,20 @@ import javaslang.collection.Iterator; import javaslang.collection.List; -import javaslang.collection.*; +import javaslang.collection.Seq; import javaslang.control.Option; -import java.io.*; -import java.lang.reflect.*; -import java.util.*; -import java.util.function.*; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; /** * 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 implements Value, Supplier, Serializable { // read http://javarevisited.blogspot.de/2014/05/double-checked-locking-on-singleton-in-java.html private transient volatile Supplier supplier; - - // does not need to be volatile, visibility piggy-backs on volatile read of `supplier` - private T value; + 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) // should not be called directly private Lazy(Supplier supplier) { @@ -135,16 +140,13 @@ public Option filter(Predicate predicate) { */ @Override public T get() { - // using a local var speeds up the double-check idiom by 25%, see Effective Java, Item 71 - Supplier tmp = supplier; - if (tmp != null) { - synchronized (this) { - tmp = supplier; - if (tmp != null) { - value = tmp.get(); - supplier = null; // free mem - } - } + return (supplier == null) ? value : computeValue(); + } + private synchronized T computeValue() { + final Supplier s = supplier; + if (s != null) { + value = s.get(); + supplier = null; } return value; } diff --git a/javaslang/src/test/java/javaslang/LazyTest.java b/javaslang/src/test/java/javaslang/LazyTest.java index 9b063f1551..20cb575e72 100644 --- a/javaslang/src/test/java/javaslang/LazyTest.java +++ b/javaslang/src/test/java/javaslang/LazyTest.java @@ -5,16 +5,19 @@ */ package javaslang; -import javaslang.collection.List; -import javaslang.collection.Seq; +import javaslang.collection.*; import javaslang.control.Option; import javaslang.control.Try; import org.junit.Test; import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import static java.util.concurrent.CompletableFuture.runAsync; import static javaslang.Serializables.deserialize; import static javaslang.Serializables.serialize; +import static javaslang.collection.Iterator.range; import static org.assertj.core.api.Assertions.assertThat; public class LazyTest { @@ -77,7 +80,7 @@ public void shouldMapOverLazyValue() { final Lazy testee = Lazy.of(() -> 42); final Lazy expected = Lazy.of(() -> 21); - assertThat(testee.map( i -> i / 2)).isEqualTo(expected); + assertThat(testee.map(i -> i / 2)).isEqualTo(expected); } @Test @@ -86,8 +89,8 @@ public void shouldFilterOverLazyValue() { final Option expectedPositive = Option.some(42); final Option expectedNegative = Option.none(); - assertThat(testee.filter( i -> i % 2 == 0)).isEqualTo(expectedPositive); - assertThat(testee.filter( i -> i % 2 != 0)).isEqualTo(expectedNegative); + assertThat(testee.filter(i -> i % 2 == 0)).isEqualTo(expectedPositive); + assertThat(testee.filter(i -> i % 2 != 0)).isEqualTo(expectedNegative); } @Test @@ -95,7 +98,7 @@ public void shouldTransformLazyValue() { final Lazy testee = Lazy.of(() -> 42); final Integer expected = 21; - final Integer actual = testee.transform( lazy -> lazy.get() / 2 ); + final Integer actual = testee.transform(lazy -> lazy.get() / 2); assertThat(actual).isEqualTo(expected); } @@ -173,9 +176,10 @@ public void shouldSerializeDeserializeNonNil() { @Test public void shouldSupportMultithreading() { - final boolean[] lock = new boolean[] { true }; + final AtomicBoolean isEvaluated = new AtomicBoolean(); + final AtomicBoolean lock = new AtomicBoolean(); final Lazy lazy = Lazy.of(() -> { - while (lock[0]) { + while (lock.get()) { Try.run(() -> Thread.sleep(300)); } return 1; @@ -184,14 +188,40 @@ public void shouldSupportMultithreading() { Try.run(() -> Thread.sleep(100)); new Thread(() -> { Try.run(() -> Thread.sleep(100)); - lock[0] = false; + lock.set(false); }).start(); - assertThat(lazy.isEvaluated()).isFalse(); + isEvaluated.compareAndSet(false, lazy.isEvaluated()); lazy.get(); }).start(); + assertThat(isEvaluated.get()).isFalse(); assertThat(lazy.get()).isEqualTo(1); } + @Test + @SuppressWarnings({ "StatementWithEmptyBody", "rawtypes" }) + public void shouldBeConsistentFromMultipleThreads() throws Exception { + for (int i = 0; i < 100; i++) { + final AtomicBoolean canProceed = new AtomicBoolean(false); + final Vector> futures = Vector.range(0, 10).map(j -> { + final AtomicBoolean isEvaluated = new AtomicBoolean(false); + final Integer expected = ((j % 2) == 1) ? null : j; + Lazy lazy = Lazy.of(() -> { + assertThat(isEvaluated.getAndSet(true)).isFalse(); + return expected; + }); + return Tuple.of(lazy, expected); + }).flatMap(t -> range(0, 5).map(j -> runAsync(() -> { + while (!canProceed.get()) { /* busy wait */ } + assertThat(t._1.get()).isEqualTo(t._2); + })) + ); + + final CompletableFuture all = CompletableFuture.allOf(futures.toJavaList().toArray(new CompletableFuture[0])); + canProceed.set(true); + all.join(); + } + } + // -- equals @Test @@ -199,7 +229,7 @@ public void shouldDetectEqualObject() { assertThat(Lazy.of(() -> 1).equals("")).isFalse(); assertThat(Lazy.of(() -> 1).equals(Lazy.of(() -> 1))).isTrue(); assertThat(Lazy.of(() -> 1).equals(Lazy.of(() -> 2))).isFalse(); - Lazy same = Lazy.of(() -> 1); + final Lazy same = Lazy.of(() -> 1); assertThat(same.equals(same)).isTrue(); }