Repository navigation
Preallocate entries slice in DeserializeEntries to reduce allocations - #285
Conversation
DeserializeEntries reads the entry count from the directory stream and then builds the result with make([]EntryV3, 0) plus append. Because the slice starts at zero capacity, append reallocates and copies the backing array as it grows - 27 times for a full-planet leaf directory (~62k entries) - and each discarded intermediate array is transient garbage. The exact entry count is already known before the append loops, so size the slice capacity to it up front. The decode logic is otherwise unchanged and the returned entries are identical. Decoding a 62,746-entry leaf (gzip), allocations measured with -benchmem: before: 8,849,368 B/op 216 allocs/op after: 1,579,005 B/op 190 allocs/op (-82% bytes, -26 allocs)
|
Have you measured if this makes a material difference? |
|
Hey Brandon, yes I did! By preallocating the slice to the known entry count instead of growing it, this eliminates the 27 reallocations it needs deserialize a 62,746-entry leaf directory page. This made our code reduce wasted allocation of memory from 8+ MB to 1.5 MB for deserializing a leaf. Since we know the slice size ahead of time (i.e. how large a leaf directory is), we can pre-allocate this memory ahead of time for optimization. Since we run this Here's a code snippet you can run to validate this memory material difference:
|
|
Released here: https://github.com/protomaps/go-pmtiles/releases/tag/v1.31.1 Thanks! |
|
That's awesome, thanks Brandon! |
Summary
DeserializeEntriesreads the entry count from the directory stream, then builds the result withmake([]EntryV3, 0)+append. Since the slice starts at zero capacity,appendreallocates and copies the backing array as it grows. For a full-planet leaf directory (~62k entries) that's 27 reallocations, each discarding the previous backing array as transient garbage.The exact entry count is already known before the append loops, so this PR sizes the slice capacity to it up front:
make([]EntryV3, 0, numEntries). The decode logic is otherwise unchanged.Why it's safe
[]EntryV3has the same elements, order, andlen(). The slice still starts at length 0, so a truncated stream behaves exactly as before.cap()or appends into the returned slice. All call sites (server.go,show.go,extract.go,IterateEntries) consume the result read-only; the onlyappend(extract.go) appends the result into another slice.numEntries. The value already bounds the existing append loop, so this introduces no new assumption about untrusted input.Benchmark
Decoding a realistic 62,746-entry leaf directory (gzip), allocations measured with
-benchmem:~82% fewer bytes allocated per decode, 26 fewer allocations, with no change in decode time. The win scales with directory size and matters most for servers that decode leaf directories on the hot path for scattered tile requests.