py/map: Make dicts preserve insertion order.#34
Draft
andrewleech wants to merge 3 commits intoreview/py-map-orderedfrom
Draft
py/map: Make dicts preserve insertion order.#34andrewleech wants to merge 3 commits intoreview/py-map-orderedfrom
andrewleech wants to merge 3 commits intoreview/py-map-orderedfrom
Conversation
|
Code size report: |
740bcf8 to
fb2071d
Compare
Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
fb2071d to
2197cfe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Some years back @dpgeorge and I had a number of discussions about making dicts ordered (micropython#6170, micropython#6173), with a couple of my earlier attempts (micropython#5514, micropython#5517) being too simple to land. That led to @dpgeorge's micropython#6173 which took the proper CPython/PyPy approach (dense key/value array with separate sparse hash index) but was left as WIP with a few open items. With an aim to close out this earlier work I've tried to build on that PR with fixes for the remaining issues, compaction, OrderedDict simplification, performance testing and flash size reduction (as much as I can).
Dicts now preserve insertion order matching CPython 3.7+. Hash indices are uint8 for small dicts (<256 entries), uint16 above that, and uint32 via
MICROPY_PY_MAP_LARGEfor dicts exceeding 65535 elements. Without LARGE the alloc is capped at 65535; at 8+ bytes per entry that's over 0.5MB of table, well past what most targets have.Deleted entries become tombstones in the dense array. Compaction runs in-place when tombstones exceed 50% of live entries or when the array is full, no allocation needed so dict operations work safely under heap lock (exception handling, GC).
dict.popitem()returns LIFO matching CPython.OrderedDictshares the same hash table backing as regular dicts now, the mutableis_orderedlinear-scan code path is removed. ROM/const maps keep linear scan viais_fixed.The
filledcount for O(1)len()is packed into the same bitfield word asusedsomp_obj_dict_tstays within one GC block. On 32-bit that gives 15 bits each (max 32767 entries), on 64-bit 31 bits.Behind
MICROPY_PY_MAP_ORDERED, defaulting on atEXTRA_FEATURESROM level. With it off the original flat open-addressing table is used, zero overhead.map->usedchanges meaning from live-entry count to high-water mark when ordered maps are enabled so external code reading dict length should use themp_map_len()macro. The core code and port pin files are converted; there are ~28kwargs->usedinstances across extmod/ and ports/ left unconverted since kwargs maps are add-only (used == filledalways holds). Open question whether those should be converted for consistency.Testing
Tested on unix (standard, coverage, minimal, nanbox, longlong), STM32 PYBV10 (cross-compiled), and PYBD-SF6W (on-device). Both
MICROPY_PY_MAP_ORDERED=1and=0build and pass. Tests cover compaction thresholds, ordering preservation, empty-dict edge cases, non-qstr keys through hash rebuild, LIFO popitem with mixed del/add, dict operations undermicropython.heap_lock(), and the alloc overflow boundary (stress/dict_create_max).Flash measured by building branch and merge-base with identical toolchain (
arm-none-eabi-gcc 14.3.1,-Os), comparingsizeon the firmware ELF:RAM via
gc.collect(); gc.mem_alloc()before/after dict creation on unix x86-64:Per-slot overhead is the hash index byte. Empty dict identical to master.
Speed on unix x86-64,
time.ticks_us()inside functions, 1000 iterations on 100-entry dicts, median of 3 runs. Measured before the latest minor fixes (alloc guard, assert); these don't affect hot paths so numbers should still be representative:Lookup and iteration within noise. Insertion ~10% slower from hash index writes, delete+add cycles ~3x faster (in-place compact vs full rehash). Globals access ~6% overhead.
On-device speed on PYBD-SF6W (STM32F767, Cortex-M7),
time.ticks_us(), 500 iterations on 50-entry dicts, compared branch firmware against master on same board:On the MCU everything is within noise except delete+add cycles which are ~40% faster and iteration which is ~3.5% slower.
Trade-offs and Alternatives
+320 bytes flash on STM32 Thumb-2, mostly the compaction path needed for heap-locked safety. Ports at CORE/BASIC ROM level can disable
MICROPY_PY_MAP_ORDEREDfor zero cost.OrderedDict no longer accepts unhashable keys (e.g. slices) since it now uses the hash table, this matches CPython behavior. MicroPython's old linear-scan OrderedDict accepted them as a side effect of not hashing. The
slice_optimise.pytest is updated to handle both paths.Generative AI
I used generative AI tools when creating this PR, but a human has checked the code and is responsible for the description above.