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


165

我是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语言不同,完全可以返回局部变量的地址。函数返回后,与变量关联的存储将保留。实际上,采用复合文字的地址会在每次对其求值时分配一个新实例,因此我们可以将后两行结合在一起。

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

但这引起了两个问题。

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

2-如果在堆栈上声明了示例2,则函数返回后如何保持可用状态?

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

Answers:


170

值得注意的是,在语言规范中,“ 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的对象。


9
非常感谢。我并不是真正在问“指针到底是什么”,它更像是“当值看起来像指针时,指针是什么”,无论如何,这种情况都将由您的回答来解决。
2012年

25
组装的简短说明将不胜感激。
ElefEnt 2014年

59
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的当前实现都会在堆上为a 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     ,

来电

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

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


非常感谢!赞成,但由于接受逃逸分析的原因,我接受了Sonia。
2012年

1
彼得,那么,您和@Sonia如何制作该程序集?你们都有相同的格式。我尝试过objdump,go工具,otool,无论命令/标志如何都无法生成它。
2015年

3
啊,知道了-gcflags。
15厘升(2015年

30

根据Go的常见问题解答

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



0
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 := S{x: 10}
20          return &s
21  }
22  
23  func F3() S {
24          s := S{x: 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()。因为这没办法。

不必担心变量是分配在堆还是堆栈上,只需使用它即可。必要时通过互斥或通道对其进行保护。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.