Разве мусор собирает части ломтиков?

Если я реализую такую ​​очередь...

package main

import(
    "fmt"
)

func PopFront(q *[]string) string {
    r := (*q)[0]
    *q = (*q)[1:len(*q)]
    return r
}

func PushBack(q *[]string, a string) {
    *q = append(*q, a)
}

func main() {
    q := make([]string, 0)

    PushBack(&q, "A")
    fmt.Println(q)
    PushBack(&q, "B")
    fmt.Println(q)
    PushBack(&q, "C")
    fmt.Println(q)

    PopFront(&q)
    fmt.Println(q)
    PopFront(&q)
    fmt.Println(q)      
}

... Я получаю массив ["A", "B", "C"], у которого нет срезов, указывающих на первые два элемента. Поскольку "начальный" указатель среза никогда не может быть уменьшен (AFAIK), эти элементы никогда не могут быть доступны.

Is Go сборщик мусора достаточно умный, чтобы освободить их?

Ответ 1

Срезы - это просто дескрипторы (небольшие структурные структуры данных), которые, если на них нет ссылок, будут должным образом собираться мусором.

С другой стороны, базовый массив для среза (на который указывает дескриптор) распределяется между всеми срезами, созданными путем его повторения: цитата из Спецификации языка Go: Типы срезов:

После инициализации срез всегда ассоциируется с базовым массивом, который содержит его элементы. Следовательно, срез совместно использует хранилище со своим массивом и с другими срезами того же массива; напротив, отдельные массивы всегда представляют собой разные хранилища.

Поэтому, если существует хотя бы один фрагмент или переменная, содержащая массив (если фрагмент был создан путем разбиения массива), он не будет собирать мусор.

Официальное выражение об этом:

Сообщение в блоге Go Slices: использование и внутреннее устройство Автор Andrew Gerrand четко заявляет следующее:

Как упоминалось ранее, повторная нарезка фрагмента не делает копию базового массива. Полный массив будет храниться в памяти до тех пор, пока на него больше не будут ссылаться. Иногда это может привести к тому, что программа сохранит все данные в памяти, когда потребуется только небольшой их фрагмент.

...

Поскольку срез ссылается на исходный массив, , пока срез удерживается вокруг сборщика мусора, не может освободить массив.

Вернуться к вашему примеру

Хотя базовый массив не будет освобожден, обратите внимание, что если вы добавляете новые элементы в очередь, встроенная функция append может иногда выделять новый массив и копировать текущие элементы в новый - но при копировании будут копироваться только элементы части, а не весь основной массив! Когда происходит такое перераспределение и копирование, "старый" массив может быть подвергнут сборке мусора, если на него не существует никакой другой ссылки.

Также еще одна очень важная вещь заключается в том, что если элемент извлекается спереди, срез будет переназначен и не будет содержать ссылку на вытесненный элемент, но поскольку базовый массив все еще содержит это значение, значение также останется в памяти (не просто массив). Рекомендуется, чтобы всякий раз, когда элемент извлекался или удалялся из вашей очереди (слайс/массив), всегда обнулял его (его соответствующий элемент в слайсе), чтобы значение не оставалось в памяти без необходимости. Это становится еще более важным, если ваш слайс содержит указатели на большие структуры данных.

func PopFront(q *[]string) string {
    r := (*q)[0]
    (*q)[0] = ""  // Always zero the removed element!
    *q = (*q)[1:len(*q)]
    return r
}

Об этом упоминается Вики-страница Slice Tricks:

Удалить без сохранения заказа

a[i] = a[len(a)-1] 
a = a[:len(a)-1]

NOTE Если типом элемента является указатель или структура с полями указателя, которые необходимо собирать мусором, то вышеупомянутые реализации Cut и Delete имеют потенциальную проблему утечки памяти: некоторые элементы со значениями все еще ссылаются на срез a и, следовательно, не могут быть собраны.

Ответ 2

Простой вопрос, простой ответ: Нет. (Но если вы продолжаете толкать срез, в какой-то момент переполняете его базовый массив, тогда неиспользуемые элементы станут доступными для освобождения.)

Ответ 3

Вопреки тому, что я читаю, Голанг наверняка собирает мусор, по крайней мере, неиспользуемые фрагменты начальных разделов. Следующий контрольный пример предоставляет доказательства.

В первом случае срез устанавливается на срез [: 1] в каждой итерации. В случае сравнения он пропускает этот шаг.

