Как сборка мусора работает с сегментом данных? - PullRequest
0 голосов
/ 05 апреля 2020

Для синтаксиса ниже Go в области действия функции f():

var fruits [5]string
fruits[0] = "Apple"

Ниже представлено представление памяти:

enter image description here

Насколько я понимаю, строка Apple сохраняется в сегменте данных , а остальные шесть строковых заголовков (ptr, длина) распределяются в сегменте стека .


Для приведенного ниже кода в области действия функции f():

numbers := [4]int{10, 20, 30, 40}

Память для {10, 20, 30, 40} выделяется в сегменте данных, но не в сегменте стека для области действия функции f .


enter image description here

Go сборщик мусора очищает сегмент кучи процесса.

Возврат из функции f(), сегмент стека Указатель очищает сегмент стека функции f()


Редактировать: Понять, семантика значения & семантика указателя в аспекте выделения строк,

Как очищается память сегмента данных (для строки Apple) после возврата из функции f?

Ответы [ 2 ]

3 голосов
/ 06 апреля 2020

Определение языка для Go не описывает действия в терминах сегментов, стеков, куч и т. Д. Таким образом, все это - детали реализации, которые могут измениться от одной реализации Go к другой.

В общем, хотя компиляторы Go выполняют анализ диапазона в реальном времени для переменных и используют escape-анализ , чтобы определить, следует ли выделять что-либо в памяти G-1095 * («куча») или в автоматически освобожденном хранилище («стек»). Строковые литералы могут, в зависимости от слишком большого количества вещей, быть выделенными во время компиляции в виде текста и ссылаться непосредственно оттуда, или копироваться в некоторую область данных, которая может быть либо heap-i sh, либо stack-i sh.

Предположим, для аргумента, что вы написали:

func f() {
    var fruits [5]string
    fruits[0] = "Apple"
}

Эта функция вообще ничего не делает, поэтому она просто исключается из сборки. Строковая константа "Apple" вообще нигде не появляется. Давайте добавим еще немного, чтобы он действительно существовал:

package main

import "fmt"

func f() {
    var fruits [5]string
    fruits[0] = "Apple"
    fmt.Println(fruits[0])
}

func main() {
    f()
    fmt.Println("foo")
}

Вот некоторая (подрезанная / очищенная) разборка main.f в полученном двоичном файле. Обратите внимание, что реализация почти наверняка будет отличаться в других версиях Go. Это было построено с Go 1.13.5 (для amd64).

main.f:
     mov    %fs:0xfffffffffffffff8,%rcx
     cmp    0x10(%rcx),%rsp
     jbe    2f

Все до вот пример: точка входа для функции проверяет, нужно ли ей вызывать среду выполнения, чтобы выделить больше стекового пространства, потому что она собирается использовать здесь 0x58 байт стекового пространства:

1:   sub    $0x58,%rsp
     mov    %rbp,0x50(%rsp)

Это конец шаблон: после следующих нескольких инструкций мы сможем вернуться с f с простым retq. Теперь мы освобождаем место в стеке для массива fruits, а также другое место, которое компилятор считает целесообразным по любой причине, и обновляем %rbp. Затем мы сохраняем заголовок строки в %(rsp) и %8%(rsp) для вызова convTstring в пакете runtime:

     lea    0x50(%rsp),%rbp
     lea    0x35305(%rip),%rax        # <go.string.*+0x24d> - the string is here
     mov    %rax,(%rsp)
     movq   $0x5,0x8(%rsp)            # this is the length of the string
     callq  408da0 <runtime.convTstring>
     mov    0x10(%rsp),%rax

Функция runtime.convTstring фактически выделяет пространство (16 байт на этом компьютере ) для другой копии заголовка строки, в «куче», затем копирует заголовок на место. Теперь эта копия готова для хранения в fruits[0] или в другом месте. Соглашение о вызовах для Go в x86_64 немного нечетное , поэтому возвращаемое значение равно 0x10(%rsp), которое мы теперь скопировали в %rax. Мы увидим, где это будет использовано в данный момент:

     xorps  %xmm0,%xmm0
     movups %xmm0,0x40(%rsp)

