From ec7bf348961485f3534ed63371042b2080f68750 Mon Sep 17 00:00:00 2001 From: Doug Lawrie Date: Thu, 1 Dec 2016 15:52:29 +0000 Subject: [PATCH 01/17] MPSC/user custom channel + primitives only support. Support for user specified back end data structures for the proxy byte code generation factory. Users need only to specify a ProxyChannelRingBuffer implementation. The protocol is to acquire/release for both loads and stores. Within these borders plain stores and plain loads are performed. Only subtle and implicit part of the API is that implementors of ProxyChannelRingBuffer must provide the constructor (int capacity, int messageSize, int referenceMessageSize). This change supports the existing MPSC channel which I've added reference support to. I've consolidated reference function from the class OffHeapFixedMessageSizeWithReferenceSupportRingBuffer into the basic class OffHeapFixedMessageSizeRingBuffer. When referenceMessageSize = 0 then the backing array will not be instantiated and nor will the reference methods be supported. This is expressed as asserts rather than throwing an exception with the expectation the lack of branch would perform better in well behaved usage scenarios and NPE in others. Extracted out the wait strategy to the org.jctools.channels package. Modified how the wait strategy works as a field of the generated proxy class and use a static util method on the factory for it's implementation, thus letting the backend channels omit implementation. Added to and heavily refactored tests to try to better separate and express test intent. Renamed arrayMessageSize to referenceMessageSize and messageSize to primitiveMessageSize to create a consistent and easy to understand distinction. Moved the impl of ProxyChannelRingBuffer up to the top of OffHeapFixedSizeRingBuffer. This may arguably be semantically ambiguous but it seemed like the easiest thing to do. I am open to discussion of removing the ProxyChannelRingBuffer interface and specifying it's API via OffHeapFixedSizeRingBuffer with protected methods instead. Added noddy MPSC just to give me an initial idea but it's not well thought out. I plan to revisit both SPSC and MPSC benchmarks in order to compare them to alternate solutions such as the disruptr, etc. --- .../mpsc/MpscProxyChannelBenchmark.java | 633 ++++++++++++++++++ .../spsc/SpscProxyChannelBenchmark.java | 67 +- .../OffHeapFixedMessageSizeRingBuffer.java | 112 +++- ...ageSizeWithReferenceSupportRingBuffer.java | 74 -- .../org/jctools/channels/WaitStrategy.java | 26 + .../channels/mpsc/MpscChannelConsumer.java | 6 +- .../channels/mpsc/MpscChannelProducer.java | 6 +- ...scFFLamportOffHeapFixedSizeRingBuffer.java | 15 +- .../mpsc/MpscOffHeapFixedSizeRingBuffer.java | 32 +- .../channels/proxy/ProxyChannelFactory.java | 233 +++++-- .../proxy/ProxyChannelRingBuffer.java | 75 +++ .../channels/spsc/SpscChannelConsumer.java | 2 +- .../channels/spsc/SpscChannelProducer.java | 2 +- .../spsc/SpscOffHeapFixedSizeRingBuffer.java | 34 +- ...xedSizeWithReferenceSupportRingBuffer.java | 195 ------ .../MpscOffHeapFixedSizeRingBufferTest.java | 6 +- .../channels/proxy/DemoProxyResult.java | 23 +- .../channels/proxy/ProxyCreationTest.java | 71 +- .../SpscOffHeapFixedSizeRingBufferTest.java | 3 +- 19 files changed, 1170 insertions(+), 445 deletions(-) create mode 100644 jctools-benchmarks/src/main/java/org/jctools/channels/mpsc/MpscProxyChannelBenchmark.java delete mode 100644 jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeWithReferenceSupportRingBuffer.java create mode 100644 jctools-experimental/src/main/java/org/jctools/channels/WaitStrategy.java create mode 100644 jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelRingBuffer.java delete mode 100644 jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.java diff --git a/jctools-benchmarks/src/main/java/org/jctools/channels/mpsc/MpscProxyChannelBenchmark.java b/jctools-benchmarks/src/main/java/org/jctools/channels/mpsc/MpscProxyChannelBenchmark.java new file mode 100644 index 00000000..b66f4e74 --- /dev/null +++ b/jctools-benchmarks/src/main/java/org/jctools/channels/mpsc/MpscProxyChannelBenchmark.java @@ -0,0 +1,633 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.channels.mpsc; + +import java.util.concurrent.TimeUnit; + +import org.jctools.channels.proxy.ProxyChannel; +import org.jctools.channels.proxy.ProxyChannelFactory; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.infra.Control; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +public class MpscProxyChannelBenchmark { + + private static final int CAPACITY = 10000; + private static final int PRODUCER_THREADS = 10; + private static final int CONSUMER_THREADS = 1; + + public interface BenchIFace { + + void noArgs(); + + void onePrimitiveArg(int x); + + void twoMixedLengthPrimitiveArgs(int x, long y); + + void oneObjectArg(Object x); + + void oneReferenceArg(CustomType x); + + // I'm curious is there's a performance cost of switching between the ref queue and the bytebuffer + void tenMixedArgs(int i, + Object o, + long l, + CustomType c0, + double d, + CustomType c1, + float f, + CustomType c2, + boolean b, + CustomType c3); + + // The first value is 'type':int that is 4 bytes which renders all of this unaligned + void unalignedPrimitiveArgs( + long l1, + double d1, + long l2, + double d2, + long l3, + double d3, + long l4, + double d4, + long l5, + double d5, + long l6, + double d6, + long l7, + double d7, + long l8, + double d8, + int i); + + // The first value is 'type':int that is implicit so we start with a 4 byte value first then all 8 bytes, then all 4 bytes, and so on + void alignedPrimitiveArgs( + int i, + long l1, + double d1, + long l2, + double d2, + long l3, + double d3, + long l4, + double d4, + long l5, + double d5, + long l6, + double d6, + long l7, + double d7, + long l8, + double d8); + } + + public static class CustomType { + + } + + private static final class BenchImpl implements BenchIFace { + private final long tokens; + + public BenchImpl(final long tokens) { + super(); + this.tokens = tokens; + } + + @Override + public void noArgs() { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void onePrimitiveArg(final int x) { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void twoMixedLengthPrimitiveArgs(final int x, final long y) { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void oneObjectArg(final Object x) { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void oneReferenceArg(final CustomType x) { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void tenMixedArgs(final int i, + final Object o, + final long l, + final CustomType c0, + final double d, + final CustomType c1, + final float f, + final CustomType c2, + final boolean b, + final CustomType c3) { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void unalignedPrimitiveArgs( + long l1, + double d1, + long l2, + double d2, + long l3, + double d3, + long l4, + double d4, + long l5, + double d5, + long l6, + double d6, + long l7, + double d7, + long l8, + double d8, + int i) { + Blackhole.consumeCPU(this.tokens); + } + + @Override + public void alignedPrimitiveArgs(int i, + long l1, + double d1, + long l2, + double d2, + long l3, + double d3, + long l4, + double d4, + long l5, + double d5, + long l6, + double d6, + long l7, + double d7, + long l8, + double d8) { + Blackhole.consumeCPU(this.tokens); + } + + } + + @AuxCounters + @State(Scope.Thread) + public static class ProcessorCounters { + public long processed; + public long processFailed; + + @Setup(Level.Iteration) + public void clean() { + this.processed = this.processFailed = 0; + } + } + + @AuxCounters + @State(Scope.Thread) + public static class CallerCounters { + public long callsFailed; + + @Setup(Level.Iteration) + public void clean() { + this.callsFailed = 0; + } + } + + public static final class StoppedException extends RuntimeException { + + } + + private static final StoppedException STOPPED = new StoppedException(); + + private static final class MyWaitStrategy + implements org.jctools.channels.WaitStrategy { + public Control control; + private int retries; + + @Override + public int idle(final int idleCounter) { + if (this.control.stopMeasurement) { + throw STOPPED; + } + this.retries = idleCounter; + return idleCounter + 1; + } + + } + + private ProxyChannel mpscChannel; + private BenchIFace proxy; + private BenchIFace impl; + private MyWaitStrategy waitStrategy; + + int intArg; + Object objArg; + long longArg; + long longArg2; + long longArg3; + long longArg4; + CustomType customType0; + CustomType customType1; + CustomType customType2; + CustomType customType3; + double doubleArg; + double doubleArg2; + double doubleArg3; + double doubleArg4; + float floatArg; + boolean booleanArg; + + @Param({ "1", "" + CAPACITY }) + private int limit; + + @Setup(Level.Iteration) + public void setupTrial() { + this.waitStrategy = new MyWaitStrategy(); + this.mpscChannel = ProxyChannelFactory.createMpscProxy(CAPACITY, BenchIFace.class, this.waitStrategy); + this.proxy = this.mpscChannel.proxy(); + this.impl = new BenchImpl(0); + + this.intArg = 7; + this.objArg = new Object(); + this.longArg = System.nanoTime(); + this.longArg2 = System.nanoTime(); + this.longArg3 = System.nanoTime(); + this.longArg4 = System.nanoTime(); + this.customType0 = new CustomType(); + this.customType1 = new CustomType(); + this.customType2 = new CustomType(); + this.customType3 = new CustomType(); + this.doubleArg = System.nanoTime(); + this.doubleArg2 = System.nanoTime(); + this.doubleArg3 = System.nanoTime(); + this.doubleArg4 = System.nanoTime(); + this.floatArg = 8.165f; + this.booleanArg = true; + } + + @Benchmark + public int oneObjectArgBaseline() { + this.impl.oneObjectArg(this.objArg); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("oneObjectArg") + @GroupThreads(PRODUCER_THREADS) + public boolean oneObjectArgCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.oneObjectArg(this.objArg); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("oneObjectArg") + @GroupThreads(CONSUMER_THREADS) + public int oneObjectArgProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int oneReferenceArgBaseline() { + this.impl.oneReferenceArg(this.customType0); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("oneReferenceArg") + @GroupThreads(PRODUCER_THREADS) + public boolean oneReferenceArgCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.oneReferenceArg(this.customType0); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("oneReferenceArg") + @GroupThreads(CONSUMER_THREADS) + public int oneReferenceArgProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int twoMixedLengthPrimitiveArgsBaseline() { + this.impl.twoMixedLengthPrimitiveArgs(this.intArg, this.longArg); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("twoMixedLengthPrimitiveArgs") + @GroupThreads(PRODUCER_THREADS) + public boolean twoMixedLengthPrimitiveArgsCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.twoMixedLengthPrimitiveArgs(this.intArg, this.longArg); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("twoMixedLengthPrimitiveArgs") + @GroupThreads(CONSUMER_THREADS) + public int twoMixedLengthPrimitiveArgsProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int onePrimitiveArgBaseline() { + this.impl.onePrimitiveArg(this.intArg); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("onePrimitiveArg") + @GroupThreads(PRODUCER_THREADS) + public boolean onePrimitiveArgCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.onePrimitiveArg(this.intArg); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("onePrimitiveArg") + @GroupThreads(CONSUMER_THREADS) + public int onePrimitiveArgProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int noArgsBaseline() { + this.impl.noArgs(); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("noArgs") + @GroupThreads(PRODUCER_THREADS) + public boolean noArgsCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.noArgs(); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("noArgs") + @GroupThreads(CONSUMER_THREADS) + public int noArgsProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int tenMixedArgsBaseline() { + this.impl.tenMixedArgs(this.intArg, + this.objArg, + this.longArg, + this.customType0, + this.doubleArg, + this.customType1, + this.floatArg, + this.customType2, + this.booleanArg, + this.customType3); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("tenMixedArgs") + @GroupThreads(PRODUCER_THREADS) + public boolean tenMixedArgsCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.tenMixedArgs(this.intArg, + this.objArg, + this.longArg, + this.customType0, + this.doubleArg, + this.customType1, + this.floatArg, + this.customType2, + this.booleanArg, + this.customType3); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("tenMixedArgs") + @GroupThreads(CONSUMER_THREADS) + public int tenMixedArgsProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int alignedPrimitiveArgsBaseline() { + this.impl.alignedPrimitiveArgs(intArg, + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4, + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("alignedPrimitiveArgs") + @GroupThreads(PRODUCER_THREADS) + public boolean alignedPrimitiveArgsCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.alignedPrimitiveArgs(intArg, + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4, + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("alignedPrimitiveArgs") + @GroupThreads(CONSUMER_THREADS) + public int alignedPrimitiveArgsProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + @Benchmark + public int unalignedPrimitiveArgsBaseline() { + this.impl.unalignedPrimitiveArgs( + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4, + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4, + intArg); + return this.waitStrategy.retries; + } + + @Benchmark + @Group("unalignedPrimitiveArgs") + @GroupThreads(PRODUCER_THREADS) + public boolean unalignedPrimitiveArgsCaller(final Control control, final CallerCounters counters) { + this.waitStrategy.control = control; + try { + this.proxy.unalignedPrimitiveArgs( + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4, + longArg, + doubleArg, + longArg2, + doubleArg2, + longArg3, + doubleArg3, + longArg4, + doubleArg4, + intArg); + counters.callsFailed = this.waitStrategy.retries; + return true; + } catch (final StoppedException e) { + return false; + } + } + + @Benchmark + @Group("unalignedPrimitiveArgs") + @GroupThreads(CONSUMER_THREADS) + public int unalignedPrimitiveArgsProcessor(final ProcessorCounters counters) { + return doProcess(mpscChannel, counters); + } + + private int doProcess(ProxyChannel proxyChannel, final ProcessorCounters counters) { + final int processed = proxyChannel.process(this.impl, this.limit); + if (processed == 0) { + counters.processFailed++; + } else { + counters.processed += processed; + } + return processed; + } + + public static void main(final String[] args) throws Exception { +// final String logFile = SpscProxyChannelBenchmark.class.getSimpleName() + ".log"; + final Options opt = new OptionsBuilder() + .include(MpscProxyChannelBenchmark.class.getSimpleName() + ".*tenMixedArgs.*") + // .jvmArgsAppend("-XX:+UnlockDiagnosticVMOptions", + // "-XX:+TraceClassLoading", + // "-XX:+LogCompilation", + // "-XX:LogFile=" + logFile, + // "-XX:+PrintAssembly") + .warmupIterations(5) + .measurementIterations(5) + .param("limit", "1") + .forks(2) + .build(); + new Runner(opt).run(); + } +} diff --git a/jctools-benchmarks/src/main/java/org/jctools/channels/spsc/SpscProxyChannelBenchmark.java b/jctools-benchmarks/src/main/java/org/jctools/channels/spsc/SpscProxyChannelBenchmark.java index e1d836f7..ddfc2565 100644 --- a/jctools-benchmarks/src/main/java/org/jctools/channels/spsc/SpscProxyChannelBenchmark.java +++ b/jctools-benchmarks/src/main/java/org/jctools/channels/spsc/SpscProxyChannelBenchmark.java @@ -13,6 +13,7 @@ */ package org.jctools.channels.spsc; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import org.jctools.channels.proxy.ProxyChannel; @@ -45,6 +46,8 @@ public class SpscProxyChannelBenchmark { private static final int CAPACITY = 10000; + private static final int PRODUCER_THREADS = 1; + private static final int CONSUMER_THREADS = 1; public interface BenchIFace { @@ -237,7 +240,7 @@ public static final class StoppedException extends RuntimeException { private static final StoppedException STOPPED = new StoppedException(); private static final class MyWaitStrategy - implements org.jctools.channels.spsc.SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.WaitStrategy { + implements org.jctools.channels.WaitStrategy { public Control control; private int retries; @@ -252,7 +255,7 @@ public int idle(final int idleCounter) { } - private ProxyChannel channel; + private ProxyChannel spscChannel; private BenchIFace proxy; private BenchIFace impl; private MyWaitStrategy waitStrategy; @@ -280,8 +283,8 @@ public int idle(final int idleCounter) { @Setup(Level.Iteration) public void setupTrial() { this.waitStrategy = new MyWaitStrategy(); - this.channel = ProxyChannelFactory.createSpscProxy(CAPACITY, BenchIFace.class, this.waitStrategy); - this.proxy = this.channel.proxy(); + this.spscChannel = ProxyChannelFactory.createSpscProxy(CAPACITY, BenchIFace.class, this.waitStrategy); + this.proxy = this.spscChannel.proxy(); this.impl = new BenchImpl(0); this.intArg = 7; @@ -310,7 +313,7 @@ public int oneObjectArgBaseline() { @Benchmark @Group("oneObjectArg") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean oneObjectArgCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -324,9 +327,9 @@ public boolean oneObjectArgCaller(final Control control, final CallerCounters co @Benchmark @Group("oneObjectArg") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int oneObjectArgProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -337,7 +340,7 @@ public int oneReferenceArgBaseline() { @Benchmark @Group("oneReferenceArg") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean oneReferenceArgCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -351,9 +354,9 @@ public boolean oneReferenceArgCaller(final Control control, final CallerCounters @Benchmark @Group("oneReferenceArg") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int oneReferenceArgProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -364,7 +367,7 @@ public int twoMixedLengthPrimitiveArgsBaseline() { @Benchmark @Group("twoMixedLengthPrimitiveArgs") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean twoMixedLengthPrimitiveArgsCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -378,9 +381,9 @@ public boolean twoMixedLengthPrimitiveArgsCaller(final Control control, final Ca @Benchmark @Group("twoMixedLengthPrimitiveArgs") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int twoMixedLengthPrimitiveArgsProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -391,7 +394,7 @@ public int onePrimitiveArgBaseline() { @Benchmark @Group("onePrimitiveArg") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean onePrimitiveArgCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -405,9 +408,9 @@ public boolean onePrimitiveArgCaller(final Control control, final CallerCounters @Benchmark @Group("onePrimitiveArg") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int onePrimitiveArgProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -418,7 +421,7 @@ public int noArgsBaseline() { @Benchmark @Group("noArgs") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean noArgsCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -432,9 +435,9 @@ public boolean noArgsCaller(final Control control, final CallerCounters counters @Benchmark @Group("noArgs") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int noArgsProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -454,7 +457,7 @@ public int tenMixedArgsBaseline() { @Benchmark @Group("tenMixedArgs") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean tenMixedArgsCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -477,9 +480,9 @@ public boolean tenMixedArgsCaller(final Control control, final CallerCounters co @Benchmark @Group("tenMixedArgs") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int tenMixedArgsProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -506,7 +509,7 @@ public int alignedPrimitiveArgsBaseline() { @Benchmark @Group("alignedPrimitiveArgs") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean alignedPrimitiveArgsCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -536,9 +539,9 @@ public boolean alignedPrimitiveArgsCaller(final Control control, final CallerCou @Benchmark @Group("alignedPrimitiveArgs") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int alignedPrimitiveArgsProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } @Benchmark @@ -566,7 +569,7 @@ public int unalignedPrimitiveArgsBaseline() { @Benchmark @Group("unalignedPrimitiveArgs") - @GroupThreads(1) + @GroupThreads(PRODUCER_THREADS) public boolean unalignedPrimitiveArgsCaller(final Control control, final CallerCounters counters) { this.waitStrategy.control = control; try { @@ -597,13 +600,13 @@ public boolean unalignedPrimitiveArgsCaller(final Control control, final CallerC @Benchmark @Group("unalignedPrimitiveArgs") - @GroupThreads(1) + @GroupThreads(CONSUMER_THREADS) public int unalignedPrimitiveArgsProcessor(final ProcessorCounters counters) { - return doProcess(counters); + return doProcess(spscChannel, counters); } - private int doProcess(final ProcessorCounters counters) { - final int processed = this.channel.process(this.impl, this.limit); + private int doProcess(ProxyChannel proxyChannel, final ProcessorCounters counters) { + final int processed = proxyChannel.process(this.impl, this.limit); if (processed == 0) { counters.processFailed++; } else { @@ -615,7 +618,7 @@ private int doProcess(final ProcessorCounters counters) { public static void main(final String[] args) throws Exception { // final String logFile = SpscProxyChannelBenchmark.class.getSimpleName() + ".log"; final Options opt = new OptionsBuilder() - .include(SpscProxyChannelBenchmark.class.getSimpleName() + ".*align.*") + .include(SpscProxyChannelBenchmark.class.getSimpleName() + ".*tenMixedArgs.*") // .jvmArgsAppend("-XX:+UnlockDiagnosticVMOptions", // "-XX:+TraceClassLoading", // "-XX:+LogCompilation", @@ -624,7 +627,7 @@ public static void main(final String[] args) throws Exception { .warmupIterations(5) .measurementIterations(5) .param("limit", "1") - .forks(0) + .forks(2) .build(); new Runner(opt).run(); } diff --git a/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeRingBuffer.java index e055dbbd..f37b7d58 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeRingBuffer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeRingBuffer.java @@ -13,15 +13,18 @@ */ package org.jctools.channels; -import org.jctools.util.JvmInfo; -import org.jctools.util.Pow2; -import org.jctools.util.UnsafeDirectByteBuffer; +import static org.jctools.util.JvmInfo.CACHE_LINE_SIZE; +import static org.jctools.util.UnsafeAccess.UNSAFE; +import static org.jctools.util.UnsafeDirectByteBuffer.alignedSlice; +import static org.jctools.util.UnsafeDirectByteBuffer.allocateAlignedByteBuffer; import java.nio.ByteBuffer; -import static org.jctools.util.JvmInfo.CACHE_LINE_SIZE; -import static org.jctools.util.UnsafeAccess.UNSAFE; -import static org.jctools.util.UnsafeDirectByteBuffer.*; +import org.jctools.channels.proxy.ProxyChannelRingBuffer; +import org.jctools.util.JvmInfo; +import org.jctools.util.Pow2; +import org.jctools.util.UnsafeDirectByteBuffer; +import org.jctools.util.UnsafeRefArrayAccess; /** * Channel protocol: @@ -29,7 +32,7 @@ * - 'null' indicator in message preceding byte (potentially use same for type mapping in future) * - Use FF algorithm relying on indicator to support in place detection of next element existence */ -public abstract class OffHeapFixedMessageSizeRingBuffer { +public abstract class OffHeapFixedMessageSizeRingBuffer extends ProxyChannelRingBuffer { public static final int READ_RELEASE_INDICATOR = 0; public static final int READ_ACQUIRE_INDICATOR = 1; @@ -38,8 +41,6 @@ public abstract class OffHeapFixedMessageSizeRingBuffer { public static final byte MESSAGE_INDICATOR_SIZE = 4; public static final int HEADER_SIZE = 4 * JvmInfo.CACHE_LINE_SIZE; - public static final long EOF = 0; - private final ByteBuffer buffy; protected final long bufferAddress; protected final long consumerIndexAddress; @@ -47,18 +48,35 @@ public abstract class OffHeapFixedMessageSizeRingBuffer { protected final long mask; protected final int messageSize; + protected final Object[] references; + protected final int referenceMessageSize; + public static int getRequiredBufferSize(final int capacity, final int messageSize) { int alignedMessageSize = (int) Pow2.align(messageSize + MESSAGE_INDICATOR_SIZE, MESSAGE_INDICATOR_SIZE); return HEADER_SIZE + (Pow2.roundToPowerOfTwo(capacity) * alignedMessageSize); } + + protected static Object[] createReferenceArray(final int capacity, int referenceMessageSize) { + if (referenceMessageSize > 0) { + return new Object[getRequiredArraySize(capacity, referenceMessageSize)]; + } else { + return null; + } + } + + public static int getRequiredArraySize(final int capacity, final int primitiveMessageSize) { + return Pow2.roundToPowerOfTwo(capacity) * primitiveMessageSize; + } - public OffHeapFixedMessageSizeRingBuffer(final int capacity, final int messageSize) { - this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), CACHE_LINE_SIZE), + public OffHeapFixedMessageSizeRingBuffer(final int capacity, final int primitiveMessageSize, int referenceMessageSize) { + this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, primitiveMessageSize), CACHE_LINE_SIZE), Pow2.roundToPowerOfTwo(capacity), true, true, true, - messageSize); + primitiveMessageSize, + createReferenceArray(capacity, referenceMessageSize), + referenceMessageSize); } /** @@ -66,15 +84,24 @@ public OffHeapFixedMessageSizeRingBuffer(final int capacity, final int messageSi * * @param buff * @param capacity in messages, actual capacity will be - * @param messageSize + * @param primitiveMessageSize */ - protected OffHeapFixedMessageSizeRingBuffer(final ByteBuffer buff, final int capacity, - final boolean isProducer, final boolean isConsumer, final boolean initialize, - final int messageSize) { + protected OffHeapFixedMessageSizeRingBuffer(final ByteBuffer buff, + final int capacity, + final boolean isProducer, + final boolean isConsumer, + final boolean initialize, + final int primitiveMessageSize, + final Object[] references, + final int referenceMessageSize) { + if (references != null && references.length < referenceMessageSize) { + throw new IllegalArgumentException("Reference array of length " + references.length + + " is insufficient to store a single message of size " + referenceMessageSize); + } int actualCapacity = Pow2.roundToPowerOfTwo(capacity); // message size is aligned to indicator size, this ensure atomic writes of indicator - this.messageSize = (int) Pow2.align(messageSize + MESSAGE_INDICATOR_SIZE, MESSAGE_INDICATOR_SIZE); + this.messageSize = (int) Pow2.align(primitiveMessageSize + MESSAGE_INDICATOR_SIZE, MESSAGE_INDICATOR_SIZE); this.buffy = alignedSlice(HEADER_SIZE + (actualCapacity * (this.messageSize)), CACHE_LINE_SIZE, buff); long alignedAddress = UnsafeDirectByteBuffer.getAddress(buffy); @@ -91,6 +118,10 @@ protected OffHeapFixedMessageSizeRingBuffer(final ByteBuffer buff, final int cap this.producerIndexAddress = this.consumerIndexAddress + 2 * JvmInfo.CACHE_LINE_SIZE; this.bufferAddress = alignedAddress + HEADER_SIZE; this.mask = actualCapacity - 1; + + this.references = references; + this.referenceMessageSize = referenceMessageSize; + // producer owns tail and headCache if (isProducer && initialize) { soProducerIndex(0); @@ -168,15 +199,62 @@ protected final long lvProducerIndex() { protected final void soProducerIndex(final long value) { UNSAFE.putOrderedLong(null, producerIndexAddress, value); } + + protected final long arrayIndexForCursor(long currentHead) { + return arrayIndexForCursor(mask, referenceMessageSize, currentHead); + } + + protected static long arrayIndexForCursor(long mask, + int referenceMessageSize, + long currentHead) { + return (currentHead & mask) * referenceMessageSize; + } + + protected long consumerReferenceArrayIndex() { + final long consumerIndex = lpConsumerIndex(); + return arrayIndexForCursor(consumerIndex); + } + + protected long producerReferenceArrayIndex() { + final long producerIndex = lpProducerIndex(); + return arrayIndexForCursor(producerIndex); + } + + /** + * Write a reference to the given position + * @param offset index into the reference array + * @param reference + */ + protected void writeReference(long offset, Object reference) { + assert referenceMessageSize != 0 : "References are not in use"; + // Is there a way to compute the element offset once and just + // arithmetic? + UnsafeRefArrayAccess.spElement(references, UnsafeRefArrayAccess.calcElementOffset(offset), reference); + } + + /** + * Read a reference at the given position + * @param offset index into the reference array + * @return + */ + protected Object readReference(long offset) { + assert referenceMessageSize != 0 : "References are not in use"; + // Is there a way to compute the element offset once and just + // arithmetic? + return UnsafeRefArrayAccess.lpElement(references, UnsafeRefArrayAccess.calcElementOffset(offset)); + } + /** * @return a base address for a message acquired to be read, or EOF if none is available */ protected abstract long readAcquire(); + /** * @param offset the base address of a message that we are done reading and can be overwritten now */ protected abstract void readRelease(long offset); + /** * @return a base address for a message acquired to be written, or EOF if none is available */ diff --git a/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeWithReferenceSupportRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeWithReferenceSupportRingBuffer.java deleted file mode 100644 index 1eee4d58..00000000 --- a/jctools-experimental/src/main/java/org/jctools/channels/OffHeapFixedMessageSizeWithReferenceSupportRingBuffer.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.jctools.channels; - -import static org.jctools.util.JvmInfo.CACHE_LINE_SIZE; -import static org.jctools.util.UnsafeDirectByteBuffer.allocateAlignedByteBuffer; - -import java.nio.ByteBuffer; - -import org.jctools.util.Pow2; - -public abstract class OffHeapFixedMessageSizeWithReferenceSupportRingBuffer extends OffHeapFixedMessageSizeRingBuffer { - - protected final Object[] references; - protected final int arrayMessageSize; - - /** - * - * @param capacity in messages, actual capacity will be - * @param messageSize size in bytes for each message - * @param arrayMessageSize size in element count for each message - */ - public OffHeapFixedMessageSizeWithReferenceSupportRingBuffer(final int capacity, - final int messageSize, - int arrayMessageSize) { - this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), CACHE_LINE_SIZE), - Pow2.roundToPowerOfTwo(capacity), - true, - true, - true, - messageSize, - createReferenceArray(capacity, arrayMessageSize), - arrayMessageSize); - } - - protected static Object[] createReferenceArray(final int capacity, int arrayMessageSize) { - return new Object[getRequiredArraySize(capacity, arrayMessageSize)]; - } - - public OffHeapFixedMessageSizeWithReferenceSupportRingBuffer(ByteBuffer buff, - int capacity, - boolean isProducer, - boolean isConsumer, - boolean initialize, - int messageSize, - Object[] references, - int arrayMessageSize) { - super(buff, capacity, isProducer, isConsumer, initialize, messageSize); - this.references = references; - this.arrayMessageSize = arrayMessageSize; - } - - public static int getRequiredArraySize(final int capacity, final int messageSize) { - return Pow2.roundToPowerOfTwo(capacity) * messageSize; - } - - protected final long arrayIndexForCursor(long currentHead) { - return arrayIndexForCursor(mask, arrayMessageSize, currentHead); - } - - protected static long arrayIndexForCursor(long mask, - int arrayMessageSize, - long currentHead) { - return (currentHead & mask) * arrayMessageSize; - } - - protected long consumerReferenceArrayIndex() { - final long consumerIndex = lpConsumerIndex(); - return arrayIndexForCursor(consumerIndex); - } - - protected long producerReferenceArrayIndex() { - final long producerIndex = lpProducerIndex(); - return arrayIndexForCursor(producerIndex); - } -} diff --git a/jctools-experimental/src/main/java/org/jctools/channels/WaitStrategy.java b/jctools-experimental/src/main/java/org/jctools/channels/WaitStrategy.java new file mode 100644 index 00000000..a97734e7 --- /dev/null +++ b/jctools-experimental/src/main/java/org/jctools/channels/WaitStrategy.java @@ -0,0 +1,26 @@ +package org.jctools.channels; + +public interface WaitStrategy { + /** + * This method can implement static or dynamic backoff. Dynamic backoff will rely on the counter for + * estimating how long the caller has been idling. The expected usage is: + * + *
+     * 
+     * int ic = 0;
+     * while(true) {
+     *   if(!isGodotArrived()) {
+     *     ic = w.idle(ic);
+     *     continue;
+     *   }
+     *   ic = 0;
+     *   // party with Godot until he goes again
+     * }
+     * 
+     * 
+ * + * @param idleCounter idle calls counter, managed by the idle method until reset + * @return new counter value to be used on subsequent idle cycle + */ + int idle(int idleCounter); +} \ No newline at end of file diff --git a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelConsumer.java b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelConsumer.java index 69099034..7328633f 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelConsumer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelConsumer.java @@ -13,11 +13,11 @@ */ package org.jctools.channels.mpsc; +import java.nio.ByteBuffer; + import org.jctools.channels.ChannelConsumer; import org.jctools.channels.ChannelReceiver; -import java.nio.ByteBuffer; - /** * Package Scoped: not part of public API. */ @@ -33,7 +33,7 @@ public MpscChannelConsumer( final int messageSize, final ChannelReceiver receiver) { - super(buffer, capacity, false, true, false, messageSize); + super(buffer, capacity, false, true, false, messageSize, null, 0); this.receiver = receiver; this.pointer = EOF; diff --git a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelProducer.java b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelProducer.java index e6037099..71279a3d 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelProducer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscChannelProducer.java @@ -13,10 +13,10 @@ */ package org.jctools.channels.mpsc; -import org.jctools.channels.ChannelProducer; - import java.nio.ByteBuffer; +import org.jctools.channels.ChannelProducer; + /** * Package Scoped: not part of public API. * @@ -31,7 +31,7 @@ public MpscChannelProducer( final int capacity, final int messageSize) { - super(buffer, capacity, true, false, true, messageSize); + super(buffer, capacity, true, false, true, messageSize, null, 0); pointer = EOF; } diff --git a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscFFLamportOffHeapFixedSizeRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscFFLamportOffHeapFixedSizeRingBuffer.java index a9624c99..4ba740c5 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscFFLamportOffHeapFixedSizeRingBuffer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscFFLamportOffHeapFixedSizeRingBuffer.java @@ -34,8 +34,8 @@ **/ public final class MpscFFLamportOffHeapFixedSizeRingBuffer extends OffHeapFixedMessageSizeRingBuffer { - public MpscFFLamportOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize) { - this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), JvmInfo.CACHE_LINE_SIZE), Pow2.roundToPowerOfTwo(capacity), true, true, true, messageSize); + public MpscFFLamportOffHeapFixedSizeRingBuffer(final int capacity, final int primitiveMessageSize, final int referenceMessageSize) { + this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, primitiveMessageSize), JvmInfo.CACHE_LINE_SIZE), Pow2.roundToPowerOfTwo(capacity), true, true, true, primitiveMessageSize, createReferenceArray(capacity, referenceMessageSize), referenceMessageSize); } private final long consumerIndexCacheAddress; @@ -51,8 +51,10 @@ protected MpscFFLamportOffHeapFixedSizeRingBuffer(final ByteBuffer buff, final boolean isProducer, final boolean isConsumer, final boolean initialize, - final int messageSize) { - super(buff, capacity, isProducer, isConsumer, initialize, messageSize); + final int primitiveMessageSize, + final Object[] references, + final int referenceMessageSize) { + super(buff, capacity, isProducer, isConsumer, initialize, primitiveMessageSize, references, referenceMessageSize); // Layout of the RingBuffer (assuming 64b cache line): // consumerIndex(8b), pad(56b) | // pad(64b) | @@ -107,6 +109,11 @@ protected final void writeRelease(long offset) { writeReleaseState(offset); } + @Override + protected final void writeRelease(long offset, int callTypeId) { + UNSAFE.putOrderedInt(null, offset, callTypeId); + } + @Override protected final long readAcquire() { final long consumerIndex = lpConsumerIndex(); diff --git a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBuffer.java index cab1558f..616ea355 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBuffer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBuffer.java @@ -30,10 +30,15 @@ */ public class MpscOffHeapFixedSizeRingBuffer extends OffHeapFixedMessageSizeRingBuffer { - - public MpscOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize) { - this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), JvmInfo.CACHE_LINE_SIZE), Pow2 - .roundToPowerOfTwo(capacity), true, true, true, messageSize); + public MpscOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize, int referenceMessageSize) { + this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), JvmInfo.CACHE_LINE_SIZE), + Pow2.roundToPowerOfTwo(capacity), + true, + true, + true, + messageSize, + createReferenceArray(capacity, referenceMessageSize), + referenceMessageSize); } /** @@ -42,10 +47,15 @@ public MpscOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize) * @param buff * @param capacity */ - protected MpscOffHeapFixedSizeRingBuffer(final ByteBuffer buff, final int capacity, - final boolean isProducer, final boolean isConsumer, final boolean initialize, - final int messageSize) { - super(buff,capacity,isProducer,isConsumer,initialize,messageSize); + protected MpscOffHeapFixedSizeRingBuffer(final ByteBuffer buff, + final int capacity, + final boolean isProducer, + final boolean isConsumer, + final boolean initialize, + final int messageSize, + final Object[] references, + final int referenceMessageSize) { + super(buff, capacity, isProducer, isConsumer, initialize, messageSize, references, referenceMessageSize); } @Override @@ -76,6 +86,12 @@ protected final void writeRelease(long offset) { writeReleaseState(offset); } + @Override + protected final void writeRelease(long offset, int callTypeId) { + assert callTypeId != 0; + UNSAFE.putOrderedInt(null, offset, callTypeId); + } + @Override protected final long readAcquire() { final long currentHead = lpConsumerIndex(); diff --git a/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelFactory.java b/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelFactory.java index d342b5f3..ea2e17b0 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelFactory.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelFactory.java @@ -6,8 +6,9 @@ import java.util.ArrayList; import java.util.List; -import org.jctools.channels.spsc.SpscOffHeapFixedSizeWithReferenceSupportRingBuffer; -import org.jctools.channels.spsc.SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.WaitStrategy; +import org.jctools.channels.WaitStrategy; +import org.jctools.channels.mpsc.MpscOffHeapFixedSizeRingBuffer; +import org.jctools.channels.spsc.SpscOffHeapFixedSizeRingBuffer; import org.jctools.util.UnsafeAccess; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -35,12 +36,87 @@ private static void printClassBytes(byte[] byteCode) { } } - public static ProxyChannel createSpscProxy(int capacity, Class iFace, WaitStrategy waitStrategy) { + public static long writeAcquireWithWaitStrategy(ProxyChannelRingBuffer channelBackend, WaitStrategy waitStrategy) { + long wOffset; + int idleCounter = 0; + while ((wOffset = channelBackend.writeAcquire()) == ProxyChannelRingBuffer.EOF) { + idleCounter = waitStrategy.idle(idleCounter); + } + return wOffset; + } + + /** + * Create a default single producer single consumer (SPSC) proxy channel. + * + * @param capacity + * The minimum capacity for unprocessed invocations the channel + * should support + * @param iFace + * Interface the proxy must implement + * @param waitStrategy + * A wait strategy to be invoked when the backing data structure + * is full + * @return A proxy channel instance + */ + public static ProxyChannel createSpscProxy(int capacity, + Class iFace, + WaitStrategy waitStrategy) { + return createProxy(capacity, + iFace, + waitStrategy, + SpscOffHeapFixedSizeRingBuffer.class); + } + + /** + * Create a default multi producer single consumer (MPSC) proxy channel. + * + * @param capacity + * The minimum capacity for unprocessed invocations the channel + * should support + * @param iFace + * Interface the proxy must implement + * @param waitStrategy + * A wait strategy to be invoked when the backing data structure + * is full + * @return A proxy channel instance + */ + public static ProxyChannel createMpscProxy(int capacity, + Class iFace, + WaitStrategy waitStrategy) { + return createProxy(capacity, + iFace, + waitStrategy, + MpscOffHeapFixedSizeRingBuffer.class); + } + + /** + * Create a proxy channel using a user supplied back end. + * + * @param capacity + * The minimum capacity for unprocessed invocations the channel + * should support + * @param iFace + * Interface the proxy must implement + * @param waitStrategy + * A wait strategy to be invoked when the backing data structure + * is full + * @param backendType + * The back end type, the proxy will inherit from this channel + * type. The back end type must define a constructor with signature: + * (int capacity, int primitiveMessageSize, int referenceMessageSize) + * @return A proxy channel instance + */ + public static ProxyChannel createProxy(int capacity, + Class iFace, + WaitStrategy waitStrategy, + Class backendType) { if (!iFace.isInterface()) { throw new IllegalArgumentException("Not an interface: " + iFace); } + + - String generatedName = Type.getInternalName(iFace) + "$JCTools$ProxyChannel"; + String generatedName = Type.getInternalName(iFace) + "$JCTools$ProxyChannel$" + backendType.getSimpleName(); Class preExisting = findExisting(generatedName, iFace); if (preExisting != null) { return instantiate(preExisting, capacity, waitStrategy); @@ -61,7 +137,7 @@ public static ProxyChannel createSpscProxy(int capacity, Class iFace, int referenceCount = 0; for (Class parameterType : method.getParameterTypes()) { if (parameterType.isPrimitive()) { - primitiveMethodSize += memorySize(parameterType); + primitiveMethodSize += primitiveMemorySize(parameterType); } else { referenceCount++; } @@ -71,23 +147,23 @@ public static ProxyChannel createSpscProxy(int capacity, Class iFace, } // We need to add an int to this for the 'type' value on the message frame - primitiveMessageSize += memorySize(int.class); - + primitiveMessageSize += primitiveMemorySize(int.class); + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); classWriter.visit(Opcodes.V1_4, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, generatedName, null, - Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), + Type.getInternalName(backendType), new String[]{Type.getInternalName(ProxyChannel.class), Type.getInternalName(iFace)}); - - implementConstructor(classWriter, primitiveMessageSize, referenceMessageSize); + implementInstanceFields(classWriter); + implementConstructor(classWriter, backendType, generatedName, primitiveMessageSize, referenceMessageSize); implementProxyInstance(classWriter, iFace, generatedName); implementProxy(classWriter, iFace, generatedName); - implementUserMethods(classWriter, relevantMethods); - implementProcess(classWriter, relevantMethods, iFace, generatedName); + implementUserMethods(classWriter, relevantMethods, generatedName, backendType); + implementProcess(classWriter, backendType, relevantMethods, iFace, generatedName); classWriter.visitEnd(); @@ -104,10 +180,10 @@ public static ProxyChannel createSpscProxy(int capacity, Class iFace, } } - private static void implementUserMethods(ClassWriter classWriter, List relevantMethods) { + private static void implementUserMethods(ClassWriter classWriter, List relevantMethods, String generatedName, Class backendType) { int type = 1; for (Method method : relevantMethods) { - implementUserMethod(method, classWriter, type++); + implementUserMethod(method, classWriter, type++, generatedName, backendType); } } @@ -140,7 +216,11 @@ private static ProxyChannel instantiate(Class proxy, int capacity, Wai } } - private static void implementProcess(ClassVisitor classVisitor, List methods, Class iFace, String generatedName) { + private static void implementProcess(ClassVisitor classVisitor, + Class backendType, + List methods, + Class iFace, + String generatedName) { // public int process (E impl, int limit) MethodVisitor methodVisitor = classVisitor.visitMethod(Opcodes.ACC_PUBLIC, "process", @@ -179,13 +259,13 @@ private static void implementProcess(ClassVisitor classVisitor, List met } // long rOffset = this.readAcquire(); - readAcquire(methodVisitor); + readAcquire(methodVisitor, backendType); methodVisitor.visitVarInsn(Opcodes.LSTORE, localIndexOfROffset); // if (rOffset == EOF) goto ; methodVisitor.visitVarInsn(Opcodes.LLOAD, localIndexOfROffset); - methodVisitor.visitLdcInsn(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.EOF); + methodVisitor.visitLdcInsn(ProxyChannelRingBuffer.EOF); methodVisitor.visitInsn(Opcodes.LCMP); methodVisitor.visitJumpInsn(Opcodes.IFEQ, loopEnd); @@ -207,7 +287,7 @@ private static void implementProcess(ClassVisitor classVisitor, List met for (Class parameterType : method.getParameterTypes()) { if (!parameterType.isPrimitive()) { // long referenceArrayIndex = this.consumerReferenceArrayIndex(); - consumerReferenceArrayIndex(methodVisitor); + consumerReferenceArrayIndex(methodVisitor, backendType); localIndexOfArrayReferenceBaseIndex = locals.newLocal(long.class); methodVisitor.visitVarInsn(Opcodes.LSTORE, localIndexOfArrayReferenceBaseIndex); break; @@ -223,16 +303,20 @@ private static void implementProcess(ClassVisitor classVisitor, List met // #PUSH: UnsafeAccess.UNSAFE.get[param.type](rOffset + #R_OFFSET_DELTA); // #R_OFFSET_DELTA += if param.type in {long, double} 8 else 4; getUnsafe(methodVisitor, parameterType, localIndexOfROffset, rOffsetDelta); - rOffsetDelta += memorySize(parameterType); + rOffsetDelta += primitiveMemorySize(parameterType); } else { - getReference(methodVisitor, parameterType, localIndexOfArrayReferenceBaseIndex, arrayReferenceBaseIndexDelta); + getReference(methodVisitor, + parameterType, + localIndexOfArrayReferenceBaseIndex, + arrayReferenceBaseIndexDelta, + backendType); arrayReferenceBaseIndexDelta++; } } // #END // this.readRelease(rOffset); - readRelease(methodVisitor, localIndexOfROffset); + readRelease(methodVisitor, localIndexOfROffset, backendType); // method.invoke(impl, ); methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, @@ -266,7 +350,17 @@ private static void implementProcess(ClassVisitor classVisitor, List met implementBridgeMethod(classVisitor, generatedName, "process", int.class, iFace, int.class); } + private static void implementInstanceFields(ClassVisitor classVisitor) { + classVisitor.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, + "waitStrategy", + Type.getDescriptor(WaitStrategy.class), + null, + null); + } + private static void implementConstructor(ClassVisitor classVisitor, + Class parentType, + String generatedName, int primitiveMessageSize, int referenceMessageSize) { MethodVisitor methodVisitor = classVisitor.visitMethod(Opcodes.ACC_PUBLIC, @@ -286,18 +380,20 @@ private static void implementConstructor(ClassVisitor classVisitor, methodVisitor.visitVarInsn(Opcodes.ILOAD, localIndexOfCapacity); methodVisitor.visitLdcInsn(primitiveMessageSize); methodVisitor.visitLdcInsn(referenceMessageSize); - methodVisitor.visitVarInsn(Opcodes.ALOAD, localIndexOfWaitStrategy); - + methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, - Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), + Type.getInternalName(parentType), "", methodDescriptor(void.class, int.class, int.class, - int.class, - WaitStrategy.class), + int.class), false); - + + methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); + methodVisitor.visitVarInsn(Opcodes.ALOAD, localIndexOfWaitStrategy); + methodVisitor.visitFieldInsn(Opcodes.PUTFIELD, generatedName, "waitStrategy", Type.getDescriptor(WaitStrategy.class)); + methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(-1, -1); @@ -385,7 +481,11 @@ private static void implementBridgeMethod(ClassVisitor classVisitor, String gene methodVisitor.visitEnd(); } - private static void implementUserMethod(Method method, ClassVisitor classVisitor, int type) { + private static void implementUserMethod(Method method, + ClassVisitor classVisitor, + int type, + String generatedName, + Class backendType) { if (method.getReturnType() != void.class) { throw new IllegalArgumentException("Method does not return void: " + method); @@ -417,14 +517,14 @@ private static void implementUserMethod(Method method, ClassVisitor classVisitor int localIndexOfWOffset = locals.newLocal(long.class); // long wOffset = this.writeAcquireWithWaitStrategy(); - writeAcquireWithWaitStrategy(methodVisitor); + writeAcquireWithWaitStrategy(methodVisitor, generatedName, backendType); methodVisitor.visitVarInsn(Opcodes.LSTORE, localIndexOfWOffset); int localIndexOfArrayReferenceBaseIndex = Integer.MIN_VALUE; if (containsReferences) { - // long arrayReferenceBaseIndex = this.producerReferenceArrayIndex(); - producerReferenceArrayIndex(methodVisitor); + // long arrayReferenceBaseIndex = this.producerReferenceArrayIndex();x + producerReferenceArrayIndex(methodVisitor, backendType); localIndexOfArrayReferenceBaseIndex = locals.newLocal(long.class); methodVisitor.visitVarInsn(Opcodes.LSTORE, localIndexOfArrayReferenceBaseIndex); } @@ -449,9 +549,14 @@ private static void implementUserMethod(Method method, ClassVisitor classVisitor if (parameterType.isPrimitive()) { varOffset += putUnsafe(methodVisitor, parameterType, localIndexOfWOffset, wOffsetDelta, varOffset); - wOffsetDelta += memorySize(parameterType); + wOffsetDelta += primitiveMemorySize(parameterType); } else { - putReference(methodVisitor, parameterType, localIndexOfArrayReferenceBaseIndex, arrayReferenceBaseIndexDelta, varOffset); + putReference(methodVisitor, + parameterType, + localIndexOfArrayReferenceBaseIndex, + arrayReferenceBaseIndexDelta, + varOffset, + backendType); varOffset += Type.getType(parameterType).getSize(); arrayReferenceBaseIndexDelta++; } @@ -459,7 +564,7 @@ private static void implementUserMethod(Method method, ClassVisitor classVisitor // #END // this.writeRelease(wOffset, #TYPE); - writeRelease(methodVisitor, localIndexOfWOffset, type); + writeRelease(methodVisitor, localIndexOfWOffset, type, backendType); // return; methodVisitor.visitInsn(Opcodes.RETURN); @@ -469,41 +574,44 @@ private static void implementUserMethod(Method method, ClassVisitor classVisitor methodVisitor.visitEnd(); } - private static void producerReferenceArrayIndex(MethodVisitor methodVisitor) { + private static void producerReferenceArrayIndex(MethodVisitor methodVisitor, Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); - methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), "producerReferenceArrayIndex", "()J", false); + methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(backendType), "producerReferenceArrayIndex", "()J", false); } - private static void consumerReferenceArrayIndex(MethodVisitor methodVisitor) { + private static void consumerReferenceArrayIndex(MethodVisitor methodVisitor, Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); - methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), "consumerReferenceArrayIndex", "()J", false); + methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(backendType), "consumerReferenceArrayIndex", "()J", false); } - private static void writeAcquireWithWaitStrategy(MethodVisitor methodVisitor) { + private static void writeAcquireWithWaitStrategy(MethodVisitor methodVisitor, String generatedName, Class backendType) { + // One of these is for the getfield bytecode, the other is as the first arg to the writeAcquireWithWaitStrategy methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); - methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), + methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); + methodVisitor.visitFieldInsn(Opcodes.GETFIELD, generatedName, "waitStrategy", Type.getDescriptor(WaitStrategy.class)); + methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + Type.getInternalName(ProxyChannelFactory.class), "writeAcquireWithWaitStrategy", - "()J", + methodDescriptor(long.class, ProxyChannelRingBuffer.class, WaitStrategy.class), false); } - private static void writeRelease(MethodVisitor methodVisitor, int wOffset, int type) { + private static void writeRelease(MethodVisitor methodVisitor, int wOffset, int type, Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); methodVisitor.visitVarInsn(Opcodes.LLOAD, wOffset); methodVisitor.visitLdcInsn(type); - methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), "writeRelease", "(JI)V", false); + methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(backendType), "writeRelease", "(JI)V", false); } - private static void readAcquire(MethodVisitor methodVisitor) { + private static void readAcquire(MethodVisitor methodVisitor, Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); - methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), "readAcquire", "()J", false); + methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(backendType), "readAcquire", "()J", false); } - private static void readRelease(MethodVisitor methodVisitor, int wOffset) { + private static void readRelease(MethodVisitor methodVisitor, int wOffset, Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); methodVisitor.visitVarInsn(Opcodes.LLOAD, wOffset); - methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), "readRelease", "(J)V", false); + methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(backendType), "readRelease", "(J)V", false); } private static int getUnsafe(MethodVisitor methodVisitor, Class parameterType, int localIndexOfROffset, int rOffsetDelta) { @@ -519,21 +627,30 @@ private static int putUnsafe(MethodVisitor methodVisitor, Class parameterType return parameterTypeUnsafe(methodVisitor, parameterType, true); } - private static void getReference(MethodVisitor methodVisitor, Class parameterType, int localIndexOfArrayReferenceBaseIndex, int arrayReferenceBaseIndexDelta) { + private static void getReference(MethodVisitor methodVisitor, + Class parameterType, + int localIndexOfArrayReferenceBaseIndex, + int arrayReferenceBaseIndexDelta, + Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); loadLocalIndexAndApplyDelta(methodVisitor, localIndexOfArrayReferenceBaseIndex, arrayReferenceBaseIndexDelta); - readReference(methodVisitor); + readReference(methodVisitor, backendType); if (parameterType != Object.class) { methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(parameterType)); } } - private static void putReference(MethodVisitor methodVisitor, Class parameterType, int localIndexOfArrayReferenceBaseIndex, int arrayReferenceBaseIndexDelta, int varOffset) { + private static void putReference(MethodVisitor methodVisitor, + Class parameterType, + int localIndexOfArrayReferenceBaseIndex, + int arrayReferenceBaseIndexDelta, + int varOffset, + Class backendType) { methodVisitor.visitVarInsn(Opcodes.ALOAD, LOCALS_INDEX_THIS); loadLocalIndexAndApplyDelta(methodVisitor, localIndexOfArrayReferenceBaseIndex, arrayReferenceBaseIndexDelta); methodVisitor.visitVarInsn(Type.getType(parameterType).getOpcode(Opcodes.ILOAD), varOffset); - writeReference(methodVisitor); + writeReference(methodVisitor, backendType); } private static void loadUnsafe(MethodVisitor methodVisitor) { @@ -567,25 +684,25 @@ private static int parameterTypeUnsafe(MethodVisitor methodVisitor, Class par return type.getSize(); } - private static void writeReference(MethodVisitor methodVisitor) { + private static void writeReference(MethodVisitor methodVisitor, Class backendType) { methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), + Type.getInternalName(backendType), "writeReference", ("(JLjava/lang/Object;)V"), false); } - private static void readReference(MethodVisitor methodVisitor) { + private static void readReference(MethodVisitor methodVisitor, Class backend) { methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - Type.getInternalName(SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.class), + Type.getInternalName(backend), "readReference", ("(J)Ljava/lang/Object;"), false); } - private static int memorySize(Class type) { + private static int primitiveMemorySize(Class type) { if (!type.isPrimitive()) { - throw new IllegalArgumentException("Cannot handle non-primtive parameter type: " + type); // TODO: Add handling for reference parameters. + throw new IllegalArgumentException("Cannot handle non-primtive parameter type: " + type); } return type == long.class || type == double.class ? 8 : 4; } diff --git a/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelRingBuffer.java new file mode 100644 index 00000000..320c13b9 --- /dev/null +++ b/jctools-experimental/src/main/java/org/jctools/channels/proxy/ProxyChannelRingBuffer.java @@ -0,0 +1,75 @@ +package org.jctools.channels.proxy; + +/** + * This is the definition of the API required for the byte code generated by the {@link ProxyChannelFactory} + */ +public abstract class ProxyChannelRingBuffer { + public static final long EOF = 0; + + /** + * Acquire an offset to write to. If there's no space available a wait + * strategy may be used. + * + * @return the offset that was acquired for writing or {@link #EOF} + */ + protected abstract long writeAcquire(); + + /** + * Ordered store of the callTypeId for the message at offset + * + * @param offset + * the offset that was released for writing + * @param callTypeId + * A unique ID for the call + */ + protected abstract void writeRelease(long offset, int callTypeId); + + /** + * Acquire an offset to read from + * + * @return the offset that was acquired for reading or {@link #EOF} + */ + protected abstract long readAcquire(); + + /** + * Release the offset from reading + * + * @param offset + * the offset to release for reading + */ + protected abstract void readRelease(long offset); + + /** + * Get the position index of the consumer in the reference array + * + * @return the consumer index + */ + protected abstract long consumerReferenceArrayIndex(); + + /** + * Get the position index of the producer in the reference array + * + * @return the producer index + */ + protected abstract long producerReferenceArrayIndex(); + + /** + * Write a reference into the index of the reference array. + * + * @param index + * the index to write to + * @param reference + * the reference to write + */ + protected abstract void writeReference(long index, Object reference); + + /** + * Read a reference from the index of the reference array. + * + * @param index + * the index to read from + * @return the reference that was read + */ + protected abstract Object readReference(long index); + +} diff --git a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelConsumer.java b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelConsumer.java index 327fe27c..b58a6238 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelConsumer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelConsumer.java @@ -33,7 +33,7 @@ public SpscChannelConsumer( final int messageSize, final ChannelReceiver receiver) { - super(buffer, capacity, false, true, false, messageSize); + super(buffer, capacity, false, true, false, messageSize, null, 0); this.receiver = receiver; this.pointer = EOF; diff --git a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelProducer.java b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelProducer.java index fb7a1b42..34810b55 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelProducer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscChannelProducer.java @@ -31,7 +31,7 @@ public SpscChannelProducer( final int capacity, final int messageSize) { - super(buffer, capacity, true, false, true, messageSize); + super(buffer, capacity, true, false, true, messageSize, null, 0); pointer = EOF; } diff --git a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBuffer.java index 4b5caeef..5d2eff10 100644 --- a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBuffer.java +++ b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBuffer.java @@ -21,6 +21,7 @@ import org.jctools.channels.OffHeapFixedMessageSizeRingBuffer; import org.jctools.util.Pow2; +import org.jctools.util.UnsafeRefArrayAccess; /** * Channel protocol: @@ -42,9 +43,15 @@ public static int getLookaheadStep(final int capacity) { return Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); } - public SpscOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize) { - this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), CACHE_LINE_SIZE), Pow2 - .roundToPowerOfTwo(capacity), true, true, true, messageSize); + public SpscOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize, final int referenceMessageSize) { + this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), CACHE_LINE_SIZE), + Pow2.roundToPowerOfTwo(capacity), + true, + true, + true, + messageSize, + createReferenceArray(capacity, referenceMessageSize), + referenceMessageSize); } /** @@ -54,10 +61,22 @@ public SpscOffHeapFixedSizeRingBuffer(final int capacity, final int messageSize) * @param capacity in messages, actual capacity will be * @param messageSize */ - protected SpscOffHeapFixedSizeRingBuffer(final ByteBuffer buff, final int capacity, - final boolean isProducer, final boolean isConsumer, final boolean initialize, - final int messageSize) { - super(buff,capacity,isProducer,isConsumer,initialize,messageSize); + protected SpscOffHeapFixedSizeRingBuffer(final ByteBuffer buff, + final int capacity, + final boolean isProducer, + final boolean isConsumer, + final boolean initialize, + final int messageSize, + final Object[] references, + final int referenceMessageSize) { + super(buff, + capacity, + isProducer, + isConsumer, + initialize, + messageSize, + references, + referenceMessageSize); this.lookAheadStep = getLookaheadStep(capacity); // Layout of the RingBuffer (assuming 64b cache line): @@ -101,6 +120,7 @@ protected final void writeRelease(long offset) { writeReleaseState(offset); } + @Override protected final void writeRelease(long offset, int type) { assert type != 0; UNSAFE.putOrderedInt(null, offset, type); diff --git a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.java b/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.java deleted file mode 100644 index 2e46c781..00000000 --- a/jctools-experimental/src/main/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jctools.channels.spsc; - -import static org.jctools.util.JvmInfo.CACHE_LINE_SIZE; -import static org.jctools.util.UnsafeAccess.UNSAFE; -import static org.jctools.util.UnsafeDirectByteBuffer.allocateAlignedByteBuffer; - -import java.nio.ByteBuffer; - -import org.jctools.channels.OffHeapFixedMessageSizeWithReferenceSupportRingBuffer; -import org.jctools.util.Pow2; -import org.jctools.util.UnsafeRefArrayAccess; - -public class SpscOffHeapFixedSizeWithReferenceSupportRingBuffer extends OffHeapFixedMessageSizeWithReferenceSupportRingBuffer { - - private static final Integer MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", - 4096); - - public static final long EOF = 0; - - private final int lookAheadStep; - private final long producerLookAheadCacheAddress; - private final WaitStrategy waitStrategy; - - public static int getLookaheadStep(final int capacity) { - return Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); - } - - public SpscOffHeapFixedSizeWithReferenceSupportRingBuffer(final int capacity, - final int messageSize, - final int arrayMessageSize, - final WaitStrategy waitStrategy) { - this(allocateAlignedByteBuffer(getRequiredBufferSize(capacity, messageSize), CACHE_LINE_SIZE), - Pow2.roundToPowerOfTwo(capacity), - true, - true, - true, - messageSize, - createReferenceArray(capacity, arrayMessageSize), - arrayMessageSize, - waitStrategy); - } - - /** - * This is to be used for an IPC queue with the direct buffer used being a memory mapped file. - * - * @param buff - * @param capacity in messages, actual capacity will be - * @param messageSize size in bytes for each message - * @param arrayMessageSize size in element count for each message - * - */ - protected SpscOffHeapFixedSizeWithReferenceSupportRingBuffer(final ByteBuffer buff, - final int capacity, - final boolean isProducer, - final boolean isConsumer, - final boolean initialize, - final int messageSize, - final Object[] references, - final int arrayMessageSize, - final WaitStrategy waitStrategy) { - super(buff, capacity, isProducer, isConsumer, initialize, messageSize, references, arrayMessageSize); - this.waitStrategy = waitStrategy; - - this.lookAheadStep = getLookaheadStep(capacity); - // Layout of the RingBuffer (assuming 64b cache line): - // consumerIndex(8b), pad(56b) | - // pad(64b) | - // producerIndex(8b), producerLookAheadCache(8b), pad(48b) | - // pad(64b) | - // buffer (capacity * messageSize) - this.producerLookAheadCacheAddress = this.producerIndexAddress + 8; - - // producer owns tail and headCache - if (isProducer && initialize) { - spLookAheadCache(0); - } - } - - public interface WaitStrategy { - /** - * This method can implement static or dynamic backoff. Dynamic backoff will rely on the counter for - * estimating how long the caller has been idling. The expected usage is: - * - *
-         * 
-         * int ic = 0;
-         * while(true) {
-         *   if(!isGodotArrived()) {
-         *     ic = w.idle(ic);
-         *     continue;
-         *   }
-         *   ic = 0;
-         *   // party with Godot until he goes again
-         * }
-         * 
-         * 
- * - * @param idleCounter idle calls counter, managed by the idle method until reset - * @return new counter value to be used on subsequent idle cycle - */ - int idle(int idleCounter); - } - - protected final long writeAcquireWithWaitStrategy() { - long wOffset; - int idleCounter = 0; - while ((wOffset = this.writeAcquire()) == SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.EOF) { - idleCounter = waitStrategy.idle(idleCounter); - } - return wOffset; - } - - @Override - protected final long writeAcquire() { - final long producerIndex = lpProducerIndex(); - final long producerLookAhead = lpLookAheadCache(); - final long producerOffset = offsetForIndex(bufferAddress, mask, messageSize, producerIndex); - // verify next lookAheadStep messages are clear to write - if (producerIndex >= producerLookAhead) { - final long nextLookAhead = producerIndex + lookAheadStep; - if (isReadReleased(offsetForIndex(nextLookAhead))) { - spLookAheadCache(nextLookAhead); - } - // OK, can't look ahead, but maybe just next item is ready? - else if (!isReadReleased(producerOffset)) { - return EOF; - } - } - soProducerIndex(producerIndex + 1); // StoreStore -// writeAcquireState(producerOffset); - // return offset for current producer index - return producerOffset; - } - - protected final void writeReference(long offset, Object reference) { - // Is there a way to compute the element offset once and just - // arithmetic? - UnsafeRefArrayAccess.spElement(references, UnsafeRefArrayAccess.calcElementOffset(offset), reference); - } - - protected final Object readReference(long offset) { - // Is there a way to compute the element offset once and just - // arithmetic? - return UnsafeRefArrayAccess.lpElement(references, UnsafeRefArrayAccess.calcElementOffset(offset)); - } - - @Override - protected final void writeRelease(long offset) { - writeReleaseState(offset); - } - - protected final void writeRelease(long offset, int type) { - assert type != 0; - UNSAFE.putOrderedInt(null, offset, type); - } - - @Override - protected final long readAcquire() { - final long consumerIndex = lpConsumerIndex(); - final long consumerOffset = offsetForIndex(consumerIndex); - if (isReadReleased(consumerOffset)) { - return EOF; - } - soConsumerIndex(consumerIndex + 1); // StoreStore -// readAcquireState(consumerOffset); - return consumerOffset; - } - - @Override - protected final void readRelease(long offset) { - readReleaseState(offset); - - } - - private long lpLookAheadCache() { - return UNSAFE.getLong(null, producerLookAheadCacheAddress); - } - - private void spLookAheadCache(final long value) { - UNSAFE.putLong(producerLookAheadCacheAddress, value); - } -} diff --git a/jctools-experimental/src/test/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBufferTest.java b/jctools-experimental/src/test/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBufferTest.java index 06d30a3c..8ff820a7 100644 --- a/jctools-experimental/src/test/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBufferTest.java +++ b/jctools-experimental/src/test/java/org/jctools/channels/mpsc/MpscOffHeapFixedSizeRingBufferTest.java @@ -20,13 +20,15 @@ public class MpscOffHeapFixedSizeRingBufferTest extends AbstractOffHeapFixedSize @Test public void test() { - MpscOffHeapFixedSizeRingBuffer rb = new MpscOffHeapFixedSizeRingBuffer(1024, 31); + // TODO: Needs test for when referenceMessageSize > 0 + MpscOffHeapFixedSizeRingBuffer rb = new MpscOffHeapFixedSizeRingBuffer(1024, 31, 0); test(rb); } @Test public void testFFLamport() { - MpscFFLamportOffHeapFixedSizeRingBuffer rb = new MpscFFLamportOffHeapFixedSizeRingBuffer(1024, 31); + // TODO: Needs test for when referenceMessageSize > 0 + MpscFFLamportOffHeapFixedSizeRingBuffer rb = new MpscFFLamportOffHeapFixedSizeRingBuffer(1024, 31, 0); test(rb); } diff --git a/jctools-experimental/src/test/java/org/jctools/channels/proxy/DemoProxyResult.java b/jctools-experimental/src/test/java/org/jctools/channels/proxy/DemoProxyResult.java index e0251d64..33e738ab 100644 --- a/jctools-experimental/src/test/java/org/jctools/channels/proxy/DemoProxyResult.java +++ b/jctools-experimental/src/test/java/org/jctools/channels/proxy/DemoProxyResult.java @@ -1,6 +1,7 @@ package org.jctools.channels.proxy; -import org.jctools.channels.spsc.SpscOffHeapFixedSizeWithReferenceSupportRingBuffer; +import org.jctools.channels.WaitStrategy; +import org.jctools.channels.spsc.SpscOffHeapFixedSizeRingBuffer; import org.jctools.util.UnsafeAccess; /** @@ -8,10 +9,12 @@ * * @author yak */ -public class DemoProxyResult extends SpscOffHeapFixedSizeWithReferenceSupportRingBuffer implements ProxyChannel, DemoIFace { - +public class DemoProxyResult extends SpscOffHeapFixedSizeRingBuffer implements ProxyChannel, DemoIFace { + private final WaitStrategy waitStrategy; + public DemoProxyResult(int capacity, WaitStrategy waitStrategy) { - super(capacity, 13, 2, waitStrategy); + super(capacity, 13, 2); + this.waitStrategy = waitStrategy; } @Override @@ -90,7 +93,7 @@ public int process(DemoIFace impl, int limit) { @Override public void call1(int x, int y) { - long wOffset = this.writeAcquireWithWaitStrategy(); + long wOffset = ProxyChannelFactory.writeAcquireWithWaitStrategy(this, waitStrategy); UnsafeAccess.UNSAFE.putInt(wOffset + 4, x); UnsafeAccess.UNSAFE.putInt(wOffset + 8, y); this.writeRelease(wOffset, 1); @@ -98,7 +101,7 @@ public void call1(int x, int y) { @Override public void call2(float x, double y, boolean z) { - long wOffset = this.writeAcquireWithWaitStrategy(); + long wOffset = ProxyChannelFactory.writeAcquireWithWaitStrategy(this, waitStrategy); UnsafeAccess.UNSAFE.putFloat(wOffset + 4, x); UnsafeAccess.UNSAFE.putDouble(wOffset + 8, y); UnsafeAccess.UNSAFE.putBoolean(null, wOffset + 16, z); @@ -108,13 +111,13 @@ public void call2(float x, double y, boolean z) { @Override public void call3() { - long wOffset = this.writeAcquireWithWaitStrategy(); + long wOffset = ProxyChannelFactory.writeAcquireWithWaitStrategy(this, waitStrategy); this.writeRelease(wOffset, 3); } @Override public void call4(Object x, CustomType y) { - long wOffset = this.writeAcquireWithWaitStrategy(); + long wOffset = ProxyChannelFactory.writeAcquireWithWaitStrategy(this, waitStrategy); long arrayReferenceBaseIndex = this.producerReferenceArrayIndex(); this.writeReference(arrayReferenceBaseIndex, x); this.writeReference(arrayReferenceBaseIndex + 1, y); @@ -123,7 +126,7 @@ public void call4(Object x, CustomType y) { @Override public void call5(CustomType x, int y, CustomType z) { - long wOffset = this.writeAcquireWithWaitStrategy(); + long wOffset = ProxyChannelFactory.writeAcquireWithWaitStrategy(this, waitStrategy); long arrayReferenceBaseIndex = this.producerReferenceArrayIndex(); this.writeReference(arrayReferenceBaseIndex, x); UnsafeAccess.UNSAFE.putInt(wOffset + 4, y); @@ -133,7 +136,7 @@ public void call5(CustomType x, int y, CustomType z) { @Override public void call6(int x, CustomType[] y, CustomType... z) { - long wOffset = this.writeAcquireWithWaitStrategy(); + long wOffset = ProxyChannelFactory.writeAcquireWithWaitStrategy(this, waitStrategy); long arrayReferenceBaseIndex = this.producerReferenceArrayIndex(); UnsafeAccess.UNSAFE.putInt(wOffset + 4, x); this.writeReference(arrayReferenceBaseIndex, y); diff --git a/jctools-experimental/src/test/java/org/jctools/channels/proxy/ProxyCreationTest.java b/jctools-experimental/src/test/java/org/jctools/channels/proxy/ProxyCreationTest.java index 7403cef1..c7213c41 100644 --- a/jctools-experimental/src/test/java/org/jctools/channels/proxy/ProxyCreationTest.java +++ b/jctools-experimental/src/test/java/org/jctools/channels/proxy/ProxyCreationTest.java @@ -1,14 +1,16 @@ package org.jctools.channels.proxy; import static org.hamcrest.Matchers.instanceOf; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import org.jctools.channels.WaitStrategy; +import org.jctools.channels.mpsc.MpscOffHeapFixedSizeRingBuffer; import org.jctools.channels.proxy.DemoIFace.CustomType; -import org.jctools.channels.spsc.SpscOffHeapFixedSizeWithReferenceSupportRingBuffer.WaitStrategy; +import org.jctools.channels.spsc.SpscOffHeapFixedSizeRingBuffer; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; public class ProxyCreationTest { private static final class ThrowExceptionOnFullQueue implements WaitStrategy { @@ -21,38 +23,35 @@ public int idle(int idleCounter) { } + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Test public void testGeneratedProxyInstance() { ProxyChannel proxyChannel = ProxyChannelFactory.createSpscProxy(10, DemoIFace.class, (idleCounter) -> 0); DemoIFace proxy = proxyChannel.proxy(); /* - * Not sure what the proper behaviour is here but I can see from the types it should at least be a DemoIFace + * Not sure what the proper behaviour is here but I can see from the + * types it should at least be a DemoIFace */ assertThat(proxyChannel.proxyInstance(proxy), instanceOf(DemoIFace.class)); } @Test - public void testGeneratedHasFullQueue() throws Exception { - // capacity of 10 results in 16 slots in the queue - ProxyChannel proxyChannel = ProxyChannelFactory.createSpscProxy(10, DemoIFace.class, new ThrowExceptionOnFullQueue()); - - DemoIFace proxy = proxyChannel.proxy(); - for (int i = 0; i < 16; i++) { - proxy.call3(); - } - try { - proxy.call3(); - fail("Exception expected"); - } catch (RuntimeException e) { - assertEquals(ThrowExceptionOnFullQueue.MESSAGE, e.getMessage()); - } + public void givenGeneratedProxyUsingSpscReferenceChannel_whenCallMethods_expectAllCallsAreProxied() throws Exception { + util_givenGeneratedProxyUsingReferenceChannel_whenCallMethods_expectAllCallsAreProxied(SpscOffHeapFixedSizeRingBuffer.class); } @Test - public void testGenerated() throws Exception { + public void givenGeneratedProxyUsingMpscReferenceChannel_whenCallMethods_expectAllCallsAreProxied() throws Exception { + util_givenGeneratedProxyUsingReferenceChannel_whenCallMethods_expectAllCallsAreProxied(MpscOffHeapFixedSizeRingBuffer.class); + } - ProxyChannel proxyChannel = ProxyChannelFactory.createSpscProxy(10, DemoIFace.class, (idleCounter) -> 0); + private static void util_givenGeneratedProxyUsingReferenceChannel_whenCallMethods_expectAllCallsAreProxied( + Class backend) { + ProxyChannel proxyChannel = + ProxyChannelFactory.createProxy(10, DemoIFace.class, (idleCounter) -> 0, backend); DemoIFace proxy = proxyChannel.proxy(); CustomType obj1 = new CustomType(); @@ -119,24 +118,38 @@ public void call6(int x, CustomType[] y, CustomType... z) { } @Test - public void testDemoHasFullQueue() throws Exception { + public void givenGeneratedProxy_andQueueIsFull_whenCallAgain_expectRuntimeException() throws Exception { + ProxyChannel proxyChannel = + ProxyChannelFactory.createSpscProxy(10, DemoIFace.class, new ThrowExceptionOnFullQueue()); // capacity of 10 results in 16 slots in the queue + util_givenProxyChannel_andQueueIsFull_whenCallAgain_expectRuntimeException(16, proxyChannel); + } + + @Test + public void givenDemoProxy_andQueueIsFull_whenCallAgain_expectRuntimeException() throws Exception { ProxyChannel proxyChannel = new DemoProxyResult(10, new ThrowExceptionOnFullQueue()); + // capacity of 10 results in 16 slots in the queue + util_givenProxyChannel_andQueueIsFull_whenCallAgain_expectRuntimeException(16, proxyChannel); + } + private void util_givenProxyChannel_andQueueIsFull_whenCallAgain_expectRuntimeException( + int capacity, + ProxyChannel proxyChannel) { DemoIFace proxy = proxyChannel.proxy(); - for (int i = 0; i < 16; i++) { - proxy.call3(); - } - try { + for (int i = 0; i < capacity; i++) { proxy.call3(); - fail("Exception expected"); - } catch (RuntimeException e) { - assertEquals(ThrowExceptionOnFullQueue.MESSAGE, e.getMessage()); } + + // Then + expectedException.expect(RuntimeException.class); + expectedException.expectMessage(ThrowExceptionOnFullQueue.MESSAGE); + + // When + proxy.call3(); } @Test - public void testDemo() throws Exception { + public void givenDemoProxyUsingSpscReferenceChannel_whenCallMethods_expectAllCallsAreProxied() throws Exception { ProxyChannel proxyChannel = new DemoProxyResult(10, (idleCounter) -> 0); diff --git a/jctools-experimental/src/test/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBufferTest.java b/jctools-experimental/src/test/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBufferTest.java index 00ccf989..549e9226 100644 --- a/jctools-experimental/src/test/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBufferTest.java +++ b/jctools-experimental/src/test/java/org/jctools/channels/spsc/SpscOffHeapFixedSizeRingBufferTest.java @@ -20,7 +20,8 @@ public class SpscOffHeapFixedSizeRingBufferTest extends AbstractOffHeapFixedSize @Test public void test() { - SpscOffHeapFixedSizeRingBuffer rb = new SpscOffHeapFixedSizeRingBuffer(1024, 31); + // TODO: Needs test for when referenceMessageSize > 0 + SpscOffHeapFixedSizeRingBuffer rb = new SpscOffHeapFixedSizeRingBuffer(1024, 31, 0); test(rb); } From 742b7ff2a64dfa234c81a52f96a62903fc9874c9 Mon Sep 17 00:00:00 2001 From: Nitsan Wakart Date: Wed, 11 Jan 2017 10:53:19 +0200 Subject: [PATCH 02/17] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e8db6127..e7342b2f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![Build Status](https://travis-ci.org/JCTools/JCTools.svg?branch=master)](https://travis-ci.org/JCTools/JCTools) +[![Gitter](https://badges.gitter.im/JCTools/JCTools.svg)](https://gitter.im/JCTools/JCTools?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + JCTools ========== Java Concurrency Tools for the JVM. This project aims to offer some concurrent data structures currently missing from From b6aa7c2d050e92dae20f17973b37be7d9b383861 Mon Sep 17 00:00:00 2001 From: Victor Parmar Date: Thu, 23 Mar 2017 21:31:12 +0100 Subject: [PATCH 03/17] add jcstress support for #10 --- README.md | 6 ++ jctools-concurrency-test/.gitignore | 5 ++ jctools-concurrency-test/pom.xml | 84 +++++++++++++++++++ .../queues/SpscArrayQueueConsumerTest.java | 35 ++++++++ .../SpscArrayQueueProducerConsumerTest.java | 38 +++++++++ .../queues/SpscArrayQueueProducerTest.java | 32 +++++++ pom.xml | 1 + 7 files changed, 201 insertions(+) create mode 100644 jctools-concurrency-test/.gitignore create mode 100644 jctools-concurrency-test/pom.xml create mode 100644 jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueConsumerTest.java create mode 100644 jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerConsumerTest.java create mode 100644 jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerTest.java diff --git a/README.md b/README.md index e7342b2f..0077d27d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,12 @@ Benchmarks JCTools is benchmarked using both JMH benchmarks and handrolled harnesses. The benchmarks and related instructions can be found in the jctools-benchmarks module README. Go wild and please let us know how it did on your hardware. +Concurrency Testing +=========== +mvn package +cd jctools-concurrency-test +java -jar target/concurrency-test.jar -v + Come up to the lab... ========== Experimental work is available under the jctools-experimental module. Most of the stuff is developed with an eye to diff --git a/jctools-concurrency-test/.gitignore b/jctools-concurrency-test/.gitignore new file mode 100644 index 00000000..73ce2e24 --- /dev/null +++ b/jctools-concurrency-test/.gitignore @@ -0,0 +1,5 @@ +*.iml +*.gz +results +target + diff --git a/jctools-concurrency-test/pom.xml b/jctools-concurrency-test/pom.xml new file mode 100644 index 00000000..fdfbe000 --- /dev/null +++ b/jctools-concurrency-test/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + + org.jctools + jctools-parent + 2.1-SNAPSHOT + + + jctools-concurrency-test + jar + concurrency-test + + + 0.2 + + + + + org.jctools + jctools-core + ${project.version} + + + org.jctools + jctools-experimental + ${project.version} + + + org.openjdk.jcstress + jcstress-core + ${jcstress.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${maven.compiler.testSource} + ${maven.compiler.testTarget} + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + + main + package + + shade + + + concurrency-test + + + org.openjdk.jcstress.Main + + + META-INF/TestList + + + + + + + + + + + + + + + + + + diff --git a/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueConsumerTest.java b/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueConsumerTest.java new file mode 100644 index 00000000..ff854c64 --- /dev/null +++ b/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueConsumerTest.java @@ -0,0 +1,35 @@ +package org.jctools.queues; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.IntResult1; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; +import static org.openjdk.jcstress.annotations.Expect.FORBIDDEN; + +@JCStressTest +@Outcome(id = "1", expect = ACCEPTABLE, desc = "All ok.") +@Outcome(expect = FORBIDDEN) +@State +public class SpscArrayQueueConsumerTest { + private final SpscArrayQueue queue = new SpscArrayQueue<>(3); + + public SpscArrayQueueConsumerTest() { + queue.offer(1); + queue.offer(2); + queue.offer(3); + } + + @Actor + public void actor1() { + queue.poll(); queue.poll(); + } + + @Arbiter + public void arbiter1(IntResult1 result) { + result.r1 = queue.size(); + } +} diff --git a/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerConsumerTest.java b/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerConsumerTest.java new file mode 100644 index 00000000..e79b842f --- /dev/null +++ b/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerConsumerTest.java @@ -0,0 +1,38 @@ +package org.jctools.queues; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.IntResult1; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; +import static org.openjdk.jcstress.annotations.Expect.FORBIDDEN; + +@JCStressTest +@Outcome(id = "1", expect = ACCEPTABLE, desc = "All ok.") +@Outcome(expect = FORBIDDEN) +@State +public class SpscArrayQueueProducerConsumerTest { + private final SpscArrayQueue queue = new SpscArrayQueue<>(3); + + public SpscArrayQueueProducerConsumerTest() { + queue.offer(1); + } + + @Actor + public void actor1() { + queue.poll(); + } + + @Actor + public void actor2() { + queue.offer(8); + } + + @Arbiter + public void arbiter1(IntResult1 result) { + result.r1 = queue.size(); + } +} diff --git a/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerTest.java b/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerTest.java new file mode 100644 index 00000000..13ae8bdb --- /dev/null +++ b/jctools-concurrency-test/src/main/java/org/jctools/queues/SpscArrayQueueProducerTest.java @@ -0,0 +1,32 @@ +package org.jctools.queues; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.BooleanResult2; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE_INTERESTING; +import static org.openjdk.jcstress.annotations.Expect.FORBIDDEN; + +@JCStressTest +@Outcome(id = "true, true", expect = ACCEPTABLE, desc = "All ok.") +@Outcome(id = "true, false", expect = ACCEPTABLE_INTERESTING, desc = "Size is broken.") +@Outcome(id = "false, true", expect = ACCEPTABLE_INTERESTING, desc = "Offer broken.") +@Outcome(id = "false, false", expect = FORBIDDEN, desc = "Nothing ran.") +@State +public class SpscArrayQueueProducerTest { + private final SpscArrayQueue queue = new SpscArrayQueue<>(3); + + @Actor + public void actor1(BooleanResult2 br2) { + br2.r1 = queue.offer(1); + } + + @Arbiter + public void arbiter1(BooleanResult2 br2) { + br2.r2 = !queue.isEmpty(); + } +} diff --git a/pom.xml b/pom.xml index a514324f..99a83954 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ jctools-core jctools-experimental jctools-benchmarks + jctools-concurrency-test From 89a92858efcaaf8a5220645a395019b7af4449f7 Mon Sep 17 00:00:00 2001 From: Bulat Shakirzyanov Date: Tue, 4 Apr 2017 09:57:30 -0400 Subject: [PATCH 04/17] fix indentation --- .../org/jctools/queues/SpscArrayQueue.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java index 94d641df..91951d06 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java @@ -190,20 +190,20 @@ public final long lvConsumerIndex() { return UNSAFE.getLongVolatile(this, C_INDEX_OFFSET); } - @Override - public boolean relaxedOffer(E message) { - return offer(message); - } - - @Override - public E relaxedPoll() { - return poll(); - } - - @Override - public E relaxedPeek() { - return peek(); - } + @Override + public boolean relaxedOffer(E message) { + return offer(message); + } + + @Override + public E relaxedPoll() { + return poll(); + } + + @Override + public E relaxedPeek() { + return peek(); + } @Override public int drain(final Consumer c) { From 39904a973d4ab6371fb1a7346b4a50f767cea470 Mon Sep 17 00:00:00 2001 From: Bulat Shakirzyanov Date: Mon, 10 Apr 2017 11:09:39 -0400 Subject: [PATCH 05/17] make message argument final --- .../src/main/java/org/jctools/queues/SpscArrayQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java index 91951d06..5c549145 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java @@ -191,7 +191,7 @@ public final long lvConsumerIndex() { } @Override - public boolean relaxedOffer(E message) { + public boolean relaxedOffer(final E message) { return offer(message); } From 5d906eaf7437e5f32f5e74dd7b8af3d28ccd8ed2 Mon Sep 17 00:00:00 2001 From: Miron Aseev Date: Fri, 5 May 2017 13:20:40 +0700 Subject: [PATCH 06/17] Fixed the exception range messages --- .../jmh/latency/spsc/RingBurstRoundTripWithGroups.java | 2 +- .../jmh/latency/spsc/RingCqBurstRoundTripWithGroups.java | 2 +- .../src/main/java/org/jctools/queues/MpmcArrayQueue.java | 2 +- .../main/java/org/jctools/queues/SpscChunkedArrayQueue.java | 4 ++-- .../main/java/org/jctools/queues/SpscGrowableArrayQueue.java | 4 ++-- .../java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingBurstRoundTripWithGroups.java b/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingBurstRoundTripWithGroups.java index 75e76054..47a46397 100644 --- a/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingBurstRoundTripWithGroups.java +++ b/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingBurstRoundTripWithGroups.java @@ -156,7 +156,7 @@ public void clear() { public void prepareChain() { // can't have group threads set to zero on a method, so can't handle the length of 1 case if (CHAIN_LENGTH < 2) { - throw new IllegalArgumentException("Chain length must be 1 or more"); + throw new IllegalArgumentException("Chain length must be 2 or more"); } // This is an estimate, but for bounded queues if the burst size is more than actual ring capacity // the benchmark will hang/ diff --git a/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingCqBurstRoundTripWithGroups.java b/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingCqBurstRoundTripWithGroups.java index d3025d03..34baeaba 100644 --- a/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingCqBurstRoundTripWithGroups.java +++ b/jctools-benchmarks/src/main/java/org/jctools/jmh/latency/spsc/RingCqBurstRoundTripWithGroups.java @@ -158,7 +158,7 @@ public void clear() { public void prepareChain() { // can't have group threads set to zero on a method, so can't handle the length of 1 case if (CHAIN_LENGTH < 2) { - throw new IllegalArgumentException("Chain length must be 1 or more"); + throw new IllegalArgumentException("Chain length must be 2 or more"); } // This is an estimate, but for bounded queues if the burst size is more than actual ring capacity // the benchmark will hang/ diff --git a/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java index 85aed347..e594a995 100755 --- a/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java @@ -122,7 +122,7 @@ public MpmcArrayQueue(final int capacity) { private static int validateCapacity(int capacity) { if(capacity < 2) - throw new IllegalArgumentException("Minimum size is 2"); + throw new IllegalArgumentException("Minimum size must be 2 or more"); return capacity; } diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java index 9a581f59..1acfb38c 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java @@ -29,11 +29,11 @@ public SpscChunkedArrayQueue(final int capacity) { public SpscChunkedArrayQueue(final int chunkSize, final int capacity) { if (capacity < 16) { - throw new IllegalArgumentException("Max capacity must be 4 or more"); + throw new IllegalArgumentException("Max capacity must be 16 or more"); } // minimal chunk size of eight makes sure minimal lookahead step is 2 if (chunkSize < 8) { - throw new IllegalArgumentException("Chunk size must be 2 or more"); + throw new IllegalArgumentException("Chunk size must be 8 or more"); } maxQueueCapacity = Pow2.roundToPowerOfTwo(capacity); diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java index 7dd6ac5e..fb34c0f6 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java @@ -28,11 +28,11 @@ public SpscGrowableArrayQueue(final int capacity) { public SpscGrowableArrayQueue(final int chunkSize, final int capacity) { if (capacity < 16) { - throw new IllegalArgumentException("Max capacity must be 4 or more"); + throw new IllegalArgumentException("Max capacity must be 16 or more"); } // minimal chunk size of eight makes sure minimal lookahead step is 2 if (chunkSize < 8) { - throw new IllegalArgumentException("Chunk size must be 2 or more"); + throw new IllegalArgumentException("Chunk size must be 8 or more"); } maxQueueCapacity = Pow2.roundToPowerOfTwo(capacity); diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java index 1eac4abc..eedbd8c7 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java @@ -31,7 +31,7 @@ public MpmcAtomicArrayQueue(int capacity) { private static int validateCapacity(int capacity) { if(capacity < 2) - throw new IllegalArgumentException("Minimum size is 2"); + throw new IllegalArgumentException("Minimum size must be 2 or more"); return capacity; } From 4f6f37fc436896595a34072e8fc95f7583cbce24 Mon Sep 17 00:00:00 2001 From: Miron Aseev Date: Sat, 6 May 2017 16:27:06 +0700 Subject: [PATCH 07/17] Implemented a consistent range handling approach --- .../org/jctools/maps/NonBlockingHashMap.java | 3 +- .../jctools/maps/NonBlockingHashMapLong.java | 19 ++- .../maps/NonBlockingIdentityHashMap.java | 5 +- .../org/jctools/maps/NonBlockingSetInt.java | 6 +- .../queues/BaseMpscLinkedArrayQueue.java | 9 +- .../org/jctools/queues/MpmcArrayQueue.java | 10 +- .../jctools/queues/MpscChunkedArrayQueue.java | 11 +- .../org/jctools/queues/MpscCompoundQueue.java | 11 +- .../queues/MpscGrowableArrayQueue.java | 5 +- .../jctools/queues/SpscChunkedArrayQueue.java | 14 +-- .../queues/SpscGrowableArrayQueue.java | 14 +-- .../queues/atomic/MpmcAtomicArrayQueue.java | 9 +- .../main/java/org/jctools/util/RangeUtil.java | 60 ++++++++++ .../java/org/jctools/util/RangeUtilTest.java | 112 ++++++++++++++++++ 14 files changed, 223 insertions(+), 65 deletions(-) create mode 100644 jctools-core/src/main/java/org/jctools/util/RangeUtil.java create mode 100644 jctools-core/src/test/java/org/jctools/util/RangeUtilTest.java diff --git a/jctools-core/src/main/java/org/jctools/maps/NonBlockingHashMap.java b/jctools-core/src/main/java/org/jctools/maps/NonBlockingHashMap.java index a08296c0..df1cfcac 100644 --- a/jctools-core/src/main/java/org/jctools/maps/NonBlockingHashMap.java +++ b/jctools-core/src/main/java/org/jctools/maps/NonBlockingHashMap.java @@ -21,6 +21,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.jctools.util.RangeUtil; /** * A lock-free alternate implementation of {@link java.util.concurrent.ConcurrentHashMap} @@ -266,7 +267,7 @@ private static int reprobe_limit( int len ) { * initial size will be rounded up internally to the next larger power of 2. */ public NonBlockingHashMap( final int initial_sz ) { initialize(initial_sz); } private final void initialize( int initial_sz ) { - if( initial_sz < 0 ) throw new IllegalArgumentException(); + RangeUtil.checkPositiveOrZero(initial_sz, "initial_sz"); int i; // Convert to next largest power-of-2 if( initial_sz > 1024*1024 ) initial_sz = 1024*1024; for( i=MIN_SIZE_LOG; (1< 1024*1024 ) initial_sz = 1024*1024; for( i=MIN_SIZE_LOG; (1<true if i was added to the set. */ public boolean add( final int i ) { - if( i < 0 ) throw new IllegalArgumentException(""+i); + RangeUtil.checkPositiveOrZero(i, "i"); return _nbsi.add(i); } /** diff --git a/jctools-core/src/main/java/org/jctools/queues/BaseMpscLinkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/BaseMpscLinkedArrayQueue.java index ab9e271f..15fe7c59 100644 --- a/jctools-core/src/main/java/org/jctools/queues/BaseMpscLinkedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/BaseMpscLinkedArrayQueue.java @@ -25,6 +25,7 @@ import java.util.Iterator; import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; abstract class BaseMpscLinkedArrayQueuePad1 extends AbstractQueue { long p01, p02, p03, p04, p05, p06, p07; @@ -105,9 +106,7 @@ public abstract class BaseMpscLinkedArrayQueue extends BaseMpscLinkedArrayQue * Must be 2 or more. */ public BaseMpscLinkedArrayQueue(final int initialCapacity) { - if (initialCapacity < 2) { - throw new IllegalArgumentException("Initial capacity must be 2 or more"); - } + RangeUtil.checkGreaterThanOrEqual(initialCapacity, 2, "initialCapacity"); int p2capacity = Pow2.roundToPowerOfTwo(initialCapacity); // leave lower bit of mask clear @@ -519,9 +518,7 @@ private void resize(long oldMask, E[] oldBuffer, long pIndex, final E e) { // ASSERT code final long cIndex = lvConsumerIndex(); final long availableInQueue = availableInQueue(pIndex, cIndex); - if (availableInQueue <= 0) { - throw new IllegalStateException(); - } + RangeUtil.checkPositive(availableInQueue, "availableInQueue"); // Invalidate racing CASs // We never set the limit beyond the bounds of a buffer diff --git a/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java index e594a995..8220b067 100755 --- a/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java @@ -18,7 +18,7 @@ import static org.jctools.util.UnsafeRefArrayAccess.lpElement; import static org.jctools.util.UnsafeRefArrayAccess.soElement; -import java.util.Objects; +import org.jctools.util.RangeUtil; abstract class MpmcArrayQueueL1Pad extends ConcurrentSequencedCircularArrayQueue { long p00, p01, p02, p03, p04, p05, p06, p07; @@ -117,13 +117,7 @@ public class MpmcArrayQueue extends MpmcArrayQueueConsumerField implements final static int RECOMENDED_POLL_BATCH = CPUs * 4; final static int RECOMENDED_OFFER_BATCH = CPUs * 4; public MpmcArrayQueue(final int capacity) { - super(validateCapacity(capacity)); - } - - private static int validateCapacity(int capacity) { - if(capacity < 2) - throw new IllegalArgumentException("Minimum size must be 2 or more"); - return capacity; + super(RangeUtil.checkGreaterThanOrEqual(capacity, 2, "capacity")); } @Override diff --git a/jctools-core/src/main/java/org/jctools/queues/MpscChunkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpscChunkedArrayQueue.java index 4c727ed6..4b08c96a 100644 --- a/jctools-core/src/main/java/org/jctools/queues/MpscChunkedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpscChunkedArrayQueue.java @@ -18,18 +18,15 @@ import static org.jctools.util.Pow2.roundToPowerOfTwo; import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; abstract class MpscChunkedArrayQueueColdProducerFields extends BaseMpscLinkedArrayQueue { protected final long maxQueueCapacity; public MpscChunkedArrayQueueColdProducerFields(int initialCapacity, int maxCapacity) { super(initialCapacity); - if (maxCapacity < 4) { - throw new IllegalArgumentException("Max capacity must be 4 or more"); - } - if (Pow2.roundToPowerOfTwo(initialCapacity) >= Pow2.roundToPowerOfTwo(maxCapacity)) { - throw new IllegalArgumentException( - "Initial capacity cannot exceed maximum capacity(both rounded up to a power of 2)"); - } + RangeUtil.checkGreaterThanOrEqual(maxCapacity, 4, "maxCapacity"); + RangeUtil.checkLessThan(roundToPowerOfTwo(initialCapacity), roundToPowerOfTwo(maxCapacity), + "initialCapacity"); maxQueueCapacity = ((long)Pow2.roundToPowerOfTwo(maxCapacity)) << 1; } } diff --git a/jctools-core/src/main/java/org/jctools/queues/MpscCompoundQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpscCompoundQueue.java index fad763ec..da71315b 100644 --- a/jctools-core/src/main/java/org/jctools/queues/MpscCompoundQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpscCompoundQueue.java @@ -13,13 +13,14 @@ */ package org.jctools.queues; -import java.util.AbstractQueue; -import java.util.Iterator; - import static org.jctools.util.JvmInfo.CPUs; import static org.jctools.util.Pow2.isPowerOfTwo; import static org.jctools.util.Pow2.roundToPowerOfTwo; +import java.util.AbstractQueue; +import java.util.Iterator; +import org.jctools.util.RangeUtil; + /** * Use a set number of parallel MPSC queues to diffuse the contention on tail. */ @@ -41,9 +42,7 @@ public MpscCompoundQueueColdFields(int capacity, int queueParallelism) { parallelQueuesMask = parallelQueues - 1; queues = new MpscArrayQueue[parallelQueues]; int fullCapacity = roundToPowerOfTwo(capacity); - if(fullCapacity < parallelQueues) { - throw new IllegalArgumentException("Queue capacity must exceed parallelism"); - } + RangeUtil.checkGreaterThanOrEqual(fullCapacity, parallelQueues, "fullCapacity"); for (int i = 0; i < parallelQueues; i++) { queues[i] = new MpscArrayQueue(fullCapacity / parallelQueues); } diff --git a/jctools-core/src/main/java/org/jctools/queues/MpscGrowableArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpscGrowableArrayQueue.java index f26235ed..6dec421d 100644 --- a/jctools-core/src/main/java/org/jctools/queues/MpscGrowableArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpscGrowableArrayQueue.java @@ -14,6 +14,7 @@ package org.jctools.queues; import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; /** @@ -45,9 +46,7 @@ public MpscGrowableArrayQueue(int initialCapacity, int maxCapacity) { @Override protected int getNextBufferSize(E[] buffer) { final long maxSize = maxQueueCapacity / 2; - if (buffer.length > maxSize) { - throw new IllegalStateException(); - } + RangeUtil.checkLessThanOrEqual(buffer.length, maxSize, "buffer.length"); final int newSize = 2 * (buffer.length - 1); return newSize + 1; } diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java index 1acfb38c..d2cf1161 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscChunkedArrayQueue.java @@ -18,6 +18,7 @@ import static org.jctools.util.UnsafeRefArrayAccess.lvElement; import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; public class SpscChunkedArrayQueue extends BaseSpscLinkedArrayQueue { private int maxQueueCapacity; @@ -28,20 +29,13 @@ public SpscChunkedArrayQueue(final int capacity) { } public SpscChunkedArrayQueue(final int chunkSize, final int capacity) { - if (capacity < 16) { - throw new IllegalArgumentException("Max capacity must be 16 or more"); - } + RangeUtil.checkGreaterThanOrEqual(capacity, 16, "capacity"); // minimal chunk size of eight makes sure minimal lookahead step is 2 - if (chunkSize < 8) { - throw new IllegalArgumentException("Chunk size must be 8 or more"); - } + RangeUtil.checkGreaterThanOrEqual(chunkSize, 8, "chunkSize"); maxQueueCapacity = Pow2.roundToPowerOfTwo(capacity); int chunkCapacity = Pow2.roundToPowerOfTwo(chunkSize); - if (chunkCapacity >= maxQueueCapacity) { - throw new IllegalArgumentException( - "Initial capacity cannot exceed maximum capacity(both rounded up to a power of 2)"); - } + RangeUtil.checkLessThan(chunkCapacity, maxQueueCapacity, "chunkCapacity"); long mask = chunkCapacity - 1; // need extra element to point at next array diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java index fb34c0f6..411f8426 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscGrowableArrayQueue.java @@ -18,6 +18,7 @@ import static org.jctools.util.UnsafeRefArrayAccess.lvElement; import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; public class SpscGrowableArrayQueue extends BaseSpscLinkedArrayQueue { private int maxQueueCapacity; // ignored by the unbounded implementation @@ -27,20 +28,13 @@ public SpscGrowableArrayQueue(final int capacity) { } public SpscGrowableArrayQueue(final int chunkSize, final int capacity) { - if (capacity < 16) { - throw new IllegalArgumentException("Max capacity must be 16 or more"); - } + RangeUtil.checkGreaterThanOrEqual(capacity, 16, "capacity"); // minimal chunk size of eight makes sure minimal lookahead step is 2 - if (chunkSize < 8) { - throw new IllegalArgumentException("Chunk size must be 8 or more"); - } + RangeUtil.checkGreaterThanOrEqual(chunkSize, 8, "chunkSize"); maxQueueCapacity = Pow2.roundToPowerOfTwo(capacity); int chunkCapacity = Pow2.roundToPowerOfTwo(chunkSize); - if (chunkCapacity >= maxQueueCapacity) { - throw new IllegalArgumentException( - "Initial capacity cannot exceed maximum capacity(both rounded up to a power of 2)"); - } + RangeUtil.checkLessThan(chunkCapacity, maxQueueCapacity, "chunkCapacity"); long mask = chunkCapacity - 1; // need extra element to point at next array diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java index eedbd8c7..482c674a 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/MpmcAtomicArrayQueue.java @@ -17,6 +17,7 @@ import java.util.concurrent.atomic.AtomicLongArray; import org.jctools.queues.QueueProgressIndicators; +import org.jctools.util.RangeUtil; public class MpmcAtomicArrayQueue extends SequencedAtomicReferenceArrayQueue implements QueueProgressIndicators { @@ -24,17 +25,11 @@ public class MpmcAtomicArrayQueue extends SequencedAtomicReferenceArrayQueue< private final AtomicLong consumerIndex; public MpmcAtomicArrayQueue(int capacity) { - super(validateCapacity(capacity)); + super(RangeUtil.checkGreaterThanOrEqual(capacity, 2, "capacity")); this.producerIndex = new AtomicLong(); this.consumerIndex = new AtomicLong(); } - private static int validateCapacity(int capacity) { - if(capacity < 2) - throw new IllegalArgumentException("Minimum size must be 2 or more"); - return capacity; - } - @Override public boolean offer(final E e) { if (null == e) { diff --git a/jctools-core/src/main/java/org/jctools/util/RangeUtil.java b/jctools-core/src/main/java/org/jctools/util/RangeUtil.java new file mode 100644 index 00000000..921dff1c --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/util/RangeUtil.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.util; + +public final class RangeUtil { + + private RangeUtil() { + } + + public static long checkPositive(long n, String name) { + if (n <= 0) { + throw new IllegalArgumentException(name + ": " + n + " (expected: > 0)"); + } + + return n; + } + + public static int checkPositiveOrZero(int n, String name) { + if (n < 0) { + throw new IllegalArgumentException(name + ": " + n + " (expected: >= 0)"); + } + + return n; + } + + public static int checkLessThan(int n, int expected, String name) { + if (n >= expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: < " + expected + ')'); + } + + return n; + } + + public static int checkLessThanOrEqual(int n, long expected, String name) { + if (n > expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: <= " + expected + ')'); + } + + return n; + } + + public static int checkGreaterThanOrEqual(int n, int expected, String name) { + if (n < expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: >= " + expected + ')'); + } + + return n; + } +} diff --git a/jctools-core/src/test/java/org/jctools/util/RangeUtilTest.java b/jctools-core/src/test/java/org/jctools/util/RangeUtilTest.java new file mode 100644 index 00000000..c9100d37 --- /dev/null +++ b/jctools-core/src/test/java/org/jctools/util/RangeUtilTest.java @@ -0,0 +1,112 @@ +package org.jctools.util; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +public class RangeUtilTest { + + @Test(expected = IllegalArgumentException.class) + public void checkPositiveMustFailIfArgumentIsZero() { + RangeUtil.checkPositive(0, "var"); + } + + @Test(expected = IllegalArgumentException.class) + public void checkPositiveMustFailIfArgumentIsLessThanZero() { + RangeUtil.checkPositive(-1, "var"); + } + + @Test + public void checkPositiveMustPassIfArgumentIsGreaterThanZero() { + final long n = 1; + final long actual = RangeUtil.checkPositive(n, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test(expected = IllegalArgumentException.class) + public void checkPositiveOrZeroMustFailIfArgumentIsNegative() { + RangeUtil.checkPositiveOrZero(-1, "var"); + } + + @Test + public void checkPositiveOrZeroMustPassIfArgumentIsZero() { + final int n = 0; + final int actual = RangeUtil.checkPositiveOrZero(n, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test + public void checkPositiveOrZeroMustPassIfArgumentIsGreaterThanZero() { + final int n = 1; + final int actual = RangeUtil.checkPositiveOrZero(n, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test(expected = IllegalArgumentException.class) + public void checkLessThanMustFailIfArgumentIsGreaterThanExpected() { + RangeUtil.checkLessThan(1, 0, "var"); + } + + @Test(expected = IllegalArgumentException.class) + public void checkLessThanMustFailIfArgumentIsEqualToExpected() { + final int n = 1; + final int actual = RangeUtil.checkLessThan(1, 1, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test + public void checkLessThanMustPassIfArgumentIsLessThanExpected() { + final int n = 0; + final int actual = RangeUtil.checkLessThan(n, 1, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test(expected = IllegalArgumentException.class) + public void checkLessThanOrEqualMustFailIfArgumentIsGreaterThanExpected() { + RangeUtil.checkLessThanOrEqual(1, 0, "var"); + } + + @Test + public void checkLessThanOrEqualMustPassIfArgumentIsEqualToExpected() { + final int n = 1; + final int actual = RangeUtil.checkLessThanOrEqual(n, 1, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test + public void checkLessThanOrEqualMustPassIfArgumentIsLessThanExpected() { + final int n = 0; + final int actual = RangeUtil.checkLessThanOrEqual(n, 1, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test(expected = IllegalArgumentException.class) + public void checkGreaterThanOrEqualMustFailIfArgumentIsLessThanExpected() { + RangeUtil.checkGreaterThanOrEqual(0, 1, "var"); + } + + @Test + public void checkGreaterThanOrEqualMustPassIfArgumentIsEqualToExpected() { + final int n = 1; + final int actual = RangeUtil.checkGreaterThanOrEqual(n, 1, "var"); + + assertThat(actual, is(equalTo(n))); + } + + @Test + public void checkGreaterThanOrEqualMustPassIfArgumentIsGreaterThanExpected() { + final int n = 1; + final int actual = RangeUtil.checkGreaterThanOrEqual(n, 0, "var"); + + assertThat(actual, is(equalTo(n))); + } +} \ No newline at end of file From 9481ebc0f2d9795d2aa6cfa60ee02f63c41dbb35 Mon Sep 17 00:00:00 2001 From: Christopher Batey Date: Thu, 11 May 2017 15:02:00 +0100 Subject: [PATCH 08/17] Remove unused imports from MpscUnboundedArrayQueue --- .../main/java/org/jctools/queues/MpscUnboundedArrayQueue.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/jctools-core/src/main/java/org/jctools/queues/MpscUnboundedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpscUnboundedArrayQueue.java index 35931521..e2451785 100644 --- a/jctools-core/src/main/java/org/jctools/queues/MpscUnboundedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpscUnboundedArrayQueue.java @@ -13,10 +13,6 @@ */ package org.jctools.queues; -import org.jctools.queues.MessagePassingQueue.Consumer; -import org.jctools.queues.MessagePassingQueue.Supplier; -import org.jctools.util.Pow2; - /** * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in linked chunks * of the initial size. The queue grows only when the current buffer is full and elements are not copied on From 50fd390bcb8a27233fc719871b0f212a914b088a Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 16:02:48 +0100 Subject: [PATCH 09/17] Created an Atomic version of the BaseSpscLinkedArrayQueue - sans UnSafe usages, replicated relevant tests and used Atomic data types wherever needed --- .../jctools/queues/IndexedQueueSizeUtil.java | 8 +- .../AtomicBaseSpscLinkedArrayQueue.java | 257 ++++++++++++++++++ .../atomic/AtomicSpscChunkedArrayQueue.java | 93 +++++++ .../AtomicSpscLinkedArrayQueueSanityTest.java | 32 +++ 4 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java create mode 100644 jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java diff --git a/jctools-core/src/main/java/org/jctools/queues/IndexedQueueSizeUtil.java b/jctools-core/src/main/java/org/jctools/queues/IndexedQueueSizeUtil.java index 16d89ad2..17fa42d3 100644 --- a/jctools-core/src/main/java/org/jctools/queues/IndexedQueueSizeUtil.java +++ b/jctools-core/src/main/java/org/jctools/queues/IndexedQueueSizeUtil.java @@ -13,13 +13,13 @@ */ package org.jctools.queues; -final class IndexedQueueSizeUtil { +public final class IndexedQueueSizeUtil { private IndexedQueueSizeUtil(){} - protected interface IndexedQueue { + public interface IndexedQueue { long lvConsumerIndex(); long lvProducerIndex(); } - static int size(IndexedQueue iq) { + public static int size(IndexedQueue iq) { /* * It is possible for a thread to be interrupted or reschedule between the read of the producer and * consumer indices, therefore protection is required to ensure size is within valid range. In the @@ -47,7 +47,7 @@ static int size(IndexedQueue iq) { } } - static boolean isEmpty(IndexedQueue iq) { + public static boolean isEmpty(IndexedQueue iq) { // Order matters! // Loading consumer before producer allows for producer increments after consumer index is read. // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there is diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java new file mode 100644 index 00000000..c924324b --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java @@ -0,0 +1,257 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.queues.IndexedQueueSizeUtil; +import org.jctools.queues.IndexedQueueSizeUtil.IndexedQueue; +import org.jctools.queues.QueueProgressIndicators; + +import java.util.AbstractQueue; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceArray; + +abstract class AtomicBaseSpscLinkedArrayQueuePrePad extends AbstractQueue { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15; + // p16, p17; drop 2 longs, the cold fields act as buffer +} + +abstract class AtomicBaseSpscLinkedArrayQueueConsumerColdFields extends AtomicBaseSpscLinkedArrayQueuePrePad { + protected long consumerMask; + protected AtomicReferenceArray consumerBuffer; +} + +abstract class AtomicBaseSpscLinkedArrayQueueConsumerField extends AtomicBaseSpscLinkedArrayQueueConsumerColdFields { + static final AtomicLongFieldUpdater C_INDEX_UPDATER = + AtomicLongFieldUpdater.newUpdater(AtomicBaseSpscLinkedArrayQueueConsumerField.class, "consumerIndex"); + protected volatile long consumerIndex; +} + +abstract class AtomicBaseSpscLinkedArrayQueueL2Pad extends AtomicBaseSpscLinkedArrayQueueConsumerField { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +abstract class AtomicBaseSpscLinkedArrayQueueProducerFields extends AtomicBaseSpscLinkedArrayQueueL2Pad { + static final AtomicLongFieldUpdater P_INDEX_UPDATER = + AtomicLongFieldUpdater.newUpdater(AtomicBaseSpscLinkedArrayQueueProducerFields.class, "producerIndex"); + protected volatile long producerIndex; + +} + +abstract class AtomicBaseSpscLinkedArrayQueueProducerColdFields extends AtomicBaseSpscLinkedArrayQueueProducerFields { + protected long producerBufferLimit; + protected long producerMask; // fixed for chunked and unbounded + + protected AtomicReferenceArray producerBuffer; +} + +abstract class AtomicBaseSpscLinkedArrayQueue extends AtomicBaseSpscLinkedArrayQueueProducerColdFields + implements QueueProgressIndicators, IndexedQueue { + + protected static final Object JUMP = new Object(); + + protected final void soProducerIndex(long v) { + P_INDEX_UPDATER.lazySet(this, v); + } + + protected final void soConsumerIndex(long v) { + C_INDEX_UPDATER.lazySet(this, v); + } + + public final long lvProducerIndex() { + return producerIndex; + } + + public final long lvConsumerIndex() { + return consumerIndex; + } + + protected static AtomicReferenceArray allocate(int capacity) { + return new AtomicReferenceArray(capacity); + } + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return this.getClass().getName(); + } + + @Override + public long currentProducerIndex() { + return lvProducerIndex(); + } + + @Override + public long currentConsumerIndex() { + return lvConsumerIndex(); + } + + + protected final void soNext(AtomicReferenceArray curr, AtomicReferenceArray next) { + soElement(curr, nextArrayOffset(curr), next); + } + + private void soElement(AtomicReferenceArray curr, int i, Object e) { + curr.lazySet(i, e); + } + + @SuppressWarnings("unchecked") + protected final AtomicReferenceArray lvNextArrayAndUnlink(AtomicReferenceArray curr) { + final int nextArrayOffset = nextArrayOffset(curr); + final AtomicReferenceArray nextBuffer = (AtomicReferenceArray) curr.get(nextArrayOffset); + // prevent GC nepotism + soElement(curr, nextArrayOffset, null); + return nextBuffer; + } + + private int nextArrayOffset(AtomicReferenceArray curr) { + return (curr.length() - 1); + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single producer thread use only. + */ + @Override + public boolean offer(final E e) { + // Objects.requireNonNull(e); + if (null == e) { + throw new NullPointerException(); + } + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = producerBuffer; + final long index = producerIndex; + final long mask = producerMask; + final int offset = calcElementOffset(index, mask); + // expected hot path + if (index < producerBufferLimit) { + writeToQueue(buffer, e, index, offset); + return true; + } + return offerColdPath(buffer, mask, e, index, offset); + } + + protected abstract boolean offerColdPath(AtomicReferenceArray buffer, long mask, E e, long pIndex, int offset); + + protected final void linkOldToNew(final long currIndex, final AtomicReferenceArray oldBuffer, final int offset, + final AtomicReferenceArray newBuffer, final int offsetInNew, final E e) { + soElement(newBuffer, offsetInNew, e);// StoreStore + // link to next buffer and add next indicator as element of old buffer + soNext(oldBuffer, newBuffer); + soElement(oldBuffer, offset, JUMP); + // index is visible after elements (isEmpty/poll ordering) + soProducerIndex(currIndex + 1);// this ensures atomic write of long on 32bit platforms + } + + protected final void writeToQueue(final AtomicReferenceArray buffer, final E e, final long index, final int offset) { + soElement(buffer, offset, e);// StoreStore + soProducerIndex(index + 1);// this ensures atomic write of long on 32bit platforms + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public E poll() { + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = consumerBuffer; + final long index = consumerIndex; + final long mask = consumerMask; + final int offset = calcElementOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + boolean isNextBuffer = e == JUMP; + if (null != e && !isNextBuffer) { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(buffer, offset, null); + return (E) e; + } else if (isNextBuffer) { + return newBufferPoll(buffer, index); + } + + return null; + } + + protected E lvElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); + } + + protected static int calcElementOffset(long index, long mask) { + return (int) (index & mask); + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public E peek() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = consumerIndex; + final long mask = consumerMask; + final int offset = calcElementOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + if (e == JUMP) { + return newBufferPeek(buffer, index); + } + + return (E) e; + } + + private E newBufferPeek(AtomicReferenceArray buffer, final long index) { + AtomicReferenceArray nextBuffer = lvNextArrayAndUnlink(buffer); + consumerBuffer = nextBuffer; + final long newMask = nextBuffer.length() - 2; + consumerMask = newMask; + final int offsetInNew = calcElementOffset(index, newMask); + return lvElement(nextBuffer, offsetInNew);// LoadLoad + } + + private E newBufferPoll(AtomicReferenceArray buffer, final long index) { + AtomicReferenceArray nextBuffer = lvNextArrayAndUnlink(buffer); + consumerBuffer = nextBuffer; + final long newMask = nextBuffer.length() - 2; + consumerMask = newMask; + final int offsetInNew = calcElementOffset(index, newMask); + final E n = lvElement(nextBuffer, offsetInNew);// LoadLoad + if (null == n) { + throw new IllegalStateException("new buffer must have at least one element"); + } else { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(nextBuffer, offsetInNew, null);// StoreStore + return n; + } + } + + @Override + public final int size() { + return IndexedQueueSizeUtil.size(this); + } + + @Override + public final boolean isEmpty() { + return IndexedQueueSizeUtil.isEmpty(this); + } +} diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java new file mode 100644 index 00000000..4c076083 --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +public class AtomicSpscChunkedArrayQueue extends AtomicBaseSpscLinkedArrayQueue { + private int maxQueueCapacity; + private long producerQueueLimit; + + public AtomicSpscChunkedArrayQueue(final int capacity) { + this(Math.max(8, Pow2.roundToPowerOfTwo(capacity / 8)), capacity); + } + + public AtomicSpscChunkedArrayQueue(final int chunkSize, final int capacity) { + RangeUtil.checkGreaterThanOrEqual(capacity, 16, "capacity"); + // minimal chunk size of eight makes sure minimal lookahead step is 2 + RangeUtil.checkGreaterThanOrEqual(chunkSize, 8, "chunkSize"); + + maxQueueCapacity = Pow2.roundToPowerOfTwo(capacity); + int chunkCapacity = Pow2.roundToPowerOfTwo(chunkSize); + RangeUtil.checkLessThan(chunkCapacity, maxQueueCapacity, "chunkCapacity"); + + long mask = chunkCapacity - 1; + // need extra element to point at next array + AtomicReferenceArray buffer = allocate(chunkCapacity + 1); + producerBuffer = buffer; + producerMask = mask; + consumerBuffer = buffer; + consumerMask = mask; + producerBufferLimit = mask - 1; // we know it's all empty to start with + producerQueueLimit = maxQueueCapacity; + soProducerIndex(0L);// serves as a StoreStore barrier to support correct publication + } + + @Override + protected final boolean offerColdPath(AtomicReferenceArray buffer, long mask, E e, long pIndex, int offset) { + // use a fixed lookahead step based on buffer capacity + final long lookAheadStep = (mask + 1) / 4; + long pBufferLimit = pIndex + lookAheadStep; + + long pQueueLimit = producerQueueLimit; + + if (pIndex >= pQueueLimit) { + // we tested against a potentially out of date queue limit, refresh it + long cIndex = lvConsumerIndex(); + producerQueueLimit = pQueueLimit = cIndex + maxQueueCapacity; + // if we're full we're full + if (pIndex >= pQueueLimit) { + return false; + } + } + // if buffer limit is after queue limit we use queue limit. We need to handle overflow so + // cannot use Math.min + if (pBufferLimit - pQueueLimit > 0) { + pBufferLimit = pQueueLimit; + } + + // go around the buffer or add a new buffer + if (pBufferLimit > pIndex + 1 && // there's sufficient room in buffer/queue to use pBufferLimit + null == lvElement(buffer, calcElementOffset(pBufferLimit, mask))) { + producerBufferLimit = pBufferLimit - 1; // joy, there's plenty of room + writeToQueue(buffer, e, pIndex, offset); + } + else if (null == lvElement(buffer, calcElementOffset(pIndex + 1, mask))) { // buffer is not full + writeToQueue(buffer, e, pIndex, offset); + } + else { + // we got one slot left to write into, and we are not full. Need to link new buffer. + // allocate new buffer of same length + final AtomicReferenceArray newBuffer = allocate((int)(mask + 2)); + producerBuffer = newBuffer; + + linkOldToNew(pIndex, buffer, offset, newBuffer, offset, e); + } + return true; + } + +} diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java new file mode 100644 index 00000000..452376b0 --- /dev/null +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java @@ -0,0 +1,32 @@ +package org.jctools.queues.atomic; + +import org.jctools.queues.QueueSanityTest; +import org.jctools.queues.SpscChunkedArrayQueue; +import org.jctools.queues.spec.ConcurrentQueueSpec; +import org.jctools.queues.spec.Ordering; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Queue; + +@RunWith(Parameterized.class) +public class AtomicSpscLinkedArrayQueueSanityTest extends QueueSanityTest { + @Parameterized.Parameters + public static Collection parameters() { + ArrayList list = new ArrayList(); + list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(64, SIZE))); + //list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(64, SIZE))); + //list.add(makeQueue(1, 1, 0, Ordering.FIFO, new AtomicSpscUnboundedArrayQueue(16))); + // minimal sizes + list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(8, 16))); + //list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(8, 16))); + return list; + } + + public AtomicSpscLinkedArrayQueueSanityTest(ConcurrentQueueSpec spec, Queue queue) { + super(spec, queue); + } + +} From c42a73b9eb6b11267984a61d07b8b5ffb4fefe7d Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 17:11:09 +0100 Subject: [PATCH 10/17] Created an Atomic version of the SpscGrowableArrayQueue - sans UnSafe usages, replicated relevant tests and used Atomic data types wherever needed --- .../org/jctools/queues/SpscArrayQueue.java | 8 +- .../atomic/AtomicSpscGrowableArrayQueue.java | 131 ++++++++++++++++++ .../AtomicSpscLinkedArrayQueueSanityTest.java | 5 +- 3 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java diff --git a/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java index 5c549145..9665c8b6 100644 --- a/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/SpscArrayQueue.java @@ -13,15 +13,15 @@ */ package org.jctools.queues; +import org.jctools.util.Pow2; +import org.jctools.util.UnsafeRefArrayAccess; + import static org.jctools.util.UnsafeAccess.UNSAFE; import static org.jctools.util.UnsafeRefArrayAccess.lvElement; import static org.jctools.util.UnsafeRefArrayAccess.soElement; -import org.jctools.util.Pow2; -import org.jctools.util.UnsafeRefArrayAccess; - abstract class SpscArrayQueueColdField extends ConcurrentCircularArrayQueue { - static final int MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); + public static final int MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); protected final int lookAheadStep; public SpscArrayQueueColdField(int capacity) { super(capacity); diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java new file mode 100644 index 00000000..899674af --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java @@ -0,0 +1,131 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.queues.SpscArrayQueue; +import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +public class AtomicSpscGrowableArrayQueue extends AtomicBaseSpscLinkedArrayQueue { + private int maxQueueCapacity; // ignored by the unbounded implementation + private long lookAheadStep;// ignored by the unbounded implementation + public AtomicSpscGrowableArrayQueue(final int capacity) { + this(Math.max(8, Pow2.roundToPowerOfTwo(capacity / 8)), capacity); + } + + public AtomicSpscGrowableArrayQueue(final int chunkSize, final int capacity) { + RangeUtil.checkGreaterThanOrEqual(capacity, 16, "capacity"); + // minimal chunk size of eight makes sure minimal lookahead step is 2 + RangeUtil.checkGreaterThanOrEqual(chunkSize, 8, "chunkSize"); + + maxQueueCapacity = Pow2.roundToPowerOfTwo(capacity); + int chunkCapacity = Pow2.roundToPowerOfTwo(chunkSize); + RangeUtil.checkLessThan(chunkCapacity, maxQueueCapacity, "chunkCapacity"); + + long mask = chunkCapacity - 1; + // need extra element to point at next array + AtomicReferenceArray buffer = allocate(chunkCapacity + 1); + producerBuffer = buffer; + producerMask = mask; + consumerBuffer = buffer; + consumerMask = mask; + producerBufferLimit = mask - 1; // we know it's all empty to start with + adjustLookAheadStep(chunkCapacity); + soProducerIndex(0L);// serves as a StoreStore barrier to support correct publication + } + + protected final boolean offerColdPath(final AtomicReferenceArray buffer, final long mask, final E e, final long index, + final int offset) { + final long lookAheadStep = this.lookAheadStep; + // normal case, go around the buffer or resize if full (unless we hit max capacity) + if (lookAheadStep > 0) { + int lookAheadElementOffset = calcElementOffset(index + lookAheadStep, mask); + // Try and look ahead a number of elements so we don't have to do this all the time + if (null == lvElement(buffer, lookAheadElementOffset)) { + producerBufferLimit = index + lookAheadStep - 1; // joy, there's plenty of room + writeToQueue(buffer, e, index, offset); + return true; + } + // we're at max capacity, can use up last element + final int maxCapacity = maxQueueCapacity; + if (mask + 1 == maxCapacity) { + if (null == lvElement(buffer, offset)) { + writeToQueue(buffer, e, index, offset); + return true; + } + // we're full and can't grow + return false; + } + // not at max capacity, so must allow extra slot for next buffer pointer + if (null == lvElement(buffer, calcElementOffset(index + 1, mask))) { // buffer is not full + writeToQueue(buffer, e, index, offset); + } else { + // allocate new buffer of same length + final AtomicReferenceArray newBuffer = allocate((int) (2*(mask +1) + 1)); + + producerBuffer = newBuffer; + producerMask = newBuffer.length() - 2; + + final int offsetInNew = calcElementOffset(index, producerMask); + linkOldToNew(index, buffer, offset, newBuffer, offsetInNew, e); + int newCapacity = (int) (producerMask + 1); + if (newCapacity == maxCapacity) { + long currConsumerIndex = lvConsumerIndex(); + // use lookAheadStep to store the consumer distance from final buffer + this.lookAheadStep = -(index - currConsumerIndex); + producerBufferLimit = currConsumerIndex + maxCapacity - 1; + } else { + producerBufferLimit = index + producerMask - 1; + adjustLookAheadStep(newCapacity); + } + } + return true; + } + // the step is negative (or zero) in the period between allocating the max sized buffer and the + // consumer starting on it + else { + final long prevElementsInOtherBuffers = -lookAheadStep; + // until the consumer starts using the current buffer we need to check consumer index to + // verify size + long currConsumerIndex = lvConsumerIndex(); + int size = (int) (index - currConsumerIndex); + int maxCapacity = (int) mask+1; // we're on max capacity or we wouldn't be here + if (size == maxCapacity) { + // consumer index has not changed since adjusting the lookAhead index, we're full + return false; + } + // if consumerIndex progressed enough so that current size indicates it is on same buffer + long firstIndexInCurrentBuffer = producerBufferLimit - maxCapacity + prevElementsInOtherBuffers; + if (currConsumerIndex >= firstIndexInCurrentBuffer) { + // job done, we've now settled into our final state + adjustLookAheadStep(maxCapacity); + } + // consumer is still on some other buffer + else { + // how many elements out of buffer? + this.lookAheadStep = (int) (currConsumerIndex - firstIndexInCurrentBuffer); + } + producerBufferLimit = currConsumerIndex + maxCapacity; + writeToQueue(buffer, e, index, offset); + return true; + } + } + + + private void adjustLookAheadStep(int capacity) { + lookAheadStep = Math.min(capacity / 4, SpscArrayQueue.MAX_LOOK_AHEAD_STEP); + } +} diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java index 452376b0..b23ab8d0 100644 --- a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java @@ -1,7 +1,6 @@ package org.jctools.queues.atomic; import org.jctools.queues.QueueSanityTest; -import org.jctools.queues.SpscChunkedArrayQueue; import org.jctools.queues.spec.ConcurrentQueueSpec; import org.jctools.queues.spec.Ordering; import org.junit.runner.RunWith; @@ -17,11 +16,11 @@ public class AtomicSpscLinkedArrayQueueSanityTest extends QueueSanityTest { public static Collection parameters() { ArrayList list = new ArrayList(); list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(64, SIZE))); - //list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(64, SIZE))); + list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(64, SIZE))); //list.add(makeQueue(1, 1, 0, Ordering.FIFO, new AtomicSpscUnboundedArrayQueue(16))); // minimal sizes list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(8, 16))); - //list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(8, 16))); + list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(8, 16))); return list; } From 947ad460a38a858263d2f709e6576f87fc5f2d82 Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 17:31:14 +0100 Subject: [PATCH 11/17] Enabled test for SpscUnboundedAtomicArrayQueue (already implemented) --- .../queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java index b23ab8d0..e34dbdeb 100644 --- a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java @@ -17,7 +17,7 @@ public static Collection parameters() { ArrayList list = new ArrayList(); list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(64, SIZE))); list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(64, SIZE))); - //list.add(makeQueue(1, 1, 0, Ordering.FIFO, new AtomicSpscUnboundedArrayQueue(16))); + list.add(makeQueue(1, 1, 0, Ordering.FIFO, new SpscUnboundedAtomicArrayQueue(16))); // minimal sizes list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(8, 16))); list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(8, 16))); From 873c3797523a2196fa297a0d7a02a1ab115dea29 Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 17:33:53 +0100 Subject: [PATCH 12/17] Renamed AtomicSpscChunkedArrayQueue to SpscChunkedAtomicArrayQueue to keep with the naming conventions started by creating SpscUnboundedAtomicArrayQueue --- ...nkedArrayQueue.java => SpscChunkedAtomicArrayQueue.java} | 6 +++--- .../queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename jctools-core/src/main/java/org/jctools/queues/atomic/{AtomicSpscChunkedArrayQueue.java => SpscChunkedAtomicArrayQueue.java} (95%) diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java similarity index 95% rename from jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java rename to jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java index 4c076083..c99119fd 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscChunkedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java @@ -18,15 +18,15 @@ import java.util.concurrent.atomic.AtomicReferenceArray; -public class AtomicSpscChunkedArrayQueue extends AtomicBaseSpscLinkedArrayQueue { +public class SpscChunkedAtomicArrayQueue extends AtomicBaseSpscLinkedArrayQueue { private int maxQueueCapacity; private long producerQueueLimit; - public AtomicSpscChunkedArrayQueue(final int capacity) { + public SpscChunkedAtomicArrayQueue(final int capacity) { this(Math.max(8, Pow2.roundToPowerOfTwo(capacity / 8)), capacity); } - public AtomicSpscChunkedArrayQueue(final int chunkSize, final int capacity) { + public SpscChunkedAtomicArrayQueue(final int chunkSize, final int capacity) { RangeUtil.checkGreaterThanOrEqual(capacity, 16, "capacity"); // minimal chunk size of eight makes sure minimal lookahead step is 2 RangeUtil.checkGreaterThanOrEqual(chunkSize, 8, "chunkSize"); diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java index e34dbdeb..1914b497 100644 --- a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java @@ -15,11 +15,11 @@ public class AtomicSpscLinkedArrayQueueSanityTest extends QueueSanityTest { @Parameterized.Parameters public static Collection parameters() { ArrayList list = new ArrayList(); - list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(64, SIZE))); + list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new SpscChunkedAtomicArrayQueue(64, SIZE))); list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(64, SIZE))); list.add(makeQueue(1, 1, 0, Ordering.FIFO, new SpscUnboundedAtomicArrayQueue(16))); // minimal sizes - list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscChunkedArrayQueue(8, 16))); + list.add(makeQueue(1, 1, 16, Ordering.FIFO, new SpscChunkedAtomicArrayQueue(8, 16))); list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(8, 16))); return list; } From 8e54da1dc68171f2be874ee77bc1a8610621e19a Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 17:36:00 +0100 Subject: [PATCH 13/17] Renamed AtomicSpscGrowableArrayQueue to SpscGrowableAtomicArrayQueue to keep with the naming conventions started by creating SpscUnboundedAtomicArrayQueue --- ...bleArrayQueue.java => SpscGrowableAtomicArrayQueue.java} | 6 +++--- .../queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename jctools-core/src/main/java/org/jctools/queues/atomic/{AtomicSpscGrowableArrayQueue.java => SpscGrowableAtomicArrayQueue.java} (97%) diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java similarity index 97% rename from jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java rename to jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java index 899674af..ec9fd377 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicSpscGrowableArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java @@ -19,14 +19,14 @@ import java.util.concurrent.atomic.AtomicReferenceArray; -public class AtomicSpscGrowableArrayQueue extends AtomicBaseSpscLinkedArrayQueue { +public class SpscGrowableAtomicArrayQueue extends AtomicBaseSpscLinkedArrayQueue { private int maxQueueCapacity; // ignored by the unbounded implementation private long lookAheadStep;// ignored by the unbounded implementation - public AtomicSpscGrowableArrayQueue(final int capacity) { + public SpscGrowableAtomicArrayQueue(final int capacity) { this(Math.max(8, Pow2.roundToPowerOfTwo(capacity / 8)), capacity); } - public AtomicSpscGrowableArrayQueue(final int chunkSize, final int capacity) { + public SpscGrowableAtomicArrayQueue(final int chunkSize, final int capacity) { RangeUtil.checkGreaterThanOrEqual(capacity, 16, "capacity"); // minimal chunk size of eight makes sure minimal lookahead step is 2 RangeUtil.checkGreaterThanOrEqual(chunkSize, 8, "chunkSize"); diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java index 1914b497..3e430874 100644 --- a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicSpscLinkedArrayQueueSanityTest.java @@ -16,11 +16,11 @@ public class AtomicSpscLinkedArrayQueueSanityTest extends QueueSanityTest { public static Collection parameters() { ArrayList list = new ArrayList(); list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new SpscChunkedAtomicArrayQueue(64, SIZE))); - list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(64, SIZE))); + list.add(makeQueue(1, 1, SIZE, Ordering.FIFO, new SpscGrowableAtomicArrayQueue(64, SIZE))); list.add(makeQueue(1, 1, 0, Ordering.FIFO, new SpscUnboundedAtomicArrayQueue(16))); // minimal sizes list.add(makeQueue(1, 1, 16, Ordering.FIFO, new SpscChunkedAtomicArrayQueue(8, 16))); - list.add(makeQueue(1, 1, 16, Ordering.FIFO, new AtomicSpscGrowableArrayQueue(8, 16))); + list.add(makeQueue(1, 1, 16, Ordering.FIFO, new SpscGrowableAtomicArrayQueue(8, 16))); return list; } From 7d7d68f4bf584978abd90f7c42b1b0e7df83e746 Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 17:39:35 +0100 Subject: [PATCH 14/17] Renamed all AtomicBaseSpsc* classes to BaseSpscXxxxxAtomicArrayQueueYyyyy to keep with the naming conventions started by creating SpscUnboundedAtomicArrayQueue And changes to depending classes i.e. SpscChunkedAtomicArrayQueue and SpscGrowableAtomicArrayQueue --- ...va => BaseSpscLinkedAtomicArrayQueue.java} | 22 +++++++++---------- .../atomic/SpscChunkedAtomicArrayQueue.java | 2 +- .../atomic/SpscGrowableAtomicArrayQueue.java | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) rename jctools-core/src/main/java/org/jctools/queues/atomic/{AtomicBaseSpscLinkedArrayQueue.java => BaseSpscLinkedAtomicArrayQueue.java} (88%) diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/BaseSpscLinkedAtomicArrayQueue.java similarity index 88% rename from jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java rename to jctools-core/src/main/java/org/jctools/queues/atomic/BaseSpscLinkedAtomicArrayQueue.java index c924324b..0ef58aa4 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicBaseSpscLinkedArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/BaseSpscLinkedAtomicArrayQueue.java @@ -22,43 +22,43 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceArray; -abstract class AtomicBaseSpscLinkedArrayQueuePrePad extends AbstractQueue { +abstract class BaseSpscLinkedAtomicArrayQueuePrePad extends AbstractQueue { long p0, p1, p2, p3, p4, p5, p6, p7; long p10, p11, p12, p13, p14, p15; // p16, p17; drop 2 longs, the cold fields act as buffer } -abstract class AtomicBaseSpscLinkedArrayQueueConsumerColdFields extends AtomicBaseSpscLinkedArrayQueuePrePad { +abstract class BaseSpscLinkedAtomicArrayQueueConsumerColdFields extends BaseSpscLinkedAtomicArrayQueuePrePad { protected long consumerMask; protected AtomicReferenceArray consumerBuffer; } -abstract class AtomicBaseSpscLinkedArrayQueueConsumerField extends AtomicBaseSpscLinkedArrayQueueConsumerColdFields { - static final AtomicLongFieldUpdater C_INDEX_UPDATER = - AtomicLongFieldUpdater.newUpdater(AtomicBaseSpscLinkedArrayQueueConsumerField.class, "consumerIndex"); +abstract class BaseSpscLinkedAtomicArrayQueueConsumerField extends BaseSpscLinkedAtomicArrayQueueConsumerColdFields { + static final AtomicLongFieldUpdater C_INDEX_UPDATER = + AtomicLongFieldUpdater.newUpdater(BaseSpscLinkedAtomicArrayQueueConsumerField.class, "consumerIndex"); protected volatile long consumerIndex; } -abstract class AtomicBaseSpscLinkedArrayQueueL2Pad extends AtomicBaseSpscLinkedArrayQueueConsumerField { +abstract class BaseSpscLinkedAtomicArrayQueueL2Pad extends BaseSpscLinkedAtomicArrayQueueConsumerField { long p0, p1, p2, p3, p4, p5, p6, p7; long p10, p11, p12, p13, p14, p15, p16, p17; } -abstract class AtomicBaseSpscLinkedArrayQueueProducerFields extends AtomicBaseSpscLinkedArrayQueueL2Pad { - static final AtomicLongFieldUpdater P_INDEX_UPDATER = - AtomicLongFieldUpdater.newUpdater(AtomicBaseSpscLinkedArrayQueueProducerFields.class, "producerIndex"); +abstract class BaseSpscLinkedAtomicArrayQueueProducerFields extends BaseSpscLinkedAtomicArrayQueueL2Pad { + static final AtomicLongFieldUpdater P_INDEX_UPDATER = + AtomicLongFieldUpdater.newUpdater(BaseSpscLinkedAtomicArrayQueueProducerFields.class, "producerIndex"); protected volatile long producerIndex; } -abstract class AtomicBaseSpscLinkedArrayQueueProducerColdFields extends AtomicBaseSpscLinkedArrayQueueProducerFields { +abstract class BaseSpscLinkedAtomicArrayQueueProducerColdFields extends BaseSpscLinkedAtomicArrayQueueProducerFields { protected long producerBufferLimit; protected long producerMask; // fixed for chunked and unbounded protected AtomicReferenceArray producerBuffer; } -abstract class AtomicBaseSpscLinkedArrayQueue extends AtomicBaseSpscLinkedArrayQueueProducerColdFields +abstract class BaseSpscLinkedAtomicArrayQueue extends BaseSpscLinkedAtomicArrayQueueProducerColdFields implements QueueProgressIndicators, IndexedQueue { protected static final Object JUMP = new Object(); diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java index c99119fd..4b883cce 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscChunkedAtomicArrayQueue.java @@ -18,7 +18,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray; -public class SpscChunkedAtomicArrayQueue extends AtomicBaseSpscLinkedArrayQueue { +public class SpscChunkedAtomicArrayQueue extends BaseSpscLinkedAtomicArrayQueue { private int maxQueueCapacity; private long producerQueueLimit; diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java index ec9fd377..864517cd 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscGrowableAtomicArrayQueue.java @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray; -public class SpscGrowableAtomicArrayQueue extends AtomicBaseSpscLinkedArrayQueue { +public class SpscGrowableAtomicArrayQueue extends BaseSpscLinkedAtomicArrayQueue { private int maxQueueCapacity; // ignored by the unbounded implementation private long lookAheadStep;// ignored by the unbounded implementation public SpscGrowableAtomicArrayQueue(final int capacity) { From 9ea39b859972501f7131a67384be0cf8d50ee780 Mon Sep 17 00:00:00 2001 From: Christopher Batey Date: Thu, 11 May 2017 17:54:49 +0100 Subject: [PATCH 15/17] Initial implementation of a MPSC linked atomic array queue Ported from the MPSC linked array queue --- .../org/jctools/queues/MpmcArrayQueue.java | 2 +- .../BaseMpscLinkedAtomicArrayQueue.java | 588 ++++++++++++++++++ .../atomic/MpscUnboundedAtomicArrayQueue.java | 76 +++ ...=> MpscUnboundedArrayQueueSanityTest.java} | 4 +- ...scUnboundedAtomicArrayQueueSanityTest.java | 27 + 5 files changed, 694 insertions(+), 3 deletions(-) create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/BaseMpscLinkedAtomicArrayQueue.java create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueue.java rename jctools-core/src/test/java/org/jctools/queues/{MpscUnboundedQueueSanityTest.java => MpscUnboundedArrayQueueSanityTest.java} (81%) create mode 100644 jctools-core/src/test/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueueSanityTest.java diff --git a/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java index 8220b067..0c57e16a 100755 --- a/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java @@ -115,7 +115,7 @@ public class MpmcArrayQueue extends MpmcArrayQueueConsumerField implements long p01, p02, p03, p04, p05, p06, p07; long p10, p11, p12, p13, p14, p15, p16, p17; final static int RECOMENDED_POLL_BATCH = CPUs * 4; - final static int RECOMENDED_OFFER_BATCH = CPUs * 4; + public final static int RECOMENDED_OFFER_BATCH = CPUs * 4; public MpmcArrayQueue(final int capacity) { super(RangeUtil.checkGreaterThanOrEqual(capacity, 2, "capacity")); } diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/BaseMpscLinkedAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/BaseMpscLinkedAtomicArrayQueue.java new file mode 100644 index 00000000..73192f6f --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/BaseMpscLinkedAtomicArrayQueue.java @@ -0,0 +1,588 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.queues.MessagePassingQueue; +import org.jctools.queues.QueueProgressIndicators; +import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; + +import java.util.AbstractQueue; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import static org.jctools.util.JvmInfo.CPUs; + +abstract class BaseMpscLinkedAtomicArrayQueuePad1 extends AbstractQueue { + long p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +abstract class BaseMpscLinkedAtomicArrayQueueProducerFields extends BaseMpscLinkedAtomicArrayQueuePad1 { + protected AtomicLong producerIndex; +} + +abstract class BaseMpscLinkedAtomicArrayQueuePad2 extends BaseMpscLinkedAtomicArrayQueueProducerFields { + long p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +abstract class BaseMpscLinkedAtomicArrayQueueConsumerFields extends BaseMpscLinkedAtomicArrayQueuePad2 { + protected long consumerMask; + protected AtomicReferenceArray consumerBuffer; + protected AtomicLong consumerIndex; +} + +abstract class BaseMpscLinkedAtomicArrayQueuePad3 extends BaseMpscLinkedAtomicArrayQueueConsumerFields { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +abstract class BaseMpscLinkedAtomicArrayQueueColdProducerFields extends BaseMpscLinkedAtomicArrayQueuePad3 { + // todo evaluate volatile + protected volatile AtomicLong producerLimit; + protected long producerMask; + protected AtomicReferenceArray producerBuffer; +} + + +/** + * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in linked chunks + * of the initial size. The queue grows only when the current buffer is full and elements are not copied on + * resize, instead a link to the new buffer is stored in the old buffer for the consumer to follow.
+ * + * @param + */ +public abstract class BaseMpscLinkedAtomicArrayQueue extends BaseMpscLinkedAtomicArrayQueueColdProducerFields + implements MessagePassingQueue, QueueProgressIndicators { + // No post padding here, subclasses must add + + // todo remove duplication + private final static int RECOMMENDED_OFFER_BATCH = CPUs * 4; + + private final static Object JUMP = new Object(); + + + /** + * @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the chunk size. + * Must be 2 or more. + */ + public BaseMpscLinkedAtomicArrayQueue(final int initialCapacity) { + RangeUtil.checkGreaterThanOrEqual(initialCapacity, 2, "initialCapacity"); + + this.consumerIndex = new AtomicLong(); + this.producerIndex = new AtomicLong(); + this.producerLimit = new AtomicLong(); + + int p2capacity = Pow2.roundToPowerOfTwo(initialCapacity); + // leave lower bit of mask clear + long mask = (p2capacity - 1) << 1; + // need extra element to point at next array + AtomicReferenceArray buffer = new AtomicReferenceArray(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + consumerBuffer = buffer; + consumerMask = mask; + soProducerLimit(mask); // we know it's all empty to start with + } + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return this.getClass().getName(); + } + + @Override + public boolean offer(final E e) { + if (null == e) { + throw new NullPointerException(); + } + + long mask; + AtomicReferenceArray buffer; + long pIndex; + + while (true) { + long producerLimit = lvProducerLimit(); + pIndex = lvProducerIndex(); + // lower bit is indicative of resize, if we see it we spin until it's cleared + if ((pIndex & 1) == 1) { + continue; + } + // pIndex is even (lower bit is 0) -> actual index is (pIndex >> 1) + + // mask/buffer may get changed by resizing -> only use for array access after successful CAS. + mask = this.producerMask; + buffer = this.producerBuffer; + // a successful CAS ties the ordering, lv(pIndex)-[mask/buffer]->cas(pIndex) + + // assumption behind this optimization is that queue is almost always empty or near empty + if (producerLimit <= pIndex) { + int result = offerSlowPath(mask, pIndex, producerLimit); + switch (result) { + case 0: + break; + case 1: + continue; + case 2: + return false; + case 3: + resize(mask, buffer, pIndex, e); + return true; + } + } + + if (casProducerIndex(pIndex, pIndex + 2)) { + break; + } + } + // INDEX visible before ELEMENT, consistent with consumer expectation + final int offset = modifiedCalcElementOffset(pIndex, mask); + buffer.lazySet(offset, e); + return true; + } + + /** + * We do not inline resize into this method because we do not resize on fill. + */ + private int offerSlowPath(long mask, long pIndex, long producerLimit) { + int result; + final long cIndex = lvConsumerIndex(); + long bufferCapacity = getCurrentBufferCapacity(mask); + result = 0;// 0 - goto pIndex CAS + if (cIndex + bufferCapacity > pIndex) { + if (!casProducerLimit(producerLimit, cIndex + bufferCapacity)) { + result = 1;// retry from top + } + } + // full and cannot grow + else if (availableInQueue(pIndex, cIndex) <= 0) { + result = 2;// -> return false; + } + // grab index for resize -> set lower bit + else if (casProducerIndex(pIndex, pIndex + 1)) { + result = 3;// -> resize + } + else { + result = 1;// failed resize attempt, retry from top + } + return result; + } + + /** + * @return available elements in queue * 2 + */ + protected abstract long availableInQueue(long pIndex, final long cIndex); + + /** + * This method assumes index is actually (index << 1) because lower bit is used for resize hence the >> 1 + */ + private static int modifiedCalcElementOffset(long index, long mask) { + return (int) (index & mask) >> 1; + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public E poll() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = consumerIndex.get(); + final long mask = consumerMask; + + final int offset = modifiedCalcElementOffset(index, mask); + Object e = buffer.get(offset); + + if (e == null) { + if (index != lvProducerIndex()) { + // poll() == null iff queue is empty, null element is not strong enough indicator, so we must + // check the producer index. If the queue is indeed not empty we spin until element is + // visible. + do { + e = buffer.get(offset); + } while (e == null); + } + else { + return null; + } + } + if (e == JUMP) { + final AtomicReferenceArray nextBuffer = getNextBuffer(buffer, mask); + return newBufferPoll(nextBuffer, index); + } + buffer.lazySet(offset, null); + soConsumerIndex(index + 2); + return (E) e; + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public E peek() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = consumerIndex.get(); + final long mask = consumerMask; + + final int offset = modifiedCalcElementOffset(index, mask); + Object e = buffer.get(offset); + if (e == null && index != lvProducerIndex()) { + // peek() == null iff queue is empty, null element is not strong enough indicator, so we must + // check the producer index. If the queue is indeed not empty we spin until element is visible. + while ((e = buffer.get(offset)) == null) + ; + } + if (e == JUMP) { + return newBufferPeek(getNextBuffer(buffer, mask), index); + } + return (E) e; + } + + @SuppressWarnings("unchecked") + private AtomicReferenceArray getNextBuffer(final AtomicReferenceArray buffer, final long mask) { + final int nextArrayOffset = nextArrayOffset(mask); + final AtomicReferenceArray nextBuffer = (AtomicReferenceArray) buffer.get(nextArrayOffset); + buffer.lazySet(nextArrayOffset, null); + return nextBuffer; + } + + private int nextArrayOffset(final long mask) { + return modifiedCalcElementOffset(mask + 2, Long.MAX_VALUE); + } + + private E newBufferPoll(AtomicReferenceArray nextBuffer, final long index) { + final int offsetInNew = newBufferAndOffset(nextBuffer, index); + final E n = nextBuffer.get(offsetInNew); + if (n == null) { + throw new IllegalStateException("new buffer must have at least one element"); + } + nextBuffer.lazySet(offsetInNew, null); + soConsumerIndex(index + 2); + return n; + } + + private E newBufferPeek(AtomicReferenceArray nextBuffer, final long index) { + final int offsetInNew = newBufferAndOffset(nextBuffer, index); + final E n = nextBuffer.get(offsetInNew); + if (null == n) { + throw new IllegalStateException("new buffer must have at least one element"); + } + return n; + } + + private int newBufferAndOffset(AtomicReferenceArray nextBuffer, final long index) { + consumerBuffer = nextBuffer; + consumerMask = (nextBuffer.length() - 2) << 1; + final int offsetInNew = modifiedCalcElementOffset(index, consumerMask); + return offsetInNew; + } + + @Override + public final int size() { + // NOTE: because indices are on even numbers we cannot use the size util. + + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + long size; + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + size = ((currentProducerIndex - after) >> 1); + break; + } + } + // Long overflow is impossible, so size is always positive. Integer overflow is possible for the unbounded + // indexed queues. + if (size > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + else { + return (int) size; + } + } + + @Override + public final boolean isEmpty() { + // Order matters! + // Loading consumer before producer allows for producer increments after consumer index is read. + // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there is + // nothing we can do to make this an exact method. + return (this.lvConsumerIndex() == this.lvProducerIndex()); + } + private long lvProducerIndex() { + return producerIndex.get(); + } + + private long lvConsumerIndex() { + return consumerIndex.get(); + } + + private void soProducerIndex(long v) { + producerIndex.set(v); + } + + private boolean casProducerIndex(long expect, long newValue) { + return producerIndex.compareAndSet(expect, newValue); + } + + private void soConsumerIndex(long v) { + consumerIndex.set(v); + } + + private long lvProducerLimit() { + return producerLimit.get(); + } + + private boolean casProducerLimit(long expect, long newValue) { + return producerLimit.compareAndSet(expect, newValue); + } + + private void soProducerLimit(long v) { + producerLimit.set(v); + } + + @Override + public long currentProducerIndex() { + return lvProducerIndex() / 2; + } + + @Override + public long currentConsumerIndex() { + return lvConsumerIndex() / 2; + } + + @Override + public abstract int capacity(); + + + @Override + public boolean relaxedOffer(E e) { + return offer(e); + } + + @SuppressWarnings("unchecked") + @Override + public E relaxedPoll() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = consumerIndex.get(); + final long mask = consumerMask; + + final int offset = modifiedCalcElementOffset(index, mask); + Object e = buffer.get(offset); + if (e == null) { + return null; + } + if (e == JUMP) { + final AtomicReferenceArray nextBuffer = getNextBuffer(buffer, mask); + return newBufferPoll(nextBuffer, index); + } + buffer.lazySet(offset, null); + soConsumerIndex(index + 2); + return (E) e; + } + + @SuppressWarnings("unchecked") + @Override + public E relaxedPeek() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = consumerIndex.get(); + final long mask = consumerMask; + + final int offset = modifiedCalcElementOffset(index, mask); + Object e = buffer.get(offset); + if (e == JUMP) { + return newBufferPeek(getNextBuffer(buffer, mask), index); + } + return (E) e; + } + + @Override + public int fill(Supplier s, int batchSize) { + long mask; + AtomicReferenceArray buffer; + long pIndex; + int claimedSlots; + while (true) { + long producerLimit = lvProducerLimit(); + pIndex = lvProducerIndex(); + // lower bit is indicative of resize, if we see it we spin until it's cleared + if ((pIndex & 1) == 1) { + continue; + } + // pIndex is even (lower bit is 0) -> actual index is (pIndex >> 1) + + // NOTE: mask/buffer may get changed by resizing -> only use for array access after successful CAS. + // Only by virtue ofloading them between the lvProcducerIndex and a successful casProducerIndex are they + // safe to use. + mask = this.producerMask; + buffer = this.producerBuffer; + // a successful CAS ties the ordering, lv(pIndex)->[mask/buffer]->cas(pIndex) + + // we want 'limit' slots, but will settle for whatever is visible to 'producerLimit' + long batchIndex = Math.min(producerLimit, pIndex + 2 * batchSize); + + if (pIndex == producerLimit || producerLimit < batchIndex) { + int result = offerSlowPath(mask, pIndex, producerLimit); + switch (result) { + case 1: + continue; + case 2: + return 0; + case 3: + resize(mask, buffer, pIndex, s.get()); + return 1; + } + } + + // claim limit slots at once + if (casProducerIndex(pIndex, batchIndex)) { + claimedSlots = (int) ((batchIndex - pIndex) / 2); + break; + } + } + + int i = 0; + for (i = 0; i < claimedSlots; i++) { + final int offset = modifiedCalcElementOffset(pIndex + 2 * i, mask); + buffer.lazySet(offset, s.get()); + } + return claimedSlots; + } + + private void resize(long oldMask, AtomicReferenceArray oldBuffer, long pIndex, final E e) { + int newBufferLength = getNextBufferSize(oldBuffer); + AtomicReferenceArray newBuffer = new AtomicReferenceArray(newBufferLength); + + producerBuffer = newBuffer; + final int newMask = (newBufferLength - 2) << 1; + producerMask = newMask; + + final int offsetInOld = modifiedCalcElementOffset(pIndex, oldMask); + final int offsetInNew = modifiedCalcElementOffset(pIndex, newMask); + + + newBuffer.lazySet(offsetInNew, e); + // todo fix + oldBuffer.set(nextArrayOffset(oldMask), (E) newBuffer); + + // ASSERT code + final long cIndex = lvConsumerIndex(); + final long availableInQueue = availableInQueue(pIndex, cIndex); + RangeUtil.checkPositive(availableInQueue, "availableInQueue"); + + // Invalidate racing CASs + // We never set the limit beyond the bounds of a buffer + soProducerLimit(pIndex + Math.min(newMask, availableInQueue)); + + // make resize visible to the other producers + soProducerIndex(pIndex + 2); + + // INDEX visible before ELEMENT, consistent with consumer expectation + + // make resize visible to consumer + oldBuffer.lazySet(offsetInOld, (E) JUMP); + } + + /** + * @return next buffer size(inclusive of next array pointer) + */ + protected abstract int getNextBufferSize(AtomicReferenceArray buffer); + + /** + * @return current buffer capacity for elements (excluding next pointer and jump entry) * 2 + */ + protected abstract long getCurrentBufferCapacity(long mask); + + @Override + public int fill(Supplier s) { + long result = 0;// result is a long because we want to have a safepoint check at regular intervals + final int capacity = capacity(); + do { + final int filled = fill(s, RECOMMENDED_OFFER_BATCH); + if (filled == 0) { + return (int) result; + } + result += filled; + } while (result <= capacity); + return (int) result; + } + + @Override + public void fill(Supplier s, + WaitStrategy w, + ExitCondition exit) { + + while (exit.keepRunning()) { + while (fill(s, RECOMMENDED_OFFER_BATCH) != 0 && exit.keepRunning()) { + continue; + } + int idleCounter = 0; + while (exit.keepRunning() && fill(s, RECOMMENDED_OFFER_BATCH) == 0) { + idleCounter = w.idle(idleCounter); + } + + } + } + + @Override + public void drain(Consumer c, WaitStrategy w, ExitCondition exit) { + int idleCounter = 0; + while (exit.keepRunning()) { + E e = relaxedPoll(); + if (e == null) { + idleCounter = w.idle(idleCounter); + continue; + } + idleCounter = 0; + c.accept(e); + } + } + + @Override + public int drain(Consumer c) { + return drain(c, capacity()); + } + + @Override + public int drain(final Consumer c, final int limit) { + /** + * Impl note: there are potentially some small gains to be had by manually inlining relaxedPoll() and hoisting + * reused fields out to reduce redundant reads. + */ + int i = 0; + E m; + for (; i < limit && (m = relaxedPoll()) != null; i++) { + c.accept(m); + } + return i; + } +} diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueue.java new file mode 100644 index 00000000..de60573d --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueue.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.queues.MessagePassingQueue; +import org.jctools.queues.MpmcArrayQueue; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in linked chunks + * of the initial size. The queue grows only when the current buffer is full and elements are not copied on + * resize, instead a link to the new buffer is stored in the old buffer for the consumer to follow.
+ * + * @param + */ +public class MpscUnboundedAtomicArrayQueue extends BaseMpscLinkedAtomicArrayQueue { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15, p16, p17; + + public MpscUnboundedAtomicArrayQueue(int chunkSize) { + super(chunkSize); + } + + + @Override + protected long availableInQueue(long pIndex, long cIndex) { + return Integer.MAX_VALUE; + } + + @Override + public int capacity() { + return MessagePassingQueue.UNBOUNDED_CAPACITY; + } + + + @Override + public int drain(Consumer c) { + return drain(c, 4096); + } + + @Override + public int fill(Supplier s) { + long result = 0;// result is a long because we want to have a safepoint check at regular intervals + final int capacity = 4096; + do { + final int filled = fill(s, MpmcArrayQueue.RECOMENDED_OFFER_BATCH); + if (filled == 0) { + return (int) result; + } + result += filled; + } while (result <= capacity); + return (int) result; + } + + @Override + protected int getNextBufferSize(AtomicReferenceArray buffer) { + return buffer.length(); + } + + @Override + protected long getCurrentBufferCapacity(long mask) { + return mask; + } +} diff --git a/jctools-core/src/test/java/org/jctools/queues/MpscUnboundedQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/MpscUnboundedArrayQueueSanityTest.java similarity index 81% rename from jctools-core/src/test/java/org/jctools/queues/MpscUnboundedQueueSanityTest.java rename to jctools-core/src/test/java/org/jctools/queues/MpscUnboundedArrayQueueSanityTest.java index ec84be6a..918da0bb 100644 --- a/jctools-core/src/test/java/org/jctools/queues/MpscUnboundedQueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/MpscUnboundedArrayQueueSanityTest.java @@ -10,7 +10,7 @@ import org.junit.runners.Parameterized; @RunWith(Parameterized.class) -public class MpscUnboundedQueueSanityTest extends QueueSanityTest { +public class MpscUnboundedArrayQueueSanityTest extends QueueSanityTest { @Parameterized.Parameters public static Collection parameters() { ArrayList list = new ArrayList(); @@ -19,7 +19,7 @@ public static Collection parameters() { return list; } - public MpscUnboundedQueueSanityTest(ConcurrentQueueSpec spec, Queue queue) { + public MpscUnboundedArrayQueueSanityTest(ConcurrentQueueSpec spec, Queue queue) { super(spec, queue); } diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueueSanityTest.java new file mode 100644 index 00000000..fdad2cd8 --- /dev/null +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/MpscUnboundedAtomicArrayQueueSanityTest.java @@ -0,0 +1,27 @@ +package org.jctools.queues.atomic; + +import org.jctools.queues.QueueSanityTest; +import org.jctools.queues.spec.ConcurrentQueueSpec; +import org.jctools.queues.spec.Ordering; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Queue; + +@RunWith(Parameterized.class) +public class MpscUnboundedAtomicArrayQueueSanityTest extends QueueSanityTest { + @Parameterized.Parameters + public static Collection parameters() { + ArrayList list = new ArrayList(); + list.add(makeQueue(0, 1, 0, Ordering.FIFO, new MpscUnboundedAtomicArrayQueue(2)));// MPSC size 1 + list.add(makeQueue(0, 1, 0, Ordering.FIFO, new MpscUnboundedAtomicArrayQueue(64)));// MPSC size SIZE + return list; + } + + public MpscUnboundedAtomicArrayQueueSanityTest(ConcurrentQueueSpec spec, Queue queue) { + super(spec, queue); + } + +} From c0d44b88cf1b085325f89cce1dcf0b1a152cb180 Mon Sep 17 00:00:00 2001 From: neomatrix369 Date: Thu, 11 May 2017 17:59:48 +0100 Subject: [PATCH 16/17] Replaced the existing SpscUnboundedAtomicArrayQueue with a newer version after converting the existing SpscUnboundedArrayQueue into an Atomic version --- .../atomic/SpscUnboundedAtomicArrayQueue.java | 249 ++---------------- .../org/jctools/queues/QueueSanityTest.java | 1 - .../queues/atomic/AtomicQueueSanityTest.java | 12 +- 3 files changed, 32 insertions(+), 230 deletions(-) diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/SpscUnboundedAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscUnboundedAtomicArrayQueue.java index e5c7a425..cca290d9 100644 --- a/jctools-core/src/main/java/org/jctools/queues/atomic/SpscUnboundedAtomicArrayQueue.java +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/SpscUnboundedAtomicArrayQueue.java @@ -13,245 +13,48 @@ */ package org.jctools.queues.atomic; -import java.util.AbstractQueue; -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReferenceArray; - -import org.jctools.queues.QueueProgressIndicators; import org.jctools.util.Pow2; -public class SpscUnboundedAtomicArrayQueue extends AbstractQueue implements QueueProgressIndicators{ - static final int MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); - protected final AtomicLong producerIndex; - protected int producerLookAheadStep; - protected long producerLookAhead; - protected int producerMask; - protected AtomicReferenceArray producerBuffer; - protected int consumerMask; - protected AtomicReferenceArray consumerBuffer; - protected final AtomicLong consumerIndex; - private static final Object HAS_NEXT = new Object(); +import java.util.concurrent.atomic.AtomicReferenceArray; - public SpscUnboundedAtomicArrayQueue(final int chunkSize) { - int p2ChunkSize = Math.max(Pow2.roundToPowerOfTwo(chunkSize), 16); +public class SpscUnboundedAtomicArrayQueue extends BaseSpscLinkedAtomicArrayQueue { - int mask = p2ChunkSize - 1; - AtomicReferenceArray buffer = new AtomicReferenceArray(p2ChunkSize + 1); + public SpscUnboundedAtomicArrayQueue(final int chunkSize) { + int chunkCapacity = Math.max(Pow2.roundToPowerOfTwo(chunkSize), 16); + long mask = chunkCapacity - 1; + AtomicReferenceArray buffer = allocate(chunkCapacity + 1); producerBuffer = buffer; producerMask = mask; - adjustLookAheadStep(p2ChunkSize); consumerBuffer = buffer; consumerMask = mask; - producerLookAhead = mask - 1; // we know it's all empty to start with - producerIndex = new AtomicLong(); - consumerIndex = new AtomicLong(); + producerBufferLimit = mask - 1; // we know it's all empty to start with soProducerIndex(0L); } @Override - public final Iterator iterator() { - throw new UnsupportedOperationException(); - } - - - @Override - public String toString() { - return this.getClass().getName(); - } + protected boolean offerColdPath(AtomicReferenceArray buffer, long mask, E e, long pIndex, int offset) { + // use a fixed lookahead step based on buffer capacity + final long lookAheadStep = (mask + 1) / 4; + long pBufferLimit = pIndex + lookAheadStep; - /** - * {@inheritDoc} - *

- * This implementation is correct for single producer thread use only. - */ - @Override - public final boolean offer(final E e) { - if (null == e) { - throw new NullPointerException(); + // go around the buffer or add a new buffer + if (null == lvElement(buffer, calcElementOffset(pBufferLimit, mask))) { + producerBufferLimit = pBufferLimit - 1; // joy, there's plenty of room + writeToQueue(buffer, e, pIndex, offset); } - // local load of field to avoid repeated loads after volatile reads - final AtomicReferenceArray buffer = producerBuffer; - final long index = lpProducerIndex(); - final int mask = producerMask; - final int offset = calcWrappedOffset(index, mask); - if (index < producerLookAhead) { - return writeToQueue(buffer, e, index, offset); - } else { - final int lookAheadStep = producerLookAheadStep; - // go around the buffer or resize if full (unless we hit max capacity) - int lookAheadElementOffset = calcWrappedOffset(index + lookAheadStep, mask); - if (null == lvElement(buffer, lookAheadElementOffset)) {// LoadLoad - producerLookAhead = index + lookAheadStep - 1; // joy, there's plenty of room - return writeToQueue(buffer, e, index, offset); - } else if (null == lvElement(buffer, calcWrappedOffset(index + 1, mask))) { // buffer is not full - return writeToQueue(buffer, e, index, offset); - } else { - resize(buffer, index, offset, e, mask); // add a buffer and link old to new - return true; - } + else if (null == lvElement(buffer, calcElementOffset(pIndex + 1, mask))) { // buffer is not full + writeToQueue(buffer, e, pIndex, offset); } - } - - private boolean writeToQueue(final AtomicReferenceArray buffer, final E e, final long index, final int offset) { - soElement(buffer, offset, e);// StoreStore - soProducerIndex(index + 1);// this ensures atomic write of long on 32bit platforms - return true; - } - - private void resize(final AtomicReferenceArray oldBuffer, final long currIndex, final int offset, final E e, - final long mask) { - final int capacity = oldBuffer.length(); - final AtomicReferenceArray newBuffer = new AtomicReferenceArray(capacity); - producerBuffer = newBuffer; - producerLookAhead = currIndex + mask - 1; - soElement(newBuffer, offset, e);// StoreStore - soNext(oldBuffer, newBuffer); - soElement(oldBuffer, offset, HAS_NEXT); // new buffer is visible after element is inserted - soProducerIndex(currIndex + 1);// this ensures correctness on 32bit platforms - } - - private void soNext(AtomicReferenceArray curr, AtomicReferenceArray next) { - soElement(curr, calcDirectOffset(curr.length() - 1), next); - } - @SuppressWarnings("unchecked") - private AtomicReferenceArray lvNext(AtomicReferenceArray curr) { - return (AtomicReferenceArray)lvElement(curr, calcDirectOffset(curr.length() - 1)); - } - /** - * {@inheritDoc} - *

- * This implementation is correct for single consumer thread use only. - */ - @SuppressWarnings("unchecked") - @Override - public final E poll() { - // local load of field to avoid repeated loads after volatile reads - final AtomicReferenceArray buffer = consumerBuffer; - final long index = lpConsumerIndex(); - final int mask = consumerMask; - final int offset = calcWrappedOffset(index, mask); - final Object e = lvElement(buffer, offset);// LoadLoad - boolean isNextBuffer = e == HAS_NEXT; - if (null != e && !isNextBuffer) { - soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms - soElement(buffer, offset, null);// StoreStore - return (E) e; - } else if (isNextBuffer) { - return newBufferPoll(buffer, index, mask); + else { + // we got one slot left to write into, and we are not full. Need to link new buffer. + // allocate new buffer of same length + final AtomicReferenceArray newBuffer = allocate((int)(mask + 2)); + producerBuffer = newBuffer; + producerBufferLimit = pIndex + mask - 1; + + linkOldToNew(pIndex, buffer, offset, newBuffer, offset, e); } - - return null; - } - - @SuppressWarnings("unchecked") - private E newBufferPoll(AtomicReferenceArray buffer, final long index, final int mask) { - AtomicReferenceArray nextBuffer = lvNext(buffer); - consumerBuffer = nextBuffer; - final int offsetInNew = calcWrappedOffset(index, mask); - final E n = (E) lvElement(nextBuffer, offsetInNew);// LoadLoad - soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms - soElement(nextBuffer, offsetInNew, null);// StoreStore - // prevent extended retention if the buffer is in old gen and the nextBuffer is in young gen - soNext(buffer, null); - return n; - } - - /** - * {@inheritDoc} - *

- * This implementation is correct for single consumer thread use only. - */ - @SuppressWarnings("unchecked") - @Override - public final E peek() { - final AtomicReferenceArray buffer = consumerBuffer; - final long index = lpConsumerIndex(); - final int mask = consumerMask; - final int offset = calcWrappedOffset(index, mask); - final Object e = lvElement(buffer, offset);// LoadLoad - if (e == HAS_NEXT) { - return newBufferPeek(lvNext(buffer), index, mask); - } - - return (E) e; - } - - @SuppressWarnings("unchecked") - private E newBufferPeek(AtomicReferenceArray nextBuffer, final long index, final int mask) { - consumerBuffer = nextBuffer; - final int offsetInNew = calcWrappedOffset(index, mask); - return (E) lvElement(nextBuffer, offsetInNew);// LoadLoad - } - - @Override - public final int size() { - /* - * It is possible for a thread to be interrupted or reschedule between the read of the producer and - * consumer indices, therefore protection is required to ensure size is within valid range. In the - * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer - * index BEFORE the producer index. - */ - long after = lvConsumerIndex(); - while (true) { - final long before = after; - final long currentProducerIndex = lvProducerIndex(); - after = lvConsumerIndex(); - if (before == after) { - return (int) (currentProducerIndex - after); - } - } - } - - private void adjustLookAheadStep(int capacity) { - producerLookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); - } - - private long lvProducerIndex() { - return producerIndex.get(); - } - - private long lvConsumerIndex() { - return consumerIndex.get(); - } - - private long lpProducerIndex() { - return producerIndex.get(); - } - - private long lpConsumerIndex() { - return consumerIndex.get(); - } - - private void soProducerIndex(long v) { - producerIndex.lazySet(v); - } - - private void soConsumerIndex(long v) { - consumerIndex.lazySet(v); - } - - private static int calcWrappedOffset(long index, int mask) { - return calcDirectOffset((int)index & mask); - } - private static int calcDirectOffset(int index) { - return index; - } - private static void soElement(AtomicReferenceArray buffer, int offset, Object e) { - buffer.lazySet(offset, e); - } - - private static Object lvElement(AtomicReferenceArray buffer, int offset) { - return buffer.get(offset); - } - - @Override - public long currentProducerIndex() { - return lvProducerIndex(); + return true; } - @Override - public long currentConsumerIndex() { - return lvConsumerIndex(); - } } diff --git a/jctools-core/src/test/java/org/jctools/queues/QueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/QueueSanityTest.java index 7617ec42..835626e5 100644 --- a/jctools-core/src/test/java/org/jctools/queues/QueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/QueueSanityTest.java @@ -1,7 +1,6 @@ package org.jctools.queues; import org.jctools.queues.atomic.AtomicQueueFactory; -import org.jctools.queues.atomic.SpscUnboundedAtomicArrayQueue; import org.jctools.queues.spec.ConcurrentQueueSpec; import org.jctools.queues.spec.Ordering; import org.jctools.queues.spec.Preference; diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicQueueSanityTest.java index 85574da4..daa31f48 100644 --- a/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicQueueSanityTest.java +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/AtomicQueueSanityTest.java @@ -1,17 +1,17 @@ package org.jctools.queues.atomic; -import static org.jctools.util.JvmInfo.CPUs; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Queue; - import org.jctools.queues.QueueSanityTest; import org.jctools.queues.spec.ConcurrentQueueSpec; import org.jctools.queues.spec.Ordering; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Queue; + +import static org.jctools.util.JvmInfo.CPUs; + @RunWith(Parameterized.class) public class AtomicQueueSanityTest extends QueueSanityTest { From 2ef1677c2474edf317093c71a2fc4ebd81702549 Mon Sep 17 00:00:00 2001 From: Christopher Batey Date: Thu, 11 May 2017 18:18:49 +0100 Subject: [PATCH 17/17] Initial implementation of MPSC atomic chunked and growable queue Ported from the non-atomic verisons --- .../atomic/MpscChunkedAtomicArrayQueue.java | 81 +++++++++++++++++++ .../atomic/MpscGrowableAtomicArrayQueue.java | 62 ++++++++++++++ ...MpscChunkedAtomicArrayQueueSanityTest.java | 27 +++++++ ...pscGrowableAtomicArrayQueueSanityTest.java | 27 +++++++ 4 files changed, 197 insertions(+) create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueue.java create mode 100644 jctools-core/src/main/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueue.java create mode 100644 jctools-core/src/test/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueueSanityTest.java create mode 100644 jctools-core/src/test/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueueSanityTest.java diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueue.java new file mode 100644 index 00000000..87892ba6 --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueue.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +import static java.lang.Math.max; +import static java.lang.Math.min; +import static org.jctools.util.Pow2.roundToPowerOfTwo; + +abstract class MpscChunkedAtomicArrayQueueColdProducerFields extends BaseMpscLinkedAtomicArrayQueue { + protected final long maxQueueCapacity; + public MpscChunkedAtomicArrayQueueColdProducerFields(int initialCapacity, int maxCapacity) { + super(initialCapacity); + RangeUtil.checkGreaterThanOrEqual(maxCapacity, 4, "maxCapacity"); + RangeUtil.checkLessThan(roundToPowerOfTwo(initialCapacity), roundToPowerOfTwo(maxCapacity), + "initialCapacity"); + maxQueueCapacity = ((long)Pow2.roundToPowerOfTwo(maxCapacity)) << 1; + } +} + +/** + * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in linked chunks + * of the initial size. The queue grows only when the current buffer is full and elements are not copied on + * resize, instead a link to the new buffer is stored in the old buffer for the consumer to follow.
+ * + * @param + */ +public class MpscChunkedAtomicArrayQueue extends MpscChunkedAtomicArrayQueueColdProducerFields { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15, p16, p17; + + public MpscChunkedAtomicArrayQueue(int maxCapacity) { + super(max(2, min(1024, roundToPowerOfTwo(maxCapacity / 8))), maxCapacity); + } + + /** + * @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the chunk size. + * Must be 2 or more. + * @param maxCapacity the maximum capacity will be rounded up to the closest power of 2 and will be the + * upper limit of number of elements in this queue. Must be 4 or more and round up to a larger + * power of 2 than initialCapacity. + */ + public MpscChunkedAtomicArrayQueue(int initialCapacity, int maxCapacity) { + super(initialCapacity, maxCapacity); + } + + @Override + protected long availableInQueue(long pIndex, long cIndex) { + return maxQueueCapacity - (pIndex - cIndex); + } + + @Override + public int capacity() { + return (int) (maxQueueCapacity/2); + } + + @Override + protected int getNextBufferSize(AtomicReferenceArray buffer) { + return buffer.length(); + } + + @Override + protected long getCurrentBufferCapacity(long mask) { + return mask; + } +} diff --git a/jctools-core/src/main/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueue.java b/jctools-core/src/main/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueue.java new file mode 100644 index 00000000..94a10808 --- /dev/null +++ b/jctools-core/src/main/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueue.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jctools.queues.atomic; + +import org.jctools.queues.MessagePassingQueue; +import org.jctools.queues.QueueProgressIndicators; +import org.jctools.util.Pow2; +import org.jctools.util.RangeUtil; + +import java.util.concurrent.atomic.AtomicReferenceArray; + + +/** + * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in linked chunks + * of the initial size. The queue grows only when the current buffer is full and elements are not copied on + * resize, instead a link to the new buffer is stored in the old buffer for the consumer to follow.
+ * + * @param + */ +public class MpscGrowableAtomicArrayQueue extends MpscChunkedAtomicArrayQueue + implements MessagePassingQueue, QueueProgressIndicators { + + public MpscGrowableAtomicArrayQueue(int maxCapacity) { + super(Math.max(2, Pow2.roundToPowerOfTwo(maxCapacity / 8)), maxCapacity); + } + + /** + * @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the chunk size. + * Must be 2 or more. + * @param maxCapacity the maximum capacity will be rounded up to the closest power of 2 and will be the + * upper limit of number of elements in this queue. Must be 4 or more and round up to a larger + * power of 2 than initialCapacity. + */ + public MpscGrowableAtomicArrayQueue(int initialCapacity, int maxCapacity) { + super(initialCapacity, maxCapacity); + } + + + @Override + protected int getNextBufferSize(AtomicReferenceArray buffer) { + final long maxSize = maxQueueCapacity / 2; + RangeUtil.checkLessThanOrEqual(buffer.length(), maxSize, "buffer.length"); + final int newSize = 2 * (buffer.length() - 1); + return newSize + 1; + } + + @Override + protected long getCurrentBufferCapacity(long mask) { + return (mask + 2 == maxQueueCapacity) ? maxQueueCapacity : mask; + } +} diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueueSanityTest.java new file mode 100644 index 00000000..05d7da23 --- /dev/null +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/MpscChunkedAtomicArrayQueueSanityTest.java @@ -0,0 +1,27 @@ +package org.jctools.queues.atomic; + +import org.jctools.queues.QueueSanityTest; +import org.jctools.queues.spec.ConcurrentQueueSpec; +import org.jctools.queues.spec.Ordering; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Queue; + +@RunWith(Parameterized.class) +public class MpscChunkedAtomicArrayQueueSanityTest extends QueueSanityTest { + @Parameterized.Parameters + public static Collection parameters() { + ArrayList list = new ArrayList(); + list.add(makeQueue(0, 1, 4, Ordering.FIFO, new MpscChunkedAtomicArrayQueue<>(2, 4)));// MPSC size 1 + list.add(makeQueue(0, 1, SIZE, Ordering.FIFO, new MpscChunkedAtomicArrayQueue<>(8, SIZE)));// MPSC size SIZE + return list; + } + + public MpscChunkedAtomicArrayQueueSanityTest(ConcurrentQueueSpec spec, Queue queue) { + super(spec, queue); + } + +} diff --git a/jctools-core/src/test/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueueSanityTest.java b/jctools-core/src/test/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueueSanityTest.java new file mode 100644 index 00000000..2ae53e92 --- /dev/null +++ b/jctools-core/src/test/java/org/jctools/queues/atomic/MpscGrowableAtomicArrayQueueSanityTest.java @@ -0,0 +1,27 @@ +package org.jctools.queues.atomic; + +import org.jctools.queues.MpscGrowableArrayQueue; +import org.jctools.queues.QueueSanityTest; +import org.jctools.queues.spec.ConcurrentQueueSpec; +import org.jctools.queues.spec.Ordering; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Queue; + +@RunWith(Parameterized.class) +public class MpscGrowableAtomicArrayQueueSanityTest extends QueueSanityTest { + @Parameterized.Parameters + public static Collection parameters() { + ArrayList list = new ArrayList(); + list.add(makeQueue(0, 1, 4, Ordering.FIFO, new MpscGrowableAtomicArrayQueue<>(2, 4)));// MPSC size 1 + list.add(makeQueue(0, 1, SIZE, Ordering.FIFO, new MpscGrowableAtomicArrayQueue<>(8, SIZE)));// MPSC size SIZE + return list; + } + + public MpscGrowableAtomicArrayQueueSanityTest(ConcurrentQueueSpec spec, Queue queue) { + super(spec, queue); + } +}