GeezerVison is showing github.com — perf: pre-size dicts when the size is known by sobolevn · Pull Request #1243 · msgspec/msgspec
Conversation
sobolevn
commented
Oct 11, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
When reading msgspec code I noticed that there are places that can benefit from using _PyDict_NewPresized. Here's how I designed this feature:
- I used a new function, which sets an upper cap for the size, so we don't allocate VERY big lists
- The cap is guessed
- We are using a non-public API, but it is stable enough to be used, public API will be release at some point in the future
- Dicts of 1–5 entries are unaffected (
_PyDict_NewPresized falls back to PyDict_New there)
Most controvertial part: The one regression is JSON decoding of a sparse total=False TypedDict, where the dict is presized for all fields but few are present; msgpack and convert know the input size and are unaffected. I consider this to be a less common case. So, basically, we can: revert it back keep as-is, allowing more allocations on required fields.
Or we can keep this version. Which is my preffered option. Why?
- It is rather uncommon to have lots of optional fields and only get a small subset of them (128 required and 4 present in my benchmark)
- The absolute time increase is rather small, percents seem big, yes
If others do not agree on this trade-off, I can revert the json.decode change.
This benchmark is AI generated:
| benchmark |
main |
this PR |
change |
msgpack.decode, dict of 8 keys |
0.184 µs |
0.174 µs |
-5 % |
msgpack.decode, dict of 1024 keys |
51.5 µs |
46.6 µs |
-10 % |
msgpack.decode, dict of 100k keys (above the 1024 cap) |
7.51 ms |
7.47 ms |
-1 % |
msgpack.decode, 250 dicts × 32 keys |
432 µs |
339 µs |
-22 % |
msgpack.decode, TypedDict with 32 fields |
0.641 µs |
0.549 µs |
-14 % |
json.decode, TypedDict with 32 fields |
0.824 µs |
0.740 µs |
-10 % |
json.decode, total=False TypedDict with 32 fields, all present |
0.822 µs |
0.735 µs |
-11 % |
json.decode, total=False TypedDict with 128 fields, 4 present |
0.130 µs |
0.182 µs |
+40 % |
convert, dict of 1024 keys → dict[str, int] |
21.9 µs |
17.0 µs |
-22 % |
convert, dict → TypedDict with 128 fields |
3.24 µs |
2.89 µs |
-11 % |
to_builtins, dict of 1024 keys |
23.6 µs |
18.7 µs |
-21 % |
to_builtins, dict of 1024 keys, order="sorted" |
129 µs |
114 µs |
-11 % |
to_builtins, Struct with 32 fields |
0.501 µs |
0.475 µs |
-5 % |
to_builtins, omit_defaults Struct with 32 fields, all omitted |
0.083 µs |
0.084 µs |
+1 % |
to_builtins, dataclass with 32 fields |
0.905 µs |
0.781 µs |
-14 % |
structs.asdict, Struct with 128 fields |
1.56 µs |
1.25 µs |
-20 % |
__reduce__, kw-only Struct with 32 fields |
0.453 µs |
0.395 µs |
-13 % |
msgpack.decode, benchmarks/ filesystem data (10 KiB) |
23.3 µs |
22.3 µs |
-4 % |
to_builtins, benchmarks/ filesystem data (1 KiB) |
1.56 µs |
1.46 µs |
-7 % |
control: json.decode, dict of 1024 keys (unchanged path) |
58.3 µs |
58.5 µs |
+1 % |
bench.py
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
sobolevn
commented
Oct 11, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Ok, on the other hand: I can open a new PR with json_decode part later.
Now json_decode change is reverted. I will create a new PR for that. So: we can merge this PR as a net-positive and decide on the corner-case in the next one.
Updated actual benchmarks:
| benchmark |
main |
this PR |
change |
msgpack.decode, dict of 8 keys |
0.184 µs |
0.174 µs |
-5 % |
msgpack.decode, dict of 1024 keys |
51.5 µs |
46.6 µs |
-10 % |
msgpack.decode, dict of 100k keys (above the 1024 cap) |
7.51 ms |
7.47 ms |
-1 % |
msgpack.decode, 250 dicts × 32 keys |
432 µs |
339 µs |
-22 % |
msgpack.decode, TypedDict with 32 fields |
0.641 µs |
0.549 µs |
-14 % |
convert, dict of 1024 keys → dict[str, int] |
21.9 µs |
17.0 µs |
-22 % |
convert, dict → TypedDict with 128 fields |
3.24 µs |
2.89 µs |
-11 % |
to_builtins, dict of 1024 keys |
23.6 µs |
18.7 µs |
-21 % |
to_builtins, dict of 1024 keys, order="sorted" |
129 µs |
114 µs |
-11 % |
to_builtins, Struct with 32 fields |
0.501 µs |
0.475 µs |
-5 % |
to_builtins, omit_defaults Struct with 32 fields, all omitted |
0.083 µs |
0.084 µs |
+1 % |
to_builtins, dataclass with 32 fields |
0.905 µs |
0.781 µs |
-14 % |
structs.asdict, Struct with 128 fields |
1.56 µs |
1.25 µs |
-20 % |
__reduce__, kw-only Struct with 32 fields |
0.453 µs |
0.395 µs |
-13 % |
msgpack.decode, benchmarks/ filesystem data (10 KiB) |
23.3 µs |
22.3 µs |
-4 % |
to_builtins, benchmarks/ filesystem data (1 KiB) |
1.56 µs |
1.46 µs |
-7 % |
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
python/cpython#139772 mentions reservations about the usage of _PyDict_NewPresized and discourages its use. It also floats the idea of a deprecation. What are your thoughts on this? IMO this doesn't necessarily mean we cannot use it, but to me at least indicates we should be cautious.
Also, did you check the mentioned unicode-only key optimisation issue with _PyDict_NewPresized?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Oh, this is very sad to see: python/cpython#139772
I though that this issue will be in 3.16 and now it is closed :(
Yes, in this case - this change does not make sense. Because I thought that we can adapt msg_presized_dict to 3.16 and use the public API, but I've missed the May status update.
This is actually really not cool: we don't have any ways to limit the allocations for dict objects now. I will try to start this duscussion once again.
|
provinzkraut reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Yeah, sad indeed, because the perf improvements did seem nice. IMO as long as it's not going to be deprecated any time soon and we don't have any other downsides (e.g. the mentioned UTF-8 optimisation), it'd be fine to use a private API. We do it in several places to get some perf benefits. Or do you think we should generally avoid private APIs unless absolutely necessary?
Seeing as the discussions around promoting such things to the public API usually involve referencing "x number of projects already use this private API, so there is a demand", it could be argued that using it is beneficial to drive interest in making it stable and public.
|
vstinner reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reviewers
Siyet
Awaiting requested review from Siyet
ofek
Awaiting requested review from ofek
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|