Repository navigation
Conversation
|
Code size report: |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #19367 +/- ##
==========================================
+ Coverage 98.55% 98.59% +0.03%
==========================================
Files 182 182
Lines 23342 23380 +38
Branches 5 5
==========================================
+ Hits 23005 23051 +46
+ Misses 336 328 -8
Partials 1 1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Since this ties in strongly with the optimised tail scan (#19363) here's a graph of them working together, again just SRAM:
|
|
Very persuasive graphs! Of note, @tannewt implemented a very similar idea in CircuitPython last year, see adafruit#10264. CircuitPython went even further and updated most of the places they call The main potential for problems I can see comes from code which might play fast and loose with what they keep in the bytearray. For example, use of |
I've heard plenty of rumblings of long-lived data and seen some of @tannewt's videos diving deep into GC. They have such a closely aligned set of problems to what we (Pimoroni) deal with (big silly fun hardware projects) so it figures! I'll have a glance over and see how it compares. (I think it's obvious, but I kinda got carried away with this stuff, since we really wanted the extra perf downstream and were happy to own scary changes)
I did wonder about having an explicit opt-in to make a memory region no-scan, but it feels like no-scan is the typically happy path (short of C bindings which allocate structs or objects that contain pointers... something I was repeatedly bitten by in the early days of my Pico exploits) so opting in to scan makes a lot of sense (as long as you, uh, don't forget). I'm currently deep into benchmarking, verifying and testing a complete rewrite to GC to peer into the hypothetical future of what we could possibly gain. For large RAM allocations this skip change still absolutely demolishes any other optimisation we could make directly to GC, and I'm poised to ship this in our Badgeware builds for road testing. |
|
I had a look at CircuitPython's version of this adafruit#10264 and I do prefer the one here:
So IMO we should try to get this PR merged. Some feedback for the changes themselves. I think the feature should be named in the positive rather than in the negative with "no-scan table":
|
bfd8146 to
d1016fa
Compare
|
Changes made. Also did a pass over the comments. Did you have any thoughts on projectgus's point, ie: adding something like |
Yes, it's probably a good idea to add such a facility. Following @projectgus 's idea, what do you think about reusing the existing Also, I think this PR can be taken out of draft. |
I think at the point we're making it optional it ought to be a new function. Though I'd agree with the between-the-lines reading that making it quietly promote unscanned RAM to scanned is also not necessarily the right shape. I'm not sure where such a function should be hung, but |
I'd rather add it in the @projectgus any thoughts/ideas about how this user-facing API could work? Either an optional argument to |
This comment was marked as outdated.
This comment was marked as outdated.
|
@projectgus and I discussed how to add a way to mark that an area of memory may contain pointers. It's also possible to store pointers with viper, inline-asm and mport machine
@micropython.asm_thumb
def addressof(r0):
pass
@micropython.viper
def store_ptr_viper(buf: ptr32, idx: int, obj: int):
buf[idx] = obj
@micropython.asm_thumb
def store_ptr_asm(r0, r1, r2):
add(r0, r0, r1)
str(r2, [r0, 0])
obj = dict()
obj_addr = addressof(obj)
print("obj_addr", hex(obj_addr))
buf = bytearray(12)
buf_addr = addressof(buf)
print(hex(buf_addr), buf.hex())
machine.mem32[buf_addr] = obj_addr
print(hex(buf_addr), buf.hex())
store_ptr_viper(buf, 1, obj_addr)
print(hex(buf_addr), buf.hex())
store_ptr_asm(buf, 8, obj_addr)
print(hex(buf_addr), buf.hex())That outputs (on RPI_PICO2): Given that there are many ways to do this (including the I suggest to make the new Python function guarded against MICROPY_ENABLE_GC so it can be used regardless of whether this GC-no-scan feature is enabled or not. Also we need a public C-level function for C users (as mentioned by @projectgus above). Here is my suggestion for the Python-level API: micropython.buf_contains_ptrs(buf)It would accept anything with the buffer protocol (so makes sense to name it "buf" and not something like "mem_contains" or "array_contains"). |
|
Looking into this, but might have added a bug in the UNIX build which needs squashing first. |
The alloc table length is computed assuming each optional table is exactly A * BLOCKS_PER_ATB / BLOCKS_PER_xTB bytes, but each table is allocated rounded up to a whole byte. When A is odd each table gains half a byte, which can push the tables past gc_pool_start and trip the assert (or overlap the pool with assertions disabled). With FTB and WTB enabled this only happens for an odd-sized heap, e.g. an unaligned start pointer. Adding further tables makes it reachable with block-aligned heaps. Reserve one byte per enabled table before computing the alloc table length. Signed-off-by: Phil Howard <[email protected]>
The mark phase conservatively scans every word of every reachable block for pointers, so a large bytearray/array buffer is scanned in full on every collection despite holding no pointers. Add an optional per-block "scan table" (STB, 1 bit/block, like the finaliser/weakref tables) and a GC_ALLOC_FLAG_CONTAINS_NO_GC_POINTERS flag. The bit is set for ordinary allocations and cleared for blocks flagged as containing no GC pointers; such head blocks are still marked, but their contents are not scanned. The mark phase also skips the chain-walk for blocks with no GC pointers, and avoids re-reading the allocation table for every block. This matters for large buffers in relatively slow PSRAM. This feature is exposed in C via m_new_with_no_gc_pointers() / m_malloc_with_no_gc_pointers() (which alias plain m_new()/gc_alloc() when disabled), and gated behind MICROPY_GC_ENABLE_CONTAINS_NO_GC_POINTERS (default off). Signed-off-by: Phil Howard <[email protected]>
array/bytearray item storage, str/bytes payloads and vstr buffers hold raw data only, never mp_obj_t or other heap pointers, so allocate them with m_new_with_no_gc_pointers() to keep them out of the GC mark scan. No change when MICROPY_GC_ENABLE_CONTAINS_NO_GC_POINTERS is disabled. Signed-off-by: Phil Howard <[email protected]>
RP2350 boards with PSRAM can hold multi-megabyte framebuffers and font data on the heap, where the mark phase dominates collection time. Costs one bit of heap per block. Signed-off-by: Phil Howard <[email protected]>
Buffers allocated as containing no GC pointers can still have object addresses written into them by C code, uctypes, machine.mem32, viper or inline assembler. This function sets the scan bit on the heap block containing ptr so the GC will scan it. ptr may point anywhere inside the block, which allows marking via a memoryview slice. It does nothing for pointers outside the heap, or when MICROPY_GC_ENABLE_CONTAINS_NO_GC_POINTERS is disabled. Signed-off-by: Phil Howard <[email protected]>
Accepts any object with the buffer protocol and marks its underlying heap block as possibly containing GC pointers. Available whenever MICROPY_ENABLE_GC is enabled, so code can call it regardless of whether the port skips scanning pure-data buffers. Signed-off-by: Phil Howard <[email protected]>
Enables MICROPY_GC_ENABLE_CONTAINS_NO_GC_POINTERS so the scan table and micropython.buf_contains_ptrs() are exercised by the coverage tests. Signed-off-by: Phil Howard <[email protected]>
Reported as bit 4 when MICROPY_GC_ENABLE_CONTAINS_NO_GC_POINTERS is enabled. The coverage build enables it, so the existing realloc test now shows the STB bit set on both blocks. Signed-off-by: Phil Howard <[email protected]>
Checks that a block allocated with no GC pointers keeps its STB bit clear when gc_realloc() relocates it, and that gc_buf_contains_ptrs() sets it via an interior pointer and ignores a non-heap pointer. Signed-off-by: Phil Howard <[email protected]>




Note: This will need rebasing upon and landing after #19707
Summary
Buffers don't contain pointers. Don't scan them for pointers. This was never really a problem until we had 8MB of PSRAM with in-RAM font data and images.
Currently this brings new
m_new_no_scan()andm_malloc_no_scan()methods for data that's guaranteed (by a contentious user who understands how painful use-after-free bugs are to trace) to contain absolutely 100% no pointers. These mirror the existing methods, using a newGC_ALLOC_FLAG_NO_SCANflag.For better or worse we'll be carrying this change downstream for Tufty 2350, since GC hangups are painful when trying to hit 30-60FPS screen updates. This doesn't eliminate them, but turns a 300ms pause into a 30ms one, the rest of which is handled by #19363
Might be of interest to @sfe-SparkFro
Testing
Aggressive multi-hour tests of both real-world examples (on Tufty 2350) and synthetic GC thrashing benchmarks.
Again this change does not make much of an impact on perfbench since we don't really benchmark GC, and in some cases it can cause a net loss (RP2040 XIP cache lottery).
Trade-offs and Alternatives
This feature sacrifices heap for the additional flag bit in the allocation table, and is thus default disabled. I'd recommend everyone shipping a board with PSRAM enable it as a matter of course, and suggest that leaving the RAM/performance tradeoff to the vendor of each board.
This is a big change to a scary part of MicroPython and as such I'm raising it as a draft in the hope others will exercise it downstream and feed back. I don't expect or need it to be merged, but it's fun to share!
Generative AI
I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.