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

Posted

技术标签:

【中文标题】Go 中结构的堆栈与堆分配,以及它们与垃圾收集的关系【英文标题】:Stack vs heap allocation of structs in Go, and how they relate to garbage collection 【发布时间】:2012-06-07 15:13:22 【问题描述】:

我是 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

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

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

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

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

引用:

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

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

但它提出了几个问题。

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

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

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

【问题讨论】:

【参考方案1】:

值得注意的是,“stack”和“heap”这两个词并没有出现在语言规范的任何地方。您的问题的措辞是“......在堆栈上声明”和“......在堆上声明”,但请注意,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

我修改了 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     ,

不要担心这里的 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     ,

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

【讨论】:

非常感谢。我并不是真的在问“指针的意义是什么”,它更像是“当值看起来像指针时指针的意义是什么”,无论如何你的回答都会使这种情况变得毫无意义。 我们将不胜感激。 那么new实际上总是在堆上分配吗?【参考方案2】:
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

在这两种情况下,Go 的当前实现都会在堆上为 MyStructType 类型的 struct 分配内存并返回其地址。功能是等价的;编译器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     ,

Calls

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

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

【讨论】:

非常感谢!投了赞成票,但我接受了索尼娅的观点,因为关于逃逸分析的内容。 peterSo,你和@Sonia 是如何制作这个程序集的?你们都有相同的格式。无论命令/标志如何,我都无法生成它,尝试了 objdump、go tool、otool。 啊,明白了 - gcflags。【参考方案3】:

根据Go's FAQ:

如果编译器不能证明该变量在之后没有被引用 函数返回,然后编译器必须在 垃圾收集堆以避免悬空指针错误。

【讨论】:

【参考方案4】:

您并不总是知道您的变量是分配在堆栈还是堆上。 ... 如果您需要知道变量的分配位置,请将“-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

【讨论】:

【参考方案5】:
func Function1() (*MyStructType, error) 
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil



func Function2() (*MyStructType, error) 
    var chunk MyStructType

    ...

    return &chunk, nil

Function1 和 Function2 可能是内联函数。并且返回变量不会逃逸。不必在堆上分配变量。

我的示例代码:

 1  package main
 2  
 3  type S struct 
 4          x int
 5  
 6  
 7  func main() 
 8          F1()
 9          F2()
10          F3()
11  
12  
13  func F1() *S 
14          s := new(S)
15          return s
16  
17  
18  func F2() *S 
19          s := Sx: 10
20          return &s
21  
22  
23  func F3() S 
24          s := Sx: 9
25          return s
26  

根据cmd的输出:

go run -gcflags -m test.go

输出:

# command-line-arguments
./test.go:13:6: can inline F1
./test.go:18:6: can inline F2
./test.go:23:6: can inline F3
./test.go:7:6: can inline main
./test.go:8:4: inlining call to F1
./test.go:9:4: inlining call to F2
./test.go:10:4: inlining call to F3
/var/folders/nr/lxtqsz6x1x1gfbyp1p0jy4p00000gn/T/go-build333003258/b001/_gomod_.go:6:6: can inline init.0
./test.go:8:4: main new(S) does not escape
./test.go:9:4: main &s does not escape
./test.go:14:10: new(S) escapes to heap
./test.go:20:9: &s escapes to heap
./test.go:19:2: moved to heap: s

如果编译器足够聪明,F1() F2() F3() 可能不会被调用。因为它没有办法。

不要关心变量是分配在堆上还是栈上,使用它即可。必要时通过互斥锁或通道保护它。

【讨论】:

您始终可以在函数前使用//go:noinline 来防止内联测试代码。问题实际上是在编译器不选择内联的情况下更多地澄清一个概念。

以上是关于Go 中结构的堆栈与堆分配,以及它们与垃圾收集的关系的主要内容,如果未能解决你的问题,请参考以下文章

堆栈与堆栈和堆与堆

java堆栈堆栈的区别

C++ 堆栈与堆分配

关于JAVA堆栈的简单说明

JVM之垃圾收集器 (GC) 与内存分配策略

从Java的堆栈到Equals和==的比較