garbage collector/memory management possibly flawed #14116
-
Recent discussions about memory allocation led to a small test-program I'd like to offer for discussion. It seems as if the garbage collector behaves a bit strange and unpredictable at times. In particular, bytearrays not freed after ›del‹. Sometimes only freed after leaving a function. Sometimes not freed at all. (Only when really forced) May it is behaving as intended, that's why I'd like to open it for discussion. Here's the code: #!/usr/bin/env python
# -*- coding: utf-8 -*-
import gc
def show_mem(n=0):
gc.collect()
print(f'({n:02}) {round(gc.mem_alloc()/1000, 1)} kB')
def works_as_expected1():
show_mem(10)
ba = bytearray(ALLOC_SIZE)
show_mem(11)
del ba
show_mem(12)
def works_as_expected2():
show_mem(20)
ba = bytearray(ALLOC_SIZE)
show_mem(21)
ll = len(ba)
print(ll)
del ba
show_mem(22)
def does_not_works_as_expected():
show_mem(30)
ba = bytearray(ALLOC_SIZE)
show_mem(31)
print(len(ba)) # somehow holds the ›ba‹ object
del ba # can't delete
show_mem(32) # ›ba‹ object still locked
ALLOC_SIZE = 150_000
works_as_expected1()
show_mem(13)
print()
works_as_expected2()
show_mem(23)
print()
# this is bad
does_not_works_as_expected()
show_mem(33) # after leaving the function, ›ba‹ is freed
print()
# this is OK
show_mem(40)
ba = bytearray(ALLOC_SIZE)
show_mem(41)
del ba
show_mem(42)
print()
# this is truly ugly
show_mem(50)
ba = bytearray(ALLOC_SIZE)
show_mem(51)
print(len(ba)) # now ba is REALLY locked
del ba # and one can't free the object
show_mem(52) # 100kB lost forever…
print()
try: # catch exception, because this WILL throw an error
ba2 = bytearray(ALLOC_SIZE) # should work
except MemoryError as me:
print(me) # but doesn't
n = gc.threshold()
gc.threshold(0) # … unless we REALLY force a full gc
show_mem(60)
gc.threshold(n)
ba2 = bytearray(ALLOC_SIZE) # now it will work
# picorun "memory.py"
# (10) 5.0 kB
# (11) 105.0 kB
# (12) 5.0 kB
# (13) 5.0 kB
#
# (20) 5.0 kB
# (21) 105.0 kB
# 100000
# (22) 5.0 kB
# (23) 5.0 kB
#
# (30) 5.0 kB
# (31) 105.0 kB
# 100000
# (32) 105.0 kB
# (33) 5.0 kB
#
# (40) 5.0 kB
# (41) 105.1 kB
# (42) 5.0 kB
#
# (50) 5.0 kB
# (51) 105.1 kB
# 100000
# (52) 105.1 kB
#
# (60) 5.1 kB |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 19 replies
-
In my week in the wilderness I found manipulating 20k files was really all about contiguous memory. In this regard import micropython; micropython.mem_info(1) helped me greatly compared to the garbage collector. Jimmo insists the gc does eventually get around to releasing blocks https://github.com/orgs/micropython/discussions/14090 but, like you, I've found it to be a bit unpredictable when it comes to deletions. |
Beta Was this translation helpful? Give feedback.
-
Python (the language) makes no guarantees about when garbage collection happens. CPython uses reference counting for most garbage collection. MicroPython uses mark and sweep. So CPython can free an object immediately after its reference count to zero. MicroPython will only free it when the mark and sweep runs. To get it to run, use In addition MicroPython's gc is conservative, because it is not necessarily always able to tell whether something on the stack is a pointer or binary data. So some objects may go uncollected if there happens to be a binary data that looks like an object pointer. |
Beta Was this translation helpful? Give feedback.
-
One more attempt to make the point: #!/usr/bin/python3
# -*- coding: UTF-8 -*-
# vim:fileencoding=UTF-8:ts=4
import gc
# comment out the two lines and it'll never work!
# leaving the dummy_func() in, but not even calling it,
# it'll throws an exception during first attempt to allocate the bytearray,
# but then works on the next attempt.
def dummy_func():
return
ALLOC_SIZE = 150_000
ba = bytearray(ALLOC_SIZE)
print(len(ba))
del ba
attempt = 0
while True:
try:
attempt = int(attempt) + 1
gc.collect()
ba = bytearray(ALLOC_SIZE)
break
except MemoryError as me:
print(attempt, me)
print('success')
|
Beta Was this translation helpful? Give feedback.
@GitHubsSilverBullet Thanks, you found a weird memory leak bug! Due to a dangling pointer in the compile stage, there was a memory address which would never be garbage collected when compiling and running Python code on the fly.
This is why the weird 336/337 thing happens, and also why the bug only triggers with random quantities of code in between the two steps (calling a particular gc function didn't make a difference, I was able to reproduce calling
print
!)If the first
ab
buffer ends up at this particular address then this will happen, and that address depended on what else is allocated in the Python heap when the code ran (hence changing buffer size helped, and also having different …