john-mlika opened a new pull request, #16619:
URL: https://github.com/apache/lucene/pull/16619

   Creating a slice of a `DirectIOIndexInput` currently costs a device read. 
`slice()` builds the new
   input and then calls `seekInternal(0L)`, which fills the buffer from the 
slice's first block. With
   `O_DIRECT` that is a round-trip to the device, every time, whether or not 
anything is ever read.
   Every other `IndexInput` makes slicing free and reads on first use.
   
   That matters for callers that create a view and throw it away. 
`KnnFloatVectorQuery#approximateSearch`
   asks for the float vector values only to null-check them and read `size()`; 
obtaining them slices
   the raw vector data, so each query pays one wasted read per segment whenever 
a `DirectIODirectory`
   subclass serves searches through `useDirectIO`.
   
   ## How this was noticed
   
   On an Elasticsearch node serving kNN queries from an index whose raw vectors 
are read through a
   `DirectIODirectory` subclass, on a cloud disk with a provisioned IOPS 
ceiling. Every query issued
   small O_DIRECT reads that nothing consumed: this one, once per segment, next 
to a prefetch of the
   same kind in the `IndexedDISI` path (addressed separately). Because O_DIRECT 
bypasses the page
   cache, each of them is an IOPS every time, not a cache hit, and together 
they exceeded the disk's
   allowance. Past that ceiling the device holds throughput flat and queues — 
on that disk class an
   8 KiB read goes from 1.5 ms at queue depth 8 to 26 ms at depth 128 — so 
every query, including the
   ones that needed no I/O at all, waited behind reads that were then thrown 
away. A JFR file-read
   profile placed every one of this PR's reads at the slice construction inside
   `Lucene99FlatVectorsReader#getFloatVectorValues`, one per query per segment. 
With this change they
   are gone: on a single-segment test index, 40 queries went from 40 reads of 
the vector file to none.
   
   ## The change
   
   Instead of reading at `slice()`, the slice remembers where it starts and 
reads on first use.
   Concretely, the constructor records the offset within the first block in a 
new field,
   `pendingDelta`, and the first `refill()` uses it to position the buffer. 
Three details keep the
   behaviour identical to today, quirks included:
   
   - `seekInternal` clears the remembered offset before it repositions, so an 
explicit seek checks
     end-of-file against its own target rather than against the slice start.
   - The first fill does exactly what the construction-time fill used to do. In 
the one case where that
     leaves nothing to read — a zero-length slice at the very end of the file — 
it falls through to the
     caller's own read, which throws `EOFException` as before rather than 
`BufferUnderflowException`.
   - `seek()` on a slice that has not been read yet fills first and only then 
checks whether the target
     is inside the buffer, so a seek past the slice's length that still lands 
in that first block
     repositions silently, exactly as it does today.
   
   A caller that does read the slice pays the same single read, just at first 
use.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to