Add pod_vector and mmap zero-copy support to generic_loader#11
Add pod_vector and mmap zero-copy support to generic_loader#11adamant-pwn wants to merge 1 commit intojermp:masterfrom
Conversation
|
Disclaimer: As you may guess, most of the changes, experiments, and the huge description above are mostly authored by @copilot (mostly Claude Opus 4.6 model) with my close supervision, so I tried my best to make sure it's informative and useful. Note: This PR (and its companion in You might want to experiment a bit with directly using mmap-based load in SSHash binary itself (I assume, tools/common.hpp is appropriate place for this, or maybe adding some params to |
|
Nice, thanks Oleksandr! This is actually something that I wanted to do sooner or later; thanks for pushing it. I'll review the changes soon. Now running into a lecture :) |
|
Hi @adamant-pwn, I've read the code. Thanks again! Actually, I don't like the fact that It would be better to rewrite a class What you do think? |
|
Hi Giulio! The way I also don't really like keeping If by explicit But if you really don't want to have them, and want a more systematic solution, Metagraph uses int_vector.hpp from sdsl that just stores On a side note: C++20 has |
|
I thought about it a bit more. I think, The only question then is, do we want to do all the construction in |
…with optional ownership owning_span<T> uses shared_ptr<const T[]> with aliasing constructor for zero-branch const T* access under three ownership models: 1. Heap-owned: constructed from any contiguous range (vector, etc.) 2. Externally-owned: raw pointer + shared_ptr owner (e.g. mmap) 3. Unowned: raw pointer without owner Changes to generic_loader: - mmap path: zero-copy view into mapped memory (POD types only) - non-mmap path: always reads into std::vector<T> then moves into Vec - Unified visit_seq handles both owning_span and std::vector
06ddc41 to
2ad3851
Compare
|
I updated the PR (and also jermp/bits#12) with a version of Will also check if it still works soon 😅 |
|
Hi Oleksandr, indeed my idea was to have a private member Regarding the construction -- yes, I was think exactly that: to use a |
|
Yes, I also think current version is pretty good!
Sorry, I probably misunderstood your suggestion at first then 🙂 |
|
No worries! I'll test the code tomorrow probably. (Did you make some test?) Thanks a lot! |
|
Not for |
|
Beautiful! |
Summary
This PR adds
pod_vector<T>, a dual-mode vector for POD types, and extendsgeneric_loaderwith an optional mmap zero-copy deserialization path. Together, these allow applications that serialize data structures built onessentialsto skip heap allocation andmemcpyduring loading by pointing directly into a memory-mapped file.No behavioral change for existing users —
pod_vectoris a drop-in replacement forstd::vectorwhen used in the default owned mode.Motivation
In Metagraph, SSHash graphs are serialized using the
essentialsvisitor framework. For large indices (tens to hundreds of GB), deserialization is dominated by allocating vectors and copying data from disk into heap memory. By switching serialized members topod_vectorand usinggeneric_loader::set_mmap(), the loader can set up non-owning views into a memory-mapped file instead, eliminating allocation and copy overhead entirely.Benchmark results
We benchmarked
metagraph query(on the graphs constructed with SSHash as underlying representation) on a production-scale index to measure end-to-end impact. The times below reflect the full Metagraph query pipeline, not SSHash in isolation.Setup:
metagraph query -p 4(i.e. only 4 cores were actually utilized)Column definitions: Load = graph deserialization; Map = k-mer lookups into graph; Anno = BRWT row_diff annotation slice; Batch = Map + Anno + misc (timed query work); Total = wall clock including load.
Warm vs cold cache: "mmap cold" means the file was not in the OS page cache before the run (
echo 3 > /proc/sys/vm/drop_caches). Pages are faulted in on demand during Map/Anno, so load is near-instant but batch work is slower. "mmap warm" means the file was already fully cached (e.g. from a prior run orcat > /dev/null), so page accesses hit RAM with no I/O stalls.Key takeaways:
RAM loading vs mmap — when each wins:
The benefit depends on the ratio of loading time to query work. With mmap, the graph is not fully resident at load time — pages are faulted in lazily as they are touched. For reasonably small queries (like the 5.6 MB FASTQ above), loading dominates the wall clock, so mmap wins decisively. For very large query batches that touch most of the graph repeatedly, the per-access overhead of page faults (cold) or kernel-mediated page table lookups (warm) can make the query phase itself slower than with a fully-materialized in-RAM copy. In the extreme case, SSHash warm mmap batch time (19.5 s) is comparable to no_mmap (19.0 s), and for cold mmap the batch time increases to 47.2 s. So mmap is most beneficial when many short queries are served from a long-lived process (warm cache, near-zero startup) or when fast startup matters more than peak throughput on a single giant batch. Note also that these benchmarks were run on NVMe SSD, where random page fault latency is low (~100 µs). On spinning disks (HDD) or network-attached storage, cold mmap performance would be substantially worse due to seek times, and RAM loading may be preferable unless the cache is warm.
Changes
pod_vector<T>(new)A dual-mode vector that operates in either:
std::vector<T>, full mutable API, drop-in replacementconst T*pointer + size into externally managed memory (e.g., mmap'd region), with an optionalstd::shared_ptr<const void>owner to keep the backing memory aliveKey methods:
set_view(data, n, owner)— switch to view modeclear()— resets to empty owned modeswap()— works across modes and withstd::vector<T>is_pod_vector<T>/is_pod_vector_v<T>(new)Type trait for compile-time detection of
pod_vectortypes, used to enable the mmap fast path.generic_loader::set_mmap()(new)Call before
visit()to enable zero-copy loading. When set,visit(pod_vector<T>&)computes the file offset from the stream position and callsset_view()pointing directly into the mmap'd region, then seeks the stream past the data. Non-pod_vector members (plain PODs,std::vector) continue using the normal read path.generic_saver/sizerrefactoringstd::vectorandpod_vectorvisit overloads into a single privatevisit_seq()template in each ofgeneric_loader,generic_saver, andsizersave_vec()function (its logic is now ingeneric_saver::visit_seq())contiguous_memory_allocatorvisit(pod_vector<T>&)overload sopod_vectormembers can be loaded via the contiguous allocator path as wellUsage example