Add JMH benchmarks comparing read I/O strategies under memory pressure#16279
Add JMH benchmarks comparing read I/O strategies under memory pressure#16279neoremind wants to merge 13 commits into
Conversation
Refactor benchmarks
| MemorySegment slice = mmapSegmentNormal.asSlice(offsets[i], readSize); | ||
| int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) readSize, MADV_WILLNEED); |
There was a problem hiding this comment.
madvise needs a page-aligned start address, so passing the raw random offset here makes it return EINVAL and do nothing — and since rc is discarded, it fails silently. On a c6id.4xlarge strace shows ~5117/5181 of these calls returning -1 EINVAL, so the prefetch never actually runs and the prefetch rows end up identical to plain mmap. The real Directory avoids this because MemorySegmentIndexInput#advise rounds the start down to the page first.
Suggested fix (mirrors what the Directory does):
| MemorySegment slice = mmapSegmentNormal.asSlice(offsets[i], readSize); | |
| int rc = (int) POSIX_MADVISE.invokeExact(slice, (long) readSize, MADV_WILLNEED); | |
| // madvise needs a page-aligned start address, otherwise it returns EINVAL and is a no-op. | |
| long offsetInPage = (mmapSegmentNormal.address() + offsets[i]) % ALIGNMENT; | |
| long aoff = offsets[i] - offsetInPage; | |
| long alen = readSize + offsetInPage; | |
| MemorySegment slice = mmapSegmentNormal.asSlice(aoff, alen); | |
| int rc = (int) POSIX_MADVISE.invokeExact(slice, alen, MADV_WILLNEED); | |
| assert rc == 0 : "posix_madvise failed: " + rc; |
Same change is needed in doMmapMadvRandomBatchedPrefetch (against mmapSegmentMadvRandom). With this, single-threaded mmap+prefetch goes from ~0.15 → ~4.2 ops/ms cold (≈7× pread at T01, ~device saturation). Might be worth asserting on rc at the other madvise call sites too so this can't silently regress again.
|
I love that we are building tooling for benchmarking cold/warm cases -- they are so tricky to properly test because at a "whole search system" level you should replay query traffic with accurate actual production arrival times (ideally, or simulate w/ Poisson process) for real measures. I'd love to see Lucene eventually be able to saturate modern NVMe SSDs when executing a single query on all CPU cores on a larger-than-RAM index but I don't think we are there yet (we don't have enough async/concurrent/prefetch IO? and we don't strongly separate dependent IO into their own paths so non-dependent IOPs don't block one another). I also want to test if Lucene is anywhere near the linux kernel bottleneck limit on page faults / sec. This is a potential risk we face with memory-mapped IO (described here, but I think there's been good progress in very modern kernels, something about maple trees -- aha, here!). I think this benchmark should be able to tease out that bottleneck by comparing mmap IO vs NIO cold concurrent IOPs. How do you simulate the memory pressure? I see you drop OS's IO cache (at the start of each run?). Is it just that the user is expected to test on a large enough file exceeding their free RAM. You could also use @rmuir's awesome Have you tried comparing this benchy to other IO benchy tools e.g. |
|
I ran the benchmark across three different hardware profiles to get quantitative measurements of how different I/O strategies work under different memory pressure. Here are the results. Platforms
The latency and throughput numbers are calculated via Benchmark Setup16k reads (4 pages) x 16 reads/op = 256KB per op. Throughput (MB/s) = ops/ms × 256k x 1000. For example, with O_DIRECT on c6id.4xlarge (Linux NVME SSD), JMH at 16 threads yields 4.9 ops/ms → I removed the O_DIRECT results from the table charts below to focus on mmap (with Random Read ResultsCASE 1: Fully warm (all data in page cache)16G file fits in RAM, pre-warmed with
Detailed JMH results (ops/ms, higher is better)Mac M3 Pro (36G RAM, unified memory, Apple SSD)
Linux c5.4xlarge (16 vCPU, 32G RAM, EBS io2 20K IOPS)
Linux c6id.4xlarge (16 vCPU, 32G RAM, local NVME SSD)
CASE 2: Memory pressure (working set close to RAM)32G file barely exceeds RAM, pre-warmed.
Detailed JMH results (ops/ms, higher is better)Mac M3 Pro (36G RAM, unified memory, Apple SSD)
Linux c5.4xlarge (16 vCPU, 32G RAM, EBS io2 20K IOPS)
Linux c6id.4xlarge (16 vCPU, 32G RAM, local NVME SSD)
CASE 3: Many cold reads (~50% cold reads)64G file (2x available RAM), ~50% cold reads, pre-warmed.
Detailed JMH results (ops/ms, higher is better)Mac M3 Pro (36G RAM, unified memory, Apple SSD)
Linux c5.4xlarge (16 vCPU, 32G RAM, EBS io2 20K IOPS)
Linux c6id.4xlarge (16 vCPU, 32G RAM, local NVME SSD)
CASE 4: Almost all cold reads64G file (2x available RAM), clear page cache before each iteration, cold start.
Detailed JMH results (ops/ms, higher is better)Mac M3 Pro (36G RAM, unified memory, Apple SSD)
Linux c5.4xlarge (16 vCPU, 32G RAM, EBS io2 20K IOPS)
Linux c6id.4xlarge (16 vCPU, 32G RAM, local NVME SSD)
Sequential Read Results64G file, page cache dropped before each iteration. 128 sequential reads per op at varying read sizes (16KB, 64KB, 128KB). Look at Linux, mmap (NORMAL) with default kernel readahead performs well across the board for sequential access. mmap (RANDOM) + batchedPrefetch is as good as mmap (NORMAL) with readahead, again, the power-of-two backoff diminishes the effect.
Detailed JMH results (ops/ms, higher is better)Mac M3 Pro (36G RAM, unified memory, Apple SSD)
Linux c5.4xlarge (16 vCPU, 32G RAM, EBS io2 20K IOPS)
Linux c6id.4xlarge (16 vCPU, 32G RAM, local NVME SSD)
fio vs. JMH benchmarksI cross-validated against fio on c6id.4xlarge (NVME). The JMH numbers match fio within about 4% overhead (like JVM indirections, jmh blackhole?):
Command used
Command used
Command used
Command used |
|
Thank you @mikemccand! I've updated the benchmark results and analysis above, please see my comments inline below.
I fully agree on the vision. I think it's genuinely possible to saturate I/O as long as we interleave computation and I/O with parallelization. I've practiced this strategy before, during the 1st PolarDB database performance competition in 2018, I implemented a KV engine saturating an Intel Optane SSD with 2.2 GB/s write and 2.5 GB/s random read (see write-up). I also built a fast InnoDB checksum tool in C++ that fully exploits hardware by separating I/O from computation, using big-block I/O to amortize kernel overhead, and making full use of multi-core. But indeed, these are either contest simplified workloads or easy utilities, Lucene's query path is much much more complex. One thing I learned from @jimczi is how
Thanks for sharing these. The per-VMA lock optimization should be available after Linux 6.4, but my EC2 hosts are still running old kernels. I will give it try once available. With the data shows above, mmap is the most powerful one when data is hot in RAM, but pread/NIOFS outperforms plain mmap under memory pressure with some degree of cold load (case 2: file size nearly RAM, case 3: 50% cold reads, case 4: almost all cold). On NVME at T16 in the all-cold case: plain mmap can only do 0.94 ops/ms while pread reaches 4.65 ops/ms, however, mmap (RANDOM) + batched WILLNEED prefetch (simulate QD=16) goes to 6.64 ops/ms, beating both pread and NIOFS. This somehow shows that I could hit the mmap bottleneck (page table contention, TLB shootdowns, mmap lock contention) on the cold path, and batched prefetch makes mmap more performant by pipelining I/O and computation.
I use files that go beyond available RAM, 32G/64G file on a 32G host. Just for the "almost all cold" case 4, I drop page cache before each JMH iteration, others no, just let program contended on memory. The dropping page cache is a switch as param. I also tried using cgroup to limit memory, but ran into a tricky issue, dropping page cache within the cgroup doesn't fully remove pages from host memory, anyway, I didn't go deeper the why. So, I ended up using a host with less RAM instead. I think @rmuir's
Yes, please see the fio section in the results above. In short: JMH numbers match fio with tiny overhead. |















Adds JMH benchmarks to compare read I/O strategies in memory constrained scenario, related to #16044.
I/O strategies tested:
NIOFSDirectory)Thread counts: 1, 4, 8, 16.
How to run