Go中结构的堆栈与堆分配,以及它们与垃圾收集的关系

Joe*_*Joe 146 heap-memory go stack-memory

我是Go的新手,我在C风格的基于堆栈的编程之间经历了一些不协调的错误,其中自动变量存在于堆栈上,并且在堆上分配了内存,以及基于Python的基于堆栈的编程,其中只有堆栈上的东西才是堆上对象的引用/指针.

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

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

我得到Go垃圾收集两个值,所以上述两种形式都没问题.

报价:

请注意,与C不同,返回局部变量的地址是完全可以的.与函数关联的存储在函数返回后仍然存在.实际上,获取复合文字的地址在每次评估时都会分配一个新实例,因此我们可以将这两行结合起来.

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

但它提出了几个问题.

1 - 在示例1中,结构在堆上声明.例2怎么样?在堆栈中声明的方式与在C中的方式相同,还是在堆上也是如此?

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

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

Son*_*nia 151

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

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

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

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

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
}
Run Code Online (Sandbox Code Playgroud)

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

--- 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     ,
Run Code Online (Sandbox Code Playgroud)

不要担心这里的myFunction1输出与peterSO(优秀)答案中的不同.我们显然运行不同的编译器.否则,请参阅我修改myFunction2以返回myStructType而不是*myStructType.对runtime.new的调用已经消失,在某些情况下这将是一件好事.等等,这是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     ,
Run Code Online (Sandbox Code Playgroud)

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

  • 对组件的简短解释将不胜感激. (18认同)
  • 非常感谢.我并没有真正问"指针的重点是什么",它更像是"当值看起来像指针一样时,指针的重点是什么",而且无论如何,这种情况都会被你的答案搞得一团糟. (7认同)

pet*_*rSO 55

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
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,Go的当前实现将为堆上struct的类型分配内存MyStructType并返回其地址.功能相同; 编译器asm源是一样的.

--- prog list "myFunction1" ---
0000 (temp.go:9) TEXT    myFunction1+0(SB),$8-12
0001 (temp.go:10) MOVL    $type."".MyStructType+0(SB),(SP)
0002 (temp.go:10) CALL    ,runtime.new+0(SB)
0003 (temp.go:10) MOVL    4(SP),BX
0004 (temp.go:12) MOVL    BX,.noname+0(FP)
0005 (temp.go:12) MOVL    $0,AX
0006 (temp.go:12) LEAL    .noname+4(FP),DI
0007 (temp.go:12) STOSL   ,
0008 (temp.go:12) STOSL   ,
0009 (temp.go:12) RET     ,

--- prog list "myFunction2" ---
0010 (temp.go:15) TEXT    myFunction2+0(SB),$8-12
0011 (temp.go:16) MOVL    $type."".MyStructType+0(SB),(SP)
0012 (temp.go:16) CALL    ,runtime.new+0(SB)
0013 (temp.go:16) MOVL    4(SP),BX
0014 (temp.go:18) MOVL    BX,.noname+0(FP)
0015 (temp.go:18) MOVL    $0,AX
0016 (temp.go:18) LEAL    .noname+4(FP),DI
0017 (temp.go:18) STOSL   ,
0018 (temp.go:18) STOSL   ,
0019 (temp.go:18) RET     ,
Run Code Online (Sandbox Code Playgroud)

呼叫

在函数调用中,函数值和参数按通常顺序计算.在评估它们之后,调用的参数通过值传递给函数,并且被调用的函数开始执行.当函数返回时,函数的返回参数通过值传递回调用函数.

所有函数和返回参数都按值传递.带类型的返回参数值*MyStructType是一个地址.

  • 啊,知道了-gcflags。 (2认同)

gch*_*ain 25

根据Go的FAQ:

如果编译器在函数返回后无法证明变量未被引用,则编译器必须在垃圾收集堆上分配变量以避免悬空指针错误.


use*_*ser 9

您并不总是知道您的变量是在堆栈还是堆上分配的.
...
如果您需要知道变量的分配位置,请将"-m"gc标志传递给"go build"或"go run"(例如go run -gcflags -m app.go).

资料来源:http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars