Skip to content

Skip scanning string and bytes data during garbage collection - #11708

Merged
dhalbert merged 1 commit into
adafruit:mainfrom
mikeysklar:fix-10491
Nov 1, 2026
Merged

dhalbert merged 1 commit into
adafruit:mainfrom
mikeysklar:fix-10491

Conversation

@mikeysklar

Copy link
Copy Markdown
Collaborator

What

Garbage collection no longer looks inside string and bytes data for pointers.

Why

Part of #10491.

That data never holds pointers on firmware, so skipping it makes collections faster.

On a real RP2350, gc.collect() with strings live dropped from 22.8 ms to 10.8 ms.

PSRAM boards, the case that motivated #10264, were not tested.

Hardware tested

Metro RP2350 (no PSRAM, about 390 KB free) and Metro M0 Express (about 15 KB free), base and patched from the same commit. Workloads are scaled to fit. gc.collect() time in µs: mean, with min and max, of 15 trials over 5 alternating flashes. Each trial is the median of 7 batches of collections.

Live data RP2350 base RP2350 patched Change M0 base M0 patched Change
Strs of 252 bytes (RP2350 1000, M0 20) 22758 (22756-22760) 10769 (10768-10771) -52.7% 3497 (3494-3497) 2547 (2545-2548) -27.2%
Bytes (RP2350 4 of 64 KB, M0 2 of 2500) 8706 (8705-8707) 3898 (3861-4364) -55.2% 3020 (3018-3021) 2252 (2249-2252) -25.4%
Dict of strs and lists 20284 (20282-20287) 17358 (17357-17359) -14.4% 4325 (4321-4327) 3825 (3824-3827) -11.6%
JSON document (no json on M0) 20501 (20499-20503) 17996 (17995-17997) -12.2%
Lists and tuples, no text (control) 17594 (17592-17595) 17577 (17576-17578) -0.1% 3623 (3619-3626) 3562 (3561-3565) -1.7%
Nothing 308 (308-308) 299 (299-300) -2.7% 1919 (1917-1921) 1860 (1859-1860) -3.1%

A build with the same call sites that still scans these blocks matched base within 0.04% on every row, so these changes come from scanning, not moved code.

String building with collections as needed, change after subtracting that build's change: RP2350 strs and bytes about -10.5%, dict -2.6%, JSON -1%, control 0%. M0 strs -1.8%, bytes -1.3%, dict -1%, control -0.2%.

Check Result
struct.pack('O', x), 'S', array('O') ValueError: bad typecode on both boards and builds
Stress test, 10 flashes per board Passes, 1500 checks on RP2350, 52 on M0 (no io, json)
Safe mode reason after every trial None
gc.mem_free() Same on both builds
Flash RP2350 +48 bytes, M0 +16 bytes (2680 free)

Other testing

Check Result
Unix coverage run-tests.py, base and patched 982 of 982 pass, per-test lists identical
Same, collecting on every 29th allocation 982 of 982 pass, per-test lists identical
basics and micropython, collecting on every 3rd allocation, counter build looking for heap pointers in unscanned blocks 689 of 689 pass, 0 found
Stress test (strs, bytes, bytearray, io, JSON, big ints, exec, exceptions), collecting between operations Passes on base and patched
Same test with a list's items, a parser chunk, or vstr_t deliberately not scanned Each one fails; the parser chunk needs forced collection on coverage
Flash Pico +40 bytes, Trinket M0 +24 bytes

Desktop lab

Desktop lab (cp-desktop-lab, x86_64, 16 MB heap), not a board. gc.collect() time in µs: median, min and max of 10 process runs, each run the median of 7 samples of 400 collections. The second base column is the same binary run again, as a control.

Live data Base Base again Patched
3000 strs, 252 bytes each 380.1 (379.0-381.5) 380.6 (379.8-382.3) 241.9 (240.9-242.5)
10 bytes, 64 KB each 124.5 (120.6-139.8) 123.1 (120.7-124.7) 72.0 (67.9-84.4)
JSON document 591.5 (573.3-593.8) 592.4 (573.3-593.0) 549.2 (531.8-551.1)
Lists and tuples, no text (control) 520.4 (519.6-521.4) 521.0 (520.6-521.5) 520.3 (519.0-521.9)

Blocks scanned per collection, from a throwaway counter build:

Live data Base Patched
Strs 53321 5369
Bytes 20639 149
JSON 32031 18904
Control 36603 36603

A run of string building and JSON with a mostly empty heap showed little change: 43.8 to 42.9 ms with strs held, 43.5 to 43.5 ms with JSON held.

Scope

Item Status
Unix standard variant struct.pack('O', obj) crashes with this patch, because its typecodes allow object pointers. Firmware and coverage reject them. That variant does not build on main today; tested with ringio turned off
Left out, small or short-lived objint.c:265, objint_mpz.c:99, persistentcode.c:204, mpz digits
Left out, can hold objects ringbuf_alloc, parser chunks, emergency exception buffer

Selective collect is CircuitPython only, so this can't go to MicroPython first.

@lynt-smitka your cp-desktop-lab gave the first numbers here, before any board run.

AI assistance

Claude wrote the patch and ran the lab and board tests. I directed the work and reviewed the results.

🤖 Generated with Claude Code

String and bytes buffers never hold pointers on firmware, so allocate them
without the collect flag and collections get cheaper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dhalbert dhalbert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - great!

@mikeysklar
mikeysklar requested a review from dhalbert November 1, 2026 21:08
@dhalbert
dhalbert merged commit ca454a6 into adafruit:main Nov 1, 2026
704 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants