A faster drop in replacement for PropertyListEncoder and PropertyListDecoder on binary property
lists. Output is plain bplist00, byte compatible with everything that already reads property lists,
so a document written here still opens in any earlier version of the app that wrote it with Foundation.
let data = try BinaryPlistEncoder().encode(project)
let project = try BinaryPlistDecoder().decode(Project.self, from: data)Existing Codable conformances are used unchanged — nothing in the model layer has to be rewritten.
PropertyListEncoder makes three passes over the data. Codable reflection builds a complete
intermediate NSDictionary / NSArray / NSData tree, CFPropertyListCreateData walks that tree and
uniques every object in it through a hash set — hashing and comparing every message body — and only
then writes bytes.
This encoder appends values to a flat object table as they are encoded and writes the byte stream from it in one pass. Strings are uniqued, because header names, hosts and methods repeat constantly. Data blobs are uniqued as well, but keyed on their length together with their first and last 32 bytes rather than on every byte, so repeated message bodies cost one exact comparison instead of a full hash of every body in the document. On a real 76 MB capture that keeps the file within 1% of what PropertyListEncoder produces, at about 2% of the encoding time.
Decoding skips the object tree entirely, reading values straight out of the byte buffer, and compares
dictionary keys against raw bytes rather than allocating a String for every key it passes over.
swift run -c release BinaryPlistBenchmark generates a synthetic document shaped like a Proxygen
project and measures the full Codable path. On an M-series Mac, 20 000 messages with 2 KB inline
bodies, a 57 MB property list:
| PropertyListEncoder/Decoder | BinaryPlistEncoder/Decoder | ||
|---|---|---|---|
| Encode | 655 ms | 282 ms | 2.3x |
| Decode | 1.12 s | 239 ms | 4.7x |
Those bodies are random, so nothing dedupes and the uniquing lookup is pure cost. A real 76 MB capture, where bodies do repeat, comes out at 2.7x encoding and 4.9x reading.
The benchmark also reports the Codable walk on its own — 90 ms here — which is the floor no encoder
built on Encoder can go below. Roughly a third of the remaining encode time is protocol dispatch
that belongs to Codable rather than to serialization.
Pass a path to a real project.plist to measure against actual data instead:
swift run -c release BinaryPlistBenchmark --iterations 10 path/to/project.plist
That mode reads and rewrites the document as an untyped PlistValue tree, so it needs no model types.
The details that have to match Foundation exactly, all covered by differential tests that compare the decoded trees of both encoders' output:
encodeNil()writes the string$null, the placeholderPropertyListEncoderuses, and the decoder reads it back as nil- dates are written as binary property list dates, seconds since 2001, not as numbers
- integers use the same 1, 2, 4 and 8 byte forms, with negatives always in the 8 byte form
- strings are ASCII when every scalar is below 128, otherwise UTF-16BE
DataandDatetake precedence over theirCodableconformances, as in Foundation
The decoder accepts everything CoreFoundation writes, including 16 byte integers, floats, sets and UIDs, and it throws rather than trapping on truncated or corrupted input.
Apache License 2.0. See LICENSE.