Эти инструкции обнуляют 16 байтов, начиная с 0x40(%rsp). Мне не ясно, для чего это нужно, тем более, что мы немедленно их перезаписываем.

     lea    0x11a92(%rip),%rcx        # <type.*+0x11140>
     mov    %rcx,0x40(%rsp)
     mov    %rax,0x48(%rsp)
     mov    0xd04a1(%rip),%rax        # <os.Stdout>
     lea    0x4defa(%rip),%rcx        # <go.itab.*os.File,io.Writer>
     mov    %rcx,(%rsp)
     mov    %rax,0x8(%rsp)
     lea    0x40(%rsp),%rax
     mov    %rax,0x10(%rsp)
     movq   $0x1,0x18(%rsp)
     movq   $0x1,0x20(%rsp)
     callq  <fmt.Fprintln>

Это, кажется, вызов fmt.Println: так как мы передаем значение интерфейса, мы должны упаковать его как тип и указатель на значение (возможно, именно поэтому сначала вызывается runtime.convTstring). У нас также есть os.stdout и его интерфейсный дескриптор, вставленный непосредственно в вызов здесь, посредством некоторой вставки (обратите внимание, что этот вызов идет непосредственно к fmt.Fprintln).

В любом случае мы передали заголовок строки, выделенный в runtime.convTstring здесь, чтобы функционировать fmt.Println.

     mov    0x50(%rsp),%rbp
     add    $0x58,%rsp
     retq
2:   callq  <runtime.morestack_noctxt>
     jmpq   1b

Вот как мы возвращаемся из функции - константы 0x50 и 0x58 зависят от того, сколько стекового пространства мы распределили - и после метки, которая начало функции может перейти к остальной части шаблона ввода функции.

В любом случае, смысл всего вышеперечисленного состоит в том, чтобы показать, что:

  • Пятибайтовая последовательность Apple вообще не выделяется во время выполнения. Вместо этого он существует в сегменте rodata, известном как go.string.*. Этот rodata -элемент фактически является программным текстом: операционная система помещает его в постоянную память, если это вообще возможно. Он просто отделен от исполняемых инструкций для организационных целей.

  • Массив fruits вообще никогда не использовался. Компилятор мог видеть, что, пока мы писали в него, мы не использовали , кроме одного вызова, поэтому он нам не понадобился.

  • Но заголовок строки, с помощью которого можно найти как длину строки, так и данные (в этом rodata сегменте), did получает выделенную кучу.

не нужно , поскольку fmt.Println не собирается сохранять этот указатель, но компилятор этого не заметил. В конце концов, среда выполнения g c освободит данные заголовка строки, выделенные в куче, если только программа не завершится полностью первой.

0 голосов
/ 05 апреля 2020

Память для {10, 20, 30, 40} выделяется в сегменте данных, но не в сегменте стека для области действия f.

Нет, она все еще будет выделена на стек. [4]int - это тип массива. Это тип значения. int является типом значения. Таким образом, весь массив будет в стеке, G C не будет иметь с ним дело.

Но если что-то выделяется в куче (я думаю, что вы подразумеваете под сегментом данных), тогда G C включился бы. Внутренние элементы - это деталь реализации и могут измениться в будущем, но, проще говоря, текущая версия будет просто начинаться с корней (глобальные переменные, стек, регистры) и отмечать все живые объекты. Все, что не помечено, будет собрано.

Редактировать: Если мы говорим о строковых литералах в частности. Строки работают подобно слайсам - это тип значения, структура с двумя полями - указатель на массив байтов и длину. Строковые литералы являются специальными, они фактически указывают на сегмент данных только для чтения, который содержит фактическое содержимое строки. Таким образом, во время выполнения распределение не происходит, и G C нечего собирать.

...