Garbage Collection ist eine sehr praktische Funktion von Go – die automatische Speicherverwaltung macht den Code sauberer und reduziert gleichzeitig die Möglichkeit von Speicherlecks. Da die Garbage Collection jedoch ein regelmäßiges Anhalten des Programms erfordert, um nicht verwendete Objekte zu sammeln, entsteht zwangsläufig zusätzlicher Overhead. Der Go-Compiler ist intelligent und entscheidet automatisch, ob eine Variable zur späteren einfachen Erfassung auf dem Heap oder direkt dem Stapelspeicher der Funktion zugewiesen werden soll. Bei Variablen, die auf dem Stapel zugewiesen sind, besteht der Unterschied zu Variablen, die auf dem Heap zugewiesen sind, darin, dass bei der Rückkehr der Funktion der Stapelspeicherplatz zerstört wird, sodass die Variablen auf dem Stapel direkt und ohne zusätzlichen Aufwand für die Speicherbereinigung zerstört werden.
Gos Escape-Analyse ist einfacher als HotSpot der Java Virtual Machine. Die Grundregel lautet: Wenn ein Verweis auf eine Variable von der Funktion zurückgegeben wird, in der sie deklariert ist, erfolgt ein „Escape“. Da er möglicherweise von anderen Inhalten außerhalb der Funktion verwendet wird, muss er auf dem Heap zugewiesen werden. Die folgenden Situationen werden komplizierter:
- Funktionen, die andere Funktionen aufrufen
- Elementvariablen als Strukturen referenzieren
- Slicing und Mapping
- Cgo-Zeiger auf Variablen
Um die Escape-Analyse zu implementieren, erstellt Go während der Kompilierungsphase Funktionsaufrufdiagramm, während der Prozess der Eingabeparameter und Rückgabewerte verfolgt wird. Wenn eine Funktion nur auf einen Parameter verweist, die Referenz die Funktion jedoch nicht zurückgibt, wird die Variable nicht maskiert. Wenn eine Funktion eine Referenz zurückgibt, die Referenz jedoch von einer anderen Funktion auf dem Stapel freigegeben wird oder die Referenz nicht zurückgibt, gibt es kein Escape. Um mehrere Beispiele zu demonstrieren, können Sie beim Kompilieren den Parameter -gcflags '-m'
hinzufügen. Dieser Parameter gibt die detaillierten Informationen der Escape-Analyse aus: -gcflags '-m'
参数,这个参数会打印逃逸分析的详细信息:
package main type S struct {} func main() { var x S _ = identity(x) } func identity(x S) S { return x }
你可以执行go run -gcflags '-m -l'
(注:原文中略了go代码文件名)来编译这个代码,-l参数是防止函数identity
被内联(换个时间再讨论内联这个话题)。你将会看到没有任何输出!Go使用值传递,所以main
函数中的x
这个变量总是会被拷贝到函数identity
的栈空间。通常情况下没有使用引用的代码都是通过栈空间来分配内存。所以不涉及逃逸分析。下面试下困难一点的:
package main type S struct {} func main() { var x S y := &x _ = *identity(y) } func identity(z *S) *S { return z }
其对应的输出是:
./escape.go:11: leaking param: z to result ~r1 ./escape.go:7: main &x does not escape
第一行显示了变量z
的“流经”:入参直接作为返回值返回了。但是函数identity
没有取走z
这个引用,所以没有发生变量逃逸。在main
函数返回后没有任何对x
的引用存在,所以x
这个变量可以在main
函数的栈空间进行内存分配。
第三次实验:
package main type S struct {} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
其输出为:
./escape.go:10: moved to heap: z ./escape.go:11: &z escapes to heap
现在有了逃逸发生。记住Go是值传递的,所以z
是对变量x
的一个拷贝。函数ref
返回一个对z
的引用,所以z
不能在栈中分配,否则当函数ref
返回时,引用会指向何处呢?于是它逃逸到了堆中。其实执行完ref
返回到main
函数中后,main
函数丢弃了这个引用而不是解除引用,但是Go的逃逸分析还不够机智去识别这种情况。
值得注意的是,在这种情况下,如果我们不停止引用,编译器将内联ref
。
如果结构体成员定义的是引用又会怎样呢?
package main type S struct { M *int } func main() { var i int refStruct(i) } func refStruct(y int) (z S) { z.M = &y return z }
其输出为:
./escape.go:12: moved to heap: y ./escape.go:13: &y escapes to heap
在这种情况下,尽管引用是结构体的成员,但Go仍然会跟踪引用的流向。由于函数refStruct
接受引用并将其返回,因此y
必须逃逸。对比如下这个例子:
package main type S struct { M *int } func main() { var i int refStruct(&i) } func refStruct(y *int) (z S) { z.M = y return z }
其输出为:
./escape.go:12: leaking param: y to result z ./escape.go:9: main &i does not escape
尽管在main
函数中对i
变量做了引用操作,并传递到了函数refStruct
中,但是这个引用的范围没有超出其声明它的栈空间。这和之前的那个程序语义上有细微的差别,这个会更高效:在上一个程序中,变量i
必须分配在main
函数的栈中,然后作为参数拷贝到函数refStruct
中,并将拷贝的这一份分配在堆上。而在这个例子中,i
package main type S struct { M *int } func main() { var x S var i int ref(&i, &x) } func ref(y *int, z *S) { z.M = y }
go run ausführen -gcflags ' -m -l'
(Hinweis: Der Name der Go-Codedatei wird im Originaltext weggelassen), um diesen Code zu kompilieren. Der Parameter -l verhindert, dass die Funktion identity
eingebunden wird (Wir werden das Thema „Inlining“ ein anderes Mal besprechen). Sie werden keine Ausgabe sehen! Go verwendet die Wertübertragung, sodass die Variable x
in der Funktion main
immer in den Stapelbereich der Funktion identity
kopiert wird. Normalerweise reserviert Code, der keine Referenzen verwendet, Speicher über den Stapelspeicher. Es ist also keine Fluchtanalyse erforderlich. Versuchen wir es mit dem schwierigeren:
./escape.go:13: leaking param: y ./escape.go:13: ref z does not escape ./escape.go:9: moved to heap: i ./escape.go:10: &i escapes to heap ./escape.go:10: main &x does not escape
rrreee
Die erste Zeile zeigt den „Fluss“ der Variablenz
: Der Eingabeparameter wird direkt als Rückgabewert zurückgegeben. Allerdings hat die Funktion identity
die Referenz von z
nicht entfernt, sodass kein Variablen-Escape aufgetreten ist. Nachdem die Funktion main
zurückgegeben wurde, gibt es keinen Verweis auf x
, sodass die Variable x
in der Funktion main
sein kann Funktion. Stapelspeicher für die Speicherzuweisung. 🎜Das dritte Experiment: 🎜rrreee🎜Die Ausgabe ist: 🎜rrreee🎜Jetzt gibt es eine Flucht. Denken Sie daran, dass Go eine Wertübergabe ist, also ist z
eine Kopie der Variablen x
. Die Funktion ref
gibt eine Referenz auf z
zurück, sodass z
nicht auf dem Stapel zugewiesen werden kann, andernfalls wenn die Funktion ref
gibt zurück: Wann wird der Referenzpunkt angezeigt? Also entkam es auf den Haufen. Tatsächlich verwirft die Funktion main
nach der Ausführung von ref
und der Rückkehr zur Funktion main
die Referenz, anstatt sie zu dereferenzieren, die Escape-Analyse von Go jedoch schon nicht genug. Seien Sie taktvoll, um diese Situation zu erkennen. 🎜Es ist erwähnenswert, dass der Compiler in diesem Fall ref
einfügt, wenn wir die Referenz nicht stoppen. 🎜Was passiert, wenn die Strukturmitglieder als Referenzen definiert sind? 🎜rrreee🎜Die Ausgabe lautet: 🎜rrreee🎜In diesem Fall verfolgt Go weiterhin den Fluss der Referenz, obwohl es ein Mitglied der Struktur ist. Da die Funktion refStruct
eine Referenz akzeptiert und zurückgibt, muss y
maskiert werden. Vergleichen Sie das folgende Beispiel: 🎜rrreee🎜Die Ausgabe lautet: 🎜rrreee🎜Obwohl die Variable i
in der Funktion main
referenziert und an die Funktion refStruct übergeben wird code>, aber der Umfang dieser Referenz überschreitet nicht den Stapelspeicher, in dem sie deklariert ist. Dies unterscheidet sich in der Semantik geringfügig vom vorherigen Programm und ist effizienter: Im vorherigen Programm muss die Variable <code>i
auf dem Stapel von main
zugewiesen werden Funktion, kopieren Sie sie dann als Parameter in die Funktion refStruct
und weisen Sie die Kopie auf dem Heap zu. In diesem Beispiel wird i
nur einmal zugewiesen und dann wird die Referenz weitergegeben. 🎜🎜Sehen wir uns ein etwas kompliziertes Beispiel an: 🎜rrreee🎜Die Ausgabe ist: 🎜./escape.go:13: leaking param: y ./escape.go:13: ref z does not escape ./escape.go:9: moved to heap: i ./escape.go:10: &i escapes to heap ./escape.go:10: main &x does not escape
问题在于,y
被赋值给了一个入参结构体的成员。Go并不能追溯这种关系(go只能追溯输入直接流向输出),所以逃逸分析失败了,所以变量只能分配到堆上。由于Go的逃逸分析的局限性,许多变量会被分配到堆上,请参考此链接,这里面记录了许多案例(从Go1.5开始)。
最后,来看下映射和切片是怎样的呢?请记住,切片和映射实际上只是具有指向堆内存的指针的Go结构:slice
结构是暴露在reflect
包中(SliceHeader
)。map
结构就更隐蔽了:存在于hmap。如果这些结构体不逃逸,将会被分配到栈上,但是其底层的数组或者哈希桶中的实际数据会被分配到堆上去。避免这种情况的唯一方法是分配一个固定大小的数组(例如[10000]int
)。
如果你剖析过你的程序堆使用情况(https://blog.golang.org/pprof
),并且想减少垃圾回收的消耗,可以将频繁分配到堆上的变量移到栈上,可能会有较好的效果。进一步研究HotSpot JVM是如何进行逃逸分析的会是一个不错的话题,可以参考这个链接,这个里面主要讲解了栈分配,以及有关何时可以消除同步的检测。
更多golang相关技术文章,请访问golang教程栏目!