From 24af021a60b2c8b19d4c35c3d62faf5c321d85ba Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sat, 29 Feb 2020 20:49:12 +0200 Subject: [PATCH 01/16] refactor benchmarks --- kombi-jmh/build.gradle | 28 +++---------------- .../cartesian/CartesianListBenchmark.kt | 10 +++++++ .../cartesian/CartesianMapBenchmark.kt | 6 ++++ .../combination/CombinationBenchmark.kt | 11 +++++++- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/kombi-jmh/build.gradle b/kombi-jmh/build.gradle index 6d8c4af..9205955 100644 --- a/kombi-jmh/build.gradle +++ b/kombi-jmh/build.gradle @@ -5,6 +5,7 @@ plugins { description = '' dependencies { jmh project(':kombi-lib') + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.22' compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile group: 'com.google.guava', name: 'guava', version: '28.2-jre' @@ -19,33 +20,12 @@ compileTestKotlin { jmhJar.archiveFileName = 'benchmarks.jar' jmh { - jmhVersion = '1.21' // Specifies JMH version - - include = ['.*'] // include pattern (regular expression) for benchmarks to be executed - jvmArgs = ['-Xms4g', '-Xmx4g'] - - - benchmarkMode = ['avgt'] // Benchmark mode. Available modes are: [Throughput/thrpt, AverageTime/avgt, SampleTime/sample, SingleShotTime/ss, All/all] - timeUnit = 'us'// Output time unit. Available time units are: [m, s, ms, us, ns]. - verbosity = 'NORMAL' // Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA] - forceGC = true // Should JMH force GC between iterations? - failOnError = false // Should JMH fail immediately if any benchmark had experienced the unrecoverable error? - - operationsPerInvocation = 1 // Operations per invocation. - batchSize = 1 // Batch size: number of benchmark method calls per operation. (some benchmark modes can ignore this setting) - warmupBatchSize = 1 // Warmup batch size: number of benchmark method calls per operation. - fork = 1 // How many times to forks a single benchmark. Use 0 to disable forking altogether - warmupForks = 0 // How many warmup forks to make for a single benchmark. 0 to disable warmup forks. - threads = 1 // Number of worker threads to run with. - - iterations = 5 // Number of measurement iterations to do. - warmupIterations = 5 // Number of warmup iterations to do. + jmhVersion = '1.22' + jvmArgs = ['-Xms512m', '-Xmx1g'] humanOutputFile = project.file("${project.buildDir}/reports/jmh/human.txt") // human-readable output file resultsFile = project.file("${project.buildDir}/reports/jmh/results.csv") // results file - resultFormat = 'CSV' // Result format type (one of CSV, JSON, NONE, SCSV, TEXT) - - //more options by the link https://github.com/melix/jmh-gradle-plugin + resultFormat = 'TEXT' // Result format type (one of CSV, JSON, NONE, SCSV, TEXT) } diff --git a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt index 6bdc374..64309e5 100644 --- a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt +++ b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt @@ -5,8 +5,14 @@ import com.google.common.collect.Sets import com.sgnatiuk.cartesian.CartesianBuilder.cartesianProductOf import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit @State(Scope.Benchmark) +@Fork(2) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) open class CartesianListBenchmark { @Param("3", "5", "7", "11") @@ -21,6 +27,10 @@ open class CartesianListBenchmark { List(i + 1) { it } } listOfSets = listOfLists.map { it.toSet() } + println("\n=================================================") + println("itemsQuantity=$itemsQuantity") + println("combinationsQuantity=${cartesianProductOf(listOfLists).combinationsCount()}") + println("=================================================\n") } @Benchmark diff --git a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt index 731d176..3cdd9c6 100644 --- a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt +++ b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt @@ -3,8 +3,14 @@ package com.sgnatiuk.benchmark.cartesian import com.sgnatiuk.cartesian.CartesianBuilder.cartesianProductOf import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit @State(Scope.Benchmark) +@Fork(2) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) open class CartesianMapBenchmark { @Param("7") diff --git a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/combination/CombinationBenchmark.kt b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/combination/CombinationBenchmark.kt index 2765fbc..09b20e4 100644 --- a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/combination/CombinationBenchmark.kt +++ b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/combination/CombinationBenchmark.kt @@ -3,9 +3,14 @@ package com.sgnatiuk.benchmark.combination import com.sgnatiuk.combination.CombinationsBuilder.combinationsOf import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole - +import java.util.concurrent.TimeUnit @State(Scope.Benchmark) +@Fork(2) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) open class CombinationBenchmark { @Param("11", "19") @@ -18,6 +23,10 @@ open class CombinationBenchmark { fun doSetup() { list = List(itemsQuantity){ it } map = (1..itemsQuantity).map { it to it.toString() }.toMap() + println("\n=================================================") + println("itemsQuantity=$itemsQuantity") + println("combinationsQuantity=${combinationsOf(list).combinationsNumber()}") + println("=================================================\n") } @Benchmark From 99a584131a5b9da5b014f59e742c318bd47cd367 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 12:00:24 +0200 Subject: [PATCH 02/16] added tests --- .../cartesian/CartesianProductSetTest.kt | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt index 4825e7a..5d51840 100644 --- a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt +++ b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt @@ -14,11 +14,43 @@ internal class CartesianProductSetTest { @Test fun `verify empty collection is returned when passed empty collection`() { val emptyCollection = emptyList>() - cartesianProductOf(emptyCollection).forEach { + checkCartesianProductIsEmpty(emptyCollection) + } + + @Test + fun `cartesian product of single zero length collection should be empty`() { + val emptyCollection = listOf(emptyList()) + checkCartesianProductIsEmpty(emptyCollection) + } + + private fun checkCartesianProductIsEmpty(list: List>) { + cartesianProductOf(list).apply { + assertEquals(0, combinationsCount().longValueExact()) + }.forEach { _ -> throw RuntimeException("expected empty collection") } } + @Test + fun `stream of cartesian product of single zero length collection should be empty`() { + val emptyCollection = listOf(emptyList()) + val count = cartesianProductOf(emptyCollection) + .stream() + .flatMap { it.stream() } + .count() + assertEquals(0, count) + } + + @Test + fun `parallel stream of cartesian product of single zero length collection should be empty`() { + val emptyCollection = listOf(emptyList()) + val count = cartesianProductOf(emptyCollection) + .stream() + .flatMap { it.stream() } + .count() + assertEquals(0, count) + } + @Test fun `verify Cartesian product set returns all possible combinations`() { val result: List> = cartesianProductOf(dataList, false).toList() From dea31bc22457ef8c84074f405995f911dc47f670 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 12:07:38 +0200 Subject: [PATCH 03/16] added tests --- .../com/sgnatiuk/cartesian/CartesianProductSetTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt index 5d51840..c4ff602 100644 --- a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt +++ b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt @@ -23,6 +23,16 @@ internal class CartesianProductSetTest { checkCartesianProductIsEmpty(emptyCollection) } + @Test + fun `cartesian product of list with at least one empty collection should be empty`() { + val emptyCollection = listOf( + emptyList(), + listOf(1), + listOf(1, 2) + ) + checkCartesianProductIsEmpty(emptyCollection) + } + private fun checkCartesianProductIsEmpty(list: List>) { cartesianProductOf(list).apply { assertEquals(0, combinationsCount().longValueExact()) From 73a5b8ec6c3d80bc4d98df31c835fdf31697ae0e Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 14:35:21 +0200 Subject: [PATCH 04/16] refactored cartesian with using arrays to improve performance --- .../cartesian/CartesianProductMap.java | 74 ++++++---- .../cartesian/CartesianProductSet.java | 131 +++++++++++++----- .../CartesianProductSpliterator.java | 65 +++++++++ .../cartesian/EncodableCartesianProduct.java | 112 +++++---------- 4 files changed, 238 insertions(+), 144 deletions(-) create mode 100644 kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSpliterator.java diff --git a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java index 81fdb83..43a8a01 100644 --- a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java +++ b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java @@ -6,22 +6,26 @@ class CartesianProductMap extends EncodableCartesianProduct> implements Serializable { - private final Map> values; - private final ArrayList dataKeys; - - CartesianProductMap(Map> values) { - this(values, false); - } + private final Map values; + private final Object[] dataKeys; + private final boolean keepOrder; CartesianProductMap(Map> values, boolean keepOrder) { this.values = copyWithOrder( values, keepOrder ? (o1, o2) -> 0 : new ValuesCountDesc<>() ); - this.dataKeys = new ArrayList<>(this.values.keySet()); + this.dataKeys = this.values.keySet().toArray(); + this.keepOrder = keepOrder; + } + + private CartesianProductMap(boolean keepOrder, Map values) { + this.values = values; + this.dataKeys = this.values.keySet().toArray(); + this.keepOrder = keepOrder; } - private Map> copyWithOrder( + private Map copyWithOrder( Map> data, Comparator>> comparator ) { @@ -34,7 +38,7 @@ private Map> copyWithOrder( )) .collect(Collectors.toMap( AbstractMap.SimpleEntry::getKey, - AbstractMap.SimpleEntry::getValue, + pair -> pair.getValue().toArray(), (v1, v2) -> { throw new IllegalStateException("Unexpected key duplication in data:" + data); }, @@ -42,15 +46,14 @@ private Map> copyWithOrder( )); } - + @SuppressWarnings("unchecked") @Override protected MaskDecoder> maskDecoder() { return encoded -> { - HashMap decoded = new HashMap<>(); + HashMap decoded = new HashMap<>(encoded.length); for (int i = 0; i < encoded.length; i++) { - K fieldKey = dataKeys.get(i); - V value = values.get(fieldKey) - .get(encoded[i]); + K fieldKey = (K) dataKeys[i]; + V value = (V) values.get(fieldKey)[encoded[i]]; decoded.put(fieldKey, value); } return decoded; @@ -58,32 +61,45 @@ protected MaskDecoder> maskDecoder() { } @Override - protected Collection> values() { - return values.values(); + protected Object[][] values() { + Object[][] data = new Object[values.size()][]; + int index = 0; + for (Object[] value : values.values()) { + data[index++] = value; + } + return data; } + @SuppressWarnings("unchecked") @Override public List>> split(int n) { - + if (n < 2) { + return Collections.singletonList(this); + } List>> splitList = new ArrayList<>(n); - Map> descSortedData = copyWithOrder(values, new ValuesCountDesc<>()); - Map.Entry> firstEntry = descSortedData.entrySet().stream().findFirst().orElseThrow( - () -> new IllegalStateException("Expected at least one item in: " + descSortedData) - ); - ArrayList firstValue = firstEntry.getValue(); - int parts = Math.min(n, firstValue.size()); + + K keyOfMaxLengthArr = (K) dataKeys[0]; + Object[] arrWithMaxLength = values.get(keyOfMaxLengthArr); + for (int i = 1; i < dataKeys.length; i++) { + Object[] nextArr = values.get(dataKeys[i]); + if (nextArr.length > arrWithMaxLength.length) { + keyOfMaxLengthArr = (K) dataKeys[i]; + arrWithMaxLength = nextArr; + } + } + int parts = Math.min(n, arrWithMaxLength.length); int from = 0; for (int i = 0; i < parts; i++) { - int valuesPerChunk = (firstValue.size() - from) / (parts - i); + int valuesPerChunk = (arrWithMaxLength.length - from) / (parts - i); int to = from + valuesPerChunk; - LinkedHashMap> newData = new LinkedHashMap<>(descSortedData); - newData.put( - firstEntry.getKey(), - firstValue.subList(from, to) + Map nd = new HashMap<>(values); + nd.put( + keyOfMaxLengthArr, + Arrays.copyOfRange(arrWithMaxLength, from, to) ); - splitList.add(new CartesianProductMap<>(newData)); + splitList.add(new CartesianProductMap<>(keepOrder, nd)); from = to; } return splitList; diff --git a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSet.java b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSet.java index 1583531..b980b09 100644 --- a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSet.java +++ b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSet.java @@ -1,77 +1,134 @@ package com.sgnatiuk.cartesian; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; +import java.util.*; class CartesianProductSet extends EncodableCartesianProduct> implements Serializable { - private final List> values; - - CartesianProductSet(Collection> values) { - this(values, false); - } + private final Object[][] valuesArr; + private final boolean keepOrder; CartesianProductSet(Collection> values, boolean keepOrder) { - this.values = convertToFixedOrderMap(values, keepOrder); + this(copyToArray(values, keepOrder), keepOrder); } - private List> convertToFixedOrderMap(Collection> data, boolean keepOrder) { - List> res = new ArrayList<>(); - for (Collection originData : data) { - res.add(new ArrayList<>(originData)); - } + private CartesianProductSet(Object[][] data, boolean keepOrder) { + this.valuesArr = data; + this.keepOrder = keepOrder; if (!keepOrder) { - res.sort(new ValuesCountDesc<>()); + Arrays.sort(valuesArr, new ArrValuesCountDesc()); } - return res; } + private static Object[][] copyToArray(Collection> data, boolean keepOrder) { + Object[][] res = new Object[data.size()][]; + int index = 0; + for (Collection originData : data) { + Object[] objects = originData.toArray(); + if (objects.length == 0) { + return new Object[0][]; + } + res[index++] = objects; + } + return res; + } @Override protected MaskDecoder> maskDecoder() { - return encoded -> { - List res = new ArrayList<>(encoded.length); - for (int i = 0; i < encoded.length; i++) { - res.add(values.get(i).get(encoded[i])); - } - return res; - }; + return DecodableCombination::new; + } + + private class DecodableCombination extends AbstractList { + + private final int[] localEncoded; + + public DecodableCombination(int[] encoded) { + this.localEncoded = new int[encoded.length]; + System.arraycopy(encoded, 0, localEncoded, 0, localEncoded.length); + } + + @Override + public int size() { + return localEncoded.length; + } + + @SuppressWarnings("unchecked") + @Override + public T get(int index) { + return (T) valuesArr[index][localEncoded[index]]; + } + + @Override + public Iterator iterator() { + return new Iterator() { + int index = 0; + int size = localEncoded.length; + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public T next() { + return get(index++); + } + }; + } } @Override - protected Collection> values() { - return values; + protected Object[][] values() { + return valuesArr; } @Override public List>> split(int n) { + if (n < 2) { + return Collections.singletonList(this); + } List>> splitList = new ArrayList<>(n); - ArrayList> descSortedData = new ArrayList<>(values); - descSortedData.sort(new ValuesCountDesc<>()); - List firstFieldValues = descSortedData.get(0); - int parts = Math.min(n, firstFieldValues.size()); + int maxLengthArrIndex = indexOfMaxLengthArray(); + int maxLength = valuesArr[maxLengthArrIndex].length; + + Object[] maxLengthArray = valuesArr[maxLengthArrIndex]; + int parts = Math.min(n, maxLength); int from = 0; for (int i = 0; i < parts; i++) { - int valuesPerChunk = (firstFieldValues.size() - from) / (parts - i); + int valuesPerChunk = (maxLength - from) / (parts - i); int to = from + valuesPerChunk; - ArrayList> newData = new ArrayList<>(descSortedData); - newData.set(0, firstFieldValues.subList(from, to)); - splitList.add(new CartesianProductSet<>(newData)); + + Object[][] data = new Object[valuesArr.length][]; + System.arraycopy(valuesArr, 0, data, 0, data.length); + data[maxLengthArrIndex] = Arrays.copyOfRange(maxLengthArray, from, to); + + splitList.add(new CartesianProductSet<>(data, keepOrder)); from = to; } return splitList; } - private static class ValuesCountDesc implements Comparator> { + private int indexOfMaxLengthArray() { + int maxLengthArrIndex = 0; + int maxLength = valuesArr[0].length; + + for (int i = 1; i < valuesArr.length; i++) { + if (valuesArr[i].length > maxLength) { + maxLength = valuesArr[i].length; + maxLengthArrIndex = i; + } + } + return maxLengthArrIndex; + } + + private static class ArrValuesCountDesc implements Comparator { + @Override - public int compare(Collection o1, Collection o2) { - return o2.size() - o1.size(); + public int compare(Object[] o1, Object[] o2) { + return o2.length - o1.length; } } } \ No newline at end of file diff --git a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSpliterator.java b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSpliterator.java new file mode 100644 index 0000000..4e3eb92 --- /dev/null +++ b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductSpliterator.java @@ -0,0 +1,65 @@ +package com.sgnatiuk.cartesian; + +import java.math.BigInteger; +import java.util.*; +import java.util.function.Consumer; + +class CartesianProductSpliterator implements Spliterator { + + private CartesianProduct cartesianProduct; + private Iterator cartesianProductIterator; + private boolean isSizeKnown; + private long size; + + CartesianProductSpliterator(CartesianProduct cartesianProduct) { + this.cartesianProduct = cartesianProduct; + cartesianProductIterator = cartesianProduct.iterator(); + + Map.Entry sizeInfo = computeSize(); + isSizeKnown = sizeInfo.getKey(); + size = sizeInfo.getValue(); + } + + @Override + public boolean tryAdvance(Consumer action) { + if (cartesianProductIterator.hasNext()) { + action.accept(cartesianProductIterator.next()); + return true; + } else { + return false; + } + } + + @Override + public Spliterator trySplit() { + List> cartesianProducts = cartesianProduct.split(2); + + cartesianProduct = cartesianProducts.get(1); + cartesianProductIterator = cartesianProduct.iterator(); + Map.Entry sizeInfo = computeSize(); + isSizeKnown = sizeInfo.getKey(); + size = sizeInfo.getValue(); + + return new CartesianProductSpliterator<>(cartesianProducts.get(0)); + } + + @Override + public long estimateSize() { + return size; + } + + @Override + public int characteristics() { + int flags = Spliterator.CONCURRENT | Spliterator.IMMUTABLE | Spliterator.ORDERED; + return isSizeKnown + ? flags | Spliterator.SIZED | Spliterator.SUBSIZED + : flags; + } + + private Map.Entry computeSize() { + BigInteger combinationsCount = cartesianProduct.combinationsCount(); + return combinationsCount.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + ? new AbstractMap.SimpleEntry<>(false, Long.MAX_VALUE) + : new AbstractMap.SimpleEntry<>(true, combinationsCount.longValueExact()); + } +} diff --git a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/EncodableCartesianProduct.java b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/EncodableCartesianProduct.java index 645f409..981daad 100644 --- a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/EncodableCartesianProduct.java +++ b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/EncodableCartesianProduct.java @@ -1,9 +1,9 @@ package com.sgnatiuk.cartesian; import java.math.BigInteger; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -11,20 +11,22 @@ abstract class EncodableCartesianProduct implements CartesianProduct { protected abstract MaskDecoder maskDecoder(); - protected abstract Collection> values(); + protected abstract Object[][] values(); + + private Lazy combinationsCount = new Lazy<>(() -> multiplySubArraysLength(values())); @Override public BigInteger combinationsCount() { - return multiplyAll(values(), Collection::size); + return combinationsCount.get(); } @Override public Iterator iterator() { - if (values().isEmpty()) { + if (values().length == 0) { return Collections.emptyIterator(); } return new Iterator() { - private final CombinationMask dataEncoder = new CombinationMask(bases()); + private final CombinationMask dataEncoder = new CombinationMask(radixes()); private final MaskDecoder maskDecoder = maskDecoder(); @Override @@ -47,87 +49,41 @@ public Stream stream() { ); } - int[] bases() { - Collection> values = values(); - int[] radixes = new int[values.size()]; - int index = 0; - for (Collection value : values) { - radixes[index++] = value.size(); + int[] radixes() { + Object[][] values = values(); + int[] radixes = new int[values.length]; + for (int i = 0; i < values.length; i++) { + radixes[i] = values[i].length; } return radixes; } - public static BigInteger multiplyAll(Iterable items, Function intValue) { + private static BigInteger multiplySubArraysLength(Object[][] items) { + if (items.length == 0) { + return BigInteger.ZERO; + } BigInteger result = BigInteger.ONE; - boolean collectionEmpty = true; - - for (T item : items) { - collectionEmpty = false; - result = result.multiply(BigInteger.valueOf( - intValue.apply(item) - )); + for (Object[] item : items) { + result = result.multiply( + BigInteger.valueOf(item.length) + ); } - - return collectionEmpty ? BigInteger.ZERO : result; + return result; } -} - -class CartesianProductSpliterator implements Spliterator { - private CartesianProduct cartesianProduct; - private Iterator cartesianProductIterator; - private boolean isSizeKnown; - private long size; + private static class Lazy { + private final Supplier supplier; + private T value; - CartesianProductSpliterator(CartesianProduct cartesianProduct) { - this.cartesianProduct = cartesianProduct; - cartesianProductIterator = cartesianProduct.iterator(); - - Map.Entry sizeInfo = computeSize(); - isSizeKnown = sizeInfo.getKey(); - size = sizeInfo.getValue(); - } - - @Override - public boolean tryAdvance(Consumer action) { - if (cartesianProductIterator.hasNext()) { - action.accept(cartesianProductIterator.next()); - return true; - } else { - return false; + private Lazy(Supplier supplier) { + this.supplier = supplier; } - } - - @Override - public Spliterator trySplit() { - List> cartesianProducts = cartesianProduct.split(2); - - cartesianProduct = cartesianProducts.get(1); - cartesianProductIterator = cartesianProduct.iterator(); - Map.Entry sizeInfo = computeSize(); - isSizeKnown = sizeInfo.getKey(); - size = sizeInfo.getValue(); - - return new CartesianProductSpliterator<>(cartesianProducts.get(0)); - } - - @Override - public long estimateSize() { - return size; - } - - @Override - public int characteristics() { - int flags = Spliterator.CONCURRENT | Spliterator.IMMUTABLE | Spliterator.ORDERED; - return isSizeKnown - ? flags | Spliterator.SIZED | Spliterator.SUBSIZED - : flags; - } - private Map.Entry computeSize() { - BigInteger combinationsCount = cartesianProduct.combinationsCount(); - return combinationsCount.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 - ? new AbstractMap.SimpleEntry<>(false, Long.MAX_VALUE) - : new AbstractMap.SimpleEntry<>(true, combinationsCount.longValueExact()); + private T get() { + if (value == null) { + value = supplier.get(); + } + return value; + } } } From a9cc794ecfb14e85fd93149aaf95bdd408159ada Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 14:42:59 +0200 Subject: [PATCH 05/16] added test --- .../com/sgnatiuk/cartesian/CartesianProductMap.java | 2 +- .../com/sgnatiuk/cartesian/CartesianProductMapTest.kt | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java index 43a8a01..9b7dcf1 100644 --- a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java +++ b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CartesianProductMap.java @@ -40,7 +40,7 @@ private Map copyWithOrder( AbstractMap.SimpleEntry::getKey, pair -> pair.getValue().toArray(), (v1, v2) -> { - throw new IllegalStateException("Unexpected key duplication in data:" + data); + throw new IllegalArgumentException("Unexpected key duplication in data:" + data); }, LinkedHashMap::new )); diff --git a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductMapTest.kt b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductMapTest.kt index 543b3b6..6f74ab9 100644 --- a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductMapTest.kt +++ b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductMapTest.kt @@ -7,6 +7,7 @@ import com.sgnatiuk.extensions.BigInt import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import java.util.* import java.util.stream.Collectors internal class CartesianProductMapTest { @@ -51,6 +52,16 @@ internal class CartesianProductMapTest { assertEquals(splitFactor, splitCartesian.size) } + @Test(expected = IllegalArgumentException::class) + fun `cartesian product should throw on duplicated key in data`() { + val duplicatedKeyMap = TreeMap> { _, _ -> -1 }.apply { + put(1, listOf(1)) + put(1, listOf(2)) + } + + cartesianProductOf(duplicatedKeyMap) + } + @Test fun `verify split with factor 1 produces the same cartesian product`() { val cartesianProductMap = cartesianProductOf(dataMap) From d50bdd9d8f7b1b13deb5bb37297a4c19cc0ab30a Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 15:03:08 +0200 Subject: [PATCH 06/16] added test --- .../cartesian/CartesianProductSetTest.kt | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt index c4ff602..b682afa 100644 --- a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt +++ b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSetTest.kt @@ -56,6 +56,7 @@ internal class CartesianProductSetTest { val emptyCollection = listOf(emptyList()) val count = cartesianProductOf(emptyCollection) .stream() + .parallel() .flatMap { it.stream() } .count() assertEquals(0, count) @@ -70,10 +71,7 @@ internal class CartesianProductSetTest { @Test fun `verify Cartesian product set with keep order returns all possible combinations`() { val result: List> = cartesianProductOf(dataList, true).toList() - assertContainsAll(expectedCartesianList, result) - expectedCartesianList.forEach { - assertTrue(result.contains(it)) - } + assertContainsAllWithOrder(expectedCartesianList, result) } @Test @@ -133,6 +131,16 @@ internal class CartesianProductSetTest { assertTrue(threads.size > 1) } + @Test + fun `parallel stream should keep order`() { + val cartesianProductSet = cartesianProductOf(dataList, true) + val actual = cartesianProductSet.stream() + .parallel() + .collect(Collectors.toSet()) + + assertContainsAllWithOrder(expectedCartesianList, actual) + } + private fun assertContainsAll( expected: Collection>, actual: Collection> @@ -148,4 +156,15 @@ internal class CartesianProductSetTest { assertTrue("Expected $expectedCombination, but not found in $actual", foundCombination) } } + + private fun assertContainsAllWithOrder( + expected: Collection>, + actual: Collection> + ) { + expected.forEach { expectedCombination -> + if (!actual.contains(expectedCombination)) { + throw AssertionError("Expected $expectedCombination in the same order, but not found in $actual") + } + } + } } \ No newline at end of file From c0185c11f2733e1d21b33aa19e87edcef1c9a246 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 15:18:07 +0200 Subject: [PATCH 07/16] spliterator with unknown size test --- .../CartesianProductSpliteratorTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSpliteratorTest.kt diff --git a/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSpliteratorTest.kt b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSpliteratorTest.kt new file mode 100644 index 0000000..46218cc --- /dev/null +++ b/kombi-lib/src/test/kotlin/com/sgnatiuk/cartesian/CartesianProductSpliteratorTest.kt @@ -0,0 +1,24 @@ +package com.sgnatiuk.cartesian + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.* + +class CartesianProductSpliteratorTest { + + @Test + fun `spliterator not should contain flag sized when cartesian product has combinations more then max long`() { + val n = 1000 + val list = List(n) { it + 1 } + val data = listOf( + list, list, + list, list, + list, list, + list + ) + + val spliterator = CartesianProductSpliterator(CartesianBuilder.cartesianProductOf(data)) + assertEquals(0, spliterator.characteristics() and Spliterator.SIZED) + + } +} \ No newline at end of file From 4cc01c5c210d1c76d3126f83a4ef59f355f9c6f3 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 15:39:59 +0200 Subject: [PATCH 08/16] removed unnecessary check --- .../src/main/java/com/sgnatiuk/cartesian/CombinationMask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CombinationMask.java b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CombinationMask.java index a90834b..fb251cf 100644 --- a/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CombinationMask.java +++ b/kombi-lib/src/main/java/com/sgnatiuk/cartesian/CombinationMask.java @@ -42,6 +42,6 @@ private boolean increment() { overflow = incrementedCell / bases[index++]; } while (overflow != 0 && index < bases.length); - return overflow > 0 && index == bases.length; + return overflow > 0; } } From 433ce419a9fbd62f003d5a96da091c8c95070982 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 17:04:32 +0200 Subject: [PATCH 09/16] added release notes file --- RELEASENOTES.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 RELEASENOTES.md diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 0000000..c237f44 --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,12 @@ +v3.0.1 +* performance tuning of a generation of the cartesian product from Collection> + +v3.0.0 +* The library is fully migrated from Kotlin to Java to avoid adding of Kotlin runtime dependency to Java-only projects. +* Small performance fixes in the generation of the cartesian product. + +v2.2 +* Provided stream support(java.util.stream.Stream) + +v2.1 +* Provided split functionality allowing to split the cartesian product or combinations generation into equals chunks, so cartesian product can be generated in few threads independently. From d7e8447338b6b67638e730312726ea30c4ad7b77 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 17:09:22 +0200 Subject: [PATCH 10/16] updated benchmarks --- .../cartesian/CartesianListBenchmark.kt | 29 ++++++++++--------- .../cartesian/CartesianMapBenchmark.kt | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt index 64309e5..e1d74a9 100644 --- a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt +++ b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianListBenchmark.kt @@ -24,48 +24,49 @@ open class CartesianListBenchmark { @Setup(Level.Trial) fun doSetup() { listOfLists = List(itemsQuantity) { i -> - List(i + 1) { it } + List(i + 1) { it + 1 } } listOfSets = listOfLists.map { it.toSet() } println("\n=================================================") - println("itemsQuantity=$itemsQuantity") println("combinationsQuantity=${cartesianProductOf(listOfLists).combinationsCount()}") + println("Data:") + listOfLists.forEach { + println(it) + } println("=================================================\n") } @Benchmark fun Kombi_cartesianProduct_Lists(blackhole: Blackhole) { for (combination in cartesianProductOf(listOfLists, false)) { - for (combinationItem in combination) { - blackhole.consume(combinationItem) - } + iterateWithIterator(combination, blackhole) } } @Benchmark fun Kombi_cartesianProduct_Lists_keepingOrder(blackhole: Blackhole) { for (combination in cartesianProductOf(listOfSets, true)) { - for (combinationItem in combination) { - blackhole.consume(combinationItem) - } + iterateWithIterator(combination, blackhole) } } @Benchmark fun Guava_cartesianProduct_Sets(blackhole: Blackhole) { for (combination in Sets.cartesianProduct(listOfSets)) { - for (combinationItem in combination) { - blackhole.consume(combinationItem) - } + iterateWithIterator(combination, blackhole) } } @Benchmark fun Guava_cartesianProduct_Lists(blackhole: Blackhole) { for (combination in Lists.cartesianProduct(listOfLists)) { - for (combinationItem in combination) { - blackhole.consume(combinationItem) - } + iterateWithIterator(combination, blackhole) + } + } + + private fun iterateWithIterator(combination: MutableList, blackhole: Blackhole) { + for (combinationItem in combination) { + blackhole.consume(combinationItem) } } } diff --git a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt index 3cdd9c6..68f76a4 100644 --- a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt +++ b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt @@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit @OutputTimeUnit(TimeUnit.MICROSECONDS) open class CartesianMapBenchmark { - @Param("7") + @Param("5, 7") var itemsQuantity: Int = 0 lateinit var mapOf: Map> From 92331c0131374bb932c2d0548ea97b47e73c7f36 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 19:45:38 +0200 Subject: [PATCH 11/16] updated benchmarks results --- README.md | 86 ++++++++++-------- kombi-jmh/charts/items_39916800.jpg | Bin 0 -> 50674 bytes kombi-jmh/charts/items_5040.jpg | Bin 0 -> 45626 bytes .../cartesian/CartesianMapBenchmark.kt | 2 +- 4 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 kombi-jmh/charts/items_39916800.jpg create mode 100644 kombi-jmh/charts/items_5040.jpg diff --git a/README.md b/README.md index e801708..c086315 100644 --- a/README.md +++ b/README.md @@ -155,49 +155,61 @@ There is an overloaded builder method `CartesianBuilder.cartesianProductOf(..., ``` ## Benchmarking -Measured throughput of generation of combination/cartesian product item (generated items per second) +Measured time of generation of combination/cartesian product items (microseconds to generate all items) -Benchmark results: +Feel free to run benchmarks by yourself: +``` +./gradlew clean kombi-jmh:jmh +``` + + +Benchmark results(less is better): ``` Ubuntu 18.04.4 LTS Intel® Core™ i7-6500U CPU @ 2.50GHz × 4 -JMH version: 1.19 -VM version: JDK 1.8.0_242, VM 25.242-b08 -VM invoker: /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -VM options: -Xms4g -Xmx4g -Warmup: 10 iterations, 1 s each -Measurement: 10 iterations, 1 s each -Timeout: 10 min per iteration -Threads: 1 thread, will synchronize iterations -Benchmark mode: Average time, time/op +# JMH version: 1.22 +# VM version: JDK 1.8.0_242, OpenJDK 64-Bit Server VM, 25.242-b08 +# VM invoker: /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java +# VM options: -Xms512m -Xmx1g +# Warmup: 5 iterations, 10 s each +# Measurement: 5 iterations, 10 s each +# Timeout: 10 min per iteration +# Threads: 1 thread, will synchronize iterations +# Benchmark mode: Average time, time/op Benchmark (itemsQuantity) Mode Cnt Score Error Units -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 3 avgt 10 0.416 ± 0.004 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 5 avgt 10 8.983 ± 0.045 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 7 avgt 10 525.439 ± 2.649 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 11 avgt 10 6402100.731 ± 208391.654 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 3 avgt 10 0.835 ± 0.010 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 5 avgt 10 16.800 ± 0.203 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 7 avgt 10 745.600 ± 13.021 us/op -c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 11 avgt 10 8956251.389 ± 54373.550 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 3 avgt 10 0.525 ± 0.002 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 5 avgt 10 10.777 ± 0.165 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 7 avgt 10 535.627 ± 44.617 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 11 avgt 10 5461936.892 ± 20583.561 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 3 avgt 10 0.608 ± 0.007 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 5 avgt 10 12.682 ± 1.236 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 7 avgt 10 578.047 ± 3.468 us/op -c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 11 avgt 10 6385427.416 ± 271354.148 us/op -c.s.b.cartesian.CartesianMapBenchmark.Kombi_cartesianProduct_Maps 7 avgt 10 1019.071 ± 15.560 us/op -c.s.b.cartesian.CartesianMapBenchmark.Kombi_cartesianProduct_Maps_keepingOrder 7 avgt 10 1086.156 ± 10.034 us/op -c.s.b.combination.CombinationBenchmark.Kombi_combinations_list 11 avgt 10 220.303 ± 1.957 us/op -c.s.b.combination.CombinationBenchmark.Kombi_combinations_list 19 avgt 10 78645.589 ± 1509.309 us/op -c.s.b.combination.CombinationBenchmark.Kombi_combinations_map 11 avgt 10 463.854 ± 1.873 us/op -c.s.b.combination.CombinationBenchmark.Kombi_combinations_map 19 avgt 10 169522.011 ± 695.623 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 3 avgt 10 0.396 ± 0.002 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 5 avgt 10 8.592 ± 0.190 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 7 avgt 10 507.613 ± 3.286 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Lists 11 avgt 10 6047357.993 ± 11218.642 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 3 avgt 10 0.363 ± 0.005 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 5 avgt 10 6.838 ± 0.176 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 7 avgt 10 374.914 ± 69.084 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists 11 avgt 10 3360446.209 ± 45311.037 us/op -``` +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 3 avgt 10 0.815 ± 0.066 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 5 avgt 10 15.611 ± 0.578 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 7 avgt 10 645.309 ± 43.153 us/op +c.s.b.cartesian.CartesianListBenchmark.Guava_cartesianProduct_Sets 11 avgt 10 7492806.803 ± 137744.113 us/op + +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 3 avgt 10 0.449 ± 0.059 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 5 avgt 10 8.432 ± 0.260 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 7 avgt 10 407.532 ± 1.454 us/op +c.s.b.cartesian.CartesianListBenchmark.Kombi_cartesianProduct_Lists_keepingOrder 11 avgt 10 4061078.368 ± 42057.603 us/op + +c.s.b.cartesian.CartesianMapBenchmark.Kombi_cartesianProduct_Maps 5 avgt 10 18.971 ± 1.902 us/op +c.s.b.cartesian.CartesianMapBenchmark.Kombi_cartesianProduct_Maps 7 avgt 10 1050.295 ± 20.054 us/op +c.s.b.cartesian.CartesianMapBenchmark.Kombi_cartesianProduct_Maps_keepingOrder 5 avgt 10 19.619 ± 2.603 us/op +c.s.b.cartesian.CartesianMapBenchmark.Kombi_cartesianProduct_Maps_keepingOrder 7 avgt 10 1212.077 ± 139.650 us/op + +c.s.b.combination.CombinationBenchmark.Kombi_combinations_list 11 avgt 10 216.704 ± 3.633 us/op +c.s.b.combination.CombinationBenchmark.Kombi_combinations_list 19 avgt 10 77641.630 ± 1561.054 us/op + +c.s.b.combination.CombinationBenchmark.Kombi_combinations_map 11 avgt 10 467.513 ± 2.014 us/op +c.s.b.combination.CombinationBenchmark.Kombi_combinations_map 19 avgt 10 170390.506 ± 3108.922 us/op -Feel free to run benchmarks by yourself: -``` -./gradlew clean kombi-jmh:jmh ``` +Comparing performance with Guava(microseconds per generation, less is better): + +![](kombi-jmh/charts/items_39916800.jpg | width=50) + diff --git a/kombi-jmh/charts/items_39916800.jpg b/kombi-jmh/charts/items_39916800.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8ab4a8a283626e01d9a19e4dba23161ea0399ec GIT binary patch literal 50674 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 AzyJUM literal 0 HcmV?d00001 diff --git a/kombi-jmh/charts/items_5040.jpg b/kombi-jmh/charts/items_5040.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c14fdd4f011d6d513739be2f96cd974227821cda GIT binary patch literal 45626 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 literal 0 HcmV?d00001 diff --git a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt index 68f76a4..5aaba8c 100644 --- a/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt +++ b/kombi-jmh/src/jmh/kotlin/com/sgnatiuk/benchmark/cartesian/CartesianMapBenchmark.kt @@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit @OutputTimeUnit(TimeUnit.MICROSECONDS) open class CartesianMapBenchmark { - @Param("5, 7") + @Param("5", "7") var itemsQuantity: Int = 0 lateinit var mapOf: Map> From 46cd6e5139c86efe7e09f591bb34526221950fc4 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 19:52:14 +0200 Subject: [PATCH 12/16] updated release notes --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c237f44..062102a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,5 @@ v3.0.1 -* performance tuning of a generation of the cartesian product from Collection> +* 'Cartesian product' from Collection> performance tuning.(~40% speed-up) v3.0.0 * The library is fully migrated from Kotlin to Java to avoid adding of Kotlin runtime dependency to Java-only projects. From d91d1df40f3f3400306dea99c34ba2423265b16c Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 19:53:48 +0200 Subject: [PATCH 13/16] updated readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c086315..b4b25b1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To get even better throughput(see [Benchmarking](#benchmarking) section), comput com.sgnatiuk kombi - 3.0.0 + 3.0.1 ``` @@ -35,7 +35,7 @@ repositories { } dependencies { - compile 'com.sgnatiuk:kombi:3.0.0' + compile 'com.sgnatiuk:kombi:3.0.1' } ``` ## Combinations From 215fde2d9b992656e039f406d429420914710dfb Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 19:57:12 +0200 Subject: [PATCH 14/16] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b4b25b1..79b6205 100644 --- a/README.md +++ b/README.md @@ -211,5 +211,5 @@ c.s.b.combination.CombinationBenchmark.Kombi_combinations_map ``` Comparing performance with Guava(microseconds per generation, less is better): -![](kombi-jmh/charts/items_39916800.jpg | width=50) +![](kombi-jmh/charts/items_39916800.jpg =250x) From 686b379eb856ab5c26f362be91104c7475d207e9 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 19:58:37 +0200 Subject: [PATCH 15/16] fix image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79b6205..a2d2510 100644 --- a/README.md +++ b/README.md @@ -211,5 +211,5 @@ c.s.b.combination.CombinationBenchmark.Kombi_combinations_map ``` Comparing performance with Guava(microseconds per generation, less is better): -![](kombi-jmh/charts/items_39916800.jpg =250x) +![](kombi-jmh/charts/items_39916800.jpg) From cf730baea11627551c2b5d67eba4737560d00557 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Sun, 1 Mar 2020 20:01:02 +0200 Subject: [PATCH 16/16] updated version --- kombi-lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kombi-lib/build.gradle b/kombi-lib/build.gradle index bfae1ea..69c85ee 100644 --- a/kombi-lib/build.gradle +++ b/kombi-lib/build.gradle @@ -13,7 +13,7 @@ plugins { } ext{ - libVersion = '3.0.0' + libVersion = '3.0.1' libPackage = 'com.sgnatiuk' libName = 'kombi' }