Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to release memory allocated by a slice? [duplicate]

Tags:

go

package main

import (
    "fmt"
    "time"
)

func main() {
    storage := []string{}

    for i := 0; i < 50000000; i++ {
        storage = append(storage, "string string string string string string string string string string string string")
    }

    fmt.Println("done allocating, emptying")

    storage = storage[:0]
    storage = nil

    for {
        time.Sleep(1 * time.Second)
    }
}

The code above will allocate about ~30mb of memory, and then won't release it. Why is that? How can I force go to release memory used by this slice? I sliced that slice and then nilled it.

The program I'm debugging is a simple HTTP input buffer: it appends all requests into large chunks, and sends these chunks over a channel to goroutine for processing. But problem is illustrated above - I can't get storage to release the memory and then eventually run out of memory.

Edit: as some people pointed out to similar question, no, it first doesn't work, second isn't what I'm asking for. The slice gets emptied, the memory does not.

like image 625
Aleksandr Makov Avatar asked Dec 19 '22 01:12

Aleksandr Makov


2 Answers

There are several things going on here.

The first one which is needed to be absorbed is that Go is a garbage-collected language; the actual algorithm of its GC is mostly irrelevant but one aspect of it is crucial to understand: it does not use reference counting, and hence there's no way to somehow make the GC immediately reclaim the memory of any given value whose storage is allocated on the heap. To recap it in more simple words, it's futile to do

s := make([]string, 10*100*100)
s = nil

as the second statement will indeed remove the sole reference to the slice's underlying memory but won't make the GC go and "mark" that memory as available for reuse.

This means two things:

  • You should know how the GC works. This explains how it works since v1.5 and up until now (v1.10 these days).
  • You should structure those of your algorythms which are memory-intensive in a way that reduces memory pressure.

The latter can be done in several ways:

  • Preallocate, when you have a sensible idea about how much to.

    In your example, you start with a slice of length 0, and then append to it a lot. Now, almost all library code which deals with growing memory buffers—the Go runtime included—deals with these allocations by 1) allocating twice the memory requested—hoping to prevent several future allocations, and 2) copies the "old" contents over, when it had to reallocate. This one is important: when reallocation happens, it means there's two memory regions now: the old one and the new one.

    If you can estimate that you may need to hold N elements on average, preallocate for them using make([]T, 0, N)— more info here and here. If you'll need to hold less than N elements, the tail of that buffer will be unused, and if you'll need to hold more than N, you'll need to reallocate, but on average, you won't need any reallocations.

  • Re-use your slice(s). Say, in your case, you could "reset" the slice by reslicing it to the zero length and then use it again for the next request. This is called "pooling", and in the case of mass-parallel access to such a pool, you could use sync.Pool to hold your buffers.

  • Limit the load on your system to make the GC be able to cope with the sustained load. A good overview of the two approaches to such limiting is this.

like image 73
kostix Avatar answered Jan 12 '23 12:01

kostix


In the program you wrote, it makes no sense to release memory because no part of code is requesting it any more.

To make a valid case, you have to request a new memory and release it inside the loop. Then you will observe that the memory consumption will stabilize at some point.

like image 40
Grzegorz Żur Avatar answered Jan 12 '23 10:01

Grzegorz Żur