get more free RAM...

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: get more free RAM...

Post by pythoncoder » Sat Dec 21, 2019 6:39 am

@jimmo I find that last example surprising, please could you explain? If I write

Code: Select all

a = object_a_that_allocates()
a = object_b_that_allocates()
by line 2 the instance of object_a_that_allocates has no references to it. Why won't the GC free its RAM? How does assigning None change this?

As an aside I have found that periodically performing explicit calls to gc.collect() reduces fragmentation.
Peter Hinch
Index to my micropython libraries.

jedie
Posts: 252
Joined: Fri Jan 29, 2016 12:32 pm
Contact:

Re: get more free RAM...

Post by jedie » Sat Dec 21, 2019 8:54 am

It happens on long run. I will post a memory map later.

I will insert some more gc.collect(). Hope that this helps.

Some day, i will compile own firmware with freeze modules. But then I would like to have a easy to use OTA update for the firmware, too. So I have to look at yaota. But it has no real documentation, so it's take more time...

User avatar
MostlyHarmless
Posts: 166
Joined: Thu Nov 21, 2019 6:25 pm
Location: Pennsylvania, USA

Re: get more free RAM...

Post by MostlyHarmless » Sat Dec 21, 2019 2:43 pm

pythoncoder wrote:
Sat Dec 21, 2019 6:39 am
@jimmo I find that last example surprising, please could you explain? If I write

Code: Select all

a = object_a_that_allocates()
a = object_b_that_allocates()
by line 2 the instance of object_a_that_allocates has no references to it. Why won't the GC free its RAM? How does assigning None change this?
It doesn't do this by itself. On an esp8266 with initially 32k of free RAM the following test fails as expected on the second assignment to "b":

Code: Select all

import gc

gc.collect()
print(gc.mem_free())

a = []
for i in range(1, 31):
    a.append('x' * 1000)

print(gc.mem_free())
    
b = 'x' * 1000
b = 'y' * 1000

print(gc.mem_free())
Note: this must be run as an uploaded script. Otherwise the compiler will work on ever line separately.

Just assigning None to "b" in between doesn't make it work. Assigning None and calling gc.collect() does. This means that an out of memory doesn't try running gc.collect() by itself.


Regards, Jan

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: get more free RAM...

Post by pythoncoder » Sun Dec 22, 2019 6:54 am

I appreciate that you have to run gc.collect() to be sure of reclaiming RAM used by orphaned objects. What baffles me is the assertion from @jimmo that assigning None has any effect on behaviour: if this is true I'd like to know the reason.
Peter Hinch
Index to my micropython libraries.

kevinkk525
Posts: 969
Joined: Sat Feb 03, 2018 7:02 pm

Re: get more free RAM...

Post by kevinkk525 » Sun Dec 22, 2019 7:26 am

I would be interested in that answer too.

The only situation where I assign None is in a loop:

Code: Select all

for string in list_object:
    a=do_something(string)
    send_string(a)
    a=None
    gc.collect()
    
Here it makes sense so the temporary veriable a gets collected after each iteration.

But I guess what jimmo means is that even:

Code: Select all

a = ...something that allocates...
gc.collect()
a = ...something else that allocates...
wouldn't be able to collect the first object as it is obviously still referenced but

Code: Select all

a = ...something that allocates...
a = None
gc.collect()
a = ...something else that allocates...
would be able to collect the first object since it got dereferenced, just like in my loop.
Kevin Köck
Micropython Smarthome Firmware (with Home-Assistant integration): https://github.com/kevinkk525/pysmartnode

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: get more free RAM...

Post by pythoncoder » Sun Dec 22, 2019 7:32 am

I certainly agree with that.
Peter Hinch
Index to my micropython libraries.

jedie
Posts: 252
Joined: Fri Jan 29, 2016 12:32 pm
Contact:

Re: get more free RAM...

Post by jedie » Sun Dec 22, 2019 9:46 am

Interesting discussion...

While we're on the subject: What's about gc.threshold() ?

http://docs.micropython.org/en/latest/l ... .threshold

Code: Select all

MicroPython v1.12 on 2019-12-20; ESP module with ESP8266
Type "help()" for more information.
>>> import gc
>>> gc.threshold()
9488
I guess that's probably the default value.

What's about to change this?
Does it help to set it to a very high value? e.g.: gc.threshold(gc.mem_alloc()+gc.mem_free()) (or to sys.maxint) ...
Is there a permanent attempt to get the RAM free? That probably leads to a slowdown, right?
But maybe it would help me not to get a memory error?!?
kevinkk525 wrote:
Sun Dec 22, 2019 7:26 am

Code: Select all

for string in list_object:
    a=do_something(string)
    send_string(a)
    a=None
    gc.collect()
    
In this case i would do this:

Code: Select all

for string in list_object:
    send_string(do_something(string))
    gc.collect()
If a variable is defined inside a function, it should be cleared afterwards, right? e.g.:

Code: Select all

def foo():
for string in list_object:
    a = do_something(string)
    send_string(a)
    
foo()
gc.collect() # 'a' should be cleared, isn't it?

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: get more free RAM...

