From 9f91d685b9179bb78193526bc2f259656e88f7ae Mon Sep 17 00:00:00 2001 From: "Mateusz \"Serafin\" Gajewski" Date: Sat, 7 Mar 2026 17:45:58 +0100 Subject: [PATCH 1/2] Add SWAR-based lastIndexOf implementation --- src/main/java/io/airlift/slice/Slice.java | 39 +++++++++++-- src/test/java/io/airlift/slice/TestSlice.java | 58 +++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/airlift/slice/Slice.java b/src/main/java/io/airlift/slice/Slice.java index 7fd12b28..c4377218 100644 --- a/src/main/java/io/airlift/slice/Slice.java +++ b/src/main/java/io/airlift/slice/Slice.java @@ -35,6 +35,7 @@ import static io.airlift.slice.SizeOf.SIZE_OF_SHORT; import static io.airlift.slice.SizeOf.instanceSize; import static io.airlift.slice.SizeOf.sizeOf; +import static java.lang.Long.numberOfLeadingZeros; import static java.lang.Long.numberOfTrailingZeros; import static java.lang.invoke.MethodHandles.byteArrayViewVarHandle; import static java.nio.ByteOrder.LITTLE_ENDIAN; @@ -1187,6 +1188,37 @@ public int indexOf(Slice pattern, int offset) return -1; } + public int lastIndexOfByte(byte b, int fromIndex) + { + if (size == 0) { + return -1; + } + long pattern = (b & 0xFFL) * 0x01010101_01010101L; + int offset = fromIndex; + + for (; offset >= 7; offset -= 8) { + long value = getLongUnchecked(offset - 7); + long xor = value ^ pattern; + long hasZero = (xor - 0x01010101_01010101L) & ~xor & 0x80808080_80808080L; + while (hasZero != 0) { + int byteIndex = 7 - (numberOfLeadingZeros(hasZero) >>> 3); + int candidateIndex = (offset - 7) + byteIndex; + if (getByteUnchecked(candidateIndex) == b) { + return candidateIndex; + } + hasZero &= ~(0x80L << (byteIndex * 8)); + } + } + + for (; offset >= 0; offset--) { + if (getByteUnchecked(offset) == b) { + return offset; + } + } + + return -1; + } + /** * Returns the index of the last occurrence of the pattern within this slice. * If the pattern is not found -1 is returned. If pattern is empty, the @@ -1226,11 +1258,8 @@ public int lastIndexOf(Slice pattern, int offset) byte firstByte = pattern.getByteUnchecked(0); while (index >= 0) { - // seek to first byte match - while (index > 0 && getByteUnchecked(index) != firstByte) { - index--; - } - if (getByteUnchecked(index) != firstByte) { + index = lastIndexOfByte(firstByte, index); + if (index < 0) { break; } diff --git a/src/test/java/io/airlift/slice/TestSlice.java b/src/test/java/io/airlift/slice/TestSlice.java index a3f1d663..de85347f 100644 --- a/src/test/java/io/airlift/slice/TestSlice.java +++ b/src/test/java/io/airlift/slice/TestSlice.java @@ -952,6 +952,64 @@ public void testIndexOfByte() assertThatThrownBy(() -> slice.indexOfByte(500)).isInstanceOf(IllegalArgumentException.class); } + @Test + public void testLastIndexOfByte() + { + Slice slice = utf8Slice("appleappleappleappleappleappleappleappleappleappleappleappleappleappleapple!"); + + assertThat(slice.lastIndexOfByte((byte) 'a', slice.length() - 1)).isEqualTo(70); + assertThat(slice.lastIndexOfByte((byte) 'p', slice.length() - 1)).isEqualTo(72); + assertThat(slice.lastIndexOfByte((byte) 'e', slice.length() - 1)).isEqualTo(74); + assertThat(slice.lastIndexOfByte((byte) '!', slice.length() - 1)).isEqualTo(slice.length() - 1); + assertThat(slice.lastIndexOfByte((byte) 'x', slice.length() - 1)).isEqualTo(-1); + + assertThat(slice.lastIndexOfByte((byte) 'a', 0)).isEqualTo(0); + assertThat(slice.lastIndexOfByte((byte) 'a', 4)).isEqualTo(0); + assertThat(slice.lastIndexOfByte((byte) 'a', 5)).isEqualTo(5); + assertThat(slice.lastIndexOfByte((byte) 'a', 10)).isEqualTo(10); + + assertThat(slice.lastIndexOfByte((byte) 'e', 3)).isEqualTo(-1); + assertThat(slice.lastIndexOfByte((byte) 'e', 4)).isEqualTo(4); + + assertThat(slice.lastIndexOfByte((byte) 'a', -1)).isEqualTo(-1); + + Slice single = utf8Slice("x"); + assertThat(single.lastIndexOfByte((byte) 'x', 0)).isEqualTo(0); + assertThat(single.lastIndexOfByte((byte) 'y', 0)).isEqualTo(-1); + + assertThat(EMPTY_SLICE.lastIndexOfByte((byte) 'a', 0)).isEqualTo(-1); + + Slice shortSlice = utf8Slice("abcdefg"); + assertThat(shortSlice.lastIndexOfByte((byte) 'a', 6)).isEqualTo(0); + assertThat(shortSlice.lastIndexOfByte((byte) 'g', 6)).isEqualTo(6); + assertThat(shortSlice.lastIndexOfByte((byte) 'd', 6)).isEqualTo(3); + + Slice eightBytes = utf8Slice("abcdefgh"); + assertThat(eightBytes.lastIndexOfByte((byte) 'a', 7)).isEqualTo(0); + assertThat(eightBytes.lastIndexOfByte((byte) 'h', 7)).isEqualTo(7); + assertThat(eightBytes.lastIndexOfByte((byte) 'd', 7)).isEqualTo(3); + assertThat(eightBytes.lastIndexOfByte((byte) 'a', 3)).isEqualTo(0); + + Slice nineBytes = utf8Slice("abcdefghi"); + assertThat(nineBytes.lastIndexOfByte((byte) 'a', 8)).isEqualTo(0); + assertThat(nineBytes.lastIndexOfByte((byte) 'i', 8)).isEqualTo(8); + assertThat(nineBytes.lastIndexOfByte((byte) 'b', 8)).isEqualTo(1); + + Slice sixteenBytes = utf8Slice("abcdefghijklmnop"); + assertThat(sixteenBytes.lastIndexOfByte((byte) 'a', 15)).isEqualTo(0); + assertThat(sixteenBytes.lastIndexOfByte((byte) 'p', 15)).isEqualTo(15); + assertThat(sixteenBytes.lastIndexOfByte((byte) 'i', 15)).isEqualTo(8); + assertThat(sixteenBytes.lastIndexOfByte((byte) 'i', 7)).isEqualTo(-1); + + Slice repeated = utf8Slice("aaaaaaaabbbbbbbbaaaaaaaa"); + assertThat(repeated.lastIndexOfByte((byte) 'a', 22)).isEqualTo(22); + assertThat(repeated.lastIndexOfByte((byte) 'a', 15)).isEqualTo(7); + assertThat(repeated.lastIndexOfByte((byte) 'a', 10)).isEqualTo(7); + assertThat(repeated.lastIndexOfByte((byte) 'a', 8)).isEqualTo(7); + assertThat(repeated.lastIndexOfByte((byte) 'b', 15)).isEqualTo(15); + assertThat(repeated.lastIndexOfByte((byte) 'b', 7)).isEqualTo(-1); + } + @Test public void testToByteBuffer() { From d0c5e0651e60f02ec32b61d532a884cf1b054c0c Mon Sep 17 00:00:00 2001 From: "Mateusz \"Serafin\" Gajewski" Date: Sat, 7 Mar 2026 18:08:22 +0100 Subject: [PATCH 2/2] Use modern main() syntax --- src/test/java/io/airlift/slice/BenchmarkHashCode.java | 2 +- src/test/java/io/airlift/slice/BenchmarkMurmur3Hash128.java | 2 +- src/test/java/io/airlift/slice/BenchmarkMurmur3Hash32.java | 2 +- src/test/java/io/airlift/slice/BenchmarkSlice.java | 2 +- src/test/java/io/airlift/slice/BenchmarkXxHash64.java | 2 +- src/test/java/io/airlift/slice/MemoryCopyBenchmark.java | 2 +- src/test/java/io/airlift/slice/SliceUtf8Benchmark.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/io/airlift/slice/BenchmarkHashCode.java b/src/test/java/io/airlift/slice/BenchmarkHashCode.java index 7f3bcccb..be1db54c 100644 --- a/src/test/java/io/airlift/slice/BenchmarkHashCode.java +++ b/src/test/java/io/airlift/slice/BenchmarkHashCode.java @@ -42,7 +42,7 @@ public long hash(BenchmarkData data, ByteCounter counter) return data.getSlice().hashCode(0, data.getSlice().length()); } - public static void main(String[] args) + static void main() throws RunnerException { Options options = new OptionsBuilder() diff --git a/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash128.java b/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash128.java index b7f04619..58f45d6c 100644 --- a/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash128.java +++ b/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash128.java @@ -71,7 +71,7 @@ public long hashLong(SingleLong data, ByteCounter counter) return Murmur3Hash128.hash64(data.getSlice(), 0, 8); } - public static void main(String[] args) + static void main() throws RunnerException { Options options = new OptionsBuilder() diff --git a/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash32.java b/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash32.java index 4a1278ad..50931afb 100644 --- a/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash32.java +++ b/src/test/java/io/airlift/slice/BenchmarkMurmur3Hash32.java @@ -78,7 +78,7 @@ public int hashLong(SingleLong data, ByteCounter counter) return Murmur3Hash32.hash(data.getSlice(), 0, 8); } - public static void main(String[] args) + static void main() throws RunnerException { Options options = new OptionsBuilder() diff --git a/src/test/java/io/airlift/slice/BenchmarkSlice.java b/src/test/java/io/airlift/slice/BenchmarkSlice.java index 22061c45..3810a53d 100644 --- a/src/test/java/io/airlift/slice/BenchmarkSlice.java +++ b/src/test/java/io/airlift/slice/BenchmarkSlice.java @@ -83,7 +83,7 @@ public void setup() } } - public static void main(String[] args) + static void main() throws Throwable { // assure the benchmarks are valid before running diff --git a/src/test/java/io/airlift/slice/BenchmarkXxHash64.java b/src/test/java/io/airlift/slice/BenchmarkXxHash64.java index f570332d..73ffa73f 100644 --- a/src/test/java/io/airlift/slice/BenchmarkXxHash64.java +++ b/src/test/java/io/airlift/slice/BenchmarkXxHash64.java @@ -49,7 +49,7 @@ public long specializedHashLong(SingleLong data, ByteCounter counter) return XxHash64.hash(data.getValue()); } - public static void main(String[] args) + static void main() throws RunnerException { Options options = new OptionsBuilder() diff --git a/src/test/java/io/airlift/slice/MemoryCopyBenchmark.java b/src/test/java/io/airlift/slice/MemoryCopyBenchmark.java index 33c7c5b0..d309b7da 100644 --- a/src/test/java/io/airlift/slice/MemoryCopyBenchmark.java +++ b/src/test/java/io/airlift/slice/MemoryCopyBenchmark.java @@ -112,7 +112,7 @@ public void doCopy(Slice data, long src, long dest, int length) public abstract void doCopy(Slice data, long src, long dest, int length); } - public static void main(String[] args) + static void main() throws RunnerException { Options options = new OptionsBuilder() diff --git a/src/test/java/io/airlift/slice/SliceUtf8Benchmark.java b/src/test/java/io/airlift/slice/SliceUtf8Benchmark.java index 8254e712..4e6ac28c 100644 --- a/src/test/java/io/airlift/slice/SliceUtf8Benchmark.java +++ b/src/test/java/io/airlift/slice/SliceUtf8Benchmark.java @@ -261,7 +261,7 @@ public Slice getBothWhitespace() } } - public static void main(String[] args) + static void main() throws RunnerException { Options options = new OptionsBuilder()