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>
60 lines
3.6 KiB
Markdown
60 lines
3.6 KiB
Markdown
# PureBasic - MsgPack - Performance investigations
|
|
Open questions raised by the [results](readme.md#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](readme.md#purebasic-621-vs-641).
|
|
|
|
### Leads in `Includes/_Strings.pbi`
|
|
* `String.s` is passed by value through `MsgPackWriteString`, `_MsgPackGetEncodedStringSize` and `_MsgPackWriteStringField`,
|
|
which may copy it at each call, and would explain why 6.40's string handling helps that much. \
|
|
Passing `@String` and its length to the private helpers would avoid it.
|
|
* Each write goes over the string at least twice, with `StringByteLength()` then `PokeS()`. \
|
|
For `#PB_Unicode` and `#PB_Ascii`, the size could come from `Len()` instead, if it doesn't scan the string.
|
|
* `PokeS()` is given `-1` as its length, so it may also look for the null terminator on its own.
|
|
* `MsgPackReadString` builds the string with `PeekS()`, then returns it, which may copy it again.
|
|
|
|
### Leads in the benchmark
|
|
* `EncodeStrings()` preallocates `FieldSize * 2 + 5` bytes per field, 64 MB in total, against 32 MB for binaries. \
|
|
The allocation is timed, see the next section, and `MsgPackFinalize` then shrinks it by half.
|
|
* `ScenarioStringRead()` calls `Len()` on each string, which may scan it on 6.21 and not on 6.41. \
|
|
Python's `len()` and .NET's `Length` don't scan.
|
|
|
|
### Experiments to run
|
|
1. Time raw `PokeS()`/`PeekS()` loops with the same sizes, without MsgPack, to split PureBasic's own cost from the include's.
|
|
2. Size the strings writer like the binaries one, and compare.
|
|
3. Try the `@String` + length helpers, and `Len()` for the fixed-width encodings.
|
|
4. 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 `GrowthIncrement` of `1`, 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 by `MsgPackWriteMapFrom`, 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.
|