Post by dhylands » Sun Dec 22, 2019 5:37 pm

I think that the reason assigning None is significant is that when you execute

Code: Select all

a = foo()
a = bar()
is that while bar is being called and allocating memory, a still has a reference to the memory allocated by foo(). It isn’t until a is actually replaced with the memory allocated by bar that it gets released. Adding

Code: Select all

a = None
between the statements causes the reference to the memory allocated by foo to be released before bar is called.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: get more free RAM...

Post by jimmo » Mon Dec 23, 2019 12:35 am

Sorry long reply, but addressing a bunch of different questions here... happy to provide citations (mostly py/gc.c) and more comprehensive example snippets.
pythoncoder wrote:
Sun Dec 22, 2019 6:54 am
What baffles me is the assertion from @jimmo that assigning None has any effect on behaviour: if this is true I'd like to know the reason.
Yep, exactly what dhylands said. Until `a` is assigned the second time (which happens _after_ the new allocation), the reference to the old data is still live, so cannot be freed.
MostlyHarmless wrote:
Sat Dec 21, 2019 2:43 pm
Assigning None and calling gc.collect() does. This means that an out of memory doesn't try running gc.collect() by itself.
When the allocator fails to allocate, it always runs a gc.collect first then tries again before actually failing the allocation.

You don't need to call gc.collect() explicitly in the scenario I'm describing.

Here's an example (Unix port, but with only 16k of heap).

Code: Select all

>>> import gc, micropython
>>> micropython.mem_info()
mem: total=5528, current=1582, peak=3161
stack: 960 out of 80000
GC: total: 16128, used: 2752, free: 13376
 No. of 1-blocks: 54, 2-blocks: 11, max blk sz: 6, max free sz: 413
>>> a = bytearray(10*1024)
>>> a = bytearray(10*1024)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
MemoryError: memory allocation failed, allocating 10240 bytes
>>> a = None
>>> a = bytearray(10*1024)   # This works
kevinkk525 wrote:
Sun Dec 22, 2019 7:26 am
The only situation where I assign None is in a loop:
...
Here it makes sense so the temporary veriable a gets collected after each iteration.
Yes, this is exactly the scenario where assigning to None will help.
jedie wrote:
Sun Dec 22, 2019 9:46 am
If a variable is defined inside a function, it should be cleared afterwards, right? e.g.:

...
Yes, this is another way of achieving the same thing. When a function returns it's local variables are now out of scope and will not be found by the gc.

This is a really important point -- don't go around just arbitrarily assigning stuff to None to try and improve memory usage. MicroPython does the right thing most of the time... Other than this one case, which it has to do, because imagine instead if the code was

Code: Select all

some_program_state = something()

def update():
  global some_program_state
  try:
    some_program_state = maybe_get_new_program_state()
  except:
    pass
Either some_program_state gets updated or it doesn't. So it has to hold onto the old value until the new value is definitely available.
pythoncoder wrote:
Sat Dec 21, 2019 6:39 am
As an aside I have found that periodically performing explicit calls to gc.collect() reduces fragmentation.
Yes -- frequent gc.collect() will help with fragmentation. I'm not sure I have a good way to put this into words without a diagram but essentially if you can synchronise your collections with your program "loop", then you'll end up playing out the same allocation pattern across RAM every time.

An important detail -- in addition to actually finding free blocks, the collection will reset the "where to search for the next allocation" back to the start of the heap.

Also, by collecting earlier, you give the allocator more of the heap to work with, and with more "pressure" to allocate earlier in the heap. In some sense, the allocator is going to be searching for gaps to fill, rather than splurging into free ram (potentially leaving behind fragments).

The other (and possibly better) thing you can do is pre-emptively gc.collect() before any long-lived allocations. The whole issue with fragmentation is that the allocator isn't smart enough to tell the difference between temporary stuff and long-lived stuff, so the temporary allocations end up having to fill the gaps between the long-lived ones. As you get more and more long-lived allocations, you end up with lots of tiny gaps but each one too small for even a modest-sized alloc. So by gc.collect()ing before a long-lived alloc, you significantly increase the likelyhood that the long-lived allocations will bunch up together.

Note: CircuitPython has some heuristics and improvements in this behavior. I've been looking at trying to implement some of the same ideas in MicroPython, but with all things GC this is tricky because:
- Any heuristics or behavior changes adds a performance cost (especially if the heuristics require additional accounting).
- Fixing fragmentation for one use case can make it dramatically worse for others.
jedie wrote:
Sun Dec 22, 2019 9:46 am
While we're on the subject: What's about gc.threshold() ?
This is related to the above. It changes the code that I described above where it will do a gc.collect() if it fails to find enough free heap, into preemptively collecting if the available heap drops below a threshold.

i.e. think of it as the default threshold is "100%".

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: get more free RAM...

Post by jimmo » Mon Dec 23, 2019 12:40 am

FYI, here's some of the CircuitPython work I referred to, which includes a cool video of the allocations -- https://github.com/adafruit/circuitpython/pull/547

Post Reply