From fb5e2cd3aa5644c294905fce3b45c2278f17e15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linas=20Med=C5=BEi=C5=ABnas?= Date: Wed, 15 Apr 2020 11:21:24 +0300 Subject: [PATCH] Efficient BytBuf search algorithms (#9914) (#9955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: We have found out that ByteBufUtil.indexOf can be inefficient for substring search on ByteBuf, both in terms of algorithm complexity (worst case O(needle.readableBytes * haystack.readableBytes)), and in constant factor (esp. on Composite buffers). With implementation of more performant search algorithms we have seen improvements on the order of magnitude. Modifications: This change introduces three search algorithms: 1. Knuth Morris Pratt - classical textbook algorithm, a good default choice. 2. Bit mask based algorithm - stable performance on any input, but limited to maximum search substring (the needle) length of 64 bytes. 3. Aho–Corasick - worse performance and higher memory consumption than [1] and [2], but it supports multiple substring (the needles) search simultaneously, by inspecting every byte of the haystack only once. Each algorithm processes every byte of underlying buffer only once, they are implemented as ByteProcessor. Result: Efficient search algorithms with linear time complexity available in Netty (I will share benchmark results in a comment on a PR). --- .../AbstractMultiSearchProcessorFactory.java | 94 +++++ .../AbstractSearchProcessorFactory.java | 115 ++++++ .../AhoCorasicSearchProcessorFactory.java | 191 ++++++++++ .../search/BitapSearchProcessorFactory.java | 77 ++++ .../search/KmpSearchProcessorFactory.java | 91 +++++ .../buffer/search/MultiSearchProcessor.java | 28 ++ .../search/MultiSearchProcessorFactory.java | 25 ++ .../netty/buffer/search/SearchProcessor.java | 30 ++ .../buffer/search/SearchProcessorFactory.java | 24 ++ .../io/netty/buffer/search/package-info.java | 20 + .../BitapSearchProcessorFactoryTest.java | 32 ++ .../search/MultiSearchProcessorTest.java | 107 ++++++ .../buffer/search/SearchProcessorTest.java | 174 +++++++++ .../util/internal/PlatformDependent.java | 12 + .../util/internal/PlatformDependent0.java | 24 ++ .../netty/microbench/search/ByteBufType.java | 52 +++ .../microbench/search/SearchBenchmark.java | 182 +++++++++ .../search/SearchRealDataBenchmark.java | 185 ++++++++++ .../netty/microbench/search/package-info.java | 19 + .../microbench/search/netty-io-news.html | 349 ++++++++++++++++++ 20 files changed, 1831 insertions(+) create mode 100644 buffer/src/main/java/io/netty/buffer/search/AbstractMultiSearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/AbstractSearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/AhoCorasicSearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/BitapSearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/KmpSearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessor.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/SearchProcessor.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/SearchProcessorFactory.java create mode 100644 buffer/src/main/java/io/netty/buffer/search/package-info.java create mode 100644 buffer/src/test/java/io/netty/buffer/search/BitapSearchProcessorFactoryTest.java create mode 100644 buffer/src/test/java/io/netty/buffer/search/MultiSearchProcessorTest.java create mode 100644 buffer/src/test/java/io/netty/buffer/search/SearchProcessorTest.java create mode 100644 microbench/src/main/java/io/netty/microbench/search/ByteBufType.java create mode 100644 microbench/src/main/java/io/netty/microbench/search/SearchBenchmark.java create mode 100644 microbench/src/main/java/io/netty/microbench/search/SearchRealDataBenchmark.java create mode 100644 microbench/src/main/java/io/netty/microbench/search/package-info.java create mode 100644 microbench/src/main/resources/io/netty/microbench/search/netty-io-news.html diff --git a/buffer/src/main/java/io/netty/buffer/search/AbstractMultiSearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/AbstractMultiSearchProcessorFactory.java new file mode 100644 index 0000000000..1a63ba5062 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/AbstractMultiSearchProcessorFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +/** + * Base class for precomputed factories that create {@link MultiSearchProcessor}s. + *
+ * The purpose of {@link MultiSearchProcessor} is to perform efficient simultaneous search for multiple {@code needles} + * in the {@code haystack}, while scanning every byte of the input sequentially, only once. While it can also be used + * to search for just a single {@code needle}, using a {@link SearchProcessorFactory} would be more efficient for + * doing that. + *
+ * See the documentation of {@link AbstractSearchProcessorFactory} for a comprehensive description of common usage. + * In addition to the functionality provided by {@link SearchProcessor}, {@link MultiSearchProcessor} adds + * a method to get the index of the {@code needle} found at the current position of the {@link MultiSearchProcessor} - + * {@link MultiSearchProcessor#getFoundNeedleId()}. + *
+ * Note: in some cases one {@code needle} can be a suffix of another {@code needle}, eg. {@code {"BC", "ABC"}}, + * and there can potentially be multiple {@code needles} found ending at the same position of the {@code haystack}. + * In such case {@link MultiSearchProcessor#getFoundNeedleId()} returns the index of the longest matching {@code needle} + * in the array of {@code needles}. + *
+ * Usage example (given that the {@code haystack} is a {@link io.netty.buffer.ByteBuf} containing "ABCD" and the + * {@code needles} are "AB", "BC" and "CD"): + *
+ *      MultiSearchProcessorFactory factory = MultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory(
+ *          "AB".getBytes(CharsetUtil.UTF_8), "BC".getBytes(CharsetUtil.UTF_8), "CD".getBytes(CharsetUtil.UTF_8));
+ *      MultiSearchProcessor processor = factory.newSearchProcessor();
+ *
+ *      int idx1 = haystack.forEachByte(processor);
+ *      // idx1 is 1 (index of the last character of the occurence of "AB" in the haystack)
+ *      // processor.getFoundNeedleId() is 0 (index of "AB" in needles[])
+ *
+ *      int continueFrom1 = idx1 + 1;
+ *      // continue the search starting from the next character
+ *
+ *      int idx2 = haystack.forEachByte(continueFrom1, haystack.readableBytes() - continueFrom1, processor);
+ *      // idx2 is 2 (index of the last character of the occurrence of "BC" in the haystack)
+ *      // processor.getFoundNeedleId() is 1 (index of "BC" in needles[])
+ *
+ *      int continueFrom2 = idx2 + 1;
+ *
+ *      int idx3 = haystack.forEachByte(continueFrom2, haystack.readableBytes() - continueFrom2, processor);
+ *      // idx3 is 3 (index of the last character of the occurrence of "CD" in the haystack)
+ *      // processor.getFoundNeedleId() is 2 (index of "CD" in needles[])
+ *
+ *      int continueFrom3 = idx3 + 1;
+ *
+ *      int idx4 = haystack.forEachByte(continueFrom3, haystack.readableBytes() - continueFrom3, processor);
+ *      // idx4 is -1 (no more occurrences of any of the needles)
+ *
+ *      // This search session is complete, processor should be discarded.
+ *      // To search for the same needles again, reuse the same {@link AbstractMultiSearchProcessorFactory}
+ *      // to get a new MultiSearchProcessor.
+ * 
+ */ +public abstract class AbstractMultiSearchProcessorFactory implements MultiSearchProcessorFactory { + + /** + * Creates a {@link MultiSearchProcessorFactory} based on + * Aho–Corasick + * string search algorithm. + *
+ * Precomputation (this method) time is linear in the size of input ({@code O(Σ|needles|)}). + *
+ * The factory allocates and retains an array of 256 * X ints plus another array of X ints, where X + * is the sum of lengths of each entry of {@code needles} minus the sum of lengths of repeated + * prefixes of the {@code needles}. + *
+ * Search (the actual application of {@link MultiSearchProcessor}) time is linear in the size of + * {@link io.netty.buffer.ByteBuf} on which the search is peformed ({@code O(|haystack|)}). + * Every byte of {@link io.netty.buffer.ByteBuf} is processed only once, sequentually, regardles of + * the number of {@code needles} being searched for. + * + * @param needles a varargs array of arrays of bytes to search for + * @return a new instance of {@link AhoCorasicSearchProcessorFactory} precomputed for the given {@code needles} + */ + public static AhoCorasicSearchProcessorFactory newAhoCorasicSearchProcessorFactory(byte[] ...needles) { + return new AhoCorasicSearchProcessorFactory(needles); + } + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/AbstractSearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/AbstractSearchProcessorFactory.java new file mode 100644 index 0000000000..d165885725 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/AbstractSearchProcessorFactory.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +/** + * Base class for precomputed factories that create {@link SearchProcessor}s. + *
+ * Different factories implement different search algorithms with performance characteristics that + * depend on a use case, so it is advisable to benchmark a concrete use case with different algorithms + * before choosing one of them. + *
+ * A concrete instance of {@link AbstractSearchProcessorFactory} is built for searching for a concrete sequence of bytes + * (the {@code needle}), it contains precomputed data needed to perform the search, and is meant to be reused + * whenever searching for the same {@code needle}. + *
+ * Note: implementations of {@link SearchProcessor} scan the {@link io.netty.buffer.ByteBuf} sequentially, + * one byte after another, without doing any random access. As a result, when using {@link SearchProcessor} + * with such methods as {@link io.netty.buffer.ByteBuf#forEachByte}, these methods return the index of the last byte + * of the found byte sequence within the {@link io.netty.buffer.ByteBuf} (which might feel counterintuitive, + * and different from {@link io.netty.buffer.ByteBufUtil#indexOf} which returns the index of the first byte + * of found sequence). + *
+ * A {@link SearchProcessor} is implemented as a + * Finite State Automaton that contains a + * small internal state which is updated with every byte processed. As a result, an instance of {@link SearchProcessor} + * should not be reused across independent search sessions (eg. for searching in different + * {@link io.netty.buffer.ByteBuf}s). A new instance should be created with {@link AbstractSearchProcessorFactory} for + * every search session. However, a {@link SearchProcessor} can (and should) be reused within the search session, + * eg. when searching for all occurrences of the {@code needle} within the same {@code haystack}. That way, it can + * also detect overlapping occurrences of the {@code needle} (eg. a string "ABABAB" contains two occurences of "BAB" + * that overlap by one character "B"). For this to work correctly, after an occurrence of the {@code needle} is + * found ending at index {@code idx}, the search should continue starting from the index {@code idx + 1}. + *
+ * Example (given that the {@code haystack} is a {@link io.netty.buffer.ByteBuf} containing "ABABAB" and + * the {@code needle} is "BAB"): + *
+ *     SearchProcessorFactory factory =
+ *         SearchProcessorFactory.newKmpSearchProcessorFactory(needle.getBytes(CharsetUtil.UTF_8));
+ *     SearchProcessor processor = factory.newSearchProcessor();
+ *
+ *     int idx1 = haystack.forEachByte(processor);
+ *     // idx1 is 3 (index of the last character of the first occurrence of the needle in the haystack)
+ *
+ *     int continueFrom1 = idx1 + 1;
+ *     // continue the search starting from the next character
+ *
+ *     int idx2 = haystack.forEachByte(continueFrom1, haystack.readableBytes() - continueFrom1, processor);
+ *     // idx2 is 5 (index of the last character of the second occurrence of the needle in the haystack)
+ *
+ *     int continueFrom2 = idx2 + 1;
+ *     // continue the search starting from the next character
+ *
+ *     int idx3 = haystack.forEachByte(continueFrom2, haystack.readableBytes() - continueFrom2, processor);
+ *     // idx3 is -1 (no more occurrences of the needle)
+ *
+ *     // After this search session is complete, processor should be discarded.
+ *     // To search for the same needle again, reuse the same factory to get a new SearchProcessor.
+ * 
+ */ +public abstract class AbstractSearchProcessorFactory implements SearchProcessorFactory { + + /** + * Creates a {@link SearchProcessorFactory} based on + * Knuth-Morris-Pratt + * string search algorithm. It is a reasonable default choice among the provided algorithms. + *
+ * Precomputation (this method) time is linear in the size of input ({@code O(|needle|)}). + *
+ * The factory allocates and retains an int array of size {@code needle.length + 1}, and retains a reference + * to the {@code needle} itself. + *
+ * Search (the actual application of {@link SearchProcessor}) time is linear in the size of + * {@link io.netty.buffer.ByteBuf} on which the search is peformed ({@code O(|haystack|)}). + * Every byte of {@link io.netty.buffer.ByteBuf} is processed only once, sequentually. + * + * @param needle an array of bytes to search for + * @return a new instance of {@link KmpSearchProcessorFactory} precomputed for the given {@code needle} + */ + public static KmpSearchProcessorFactory newKmpSearchProcessorFactory(byte[] needle) { + return new KmpSearchProcessorFactory(needle); + } + + /** + * Creates a {@link SearchProcessorFactory} based on Bitap string search algorithm. + * It is a jump free algorithm that has very stable performance (the contents of the inputs have a minimal + * effect on it). The limitation is that the {@code needle} can be no more than 64 bytes long. + *
+ * Precomputation (this method) time is linear in the size of the input ({@code O(|needle|)}). + *
+ * The factory allocates and retains a long[256] array. + *
+ * Search (the actual application of {@link SearchProcessor}) time is linear in the size of + * {@link io.netty.buffer.ByteBuf} on which the search is peformed ({@code O(|haystack|)}). + * Every byte of {@link io.netty.buffer.ByteBuf} is processed only once, sequentually. + * + * @param needle an array of no more than 64 bytes to search for + * @return a new instance of {@link BitapSearchProcessorFactory} precomputed for the given {@code needle} + */ + public static BitapSearchProcessorFactory newBitapSearchProcessorFactory(byte[] needle) { + return new BitapSearchProcessorFactory(needle); + } + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/AhoCorasicSearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/AhoCorasicSearchProcessorFactory.java new file mode 100644 index 0000000000..301ff6f8f7 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/AhoCorasicSearchProcessorFactory.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import io.netty.util.internal.PlatformDependent; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Queue; + +/** + * Implements Aho–Corasick + * string search algorithm. + * Use static {@link AbstractMultiSearchProcessorFactory#newAhoCorasicSearchProcessorFactory} + * to create an instance of this factory. + * Use {@link AhoCorasicSearchProcessorFactory#newSearchProcessor} to get an instance of + * {@link io.netty.util.ByteProcessor} implementation for performing the actual search. + * @see AbstractMultiSearchProcessorFactory + */ +public class AhoCorasicSearchProcessorFactory extends AbstractMultiSearchProcessorFactory { + + private final int[] jumpTable; + private final int[] matchForNeedleId; + + static final int BITS_PER_SYMBOL = 8; + static final int ALPHABET_SIZE = 1 << BITS_PER_SYMBOL; + + private static class Context { + int[] jumpTable; + int[] matchForNeedleId; + } + + public static class Processor implements MultiSearchProcessor { + + private final int[] jumpTable; + private final int[] matchForNeedleId; + private long currentPosition; + + Processor(int[] jumpTable, int[] matchForNeedleId) { + this.jumpTable = jumpTable; + this.matchForNeedleId = matchForNeedleId; + } + + @Override + public boolean process(byte value) { + currentPosition = PlatformDependent.getInt(jumpTable, currentPosition | (value & 0xffL)); + if (currentPosition < 0) { + currentPosition = -currentPosition; + return false; + } + return true; + } + + @Override + public int getFoundNeedleId() { + return matchForNeedleId[(int) currentPosition >> AhoCorasicSearchProcessorFactory.BITS_PER_SYMBOL]; + } + + @Override + public void reset() { + currentPosition = 0; + } + } + + AhoCorasicSearchProcessorFactory(byte[] ...needles) { + + for (byte[] needle: needles) { + if (needle.length == 0) { + throw new IllegalArgumentException("Needle must be non empty"); + } + } + + Context context = buildTrie(needles); + jumpTable = context.jumpTable; + matchForNeedleId = context.matchForNeedleId; + + linkSuffixes(); + + for (int i = 0; i < jumpTable.length; i++) { + if (matchForNeedleId[jumpTable[i] >> BITS_PER_SYMBOL] >= 0) { + jumpTable[i] = -jumpTable[i]; + } + } + } + + private static Context buildTrie(byte[][] needles) { + + ArrayList jumpTableBuilder = new ArrayList(ALPHABET_SIZE); + for (int i = 0; i < ALPHABET_SIZE; i++) { + jumpTableBuilder.add(-1); + } + + ArrayList matchForBuilder = new ArrayList(); + matchForBuilder.add(-1); + + for (int needleId = 0; needleId < needles.length; needleId++) { + byte[] needle = needles[needleId]; + int currentPosition = 0; + + for (byte ch0: needle) { + + final int ch = ch0 & 0xff; + final int next = currentPosition + ch; + + if (jumpTableBuilder.get(next) == -1) { + jumpTableBuilder.set(next, jumpTableBuilder.size()); + for (int i = 0; i < ALPHABET_SIZE; i++) { + jumpTableBuilder.add(-1); + } + matchForBuilder.add(-1); + } + + currentPosition = jumpTableBuilder.get(next); + } + + matchForBuilder.set(currentPosition >> BITS_PER_SYMBOL, needleId); + } + + Context context = new Context(); + + context.jumpTable = new int[jumpTableBuilder.size()]; + for (int i = 0; i < jumpTableBuilder.size(); i++) { + context.jumpTable[i] = jumpTableBuilder.get(i); + } + + context.matchForNeedleId = new int[matchForBuilder.size()]; + for (int i = 0; i < matchForBuilder.size(); i++) { + context.matchForNeedleId[i] = matchForBuilder.get(i); + } + + return context; + } + + private void linkSuffixes() { + + Queue queue = new ArrayDeque(); + queue.add(0); + + int[] suffixLinks = new int[matchForNeedleId.length]; + Arrays.fill(suffixLinks, -1); + + while (!queue.isEmpty()) { + + final int v = queue.remove(); + int vPosition = v >> BITS_PER_SYMBOL; + final int u = suffixLinks[vPosition] == -1 ? 0 : suffixLinks[vPosition]; + + if (matchForNeedleId[vPosition] == -1) { + matchForNeedleId[vPosition] = matchForNeedleId[u >> BITS_PER_SYMBOL]; + } + + for (int ch = 0; ch < ALPHABET_SIZE; ch++) { + + final int vIndex = v | ch; + final int uIndex = u | ch; + + final int jumpV = jumpTable[vIndex]; + final int jumpU = jumpTable[uIndex]; + + if (jumpV != -1) { + suffixLinks[jumpV >> BITS_PER_SYMBOL] = v > 0 && jumpU != -1 ? jumpU : 0; + queue.add(jumpV); + } else { + jumpTable[vIndex] = jumpU != -1 ? jumpU : 0; + } + } + } + } + + /** + * Returns a new {@link Processor}. + */ + @Override + public Processor newSearchProcessor() { + return new Processor(jumpTable, matchForNeedleId); + } + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/BitapSearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/BitapSearchProcessorFactory.java new file mode 100644 index 0000000000..a41e3ffc6e --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/BitapSearchProcessorFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import io.netty.util.internal.PlatformDependent; + +/** + * Implements Bitap string search algorithm. + * Use static {@link AbstractSearchProcessorFactory#newBitapSearchProcessorFactory} + * to create an instance of this factory. + * Use {@link BitapSearchProcessorFactory#newSearchProcessor} to get an instance of {@link io.netty.util.ByteProcessor} + * implementation for performing the actual search. + * @see AbstractSearchProcessorFactory + */ +public class BitapSearchProcessorFactory extends AbstractSearchProcessorFactory { + + private final long[] bitMasks = new long[256]; + private final long successBit; + + public static class Processor implements SearchProcessor { + + private final long[] bitMasks; + private final long successBit; + private long currentMask; + + Processor(long[] bitMasks, long successBit) { + this.bitMasks = bitMasks; + this.successBit = successBit; + } + + @Override + public boolean process(byte value) { + currentMask = ((currentMask << 1) | 1) & PlatformDependent.getLong(bitMasks, value & 0xffL); + return (currentMask & successBit) == 0; + } + + @Override + public void reset() { + currentMask = 0; + } + } + + BitapSearchProcessorFactory(byte[] needle) { + if (needle.length > 64) { + throw new IllegalArgumentException("Maximum supported search pattern length is 64, got " + needle.length); + } + + long bit = 1L; + for (byte c: needle) { + bitMasks[c & 0xff] |= bit; + bit <<= 1; + } + + successBit = 1L << (needle.length - 1); + } + + /** + * Returns a new {@link Processor}. + */ + @Override + public Processor newSearchProcessor() { + return new Processor(bitMasks, successBit); + } + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/KmpSearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/KmpSearchProcessorFactory.java new file mode 100644 index 0000000000..975177adc6 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/KmpSearchProcessorFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import io.netty.util.internal.PlatformDependent; + +/** + * Implements + * Knuth-Morris-Pratt + * string search algorithm. + * Use static {@link AbstractSearchProcessorFactory#newKmpSearchProcessorFactory} + * to create an instance of this factory. + * Use {@link KmpSearchProcessorFactory#newSearchProcessor} to get an instance of {@link io.netty.util.ByteProcessor} + * implementation for performing the actual search. + * @see AbstractSearchProcessorFactory + */ +public class KmpSearchProcessorFactory extends AbstractSearchProcessorFactory { + + private final int[] jumpTable; + private final byte[] needle; + + public static class Processor implements SearchProcessor { + + private final byte[] needle; + private final int[] jumpTable; + private long currentPosition; + + Processor(byte[] needle, int[] jumpTable) { + this.needle = needle; + this.jumpTable = jumpTable; + } + + @Override + public boolean process(byte value) { + while (currentPosition > 0 && PlatformDependent.getByte(needle, currentPosition) != value) { + currentPosition = PlatformDependent.getInt(jumpTable, currentPosition); + } + if (PlatformDependent.getByte(needle, currentPosition) == value) { + currentPosition++; + } + if (currentPosition == needle.length) { + currentPosition = PlatformDependent.getInt(jumpTable, currentPosition); + return false; + } + + return true; + } + + @Override + public void reset() { + currentPosition = 0; + } + } + + KmpSearchProcessorFactory(byte[] needle) { + this.needle = needle.clone(); + this.jumpTable = new int[needle.length + 1]; + + int j = 0; + for (int i = 1; i < needle.length; i++) { + while (j > 0 && needle[j] != needle[i]) { + j = jumpTable[j]; + } + if (needle[j] == needle[i]) { + j++; + } + jumpTable[i + 1] = j; + } + } + + /** + * Returns a new {@link Processor}. + */ + @Override + public Processor newSearchProcessor() { + return new Processor(needle, jumpTable); + } + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessor.java b/buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessor.java new file mode 100644 index 0000000000..702e869323 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +/** + * Interface for {@link SearchProcessor} that implements simultaneous search for multiple strings. + * @see MultiSearchProcessorFactory + */ +public interface MultiSearchProcessor extends SearchProcessor { + + /** + * @return the index of found search string (if any, or -1 if none) at current position of this MultiSearchProcessor + */ + int getFoundNeedleId(); + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessorFactory.java new file mode 100644 index 0000000000..571392d798 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/MultiSearchProcessorFactory.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +public interface MultiSearchProcessorFactory extends SearchProcessorFactory { + + /** + * Returns a new {@link MultiSearchProcessor}. + */ + @Override + MultiSearchProcessor newSearchProcessor(); + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/SearchProcessor.java b/buffer/src/main/java/io/netty/buffer/search/SearchProcessor.java new file mode 100644 index 0000000000..a95bc66393 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/SearchProcessor.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import io.netty.util.ByteProcessor; + +/** + * Interface for {@link ByteProcessor} that implements string search. + * @see SearchProcessorFactory + */ +public interface SearchProcessor extends ByteProcessor { + + /** + * Resets the state of SearchProcessor. + */ + void reset(); + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/SearchProcessorFactory.java b/buffer/src/main/java/io/netty/buffer/search/SearchProcessorFactory.java new file mode 100644 index 0000000000..513f201fb2 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/SearchProcessorFactory.java @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +public interface SearchProcessorFactory { + + /** + * Returns a new {@link SearchProcessor}. + */ + SearchProcessor newSearchProcessor(); + +} diff --git a/buffer/src/main/java/io/netty/buffer/search/package-info.java b/buffer/src/main/java/io/netty/buffer/search/package-info.java new file mode 100644 index 0000000000..0c932cbae7 --- /dev/null +++ b/buffer/src/main/java/io/netty/buffer/search/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ + +/** + * Utility classes for performing efficient substring search within {@link io.netty.buffer.ByteBuf}. + */ +package io.netty.buffer.search; diff --git a/buffer/src/test/java/io/netty/buffer/search/BitapSearchProcessorFactoryTest.java b/buffer/src/test/java/io/netty/buffer/search/BitapSearchProcessorFactoryTest.java new file mode 100644 index 0000000000..e7068bc8df --- /dev/null +++ b/buffer/src/test/java/io/netty/buffer/search/BitapSearchProcessorFactoryTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import org.junit.Test; + +public class BitapSearchProcessorFactoryTest { + + @Test + public void testAcceptMaximumLengthNeedle() { + new BitapSearchProcessorFactory(new byte[64]); + } + + @Test(expected = IllegalArgumentException.class) + public void testRejectTooLongNeedle() { + new BitapSearchProcessorFactory(new byte[65]); + } + +} diff --git a/buffer/src/test/java/io/netty/buffer/search/MultiSearchProcessorTest.java b/buffer/src/test/java/io/netty/buffer/search/MultiSearchProcessorTest.java new file mode 100644 index 0000000000..aa25f9cc9c --- /dev/null +++ b/buffer/src/test/java/io/netty/buffer/search/MultiSearchProcessorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class MultiSearchProcessorTest { + + @Test + public void testSearchForMultiple() { + final ByteBuf haystack = Unpooled.copiedBuffer("one two three one", CharsetUtil.UTF_8); + final int length = haystack.readableBytes(); + + final MultiSearchProcessor processor = AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory( + bytes("one"), + bytes("two"), + bytes("three") + ).newSearchProcessor(); + + assertEquals(-1, processor.getFoundNeedleId()); + + assertEquals(2, haystack.forEachByte(processor)); + assertEquals(0, processor.getFoundNeedleId()); // index of "one" in needles[] + + assertEquals(6, haystack.forEachByte(3, length - 3, processor)); + assertEquals(1, processor.getFoundNeedleId()); // index of "two" in needles[] + + assertEquals(12, haystack.forEachByte(7, length - 7, processor)); + assertEquals(2, processor.getFoundNeedleId()); // index of "three" in needles[] + + assertEquals(16, haystack.forEachByte(13, length - 13, processor)); + assertEquals(0, processor.getFoundNeedleId()); // index of "one" in needles[] + + assertEquals(-1, haystack.forEachByte(17, length - 17, processor)); + + haystack.release(); + } + + @Test + public void testSearchForMultipleOverlapping() { + final ByteBuf haystack = Unpooled.copiedBuffer("abcd", CharsetUtil.UTF_8); + final int length = haystack.readableBytes(); + + final MultiSearchProcessor processor = AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory( + bytes("ab"), + bytes("bc"), + bytes("cd") + ).newSearchProcessor(); + + assertEquals(1, haystack.forEachByte(processor)); + assertEquals(0, processor.getFoundNeedleId()); // index of "ab" in needles[] + + assertEquals(2, haystack.forEachByte(2, length - 2, processor)); + assertEquals(1, processor.getFoundNeedleId()); // index of "bc" in needles[] + + assertEquals(3, haystack.forEachByte(3, length - 3, processor)); + assertEquals(2, processor.getFoundNeedleId()); // index of "cd" in needles[] + + haystack.release(); + } + + @Test + public void findLongerNeedleInCaseOfSuffixMatch() { + final ByteBuf haystack = Unpooled.copiedBuffer("xabcx", CharsetUtil.UTF_8); + + final MultiSearchProcessor processor1 = AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory( + bytes("abc"), + bytes("bc") + ).newSearchProcessor(); + + assertEquals(3, haystack.forEachByte(processor1)); // end of "abc" in haystack + assertEquals(0, processor1.getFoundNeedleId()); // index of "abc" in needles[] + + final MultiSearchProcessor processor2 = AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory( + bytes("bc"), + bytes("abc") + ).newSearchProcessor(); + + assertEquals(3, haystack.forEachByte(processor2)); // end of "abc" in haystack + assertEquals(1, processor2.getFoundNeedleId()); // index of "abc" in needles[] + + haystack.release(); + } + + private static byte[] bytes(String s) { + return s.getBytes(CharsetUtil.UTF_8); + } + +} diff --git a/buffer/src/test/java/io/netty/buffer/search/SearchProcessorTest.java b/buffer/src/test/java/io/netty/buffer/search/SearchProcessorTest.java new file mode 100644 index 0000000000..0b02c29098 --- /dev/null +++ b/buffer/src/test/java/io/netty/buffer/search/SearchProcessorTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.buffer.search; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; + +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class SearchProcessorTest { + + private enum Algorithm { + KNUTH_MORRIS_PRATT { + @Override + SearchProcessorFactory newFactory(byte[] needle) { + return AbstractSearchProcessorFactory.newKmpSearchProcessorFactory(needle); + } + }, + BITAP { + @Override + SearchProcessorFactory newFactory(byte[] needle) { + return AbstractSearchProcessorFactory.newBitapSearchProcessorFactory(needle); + } + }, + AHO_CORASIC { + @Override + SearchProcessorFactory newFactory(byte[] needle) { + return AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory(needle); + } + }; + abstract SearchProcessorFactory newFactory(byte[] needle); + } + + @Parameters(name = "{0} algorithm") + public static Object[] algorithms() { + return Algorithm.values(); + } + + @Parameter + public Algorithm algorithm; + + @Test + public void testSearch() { + final ByteBuf haystack = Unpooled.copiedBuffer("abc☺", CharsetUtil.UTF_8); + + assertEquals(0, haystack.forEachByte(factory("a").newSearchProcessor())); + assertEquals(1, haystack.forEachByte(factory("ab").newSearchProcessor())); + assertEquals(2, haystack.forEachByte(factory("abc").newSearchProcessor())); + assertEquals(5, haystack.forEachByte(factory("abc☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("abc☺☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("abc☺x").newSearchProcessor())); + + assertEquals(1, haystack.forEachByte(factory("b").newSearchProcessor())); + assertEquals(2, haystack.forEachByte(factory("bc").newSearchProcessor())); + assertEquals(5, haystack.forEachByte(factory("bc☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("bc☺☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("bc☺x").newSearchProcessor())); + + assertEquals(2, haystack.forEachByte(factory("c").newSearchProcessor())); + assertEquals(5, haystack.forEachByte(factory("c☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("c☺☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("c☺x").newSearchProcessor())); + + assertEquals(5, haystack.forEachByte(factory("☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("☺☺").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("☺x").newSearchProcessor())); + + assertEquals(-1, haystack.forEachByte(factory("z").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("aa").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("ba").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("abcd").newSearchProcessor())); + assertEquals(-1, haystack.forEachByte(factory("abcde").newSearchProcessor())); + + haystack.release(); + } + + @Test + public void testRepeating() { + final ByteBuf haystack = Unpooled.copiedBuffer("abcababc", CharsetUtil.UTF_8); + final int length = haystack.readableBytes(); + SearchProcessor processor = factory("ab").newSearchProcessor(); + + assertEquals(1, haystack.forEachByte(processor)); + assertEquals(4, haystack.forEachByte(2, length - 2, processor)); + assertEquals(6, haystack.forEachByte(5, length - 5, processor)); + assertEquals(-1, haystack.forEachByte(7, length - 7, processor)); + + haystack.release(); + } + + @Test + public void testOverlapping() { + final ByteBuf haystack = Unpooled.copiedBuffer("ababab", CharsetUtil.UTF_8); + final int length = haystack.readableBytes(); + SearchProcessor processor = factory("bab").newSearchProcessor(); + + assertEquals(3, haystack.forEachByte(processor)); + assertEquals(5, haystack.forEachByte(4, length - 4, processor)); + assertEquals(-1, haystack.forEachByte(6, length - 6, processor)); + + haystack.release(); + } + + @Test + public void testLongInputs() { + final int haystackLen = 1024; + final int needleLen = 64; + + final byte[] haystackBytes = new byte[haystackLen]; + haystackBytes[haystackLen - 1] = 1; + final ByteBuf haystack = Unpooled.copiedBuffer(haystackBytes); // 00000...00001 + + final byte[] needleBytes = new byte[needleLen]; // 000...000 + assertEquals(needleLen - 1, haystack.forEachByte(factory(needleBytes).newSearchProcessor())); + + needleBytes[needleLen - 1] = 1; // 000...001 + assertEquals(haystackLen - 1, haystack.forEachByte(factory(needleBytes).newSearchProcessor())); + + needleBytes[needleLen - 1] = 2; // 000...002 + assertEquals(-1, haystack.forEachByte(factory(needleBytes).newSearchProcessor())); + + needleBytes[needleLen - 1] = 0; + needleBytes[0] = 1; // 100...000 + assertEquals(-1, haystack.forEachByte(factory(needleBytes).newSearchProcessor())); + } + + @Test + public void testUniqueLen64Substrings() { + final byte[] haystackBytes = new byte[32 * 65]; // 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, ... + int pos = 0; + for (int i = 1; i <= 64; i++) { + for (int j = 0; j < i; j++) { + haystackBytes[pos++] = (byte) i; + } + } + final ByteBuf haystack = Unpooled.copiedBuffer(haystackBytes); + + for (int start = 0; start < haystackBytes.length - 64; start++) { + final byte[] needle = Arrays.copyOfRange(haystackBytes, start, start + 64); + assertEquals(start + 63, haystack.forEachByte(factory(needle).newSearchProcessor())); + } + } + + private SearchProcessorFactory factory(byte[] needle) { + return algorithm.newFactory(needle); + } + + private SearchProcessorFactory factory(String needle) { + return factory(needle.getBytes(CharsetUtil.UTF_8)); + } + +} diff --git a/common/src/main/java/io/netty/util/internal/PlatformDependent.java b/common/src/main/java/io/netty/util/internal/PlatformDependent.java index 80808edf2b..222ac2f11e 100644 --- a/common/src/main/java/io/netty/util/internal/PlatformDependent.java +++ b/common/src/main/java/io/netty/util/internal/PlatformDependent.java @@ -519,6 +519,10 @@ public final class PlatformDependent { return PlatformDependent0.getByte(data, index); } + public static byte getByte(byte[] data, long index) { + return PlatformDependent0.getByte(data, index); + } + public static short getShort(byte[] data, int index) { return PlatformDependent0.getShort(data, index); } @@ -527,10 +531,18 @@ public final class PlatformDependent { return PlatformDependent0.getInt(data, index); } + public static int getInt(int[] data, long index) { + return PlatformDependent0.getInt(data, index); + } + public static long getLong(byte[] data, int index) { return PlatformDependent0.getLong(data, index); } + public static long getLong(long[] data, long index) { + return PlatformDependent0.getLong(data, index); + } + private static long getLongSafe(byte[] bytes, int offset) { if (BIG_ENDIAN_NATIVE_ORDER) { return (long) bytes[offset] << 56 | diff --git a/common/src/main/java/io/netty/util/internal/PlatformDependent0.java b/common/src/main/java/io/netty/util/internal/PlatformDependent0.java index e6c89480c3..44976f18cb 100644 --- a/common/src/main/java/io/netty/util/internal/PlatformDependent0.java +++ b/common/src/main/java/io/netty/util/internal/PlatformDependent0.java @@ -39,6 +39,10 @@ final class PlatformDependent0 { private static final InternalLogger logger = InternalLoggerFactory.getInstance(PlatformDependent0.class); private static final long ADDRESS_FIELD_OFFSET; private static final long BYTE_ARRAY_BASE_OFFSET; + private static final long INT_ARRAY_BASE_OFFSET; + private static final long INT_ARRAY_INDEX_SCALE; + private static final long LONG_ARRAY_BASE_OFFSET; + private static final long LONG_ARRAY_INDEX_SCALE; private static final Constructor DIRECT_BUFFER_CONSTRUCTOR; private static final Throwable EXPLICIT_NO_UNSAFE_CAUSE = explicitNoUnsafeCause0(); private static final Method ALLOCATE_ARRAY_METHOD; @@ -208,6 +212,10 @@ final class PlatformDependent0 { if (unsafe == null) { ADDRESS_FIELD_OFFSET = -1; BYTE_ARRAY_BASE_OFFSET = -1; + LONG_ARRAY_BASE_OFFSET = -1; + LONG_ARRAY_INDEX_SCALE = -1; + INT_ARRAY_BASE_OFFSET = -1; + INT_ARRAY_INDEX_SCALE = -1; UNALIGNED = false; DIRECT_BUFFER_CONSTRUCTOR = null; ALLOCATE_ARRAY_METHOD = null; @@ -263,6 +271,10 @@ final class PlatformDependent0 { DIRECT_BUFFER_CONSTRUCTOR = directBufferConstructor; ADDRESS_FIELD_OFFSET = objectFieldOffset(addressField); BYTE_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + INT_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(int[].class); + INT_ARRAY_INDEX_SCALE = UNSAFE.arrayIndexScale(int[].class); + LONG_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(long[].class); + LONG_ARRAY_INDEX_SCALE = UNSAFE.arrayIndexScale(long[].class); final boolean unaligned; Object maybeUnaligned = AccessController.doPrivileged(new PrivilegedAction() { @Override @@ -525,6 +537,10 @@ final class PlatformDependent0 { return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index); } + static byte getByte(byte[] data, long index) { + return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index); + } + static short getShort(byte[] data, int index) { return UNSAFE.getShort(data, BYTE_ARRAY_BASE_OFFSET + index); } @@ -533,10 +549,18 @@ final class PlatformDependent0 { return UNSAFE.getInt(data, BYTE_ARRAY_BASE_OFFSET + index); } + static int getInt(int[] data, long index) { + return UNSAFE.getInt(data, INT_ARRAY_BASE_OFFSET + INT_ARRAY_INDEX_SCALE * index); + } + static long getLong(byte[] data, int index) { return UNSAFE.getLong(data, BYTE_ARRAY_BASE_OFFSET + index); } + static long getLong(long[] data, long index) { + return UNSAFE.getLong(data, LONG_ARRAY_BASE_OFFSET + LONG_ARRAY_INDEX_SCALE * index); + } + static void putByte(long address, byte value) { UNSAFE.putByte(address, value); } diff --git a/microbench/src/main/java/io/netty/microbench/search/ByteBufType.java b/microbench/src/main/java/io/netty/microbench/search/ByteBufType.java new file mode 100644 index 0000000000..f67e153cc1 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/search/ByteBufType.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.microbench.search; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; + +public enum ByteBufType { + HEAP { + @Override + ByteBuf newBuffer(byte[] bytes) { + return Unpooled.wrappedBuffer(bytes, 0, bytes.length); + } + }, + COMPOSITE { + @Override + ByteBuf newBuffer(byte[] bytes) { + CompositeByteBuf buf = Unpooled.compositeBuffer(); + int length = bytes.length; + int offset = 0; + int capacity = length / 8; // 8 buffers per composite + + while (length > 0) { + buf.addComponent(true, Unpooled.wrappedBuffer(bytes, offset, Math.min(length, capacity))); + length -= capacity; + offset += capacity; + } + return buf; + } + }, + DIRECT { + @Override + ByteBuf newBuffer(byte[] bytes) { + return Unpooled.directBuffer(bytes.length).writeBytes(bytes); + } + }; + abstract ByteBuf newBuffer(byte[] bytes); +} diff --git a/microbench/src/main/java/io/netty/microbench/search/SearchBenchmark.java b/microbench/src/main/java/io/netty/microbench/search/SearchBenchmark.java new file mode 100644 index 0000000000..c9520722fb --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/search/SearchBenchmark.java @@ -0,0 +1,182 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.microbench.search; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.buffer.search.AbstractMultiSearchProcessorFactory; +import io.netty.buffer.search.AbstractSearchProcessorFactory; +import io.netty.buffer.search.SearchProcessorFactory; +import io.netty.microbench.util.AbstractMicrobenchmark; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.CompilerControl; +import org.openjdk.jmh.annotations.CompilerControl.Mode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(1) +public class SearchBenchmark extends AbstractMicrobenchmark { + + private static final long SEED = 123; + + public enum Input { + RANDOM_256B { + @Override + byte[] getNeedle(Random rnd) { + return new byte[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; + } + @Override + byte[] getHaystack(Random rnd) { + return randomBytes(rnd, 256, ' ', 127); + } + }, + RANDOM_2KB { + @Override + byte[] getNeedle(Random rnd) { + return new byte[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; + } + @Override + byte[] getHaystack(Random rnd) { + return randomBytes(rnd, 2048, ' ', 127); + } + }, + PREDICTABLE { + @Override + byte[] getNeedle(Random rnd) { + // all 0s + return new byte[64]; + } + @Override + byte[] getHaystack(Random rnd) { + // no 0s except in the very end + byte[] bytes = randomBytes(rnd, 2048, 1, 255); + Arrays.fill(bytes, bytes.length - 64, bytes.length, (byte) 0); + return bytes; + } + }, + UNPREDICTABLE { + @Override + byte[] getNeedle(Random rnd) { + return randomBytes(rnd, 64, 0, 1); + } + @Override + byte[] getHaystack(Random rnd) { + return randomBytes(rnd, 2048, 0, 1); + } + }, + WORST_CASE { // Bitap will fail on it because the needle is >64 bytes long + @Override + byte[] getNeedle(Random rnd) { + // aa(...)aab + byte[] needle = new byte[1024]; + Arrays.fill(needle, (byte) 'a'); + needle[needle.length - 1] = 'b'; + return needle; + } + @Override + byte[] getHaystack(Random rnd) { + // aa(...)aaa + byte[] haystack = new byte[2048]; + Arrays.fill(haystack, (byte) 'a'); + return haystack; + } + }; + + abstract byte[] getNeedle(Random rnd); + abstract byte[] getHaystack(Random rnd); + } + + @Param + public Input input; + + @Param + public ByteBufType bufferType; + + private Random rnd; + private ByteBuf needle, haystack; + private byte[] needleBytes, haystackBytes; + private SearchProcessorFactory kmpFactory, bitapFactory, ahoCorasicFactory; + + @Setup + public void setup() { + rnd = new Random(SEED); + + needleBytes = input.getNeedle(rnd); + haystackBytes = input.getHaystack(rnd); + + needle = Unpooled.wrappedBuffer(needleBytes); + haystack = bufferType.newBuffer(haystackBytes); + + kmpFactory = AbstractSearchProcessorFactory.newKmpSearchProcessorFactory(needleBytes); + ahoCorasicFactory = AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory(needleBytes); + + if (needleBytes.length <= 64) { + bitapFactory = AbstractSearchProcessorFactory.newBitapSearchProcessorFactory(needleBytes); + } + } + + @TearDown + public void teardown() { + needle.release(); + haystack.release(); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public int indexOf() { + return ByteBufUtil.indexOf(needle, haystack); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public int kmp() { + return haystack.forEachByte(kmpFactory.newSearchProcessor()); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public int bitap() { + return haystack.forEachByte(bitapFactory.newSearchProcessor()); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public int ahoCorasic() { + return haystack.forEachByte(ahoCorasicFactory.newSearchProcessor()); + } + + private static byte[] randomBytes(Random rnd, int size, int from, int to) { + byte[] bytes = new byte[size]; + for (int i = 0; i < size; i++) { + bytes[i] = (byte) (from + rnd.nextInt(to - from + 1)); + } + return bytes; + } + +} diff --git a/microbench/src/main/java/io/netty/microbench/search/SearchRealDataBenchmark.java b/microbench/src/main/java/io/netty/microbench/search/SearchRealDataBenchmark.java new file mode 100644 index 0000000000..026f6450c8 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/search/SearchRealDataBenchmark.java @@ -0,0 +1,185 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.microbench.search; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.search.AbstractMultiSearchProcessorFactory; +import io.netty.buffer.search.AbstractSearchProcessorFactory; +import io.netty.buffer.search.SearchProcessor; +import io.netty.buffer.search.SearchProcessorFactory; +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.internal.ResourcesUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.CompilerControl; +import org.openjdk.jmh.annotations.CompilerControl.Mode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(1) +public class SearchRealDataBenchmark extends AbstractMicrobenchmark { + + public enum Algorithm { + AHO_CORASIC { + @Override + SearchProcessorFactory newFactory(byte[] needle) { + return AbstractMultiSearchProcessorFactory.newAhoCorasicSearchProcessorFactory(needle); + } + }, + KMP { + @Override + SearchProcessorFactory newFactory(byte[] needle) { + return AbstractSearchProcessorFactory.newKmpSearchProcessorFactory(needle); + } + }, + BITAP { + @Override + SearchProcessorFactory newFactory(byte[] needle) { + return AbstractSearchProcessorFactory.newBitapSearchProcessorFactory(needle); + } + }; + abstract SearchProcessorFactory newFactory(byte[] needle); + } + + @Param + public Algorithm algorithm; + + @Param + public ByteBufType bufferType; + + private ByteBuf haystack; + private SearchProcessorFactory[] searchProcessorFactories; + private SearchProcessorFactory searchProcessorFactory; + + private static final byte[][] NEEDLES = { + "Thank You".getBytes(), + "* Does not exist *".getBytes(), + "
  • ".getBytes(), + "".getBytes(), + "
  • ".getBytes(), + "github.com".getBytes(), + " Does not exist 2 ".getBytes(), + "".getBytes(), + "\"https://".getBytes(), + "Netty 4.1.45.Final released".getBytes() + }; + + private int needleId, searchFrom, haystackLength; + + @Setup + public void setup() throws IOException { + File haystackFile = ResourcesUtil.getFile(SearchRealDataBenchmark.class, "netty-io-news.html"); + byte[] haystackBytes = readBytes(haystackFile); + haystack = bufferType.newBuffer(haystackBytes); + + needleId = 0; + searchFrom = 0; + haystackLength = haystack.readableBytes(); + + searchProcessorFactories = new SearchProcessorFactory[NEEDLES.length]; + for (int i = 0; i < NEEDLES.length; i++) { + searchProcessorFactories[i] = algorithm.newFactory(NEEDLES[i]); + } + } + + @Setup(Level.Invocation) + public void invocationSetup() { + needleId = (needleId + 1) % searchProcessorFactories.length; + searchProcessorFactory = searchProcessorFactories[needleId]; + } + + @TearDown + public void teardown() { + haystack.release(); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public int findFirst() { + return haystack.forEachByte(searchProcessorFactory.newSearchProcessor()); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public int findFirstFromIndex() { + searchFrom = (searchFrom + 100) % haystackLength; + return haystack.forEachByte( + searchFrom, haystackLength - searchFrom, searchProcessorFactory.newSearchProcessor()); + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public void findAll(Blackhole blackHole) { + SearchProcessor searchProcessor = searchProcessorFactory.newSearchProcessor(); + int pos = 0; + do { + pos = haystack.forEachByte(pos, haystackLength - pos, searchProcessor) + 1; + blackHole.consume(pos); + } while (pos > 0); + } + + private static byte[] readBytes(File file) throws IOException { + InputStream in = new FileInputStream(file); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + byte[] buf = new byte[8192]; + for (;;) { + int ret = in.read(buf); + if (ret < 0) { + break; + } + out.write(buf, 0, ret); + } + return out.toByteArray(); + } finally { + safeClose(out); + } + } finally { + safeClose(in); + } + } + + private static void safeClose(InputStream in) { + try { + in.close(); + } catch (IOException ignored) { } + } + + private static void safeClose(OutputStream out) { + try { + out.close(); + } catch (IOException ignored) { } + } + +} diff --git a/microbench/src/main/java/io/netty/microbench/search/package-info.java b/microbench/src/main/java/io/netty/microbench/search/package-info.java new file mode 100644 index 0000000000..17a14a10cd --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/search/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you 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. + */ +/** + * Benchmarks for search ({@link io.netty.buffer.search} and {@link io.netty.buffer.ByteBufUtil#indexOf}). + */ +package io.netty.microbench.search; diff --git a/microbench/src/main/resources/io/netty/microbench/search/netty-io-news.html b/microbench/src/main/resources/io/netty/microbench/search/netty-io-news.html new file mode 100644 index 0000000000..fbdf082d6c --- /dev/null +++ b/microbench/src/main/resources/io/netty/microbench/search/netty-io-news.html @@ -0,0 +1,349 @@ + + + + + Netty.news: Netty 4.1.45.Final released + Netty: Netty 4.1.45.Final released + + + + + + + + + + + + + + + +Skip navigation + +
    +
    +
    +
    +
    +

    + Netty 4.1.45.Final released +

    + +
    +

    I am happy to announce the release of netty 4.1.45.Final, our first release of 2020. This is a bug-fix release which also fixes two regressions. Please upgrade as soon as possible.

    +

    For more details please read-on.

    +

    The most important changes in this release are:

    +
      +
    • Fix BufferOverflowException during non-Unsafe PooledDirectByteBuf resize (#9912)
    • +
    • FlushConsolidationHandler may suppress flushes by mistake (#9931)
    • +
    • Utf8FrameValidator must release buffer when validation fails (#9909)
    • +
    • Avoid possible comparison contract violation (#9883)
    • +
    • Ignore inline comments when parsing nameservers (#9894)
    • +
    +

    For the details and all changes, please browse our issue tracker for 4.1.45.Final.

    +

    Thank You

    +

    Every idea and bug-report counts and so we thought it is worth mentioning those who helped in this area. Please report an unintended omission.

    + +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + + + + + +