Repository navigation
fix/perf(store): output matches in-memory, 12x faster when RAM is tight, 67x less disk writing, 3.2% less CPU - #943
Conversation
…rites Sorting output objects scattered writes into the mmap'd store files, so the kernel rewrote the same dirty pages over and over. Germany with an 8 GB limit: 69+ min (1.3 TB written) -> 5:36 (20 GB written), identical tile content. Scatter into the existing heap buffer and copy back sequentially instead. Also release store file handles after mapping, so many-threaded --store runs no longer abort with 'Failed to open mmap file'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gain: callgrind on Luxembourg --store drops from 125.7 to 121.7 G instructions (-3.2%); output identical (all 273,080 Germany tiles). ShardedNodeStore didn't override the batched at(ids, n, out) from systemed#939, so --store looked up every node of every way on its own plus a contains() check, the top entry in a --store profile. Now the whole batch is looked up in the shard of its first node, with per-node lookups only for ways crossing a shard boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thank you! I’ll run a full planet this weekend and report back. |
…re output now matches in-memory) With shards, a relation was read only in the pass for the shard of its first member way; if that way wasn't in the extract, every pass skipped it. Germany --store vs in-memory: 119 missing tiles and ~5,700 missing features (Szczecin Lagoon, Usedom, border nature reserves) -> 0 differences in 273,199 tiles. Decide the shard by the first member way that is in the store; relations with no stored member ways are read in the first pass. Bug predates systemed#939. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
I re-ran the benchmarks on bare-metal Linux (96 threads, 503 GB RAM, NVMe RAID1, Ubuntu 22.04), Germany extract, with Correctness: confirmed. Master CPU: confirmed, slightly smaller than I measured. 3 interleaved pairs, Memory-pressure speed-up: does not reproduce here.
No thrashing or write blow-up on master at 8 GB (single runs, so the 6 GB rows are within noise). So the 69+ min / 1.3 TB numbers in the description look specific to my WSL2 setup (possibly its page cache / VHDX writeback) rather than something master does on Linux in general. Didn't hit the open-files crash on Germany at 96 threads with Planet run in progress, will follow up. |
|
Planet follow-up (same machine and setup as above: 96 threads, Open-files crash: confirmed, and it hits the planet at default settings. With the default The store reaches 1,639 files on the planet, so anyone running a planet build with Speed: no difference. With the same limit (
(The PR's first planet run, at Output: equivalent. Both have exactly 346,003,119 tiles with identical per-zoom counts. In a random sample of 200,000 tiles: 183,749 byte-identical, 15,978 with the same features in a different order, 273 differing only in Summary: the correctness fix (extracts) and the open-files fix (planet on many cores) both check out; planet speed is unchanged; the Germany CPU saving is ~2.4%; the low-memory speedup I measured under WSL2 didn't reproduce on bare-metal Linux. |
|
Thank you! Together with #956 this takes the planet down from 5hr11 to 4hr54 on my setup and it looks like the gain will be really noticeable on more constrained machines. |
--storeruns that hung for over an hour now finish in about 5 minutes.Gains at a glance
--storerun (callgrind, Luxembourg). Node lookups now use the batched fast path from perf: 3.4x faster planet build (2h10 -> 39 min), less RAM, deterministic output #939.--storeused to abort withFailed to open mmap fileonce there were more than ~1000 store files.--storeoutput now matches in-memory output exactly. Before,--storesilently dropped multipolygon relations that cross the extract boundary. On Germany that meant 119 missing tiles and ~5,700 missing features: the Szczecin Lagoon, Usedom, and border nature reserves such as Pommersche Bucht, Gelderse Poort and Dinkelland. Now all 273,199 tiles match the in-memory run. This bug predates perf: 3.4x faster planet build (2h10 -> 39 min), less RAM, deterministic output #939.--storeFollow-up to #939, aimed at
--storeon machines where the store doesn't fit comfortably in RAM. This is the IO-bound case where #939 gave only 5h28 → 5h11 on the planet.Problem
#939 sorts the output objects of each z6 tile with a counting sort. With
--store, those objects live in the mmap'd store files, and the sort's final pass scattered them directly into their target slots: tens of thousands of interleaved write streams across the store. Once the page cache is under pressure, the kernel has to write the same dirty pages back to disk over and over.Measured on Germany (4.9 GB PBF, 7.1 GB store, 22 threads,
--store, memory capped with a cgroup limit):Fix
tile_sorting.cpp). The sort now counts keys straight from the objects, scatters copies into the temporary heap buffer it already allocated, then copies the buffer back in order. Each store page is written once, sequentially. Memory use and the resulting order are unchanged.mmap_allocator.cpp). The mapped region stays valid without the handle. Before this, a large--storerun with many threads, which uses one ~1 GB file per thread and more as they fill, could exceed the default open-files limit of 1024 and abort withFailed to open mmap file.ShardedNodeStore(sharded_node_store.cpp), which--storeuses. It didn't override the batchedat(ids, n, out)from perf: 3.4x faster planet build (2h10 -> 39 min), less RAM, deterministic output #939, so every node of every way was looked up on its own, plus an extracontains()check. That madeSortedNodeStore::containsthe top entry in a--storeprofile (3.6%). Now the whole way is looked up as one batch in the shard of its first node. Only ways that cross a shard boundary fall back to per-node lookups.pbf_processor.cpp). With shards, a relation was read only in the pass for the shard of its first member way. If that way wasn't in the extract, every pass skipped the relation. Now the shard is decided by the first member way that is in the store. A relation with no stored member ways is read once, in the first pass.Verification
Setup: laptop (Core Ultra 7 165H, 22 threads, NVMe under WSL2), Germany 4.9 GB PBF, store 7.1 GB, memory capped with a cgroup limit.
Output: all 273,080 Germany tiles are semantically identical between before and after, and between the 8 GB and 12 GB runs. Monaco and Luxembourg match the reference in all config variants tested.
Border relations: Germany with
--storevs in memory. Before: 119 tiles missing and 4,421 tiles with different content. After: 0 missing and 0 differing out of 273,199.Batched lookups: callgrind on Luxembourg with
--storedrops from 125.7 to 121.7 billion instructions (−3.2%). Germany output is again identical (0 of 273,080 tiles differ).The open-files fix was checked by running with
ulimit -n 48: before, tilemaker aborts with "Too many open files"; after, it completes.Notes
--storemode, output is not byte-identical between runs. Tile content is identical, but the feature order within a tile can differ. That was already the case before this PR. The determinism claim in perf: 3.4x faster planet build (2h10 -> 39 min), less RAM, deterministic output #939 holds for tile content in this mode, not for the bytes.--storestill thrashes. At that point the node and way stores themselves (4.8 GB) no longer fit, and tile generation reads them in random order. Fixing that would need a spatially ordered store, which is out of scope here.--storesetup would be very welcome.🤖 Generated with Claude Code