Во втором случае объем памяти, использованный в первом случае, уменьшается. Но почему?

func TestArrayShiftMem(t *testing.T) {
    slice := [][1024]byte{}

    mem := runtime.MemStats{}
    mem2 := runtime.MemStats{}
    runtime.GC()
    runtime.ReadMemStats(&mem)

    for i := 0; i < 1024*1024*1024*1024; i++ {
        slice = append(slice, [1024]byte{})
        slice = slice[1:]
        runtime.GC()

        if i%(1024) == 0 {
            runtime.ReadMemStats(&mem2)
            fmt.Println(mem2.HeapInuse - mem.HeapInuse)
            fmt.Println(mem2.StackInuse - mem.StackInuse)
            fmt.Println(mem2.HeapAlloc - mem.HeapAlloc)

        }
    }
}


func TestArrayShiftMem3(t *testing.T) {
    slice := [][1024]byte{}

    mem := runtime.MemStats{}
    mem2 := runtime.MemStats{}
    runtime.GC()
    runtime.ReadMemStats(&mem)

    for i := 0; i < 1024*1024*1024*1024; i++ {
        slice = append(slice, [1024]byte{})
        // slice = slice[1:]
        runtime.GC()

        if i%(1024) == 0 {
            runtime.ReadMemStats(&mem2)
            fmt.Println(mem2.HeapInuse - mem.HeapInuse)
            fmt.Println(mem2.StackInuse - mem.StackInuse)
            fmt.Println(mem2.HeapAlloc - mem.HeapAlloc)

        }
    }
}

Выходной тест1:

go test -run=.Mem -v .
...
0
393216
21472
^CFAIL  github.com/ds0nt/cs-mind-grind/arrays   1.931s

Вывод Test3:

go test -run=.Mem3 -v .
...
19193856
393216
19213888
^CFAIL  github.com/ds0nt/cs-mind-grind/arrays   2.175s

Если вы отключите сборку мусора в первом тесте, действительно, память взлетит до небес. Полученный код выглядит следующим образом:

func TestArrayShiftMem2(t *testing.T) {
    debug.SetGCPercent(-1)

    slice := [][1024]byte{}

    mem := runtime.MemStats{}
    mem2 := runtime.MemStats{}
    runtime.GC()
    runtime.ReadMemStats(&mem)
    // 1kb per

    for i := 0; i < 1024*1024*1024*1024; i++ {
        slice = append(slice, [1024]byte{})
        slice = slice[1:]
        // runtime.GC()

        if i%(1024) == 0 {
            fmt.Println("len, cap:", len(slice), cap(slice))
            runtime.ReadMemStats(&mem2)
            fmt.Println(mem2.HeapInuse - mem.HeapInuse)
            fmt.Println(mem2.StackInuse - mem.StackInuse)
            fmt.Println(mem2.HeapAlloc - mem.HeapAlloc)

        }
    }
} 

Ответ 4

Нет. На момент написания этой статьи сборщик мусора Go не был достаточно умен, чтобы собирать начало базового массива в срез, , даже если он недоступен.

Как уже упоминалось здесь другими, срез (под капотом) представляет собой структуру ровно из трех вещей: указатель на базовый массив, длину среза (значения доступны без изменения) и емкость среза (значения, доступные с помощью reslicing). В блоге Go внутренняя часть слайса обсуждается подробно. Вот еще одна статья, которая мне нравится о макетах памяти Go.

Когда вы срезаете и обрезаете хвостовую часть tail end среза, становится очевидным (при понимании внутренних элементов), что базовый массив, указатель на базовый массив и емкость среза остаются неизменными; обновляется только поле длины среза. Когда вы повторно срезаете и обрезаете начало среза, вы действительно меняете указатель на базовый массив вместе с длиной и емкостью. В этом случае, как правило, неясно (основываясь на моих показаниях), почему GC не очищает эту недоступную часть базового массива, потому что вы не можете повторно разрезать массив, чтобы получить к нему доступ снова. Я предполагаю, что базовый массив обрабатывается как один блок памяти с точки зрения GC. Если вы можете указать на любую часть базового массива, все это не подходит для освобождения.

Я знаю, о чем ты думаешь... как настоящий ученый-компьютерщик, тебе могут понадобиться доказательства. Я побалую тебя:

https://goplay.space/#tDBQs1DfE2B

Как упоминалось другими и как показано в примере кода, использование append может вызвать перераспределение и копирование базового массива, что позволяет собирать мусор в старом базовом массиве.