Repository navigation
Pre-computed encoded size for buffer pre-allocation #67
SeanTAllen
started this conversation in
Research
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
The problem
When encoding a msgpack message, the
Writerstarts with a small internal buffer and grows it as data is appended. Each time the buffer's capacity is exceeded, the runtime allocates a larger array and copies the existing contents into it. For small messages this cost is negligible — a few resizes of a sub-KB buffer. For large messages with many values, the cumulative cost of repeated allocations and copies becomes significant.The msgpack wire format makes encoded sizes fully deterministic. Given a value (for integers) or a byte length (for strings, binary, extensions), the exact number of bytes the encoder will write is known before encoding begins. This means the total encoded size of a message can be computed in advance, allowing the caller to pre-allocate the writer's buffer — reducing or eliminating intermediate reallocations.
Where it matters
The benefit scales with message size and complexity:
The trade-off
Pre-computing the size requires the caller to traverse their data twice — once to compute the total size, once to encode. For simple flat messages this is trivial. For deeply nested or dynamically constructed messages, the double traversal adds CPU work. Whether this is a net win depends on the workload: if the avoided reallocations (which involve both allocation and memcpy) outweigh the size computation pass, pre-allocation wins.
This is explicitly opt-in. Callers who don't compute sizes get exactly the behavior they have today — dynamic buffer growth with no API changes.
How other libraries handle this
tinylib/msgp (Go)
The
msgpcode generator produces aMsgsize()method on every Go type that implements theSizerinterface.Msgsize()returns the worst-case encoded size of the value. The library provides constants for fixed-size types and prefix sizes for variable-length types:Callers use
Msgsize()to pre-allocate buffers viamake([]byte, 0, msg.Msgsize()). The constants are worst-case rather than exact — a uint that fits in a fixint still reportsInt64Size(9 bytes). This simplifies the API at the cost of slight over-allocation.General pattern
Most msgpack libraries (Python, Ruby, JS, C#, Java) do not provide size pre-computation. They rely on dynamic buffer growth internally. The Go library is the notable exception, driven by Go's performance culture and the code generation approach that makes per-type
Msgsize()methods free to maintain.Design
A new primitive:
MessagePackSizeA stateless primitive that mirrors
MessagePackEncoder's compact methods. Each method computes the exact byte count that the corresponding encoder method would write, without actually writing anything.Why a separate primitive
The encoder already has ~46 methods. Adding a parallel set of size methods would double its surface and mix two concerns — writing bytes and computing byte counts. A separate
MessagePackSizeprimitive keeps each focused:MessagePackEncoderwrites bytes to aWriter.MessagePackSizecomputes how many bytes would be written.The two are companions, not competitors. Callers who want pre-allocation use both; callers who don't use only the encoder, as they do today.
Exact sizes, not worst-case
Unlike Go's
tinylib/msgpwhich uses worst-case constants,MessagePackSizecomputes exact sizes. The branching logic mirrors the encoder's format selection — same thresholds, same conditions. This means the computed size is always exactly the number of bytes the encoder will write, with zero over-allocation.The cost is that the caller must pass values (for integers) or data lengths (for strings, binary, extensions) to the size methods, rather than using constant values. This is the right trade-off for a library where the caller controls encoding directly.
Variable-length methods take lengths, not data
The encoder's
str,bin, andextmethods take the actual data (ByteSeq) because they need to write it. The size methods only need the byte length to determine the wire format and compute the total size. TakingUSizeinstead ofByteSeqis more flexible — the caller might know the length before the data is constructed.Container sizes are header-only
arrayandmapreturn only the header size, not the total size of the container. The library writes only headers for containers — the caller writes each element individually. The same split applies to sizes: the caller computes element sizes individually and sums them with the header size. See the usage examples below.Error cases mirror the encoder
Methods that can error on the encoder (
str,bin,ext,timestamp) can also error on the size primitive, for the same reasons — data too large, or invalid timestamp values. Methods that are infallible on the encoder (uint,int,array,map,nil,bool,float_32,float_64) are also infallible on the size primitive.Format-specific sizes
For callers using format-specific encoding methods, the sizes are trivially derivable from the msgpack spec:
nilpositive_fixintnegative_fixintuint_8/int_8uint_16/int_16uint_32/int_32/float_32uint_64/int_64/float_64fixstr(len)str_8(len)/bin_8(len)str_16(len)/bin_16(len)str_32(len)/bin_32(len)fixarray/fixmaparray_16/map_16array_32/map_32fixext_1fixext_2fixext_4fixext_8fixext_16ext_8(len)ext_16(len)ext_32(len)timestamp_32timestamp_64timestamp_96Since these are constants (or constant + data length), format-specific size methods would add API surface with minimal value. A table in the documentation (like the one above) may be sufficient. Whether to also expose them as methods on
MessagePackSizeis an implementation decision that can be deferred.Documentation and usage guidance
Pre-allocation is a performance optimization. The documentation should make clear when it helps, when it doesn't, and how to use it correctly.
When to use pre-allocation
Pre-allocation helps most when:
When to skip it
Pre-allocation adds complexity with little benefit when:
Usage pattern
The flow is: compute total size, reserve buffer, encode.
Without pre-allocation, the code is just step 3 — the encoding calls. Pre-allocation adds step 1 (a parallel traversal computing sizes) and step 2 (a single
reserve_currentcall). The encoding itself is unchanged.How
Writer.reserve_currentworksWriter.reserve_current(n)pre-allocates space in the writer's current internal array. This is the buffer that accumulates format bytes, length prefixes, integer values, and small payloads (strings and binary data up to 64 bytes). For payloads larger than 64 bytes,Writer.write()pushes the data directly to the writer's chunk list as a separate entry — these large payloads are not affected byreserve_current.This means pre-allocation is most effective for messages composed of many small-to-medium values (integers, short strings, headers, small binary data). For messages dominated by a few large payloads, the large payloads bypass the internal buffer regardless, and the pre-allocation benefit is limited to the surrounding headers and metadata.
Container sizing
Container sizes deserve explicit attention in documentation because they're the most common source of confusion. The
arrayandmapsize methods return the header size only — they don't include the sizes of contained elements. The caller must sum the element sizes separately:This mirrors how encoding works —
MessagePackEncoder.arraywrites only the header, and the caller writes each element. The size computation follows the same structure.Helper pattern for application types
For applications that encode the same type repeatedly, a natural pattern is to write a size method alongside the encode method:
This keeps the size computation and encoding logic adjacent, making it easy to verify they stay in sync. If a field is added to
encode, forgetting to add it toencoded_sizewill result in under-allocation (the writer falls back to dynamic growth — correctness is preserved, but the pre-allocation benefit is lost for that message).Scope
This proposal covers:
MessagePackSizeprimitive with compact size methods mirroringMessagePackEncoder's compact encoding methods.Writer.reserve_currentfor buffer pre-allocation.This proposal does not cover:
Writeror any other stdlib type.All reactions