Подробный анализ побега из памяти

Go

Пример анализа странных явлений

Оригинальный почтовый адрес: https://gocn.io/question/1852

package main

import (
    "fmt"
)

func main(){
    s := []byte("")

    s1 := append(s, 'a')
    s2 := append(s, 'b')

    // 如果有此行,打印的结果是 a b,否则打印的结果是b b
    // fmt.Println(s1, "===", s2)
    fmt.Println(string(s1), string(s2))
}

Странное явление: если есть код на линии 14, результат, напечатанный на линии 15a b, иначе напечатанный результатb b, версия go, проанализированная в этой статье:

$ go version
go version go1.9.2 darwin/amd64

первоначальный анализ

В первую очередь разбираем, почему в напечатанном результате отсутствует 14 строкаb b, эта проблема относительно проста, если вы знакомы сsliceПринцип реализации, кратко проанализируемappendМожно сделать вывод о принципе реализации.

анализ структуры среза

Если вы знакомы с принципом среза, вы можете пропустить эту главу.

Во-первых, краткое понимание структуры срезаопределение структуры sliceсоответствующийruntimeСм. соответствующий исходный код пакета. См.: https://golang.org/src/runtime/slice.go.

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

var slice []intВнутренняя структура определяемой переменной следующая:

slice.array = nil
slice.len = 0
slice.cap = 0

Если мы объявим переменнуюslice := []int{}илиslice := make([]int, 0)Внутренняя структура выглядит следующим образом:

slice.array = 0xxxxxxxx  // 分配了地址
slice.len = 0
slice.cap = 18208800

При использованииmake([]byte, 5)Если определено, структура выглядит следующим образом:

При использованииs := s[2:4], Структура, как показано ниже:

после анализаsliceРеализация отражения de:Go Slices: usage and internals, также можно проанализировать в программе.sliceСоответствующая структура в отражении

// slice 对应的结构体
type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

// string 对应结构体
type StringHeader struct {
        Data uintptr
        Len  int
}

Следующие функции могут быть получены непосредственноsliceБазовый указатель:

func bytePointer(b []byte) unsafe.Pointer {
   // slice 的指针本质是*reflect.SliceHeader
  p := (*reflect.SliceHeader)(unsafe.Pointer(&b))
  return unsafe.Pointer(p.Data)
}

Приложение Принцип реализации

Добавить псевдокод реализации, код уже поддерживает его по умолчаниюsliceзаnilСлучай

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

appendПрототип функции выглядит следующим образом, где T — универсальный тип.

func append(s []T, x ...T) []T

Развернуть анализ

Чтобы облегчить анализ программы, мы добавляем в программу информацию о печати.Код и результаты выглядят следующим образом:

package main

import (
    "fmt"
)

func main() {
    s := []byte("")
    println(s) // 添加用于打印信息, println() print() 为go内置函数,直接输出到 stderr 无缓存

    s1 := append(s, 'a')
    s2 := append(s, 'b')

    // fmt.Println(s1, "===", s2)
    fmt.Println(string(s1), string(s2))
}

Результат работы программы следующий:

$ go run q.go
[0/32]0xc420045ef8
b b

После запуска результатаs := []byte("")После инициализации внутренняя структура выглядит следующим образом:

s.len = 0 
s.cap = 32
s.ptr = 0xc420045ef8

Давайте проанализируем, что происходит, когда вызываются следующие две строки кода:

s1 := append(s, 'a')
s2 := append(s, 'b')

s1 := append(s, 'a')Анализ кодовых вызовов:

