diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 42a6e8073576..1fd616315c8e 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -299,7 +299,7 @@ Bug Fixes Other --------------------- -(No changes) +* GITHUB#16279: Add JMH benchmarks comparing read I/O strategies under memory pressure (neoremind) ======================= Lucene 10.5.0 ======================= diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java new file mode 100644 index 000000000000..b44f16672d19 --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/AbstractReadIOBenchmark.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 org.apache.lucene.benchmark.jmh; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Locale; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +/** + * Abstract base for read I/O benchmarks. Provides shared FFI handles (pread, open, close, + * posix_madvise, fcntl), thread-local buffers, file/mmap setup, and configuration parsing. + */ +@State(Scope.Benchmark) +public abstract class AbstractReadIOBenchmark { + + protected static final long ALIGNMENT = 4096; + + /** Max read size for buffer pre-allocation. Actual read size is a @Param on subclasses. */ + protected static final int MAX_READ_SIZE = 1024 * 1024; // 1MB max + + protected static final long FILE_SIZE = + Long.parseLong(envOrProp("BENCH_FILE_SIZE_MB", "bench.fileSizeMB", "1024")) * 1024L * 1024L; + + protected static final String BENCH_FILE = + envOrProp("BENCH_FILE", "bench.file", "/tmp/pread-bench.dat"); + + protected static final boolean DROP_CACHES = + Boolean.parseBoolean(envOrProp("BENCH_DROP_CACHES", "bench.dropCaches", "false")); + + protected static final int MADV_NORMAL = 0; + protected static final int MADV_RANDOM = 1; + protected static final int MADV_WILLNEED = 3; + + private static final boolean IS_MAC = + System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); + private static final boolean IS_LINUX = + System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("linux"); + + // FFI handles + protected static final MethodHandle PREAD; + protected static final MethodHandle OPEN; + protected static final MethodHandle CLOSE; + protected static final MethodHandle POSIX_MADVISE; + protected static final MethodHandle FCNTL; + + static { + Linker linker = Linker.nativeLinker(); + SymbolLookup lookup = linker.defaultLookup(); + + PREAD = + linker.downcallHandle( + lookup.find("pread").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_LONG)); + + OPEN = + linker.downcallHandle( + lookup.find("open").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); + + CLOSE = + linker.downcallHandle( + lookup.find("close").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + + POSIX_MADVISE = + linker.downcallHandle( + lookup.find("posix_madvise").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT)); + + FCNTL = + linker.downcallHandle( + lookup.find("fcntl").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT)); + } + + /** Per-thread pre-allocated buffers sized to MAX_READ_SIZE. */ + @State(Scope.Thread) + public static class ThreadBuffers { + public ByteBuffer directBuf; + public ByteBuffer heapBuf; + public Arena ffiArena; + public MemorySegment ffiBuf; + public MemorySegment ffiDirectIoBuf; + + @Setup(Level.Trial) + public void setup() { + directBuf = ByteBuffer.allocateDirect(MAX_READ_SIZE); + heapBuf = ByteBuffer.allocate(MAX_READ_SIZE); + ffiArena = Arena.ofConfined(); + ffiBuf = ffiArena.allocate(MAX_READ_SIZE); + ffiDirectIoBuf = ffiArena.allocate(MAX_READ_SIZE, 4096); + } + + @TearDown(Level.Trial) + public void tearDown() { + ffiArena.close(); + } + } + + protected Path tempFile; + protected FileChannel fileChannel; + protected MemorySegment mmapSegmentNormal; + protected MemorySegment mmapSegmentMadvRandom; + protected int nativeFd; + protected int directIoFd; + protected Arena arena; + + protected String benchmarkName() { + return getClass().getSimpleName(); + } + + protected void validateReadSize(int readSize) { + if (readSize > MAX_READ_SIZE) { + throw new IllegalArgumentException( + "readSize (" + readSize + ") exceeds MAX_READ_SIZE (" + MAX_READ_SIZE + ")."); + } + } + + @Setup(Level.Trial) + public void setup() throws Exception { + tempFile = Path.of(BENCH_FILE); + if (!Files.exists(tempFile)) { + throw new IOException( + "Benchmark file not found: " + + tempFile + + "\nCreate it with: dd if=/dev/urandom of=" + + BENCH_FILE + + " bs=1M count=" + + (FILE_SIZE / (1024 * 1024))); + } + long size = Files.size(tempFile); + if (size < FILE_SIZE) { + throw new IOException( + "Benchmark file too small: " + + size + + " bytes, expected at least " + + FILE_SIZE + + "\nRecreate with: dd if=/dev/urandom of=" + + BENCH_FILE + + " bs=1M count=" + + (FILE_SIZE / (1024 * 1024))); + } + + fileChannel = FileChannel.open(tempFile, StandardOpenOption.READ); + + arena = Arena.ofShared(); + + mmapSegmentNormal = fileChannel.map(MapMode.READ_ONLY, 0, FILE_SIZE, arena); + + mmapSegmentMadvRandom = fileChannel.map(MapMode.READ_ONLY, 0, FILE_SIZE, arena); + try { + int rc = (int) POSIX_MADVISE.invokeExact(mmapSegmentMadvRandom, FILE_SIZE, MADV_RANDOM); + if (rc != 0) { + throw new IllegalStateException("WARNING: posix_madvise(MADV_RANDOM) returned " + rc); + } + } catch (Throwable t) { + throw new RuntimeException("posix_madvise(MADV_RANDOM) failed", t); + } + + MemorySegment pathStr = arena.allocateFrom(tempFile.toString()); + int O_RDONLY = 0; + try { + nativeFd = (int) OPEN.invokeExact(pathStr, O_RDONLY); + } catch (Throwable t) { + throw new RuntimeException("Failed to open file via FFI", t); + } + if (nativeFd < 0) { + throw new IOException("FFI open() returned " + nativeFd); + } + + // Direct I/O: Linux uses O_DIRECT, macOS uses fcntl(F_NOCACHE) + try { + if (IS_LINUX) { + int O_DIRECT = 0x4000; + directIoFd = (int) OPEN.invokeExact(pathStr, O_RDONLY | O_DIRECT); + } else if (IS_MAC) { + directIoFd = (int) OPEN.invokeExact(pathStr, O_RDONLY); + if (directIoFd >= 0) { + int F_NOCACHE = 48; + int rc = (int) FCNTL.invokeExact(directIoFd, F_NOCACHE, 1); + if (rc != 0) { + CLOSE.invokeExact(directIoFd); + throw new IllegalStateException("WARNING: fcntl(F_NOCACHE) failed"); + } + } + } else { + directIoFd = -1; + } + } catch (Throwable t) { + throw new RuntimeException("Failed to open file for direct I/O", t); + } + if (directIoFd < 0) { + directIoFd = -1; + } + } + + @TearDown(Level.Trial) + @SuppressWarnings({"restricted", "unused"}) + public void tearDown() throws Exception { + fileChannel.close(); + try { + int rc = (int) CLOSE.invokeExact(nativeFd); + if (directIoFd >= 0) { + rc = (int) CLOSE.invokeExact(directIoFd); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + arena.close(); + } + + @Setup(Level.Iteration) + public void setupIteration() throws IOException { + if (DROP_CACHES) { + dropPageCaches(); + } + } + + private static void dropPageCaches() throws IOException { + if (IS_MAC) { + Process purge = new ProcessBuilder("/usr/bin/sudo", "purge").inheritIO().start(); + try { + if (purge.waitFor() != 0) { + throw new IOException("purge failed with exit code " + purge.exitValue()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during purge", e); + } + } else { + Process sync = new ProcessBuilder("/usr/bin/sync").inheritIO().start(); + try { + if (sync.waitFor() != 0) { + throw new IOException("sync failed with exit code " + sync.exitValue()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during sync", e); + } + Process drop = + new ProcessBuilder( + "/usr/bin/sudo", "/usr/bin/bash", "-c", "echo 3 > /proc/sys/vm/drop_caches") + .inheritIO() + .start(); + try { + if (drop.waitFor() != 0) { + throw new IOException("Failed to drop page caches (exit code " + drop.exitValue() + ")."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during drop_caches", e); + } + } + } + + /** Reads a config value from env var first, then system property, then default. */ + protected static String envOrProp(String envKey, String propKey, String defaultValue) { + String env = System.getenv(envKey); + if (env != null && !env.isEmpty()) { + return env; + } + return System.getProperty(propKey, defaultValue); + } +} diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java new file mode 100644 index 000000000000..a7dafed80481 --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/RandomReadIOBenchmark.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 org.apache.lucene.benchmark.jmh; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** Benchmark comparing random read I/O strategies under varying concurrency and memory pressure. */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 3) +@Measurement(iterations = 3, time = 5) +@Fork( + value = 2, + jvmArgsPrepend = {"--enable-native-access=ALL-UNNAMED", "-Xms2g", "-Xmx2g"}) +public class RandomReadIOBenchmark extends AbstractReadIOBenchmark { + + @Param({"4096"}) + public int readSize; + + @Param({"16"}) + public int readsPerOp; + + private long maxOffset; + private long maxAlignedOffset; + + @Setup(Level.Trial) + public void validateParams() { + validateReadSize(readSize); + maxOffset = FILE_SIZE - readSize; + maxAlignedOffset = (maxOffset / ALIGNMENT) * ALIGNMENT; + } + + // ======== mmap NORMAL ======== + + @Benchmark + @Threads(1) + public void mmap_T01(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmap_T04(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmap_T08(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmap_T16(ThreadBuffers tb, Blackhole bh) { + doMmapNormalReads(tb, bh); + } + + // ======== mmap + MADV_RANDOM ======== + + @Benchmark + @Threads(1) + public void mmapMadvRandom_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapMadvRandom_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapMadvRandom_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapMadvRandom_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomReads(tb, bh); + } + + // ======== mmap NORMAL + batched MADV_WILLNEED ======== + + @Benchmark + @Threads(1) + public void mmapBatchedPrefetch_T01(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapBatchedPrefetch_T04(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapBatchedPrefetch_T08(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapBatchedPrefetch_T16(ThreadBuffers tb, Blackhole bh) { + doMmapBatchedPrefetch(tb, bh); + } + + // ======== mmap RANDOM + batched MADV_WILLNEED ======== + + @Benchmark + @Threads(1) + public void mmapMadvRandomBatchedPrefetch_T01(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); + } + + @Benchmark + @Threads(4) + public void mmapMadvRandomBatchedPrefetch_T04(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); + } + + @Benchmark + @Threads(8) + public void mmapMadvRandomBatchedPrefetch_T08(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); + } + + @Benchmark + @Threads(16) + public void mmapMadvRandomBatchedPrefetch_T16(ThreadBuffers tb, Blackhole bh) { + doMmapMadvRandomBatchedPrefetch(tb, bh); + } + + // ======== FFI pread ======== + + @Benchmark + @Threads(1) + public void ffiPread_T01(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void ffiPread_T04(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void ffiPread_T08(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void ffiPread_T16(ThreadBuffers tb, Blackhole bh) { + doFfiReads(tb, bh); + } + + // ======== FileChannel + DirectByteBuffer ======== + + @Benchmark + @Threads(1) + public void fileChannelDirectBuffer_T01(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(4) + public void fileChannelDirectBuffer_T04(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(8) + public void fileChannelDirectBuffer_T08(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + @Benchmark + @Threads(16) + public void fileChannelDirectBuffer_T16(ThreadBuffers tb, Blackhole bh) throws IOException { + doFileChannelDirectReads(tb, bh); + } + + // ======== FFI pread + O_DIRECT ======== + + // @Benchmark + @Threads(1) + public void ffiPreadDirectIO_T01(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + // @Benchmark + @Threads(4) + public void ffiPreadDirectIO_T04(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + // @Benchmark + @Threads(8) + public void ffiPreadDirectIO_T08(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + // @Benchmark + @Threads(16) + public void ffiPreadDirectIO_T16(ThreadBuffers tb, Blackhole bh) { + doFfiDirectIoReads(tb, bh); + } + + // ======== Implementation ======== + + private void doMmapNormalReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); + bh.consume(dst[0]); + } + } + + private void doMmapMadvRandomReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + MemorySegment.copy(mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); + bh.consume(dst[0]); + } + } + + @SuppressWarnings("unused") + private void doMmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + long[] offsets = new long[readsPerOp]; + try { + for (int i = 0; i < readsPerOp; i++) { + offsets[i] = rng.nextLong(maxOffset); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment slice = mmapSegmentNormal.asSlice(offsets[i], readSize); + int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) readSize, MADV_WILLNEED); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offsets[i], dst, 0, readSize); + bh.consume(dst[0]); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @SuppressWarnings("unused") + private void doMmapMadvRandomBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + byte[] dst = tb.heapBuf.array(); + long[] offsets = new long[readsPerOp]; + try { + for (int i = 0; i < readsPerOp; i++) { + offsets[i] = rng.nextLong(maxOffset); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment slice = mmapSegmentMadvRandom.asSlice(offsets[i], readSize); + int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) readSize, MADV_WILLNEED); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy( + mmapSegmentMadvRandom, ValueLayout.JAVA_BYTE, offsets[i], dst, 0, readSize); + bh.consume(dst[0]); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private void doFileChannelDirectReads(ThreadBuffers tb, Blackhole bh) throws IOException { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + ByteBuffer buf = tb.directBuf; + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + buf.clear().limit(readSize); + int n = fileChannel.read(buf, offset); + bh.consume(n); + } + } + + private void doFfiReads(ThreadBuffers tb, Blackhole bh) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + MemorySegment buf = tb.ffiBuf; + try { + for (int i = 0; i < readsPerOp; i++) { + long offset = rng.nextLong(maxOffset); + long n = (long) PREAD.invokeExact(nativeFd, buf, (long) readSize, offset); + bh.consume(n); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private void doFfiDirectIoReads(ThreadBuffers tb, Blackhole bh) { + if (directIoFd < 0) { + bh.consume(0); + return; + } + ThreadLocalRandom rng = ThreadLocalRandom.current(); + MemorySegment buf = tb.ffiDirectIoBuf; + try { + for (int i = 0; i < readsPerOp; i++) { + long offset = (rng.nextLong(maxAlignedOffset / ALIGNMENT)) * ALIGNMENT; + long n = (long) PREAD.invokeExact(directIoFd, buf, (long) readSize, offset); + bh.consume(n); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java new file mode 100644 index 000000000000..536e2dddb5cc --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/SequentialReadIOBenchmark.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 org.apache.lucene.benchmark.jmh; + +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmark comparing sequential (whole-file scan) I/O strategies. Single-threaded. Each op reads + * {@code readsPerOp} pages sequentially forward from the current file position, wrapping at EOF. + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 1, time = 2) +@Measurement(iterations = 1, time = 3) +@Fork( + value = 1, + jvmArgsPrepend = {"--enable-native-access=ALL-UNNAMED", "-Xms2g", "-Xmx2g"}) +@Threads(1) +public class SequentialReadIOBenchmark extends AbstractReadIOBenchmark { + + @Param({"4096"}) + public int readSize; + + @Param({"64"}) + public int readsPerOp; + + /** Sliding prefetch window size. */ + private static final long PREFETCH_WINDOW = 2 * 1024 * 1024; + + /** Current sequential scan position — advances across JMH invocations, wraps at EOF. */ + private long seqPosition = 0; + + // ======== mmap NORMAL ======== + + @Benchmark + public void mmap(ThreadBuffers tb, Blackhole bh) { + byte[] dst = tb.heapBuf.array(); + long offset = seqPosition; + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); + bh.consume(dst[0]); + offset += readSize; + if (offset >= FILE_SIZE) { + offset = 0; + } + } + seqPosition = offset; + } + + // ======== mmap NORMAL + sliding 2MB WILLNEED prefetch window ======== + + @Benchmark + @SuppressWarnings("unused") + public void mmapBatchedPrefetch(ThreadBuffers tb, Blackhole bh) { + byte[] dst = tb.heapBuf.array(); + long offset = seqPosition; + try { + long prefetchOff = offset + (long) readsPerOp * readSize; + if (prefetchOff + PREFETCH_WINDOW <= FILE_SIZE) { + MemorySegment slice = mmapSegmentNormal.asSlice(prefetchOff, PREFETCH_WINDOW); + int rc = (int) POSIX_MADVISE.invokeExact(slice, PREFETCH_WINDOW, MADV_WILLNEED); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + for (int i = 0; i < readsPerOp; i++) { + MemorySegment.copy(mmapSegmentNormal, ValueLayout.JAVA_BYTE, offset, dst, 0, readSize); + bh.consume(dst[0]); + offset += readSize; + if (offset >= FILE_SIZE) { + offset = 0; + } + } + seqPosition = offset; + } + + // ======== FFI pread ======== + + @Benchmark + public void ffiPread(ThreadBuffers tb, Blackhole bh) { + MemorySegment buf = tb.ffiBuf; + long offset = seqPosition; + try { + for (int i = 0; i < readsPerOp; i++) { + long n = (long) PREAD.invokeExact(nativeFd, buf, (long) readSize, offset); + bh.consume(n); + offset += readSize; + if (offset >= FILE_SIZE) { + offset = 0; + } + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + seqPosition = offset; + } + + // ======== FileChannel + DirectByteBuffer ======== + + @Benchmark + public void fileChannelDirectBuffer(ThreadBuffers tb, Blackhole bh) throws IOException { + ByteBuffer buf = tb.directBuf; + long offset = seqPosition; + for (int i = 0; i < readsPerOp; i++) { + buf.clear().limit(readSize); + int n = fileChannel.read(buf, offset); + bh.consume(n); + offset += readSize; + if (offset >= FILE_SIZE) offset = 0; + } + seqPosition = offset; + } + + // ======== FFI pread + O_DIRECT ======== + + @Benchmark + public void ffiPreadDirectIO(ThreadBuffers tb, Blackhole bh) { + if (directIoFd < 0) { + bh.consume(0); + return; + } + MemorySegment buf = tb.ffiDirectIoBuf; + long offset = (seqPosition / ALIGNMENT) * ALIGNMENT; + try { + for (int i = 0; i < readsPerOp; i++) { + long n = (long) PREAD.invokeExact(directIoFd, buf, (long) readSize, offset); + bh.consume(n); + offset += readSize; + if (offset >= FILE_SIZE) offset = 0; + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + seqPosition = offset; + } +}