Go 中结构的堆栈与堆分配,以及它们与垃圾收集的关系 [英] Stack vs heap allocation of structs in Go, and how they relate to garbage collection

查看:28
本文介绍了Go 中结构的堆栈与堆分配,以及它们与垃圾收集的关系的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是 Go 的新手,我在 C 风格的基于堆栈的编程和 Python 风格的基于堆栈的编程之间遇到了一些认知失调,其中自动变量存在于堆栈中,分配的内存存在于堆中唯一存在于堆栈中的是指向堆上对象的引用/指针.

I'm new to Go and I'm experiencing a bit of cognitive dissonance between C-style stack-based programming where automatic variables live on the stack and allocated memory lives on the heap and Python-style stack-based-programming where the only thing that lives on the stack are references/pointers to objects on the heap.

据我所知,以下两个函数给出了相同的输出:

As far as I can tell, the two following functions give the same output:

func myFunction() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


func myFunction() (*MyStructType, error) {
    var chunk MyStructType

    ...

    return &chunk, nil
}

即分配一个新的结构体并返回它.

i.e., allocate a new struct and return it.

如果我用 C 编写它,第一个将把一个对象放在堆上,第二个将把它放在堆栈上.第一个将返回一个指向堆的指针,第二个将返回一个指向堆栈的指针,当函数返回时它会消失,这将是一件坏事.

If I'd written that in C, the first one would have put an object on the heap and the second would have put it on the stack. The first would return a pointer to the heap, the second would return a pointer to the stack, which would have evaporated by the time the function had returned, which would be a Bad Thing.