// slice = s  data = `a`   slice.len = 0 slice.cap = 32      
func Append(slice, data []byte) []byte {
    l := len(slice) // l = 0

    // l = 0 len(data) = 1  cap(slice) = 32   1 + 1 > 32 false
    if l + len(data) > cap(slice) { 
        newSlice := make([]byte, (l+len(data))*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    // l = 0 len(data) = 1
    slice = slice[0:l+len(data)] // slice = slice[0:1]
    copy(slice[l:], data)  // 调用变成: copy(slice[0:], 'a') 
    return slice // 由于未涉及到重分配,因此返回的还是原来的 slice 对象
}

s2 := append(s, 'b')Анализ точно такой же.

упрощатьapendПуть обработки функции, когда нетsliceВ случае перераспределения памяти разверните анализ напрямую:

s1 := append(s, 'a')
s2 := append(s, 'b')

Эквивалентно

s1 := copy(s[0:], 'a')
s2 := copy(s[0:], 'b') // 直接覆盖了上的赋值

Основываясь на приведенном выше анализе, вывод кода можно хорошо объяснить.b bСлучай. Но как избежать такой ситуации? Проблема в этом утверждении

s := []byte("")

Выполнение заявленияs.len = 0 s.cap = 32, приводит кappendРабота не работает должным образом, так как его использовать нормально? просто поставьs.len = s.cap = 0приведет кsliceсуществуетappendЭтого можно избежать путем перераспределения в .

Правильное написание должно быть:

func main() {
    // Notice []byte("") ->  []byte{}    或者  var s []byte
    s := []byte{}  

    s1 := append(s, 'a')
    s2 := append(s, 'b')

    // fmt.Println(s1, "===", s2)
    fmt.Println(string(s1), string(s2))
}

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

Углубленный анализ

Итак, поскольку ошибка появилась вs := []byte("")В этом предложении, то почему это предложение приводит кs.cap = 32Шерстяная ткань? Какова логика этого утверждения?

s := []byte("")Эквивалентно следующему коду:

// 初始化字符串
str := ""

// 将字符串转换成 []byte
s := []byte(str)

на языке гоs := []byte(str)Основополагающий факт называетсяstringtoslicebyteРеализована, эта функция есть в GOruntimeв сумке.

const tmpStringBufSize = 32

type tmpBuf [tmpStringBufSize]byte

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
    var b []byte
    // 如果字符串 s 的长度内部长度不超过 32, 那么就直接分配一个 32 直接的大小
    if buf != nil && len(s) <= len(buf) { 
        *buf = tmpBuf{}
        b = buf[:len(s)]
    } else {
        b = rawbyteslice(len(s))
    }
    copy(b, s)
    return b
}

Если размер строки не превышает длины 32, по умолчанию выделяется buf длиной 32, что также является причиной, которую мы проанализировали выше.s.cap = 32происхождение.

Пока мы еще не проанализировали проблемуfmt.Println(s1, "===", s2)Причина, по которой эта закомментированная печать работает нормально? Так какова ситуация в итоге?

окончательный анализ

Наконец, мы включаем волшебный переключательfmt.Println(s1, "===", s2)Провести оглашение последней тайны:

package main

import (
    "fmt"
)

func main() {
    s := []byte("")
    println(s) // 添加用于打印信息

    s1 := append(s, 'a')
    s2 := append(s, 'b')

    fmt.Println(s1, "===", s2)
    fmt.Println(string(s1), string(s2))
}
$ go run q.go
[0/0]0x115b820   # 需要注意 s.len = 0 s.cap = 0
[97] === [98]    # 取消了打印的注释
a b              # 打印一切正常
$ go run -gcflags '-S -S' q.go
....
    0x0032 00050 (q.go:8)   MOVQ    $0, (SP)
    0x003a 00058 (q.go:8)   MOVQ    $0, 8(SP)
    0x0043 00067 (q.go:8)   MOVQ    $0, 16(SP)
    0x004c 00076 (q.go:8)   PCDATA  $0, $0
    0x004c 00076 (q.go:8)   CALL    runtime.stringtoslicebyte(SB)
    0x0051 00081 (q.go:8)   MOVQ    32(SP), AX
    0x0056 00086 (q.go:8)   MOVQ    AX, "".s.len+96(SP)
    0x005b 00091 (q.go:8)   MOVQ    40(SP), CX
    0x0060 00096 (q.go:8)   MOVQ    CX, "".s.cap+104(SP)
    0x0065 00101 (q.go:8)   MOVQ    24(SP), DX
    0x006a 00106 (q.go:8)   MOVQ    DX, "".s.ptr+136(SP)

....

В результате анализа обнаружено, что основной вызов по-прежнемуruntime.stringtoslicebyte(), но поведение изменилосьs.len = s.cap = 0, очевидно из-заfmt.Println(s1, "===", s2)Появление линии приводит кs := []byte("")Ситуация с выделением памяти изменилась.

Мы можем проанализировать это с помощью инструмента выделения памяти, предоставляемого go build:

$ go build -gcflags "-m -m" q.go
# command-line-arguments
./q.go:7:6: cannot inline main: non-leaf function
./q.go:14:13: s1 escapes to heap
./q.go:14:13:   from ... argument (arg to ...) at ./q.go:14:13
./q.go:14:13:   from *(... argument) (indirection) at ./q.go:14:13
./q.go:14:13:   from ... argument (passed to call[argument content escapes]) at ./q.go:14:13
./q.go:8:13: ([]byte)("") escapes to heap
./q.go:8:13:    from s (assigned) at ./q.go:8:4
./q.go:8:13:    from s1 (assigned) at ./q.go:11:5
./q.go:8:13:    from s1 (interface-converted) at ./q.go:14:13
./q.go:8:13:    from ... argument (arg to ...) at ./q.go:14:13
./q.go:8:13:    from *(... argument) (indirection) at ./q.go:14:13
./q.go:8:13:    from ... argument (passed to call[argument content escapes]) at ./q.go:14:13

в приведенном выше выводеs1 escapes to heapи([]byte)("") escapes to heapпоказывает, что из-заfmt.Println(s1, "===", s2)Введение кода привело к изменениям в модели назначения переменных. Проще говоря, он убегает из стека в кучу. Анализ побегов памяти будет подробно представлен в следующих главах. На данный момент общая идея уже есть, но как мы можем проверить это на уровне кода?runtime.stringtoslicebyteместо для начала. Файл, вызываемый поиском, находится вcmd/compile/internal/gc/walk.go

О строке [] следующий код называется байтовым анализом

    case OSTRARRAYBYTE:
        a := nodnil()  // 分配到堆上的的默认行为

        if n.Esc == EscNone {
            // Create temporary buffer for slice on stack.
            t := types.NewArray(types.Types[TUINT8], tmpstringbufsize)

            a = nod(OADDR, temp(t), nil)  // 分配在栈上,大小为32
        }

        n = mkcall("stringtoslicebyte", n.Type, init, a, conv(n.Left, types.Types[TSTRING]))

OSTRARRAYBYTE Определение

OSTRARRAYBYTE    // Type(Left) (Type is []byte, Left is a string)

в приведенном выше кодеn.Esc == EscNoneУсловный анализ показывает, что способ инициализации различен, когда происходит выход из памяти и когда не происходит выхода из памяти.Определение EscNone:

EscNone           // Does not escape to heap, result, or parameters.

Благодаря приведенному выше анализу мы, наконец, нашли высшую тайну магии. Проанализированная выше версия языка go основана на 1.9.2.Механизм выделения памяти для разных языков go может быть разным.За подробностями обращайтесь к более подробному анализу моего коллеги.Ловушка преобразования строки в []byte в Go.md

Перейти к управлению памятью

Язык Go может автоматически управлять памятью, избегая проблем с самоуправлением памятью в языке C, но в то же время инкапсулируя управление памятью и повторяя детали кода, что также потенциально увеличивает сложность отладки и оптимизации системы. В то же время автоматическое управление памятью тоже очень сложная штука.Например, многоуровневые вызовы функций, вызовы замыканий, множественные назначения структур или пайпов, слайсов, вызовы MAP, CGO и т.д. механизм оптимизации управления дает сбой и вырождается в исходное состояние управления; действующая стратегия восстановления памяти (GC) также постоянно оптимизирует процесс. Начиная с первой версии Golang, GC подвергался наибольшей критике со стороны всех, но выпуск каждой версии в основном сопровождается GC улучшение. Некоторые из наиболее важных изменений перечислены ниже.

  • v1.1 STW
  • v1.3 Пометка STW, параллельная развертка
  • v1.5 Метод трехцветной маркировки
  • v1.8 hybrid write barrier

Основы разминки:How do I know whether a variable is allocated on the heap or the stack?

Анализ побега-Анализ побега

Для более глубокого и подробного понимания рекомендуется прочитать4 сообщения Уильяма Кеннеди

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

package main

func main() {
    str := GetString()
    _ = str
}

func GetString() *string {
    var s string
    s = "hello"
    return &s
}

переменная в строке 10s = "hello"несмотря на заявлениеGetString()В функции, но вmainВозвращенная переменная все еще может быть доступна в функции; такое поведение, которое, как локальная переменная, определенная в функции, может пробиться через свой собственный объем и доступна внешне, называется Escape, то есть переменная выделяется на кучу с помощью выхода, который можно выполнить по границам. Обмен данными.

Escape AnalysisТехнология существует для этого сценария;Escape Analysisтехнологии, компилятор будет анализировать код на этапе компиляции.Когда он обнаружит, что переменные в текущей области видимости не выходят за пределы области действия функции, они будут автоматически размещены в области видимости.stack, иначе выделяется вheapначальство. Освобождение памяти Go также нацелено на объекты в куче. идти на языкеEscape Analysisофициалов пока не виделspecдокументации, поэтому многие функции требуют пробного кода и анализа, чтобы сделать выводы и перейти кEscape AnalysisСуществует множество реализацийнесовершенное место.

stack allocation is cheap and heap allocation is expensive.

Реализация escape-анализа языка Go

Читать больше рекомендаций по памятиAllocation efficiency in high-performance Go services

2.go

package main

import "fmt"

func main() {
        x := 42
        fmt.Println(x)
}

Флаг в Go Good Tool-gcflags '-m'Резюме ситуации, которая может быть использована для анализа побегов памяти. Можно предоставить до 4 "-m". Чем больше m, тем более подробный анализ. В общем, мы можем использовать анализ двух m.

$ go build -gcflags '-m -l' 2.go
# command-line-arguments
./2.go:7:13: x escapes to heap
./2.go:7:13: main ... argument does not escape

# -l disable inline, 也可以调用的函数前添加注释 
$ go build -gcflags '-m -m -l' 2.go
# command-line-arguments
./2.go:7:13: x escapes to heap
./2.go:7:13:    from ... argument (arg to ...) at ./2.go:7:13
./2.go:7:13:    from *(... argument) (indirection) at ./2.go:7:13
./2.go:7:13:    from ... argument (passed to call[argument content escapes]) at ./2.go:7:13
./2.go:7:13: main ... argument does not escape

в примере вышеx escapes to heapпоказывает, что переменнаяxПеременная убегает в кучу. в-lзначит не включенinlineВызовы шаблонов, иначе это усложнит анализ, также можно добавить комментарии над функцией//go:noinlineВстроенные вызовы функций запрещены. Что касается вызоваfmt.Println()зачем вызыватьx escapes to heap, вы можете обратиться кIssue #19720иIssue #8618, для вышеуказанногоfmt.Println()Мы можем выполнить простой симуляционный тест со следующим кодом, и эффект в основном такой же:

package main

type pp struct {
    arg interface{}
}

func MyPrintln(a ...interface{}) {
    Fprintln(a...)
}

func Fprintln(a ...interface{}) (n int, err error) {
    pp := new(pp)
    pp.arg = a  // 此处导致了内存的逃逸
    return
}

func main() {
    x := 42
    MyPrintln(x)
}

Результаты анализа выхода из памяти следующие:

$ go build -gcflags '-m -m -l' 3.go
# command-line-arguments
./3.go:13:9: a escapes to heap
./3.go:13:9:    from pp.arg (star-dot-equals) at ./3.go:13:9
./3.go:11:45: leaking param: a
./3.go:11:45:   from a (interface-converted) at ./3.go:13:9
./3.go:11:45:   from pp.arg (star-dot-equals) at ./3.go:13:9
./3.go:12:11: Fprintln new(pp) does not escape
./3.go:7:21: leaking param: a
./3.go:7:21:    from a (passed to call[argument escapes]) at ./3.go:8:10
./3.go:19:11: ... argument escapes to heap
./3.go:19:11:   from ... argument (passed to call[argument escapes]) at ./3.go:19:11
./3.go:19:11: x escapes to heap
./3.go:19:11:   from ... argument (arg to ...) at ./3.go:19:11
./3.go:19:11:   from ... argument (passed to call[argument escapes]) at ./3.go:19:11

Для общего анализа побега см.: Http://www.agardner.me/golang/grast/collection/gc/escape/analysys/2015/10/18/go-escape-analysis.html

Основные причины следующие: ПеременныеxХотя он имеет тип int, он передается в функциюMyPrintlnфункция преобразуется вinterface{}типа потому чтоinterface{}Тип содержит адрес к данным, поэтомуxпередается в функциюMyPrintlnВо время процесса осуществляется процесс перераспределения памяти, за счетpp.arg = aСсылка на назначение поля в структуре приводит к тому, что последующие переменные перемещаются в кучу. Если вышеpp.arg = aЗакомментируйте, памяти не будет.

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

Кратко, есть следующие категории:

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

  2. slicesЗначение — это указатель на указатель или поле указателя. Один пример похож[] *stringтип. это всегда приводит кsliceпобег. Несмотря на то, что базовый массив хранения среза может по-прежнему находиться в стеке, ссылки на данные перемещаются в кучу.

  3. sliceтак какappendработа превышает свои возможности, что приводит кsliceпереназначить. В этом случае, поскольку во время компиляцииsliceбудет размещен в стеке, когда известен начальный размер. еслиsliceБазовое хранилище должно масштабироваться только на основе данных времени выполнения, тогда оно будет выделено в куче.

  4. Вызовите метод типа интерфейса. Способ вызовов типов интерфейсов динамически отправляются - бетонная реализация фактически используемая может быть определена только во время выполнения. Рассмотрим тип интерфейса какio.Readerпеременная р. правильноr.Read(b)вызов приведет кrзначения и байтовые срезыbпоследующие побеги и, следовательно, размещаются в куче. Ссылка http://npat-efault.github.io/programming/2016/10/10/escape-analysis-and-interfaces.html

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

О указателях

Что касается использования указателей, то в большинстве случаев на нас влияет посылка: «Процесс передачи указателя не включает копирование базовых данных, поэтому он более эффективен», и в целом это верно.

Однако, поскольку доступ к указателю является косвенной адресацией, то есть после доступа к адресу, сохраненному указателем, необходимо выполнить другой доступ по сохраненному адресу, чтобы получить данные, на которые указывает указатель. Также необходимо оценивать ситуацию nil для предотвращения возникновения паники.Что еще более важно,большинство адресов,на которые указывают указатели,хранятся в куче.В случае восстановления памяти наличие указателей может заставить программу выполнять лучше скидка. Кроме того, из-за непрямого доступа к указателю оптимизация кеша также будет недействительной.Locality of reference, производительность копирования небольшого количества данных в кэш в основном эквивалентна производительности доступа по указателю.

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

"сборщик мусора будет пропускать области памяти, которые, как он может доказать, не содержат указателей"

Проще говоря, если в структуре, выделенной в куче, будет меньше указателей, то механизм рециркуляции будет относительно простым, а эффективность рециркуляции должна быть повышена, необходимо понимать алгоритм go рециркуляции для соответствующих тестов. СДЕЛАТЬ

О конвертере интерфейсов

Для реализации интерфейса см.:Go Data Structures: Interfaces Go interfaces: static vs dynamic binding

На рисунке выше показана структура данных объекта Binary, преобразованного в интерфейс Stringer. Проверьте, совпадают ли типыs.tab->typeВот и все.

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

type Stringer interface {
    String() string
}

if v, ok := any.(Stringer); ok {
        return v.String()
 }

оItabРасчет конструкции, за счет (interface,type), компилятор или компоновщик go не может вычислить соответствующую взаимосвязь между ними во время компиляции, и даже если она может быть вычислена, большинство соответствующих взаимосвязей могут быть неприменимы на практике; поэтому компилятор go будет используется при компиляции дляinterfaceиtypeМетод генерирует ассоциированную структуру описания, которая записывается отдельно.interfaceиtypeСоответствующий набор методов языка go будет вtypeФактическое динамическое преобразование вinterfaceВ ходе процессаinterafceМетод, определенный вtypeСравнивайте и ищите один за другим и улучшайтеItabструктура и воляItabСтруктура кэшируется для повышения производительности.

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

Следующий образец ссылки: http://npat-efault.github.io/programming/2016/10/10/escape-analysis-and-interfaces.html

package main

// go build -gcflags '-m -m -l' 5.go

type S struct {
    s1 int
}

func (s *S) M1(i int) { s.s1 = i }

type I interface {
    M1(int)
}

func main() {
    var s1 S // this escapes
    var s2 S // this does not

    f1(&s1)
    f2(&s2)
}

func f1(s I)  { s.M1(42) }
func f2(s *S) { s.M1(42) }

Анализ побега подтверждает:

go build -gcflags '-m -m -l' 5.go
# command-line-arguments
./5.go:9:18: (*S).M1 s does not escape
./5.go:23:11: leaking param: s
./5.go:23:11:   from s.M1(42) (receiver in indirect call) at ./5.go:23:21
./5.go:24:12: f2 s does not escape
./5.go:19:5: &s1 escapes to heap
./5.go:19:5:    from &s1 (passed to call[argument escapes]) at ./5.go:19:4
./5.go:19:5: &s1 escapes to heap
./5.go:19:5:    from &s1 (interface-converted) at ./5.go:19:5
./5.go:19:5:    from &s1 (passed to call[argument escapes]) at ./5.go:19:4
./5.go:16:6: moved to heap: s1
./5.go:20:5: main &s2 does not escape
<autogenerated>:1:0: leaking param: .this
<autogenerated>:1:0:    from .this.M1(.anon0) (receiver in indirect call) at <autogenerated>:1:0

Анализ тестов производительности:

package main_test

import "testing"

// go test -bench . --benchmem -gcflags "-N -l" 5_test.go

type S struct {
    s1 int
}

func (s *S) M1(i int) {
    s.s1 = i
}

type I interface {
    M1(int)
}

func f1(s I)  { s.M1(86) }
func f2(s *S) { s.M1(86) }

func BenchmarkTestInterface(b *testing.B) {
    var s1 S
    for i := 0; i < b.N; i++ {
        f1(&s1)
    }
}

func BenchmarkTestNoInterface(b *testing.B) {
    var s2 S
    for i := 0; i < b.N; i++ {
        f2(&s2)
    }
}

Запретить использованиеinlineОтчет о производительности вызова функции способа:

# 禁止使用 inline
$ go test -bench . --benchmem -gcflags "-N -l" 5_test.go
goos: darwin
goarch: amd64
BenchmarkTestInterface-8        300000000            4.50 ns/op        0 B/op          0 allocs/op
BenchmarkTestNoInterface-8      500000000            3.80 ns/op        0 B/op          0 allocs/op
PASS
ok      command-line-arguments  4.094s

включеноinlineОтчет о производительности вызова функции:

# 如果启用了 inline,性能差别非常明显
$ go test -bench . --benchmem  5_test.go
goos: darwin
goarch: amd64
BenchmarkTestInterface-8        500000000            3.45 ns/op        0 B/op          0 allocs/op
BenchmarkTestNoInterface-8      2000000000           0.29 ns/op        0 B/op          0 allocs/op
PASS
ok      command-line-arguments  2.685s

О фрагментах

Поскольку слайсы обычно используются в сценариях передачи функций, а слайсыappendЭто может быть связано с перераспределением памяти.Если размер слайса не может быть подтвержден во время компиляции или размер превышает предел стека, в большинстве случаев он будет выделен в кучу.

Проверка размера

package main

func main() {
    s := make([]byte, 1, 1*1024)
    _ = s
}
$ go build -gcflags "-m -m" slice_esc.go
# command-line-arguments
./slice_esc.go:3:6: can inline main as: func() { s := make([]byte, 1, 1 * 1024); _ = s }
./slice_esc.go:4:11: main make([]byte, 1, 1 * 1024) does not escape

еслиsliceЕсли размер превышает 64 КБ, он будет выделен в куче (перейдите к 1.9.2).

package main

func main() {
    s := make([]byte, 1, 64*1024) // 64k
    _ = s
}
$ go build -gcflags "-m -m" slice_esc.go
# command-line-arguments
./slice_esc.go:3:6: can inline main as: func() { s := make([]byte, 1, 64 * 1024); _ = s }
./slice_esc.go:4:11: make([]byte, 1, 64 * 1024) escapes to heap
./slice_esc.go:4:11:    from make([]byte, 1, 64 * 1024) (too large for stack) at ./slice_esc.go:4:11

Проверка среза типа указателя

package main

func main() {
    s := make([]*string, 1, 100)
    str := "hello"
    s = append(s, &str)
    _ = s
}
$ go build -gcflags "-m -m -l" slice_esc.go
# command-line-arguments
./slice_esc.go:6:16: &str escapes to heap
./slice_esc.go:6:16:    from append(s, &str) (appended to slice) at ./slice_esc.go:6:12
./slice_esc.go:5:9: moved to heap: str
./slice_esc.go:4:11: main make([]*string, 1, 100) does not escape

для экономии в[]*stringСтроки выделяются непосредственно в куче.

package main

import "math/rand"

func main() {
    randSize := rand.Int()
    s := make([]*string, 0, randSize)
    str := "hello"
    s = append(s, &str)
    _ = s
}
$ go build -gcflags "-m -m -l" slice_esc.go
# command-line-arguments
./slice_esc.go:7:11: make([]*string, 0, randSize) escapes to heap
./slice_esc.go:7:11:    from make([]*string, 0, randSize) (too large for stack) at ./slice_esc.go:7:11
./slice_esc.go:9:16: &str escapes to heap
./slice_esc.go:9:16:    from append(s, &str) (appended to slice) at ./slice_esc.go:9:12
./slice_esc.go:8:9: moved to heap: str

так какs := make([]*string, 0, randSize)Размер не может быть определен при компиляции, поэтому он уходит в кучу.

Ссылаться на

  1. Анализ выхода из памяти Golang
  2. Углубленный анализ базовой реализации Slice in Go ***
  3. Понимание Go Memory Escapes с точки зрения C
  4. Сравнение строки golang и []byte
  5. Go Slices: usage and internals
  6. Where is append() implementation?
  7. SliceTricks ***
  8. Variadic func changes []byte(s) cap #24972
  9. spec: clarify that conversions to slices don't guarantee slice capacity? #24163
  10. Golang escape analysis ***
  11. Go Escape Analysis Flaws
  12. Escape Analysis for Java
  13. Language Mechanics On Escape Analysis китайский язык китайский 2
  14. Allocation efficiency in high-performance Go services ***
  15. Profiling Go Programs
  16. Github.com/mushroom sir ...
  17. the-go-programming-language-report
  18. https://golang.org/doc/faq
  19. Инвентаризация на конец года! Суперценные статьи Golang 2017 года
  20. Анатомия сборки мусора Голанга
  21. Углубитесь в сборку мусора Голанга