mirror of
https://github.com/aziascreations/PB-MessagePack.git
synced 2026-11-03 17:01:24 +01:00
9facb66ee8
Co-Authored-By: Claude <noreply@anthropic.com>
3.6 KiB
3.6 KiB
PureBasic - MsgPack - Performance investigations
Open questions raised by the results, with leads to check.
Nothing below is confirmed yet, update or remove each entry once it is looked into.
Measured on 2026-11-03, see the results' CSVs in Results/.
Strings are slow compared to binary data
x64 with the C backend, PureBasic 6.41, ns per field:
| Payload | Binary write | String write | Binary read | String read |
|---|---|---|---|---|
| 1 KB, UTF-8 | 190.6 | 720.9 (3.8x) | 50.2 | 570.0 (11.4x) |
| 1 KB, UTF-16 (2 KB) | - | 526.0 | - | 282.7 |
| 64 KB vs 1 MB | ~5.0 GB/s | ~1.4 GB/s | ~19 GB/s | ~2.1 GB/s |
- UTF-16 is faster than UTF-8 while being twice as big, so the encoding itself costs a lot.
- MessagePack-CSharp reads 1 KB strings in 150 ns, and writes 8 B strings in 13 ns against 60 ns here.
- PureBasic 6.21 is another 2.5 to 4 times slower, see PureBasic 6.21 vs 6.41.
Leads in Includes/_Strings.pbi
String.sis passed by value throughMsgPackWriteString,_MsgPackGetEncodedStringSizeand_MsgPackWriteStringField, which may copy it at each call, and would explain why 6.40's string handling helps that much.
Passing@Stringand its length to the private helpers would avoid it.- Each write goes over the string at least twice, with
StringByteLength()thenPokeS().
For#PB_Unicodeand#PB_Ascii, the size could come fromLen()instead, if it doesn't scan the string. PokeS()is given-1as its length, so it may also look for the null terminator on its own.MsgPackReadStringbuilds the string withPeekS(), then returns it, which may copy it again.
Leads in the benchmark
EncodeStrings()preallocatesFieldSize * 2 + 5bytes per field, 64 MB in total, against 32 MB for binaries.
The allocation is timed, see the next section, andMsgPackFinalizethen shrinks it by half.ScenarioStringRead()callsLen()on each string, which may scan it on 6.21 and not on 6.41.
Python'slen()and .NET'sLengthdon't scan.
Experiments to run
- Time raw
PokeS()/PeekS()loops with the same sizes, without MsgPack, to split PureBasic's own cost from the include's. - Size the strings writer like the binaries one, and compare.
- Try the
@String+ length helpers, andLen()for the fixed-width encodings. - Run 6.40 too, to check that the gain comes from 6.40 and not 6.41.
Buffers are zero-filled
MsgPackCreate uses AllocateMemory() and MsgPackGrow uses ReAllocateMemory(), both without #PB_Memory_NoClear,
so every buffer is zeroed before being written over.
- That cost is in every write scenario with a preallocated buffer, more so for the big ones.
- With a
GrowthIncrementof1, small writes take ~440 ns against ~12 ns preallocated, the reallocation and copy probably cost more than the zeroing, but it adds up.
To try: #PB_Memory_NoClear on both, as only the written bytes are ever read, then re-run every folder.
Smaller oddities
- Maps: writing them is 8.6 times slower than MessagePack-CSharp and 1.9 times slower than msgpack.
Each map is written in its own structure, then checked, counted and copied byMsgPackWriteMapFrom, see the main readme. Its 16 keys are strings too, see above. - ASM backend: skipping small fields is 3.2 times slower than with the C backend (57.9 vs 18.3 ns), against 1 to 2 times for the rest.
- 6.21 faster than 6.41: skip (14.7 vs 18.3 ns) and 1 KB binaries (0.8 to 0.9x).
Those runs only last 1 to 2 ms, so it may be noise, re-run them with more iterations before looking further.