如果我用 Python(或除 C# 之外的许多其他现代语言)编写它,示例 2 将不可能实现.

If I'd written it in Python (or many other modern languages except C#) example 2 would not have been possible.

我知道 Go 垃圾收集了两个值,所以上述两种形式都可以.

I get that Go garbage collects both values, so both of the above forms are fine.

引用:

请注意,与 C 不同,返回一个地址是完全可以的局部变量;与变量关联的存储仍然存在函数返回后.事实上,取复合地址每次计算时,literal 都会分配一个新实例,所以我们可以结合最后两行.

Note that, unlike in C, it's perfectly OK to return the address of a local variable; the storage associated with the variable survives after the function returns. In fact, taking the address of a composite literal allocates a fresh instance each time it is evaluated, so we can combine these last two lines.

http://golang.org/doc/effective_go.html#functions

但这引发了几个问题.

  1. 在示例 1 中,结构体是在堆上声明的.例子2呢?它是在堆栈上以与在 C 中相同的方式声明还是也在堆上声明?

  1. In example 1, the struct is declared on the heap. What about example 2? Is that declared on the stack in the same way it would be in C or does it go on the heap too?

如果示例 2 声明在堆栈上,那么在函数返回后它如何保持可用?

If example 2 is declared on the stack, how does it stay available after the function returns?

如果示例 2 实际上是在堆上声明的,那么结构体是如何通过值而不是通过引用传递的?在这种情况下,指针的意义是什么?

If example 2 is actually declared on the heap, how is it that structs are passed by value rather than by reference? What's the point of pointers in this case?

推荐答案

值得注意的是,堆栈"和堆"这两个词没有出现在语言规范中的任何地方.您的问题的措辞是...在堆栈上声明"和...在堆上声明",但请注意,Go 声明语法并未提及堆栈或堆.

It's worth noting that the words "stack" and "heap" do not appear anywhere in the language spec. Your question is worded with "...is declared on the stack," and "...declared on the heap," but note that Go declaration syntax says nothing about stack or heap.

从技术上讲,这使您所有问题的答案都依赖于实现.实际上,当然,有一个堆栈(每个 goroutine!)和一个堆,有些东西在堆栈上,有些在堆上.在某些情况下,编译器遵循严格的规则(例如new 总是在堆上分配"),而在其他情况下,编译器会进行转义分析"来决定一个对象是否可以存在于堆栈中,或者它是否必须在堆上分配.

That technically makes the answer to all of your questions implementation dependent. In actuality of course, there is a stack (per goroutine!) and a heap and some things go on the stack and some on the heap. In some cases the compiler follows rigid rules (like "new always allocates on the heap") and in others the compiler does "escape analysis" to decide if an object can live on the stack or if it must be allocated on the heap.

在您的示例 2 中,转义分析将显示指向转义结构的指针,因此编译器必须分配该结构.我认为 Go 的当前实现在这种情况下遵循一个严格的规则,即如果地址取自结构的任何部分,则结构将进入堆.

In your example 2, escape analysis would show the pointer to the struct escaping and so the compiler would have to allocate the struct. I think the current implementation of Go follows a rigid rule in this case however, which is that if the address is taken of any part of a struct, the struct goes on the heap.

对于问题 3,我们可能会对术语感到困惑.Go 中的一切都是按值传递的,没有按引用传递.在这里,您将返回一个指针值.指针的意义是什么?考虑对您的示例进行以下修改:

For question 3, we risk getting confused about terminology. Everything in Go is passed by value, there is no pass by reference. Here you are returning a pointer value. What's the point of pointers? Consider the following modification of your example:

type MyStructType struct{}

func myFunction1() (*MyStructType, error) {
    var chunk *MyStructType = new(MyStructType)
    // ...
    return chunk, nil
}

func myFunction2() (MyStructType, error) {
    var chunk MyStructType
    // ...
    return chunk, nil
}

type bigStruct struct {
    lots [1e6]float64
}

func myFunction3() (bigStruct, error) {
    var chunk bigStruct
    // ...
    return chunk, nil
}

我修改了 myFunction2 以返回结构而不是结构的地址.现在比较 myFunction1 和 myFunction2 的汇编输出,

I modified myFunction2 to return the struct rather than the address of the struct. Compare the assembly output of myFunction1 and myFunction2 now,

--- prog list "myFunction1" ---
0000 (s.go:5) TEXT    myFunction1+0(SB),$16-24
0001 (s.go:6) MOVQ    $type."".MyStructType+0(SB),(SP)
0002 (s.go:6) CALL    ,runtime.new+0(SB)
0003 (s.go:6) MOVQ    8(SP),AX
0004 (s.go:8) MOVQ    AX,.noname+0(FP)
0005 (s.go:8) MOVQ    $0,.noname+8(FP)
0006 (s.go:8) MOVQ    $0,.noname+16(FP)
0007 (s.go:8) RET     ,

--- prog list "myFunction2" ---
0008 (s.go:11) TEXT    myFunction2+0(SB),$0-16
0009 (s.go:12) LEAQ    chunk+0(SP),DI
0010 (s.go:12) MOVQ    $0,AX
0011 (s.go:14) LEAQ    .noname+0(FP),BX
0012 (s.go:14) LEAQ    chunk+0(SP),BX
0013 (s.go:14) MOVQ    $0,.noname+0(FP)
0014 (s.go:14) MOVQ    $0,.noname+8(FP)
0015 (s.go:14) RET     ,

不要担心这里的 myFunction1 输出与 peterSO 的(优秀)答案不同.我们显然正在运行不同的编译器.否则,请注意我修改了 myFunction2 以返回 myStructType 而不是 *myStructType.对 runtime.new 的调用消失了,这在某些情况下是一件好事.等等,这是 myFunction3,

Don't worry that myFunction1 output here is different than in peterSO's (excellent) answer. We're obviously running different compilers. Otherwise, see that I modfied myFunction2 to return myStructType rather than *myStructType. The call to runtime.new is gone, which in some cases would be a good thing. Hold on though, here's myFunction3,

--- prog list "myFunction3" ---
0016 (s.go:21) TEXT    myFunction3+0(SB),$8000000-8000016
0017 (s.go:22) LEAQ    chunk+-8000000(SP),DI
0018 (s.go:22) MOVQ    $0,AX
0019 (s.go:22) MOVQ    $1000000,CX
0020 (s.go:22) REP     ,
0021 (s.go:22) STOSQ   ,
0022 (s.go:24) LEAQ    chunk+-8000000(SP),SI
0023 (s.go:24) LEAQ    .noname+0(FP),DI
0024 (s.go:24) MOVQ    $1000000,CX
0025 (s.go:24) REP     ,
0026 (s.go:24) MOVSQ   ,
0027 (s.go:24) MOVQ    $0,.noname+8000000(FP)
0028 (s.go:24) MOVQ    $0,.noname+8000008(FP)
0029 (s.go:24) RET     ,

仍然没有调用runtime.new,是的,它确实可以按值返回一个8MB的对象.它有效,但您通常不想这样做.这里的指针的目的是避免推送大约 8MB 的对象.

Still no call to runtime.new, and yes it really works to return an 8MB object by value. It works, but you usually wouldn't want to. The point of a pointer here would be to avoid pushing around 8MB objects.

这篇关于Go 中结构的堆栈与堆分配,以及它们与垃圾收集的关系的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