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

2ic8powd  于 2022-12-31  发布在  Go
关注(0)|答案(6)|浏览(217)

我在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不同,返回局部变量的地址是完全可以的;与变量相关的存储空间在函数返回后仍然存在。2实际上,每次赋值时,取复合字面值的地址都会分配一个新的示例,所以我们可以把最后两行合并起来。
http://golang.org/doc/effective_go.html#functions
但这也引发了一些问题。
1.在例1中,结构体是在堆上声明的,那么例2呢?它在栈上声明的方式和C中声明的方式一样吗?还是也在堆上声明?
1.如果示例2是在堆栈上声明的,那么它在函数返回后如何保持可用?
1.如果例2实际上是在堆上声明的,那么结构体是如何通过值传递而不是通过引用传递的呢?在这种情况下指针的作用是什么?

ht4b089n

ht4b089n1#

值得注意的是,“栈”和“堆”这两个词并没有出现在语言规范的任何地方,你的问题是用“......在栈上声明”和“......在堆上声明”来表达的,但是注意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
}

我修改了myFunction 2以返回结构体而不是结构体的地址。现在比较myFunction 1和myFunction 2的程序集输出,

--- 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     ,

不要担心myFunction 1在这里的输出与peterSO(优秀)答案中的不同。显然我们运行的是不同的编译器。另外,请注意我修改了myFunction 2以返回myStructType而不是 *myStructType。对www.example.com的调用runtime.new消失了,这在某些情况下是件好事。但是请稍等,这里是myFunction 3,

--- 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,并且确实可以通过值返回8 MB的对象。它可以工作,但通常你不想这样做。这里的指针的作用是避免在8 MB的对象周围移动。

xn1cxnb4

xn1cxnb42#

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     ,

电话
在函数调用中,函数值和参数按通常的顺序求值。求值后,调用的参数按值传递给函数,被调用函数开始执行。函数返回时,函数的返回参数按值传递回调用函数。
所有函数和返回参数都是按值传递的。类型为*MyStructType的返回参数值是地址。

6tdlim6h

6tdlim6h3#

根据Go的FAQ:
如果编译器不能证明该变量在函数返回之后未被引用,则编译器必须将该变量分配在垃圾收集堆上以避免悬空指针错误。

insrf1ej

insrf1ej4#

你并不总是知道你的变量是在栈上还是堆上分配的。
...
如果你需要知道你的变量被分配到了哪里,可以传递“-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

ncgqoxb0

ncgqoxb05#

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

    ...

    return chunk, nil
}

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

    ...

    return &chunk, nil
}

Function1和Function2可以是内联函数,并且返回变量不会转义,不需要在堆上分配变量。
我的示例代码:

package main
   
   type S struct {
           x int
   }
   
   func main() {
           F1()
           F2()
          F3()
  }
  
  func F1() *S {
          s := new(S)
          return s
  }
  
  func F2() *S {
          s := S{x: 10}
          return &s
  }
  
  func F3() S {
          s := S{x: 9}
          return s
  }

根据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() 可能不会被调用,因为它没有任何意义。
不要在意变量是在堆还是栈上分配的,只要使用它就行了。如果需要的话,用互斥锁或者通道来保护它。

olmpazwi

olmpazwi6#

下面是关于A Guide to the Go Garbage Collector中堆栈堆和GC的另一个讨论

Where Go价值观现场直播

  • 栈分配
  • 存储在局部变量中的非指针Go语言的值可能根本不受Go语言GC的管理,Go语言会根据创建该值的词法作用域来分配内存。一般来说,这比依赖GC更有效,因为Go语言编译器能够预先确定内存释放的时间,并发出清理内存的机器指令。通常,我们把这种为Go语言的值分配内存的方式称为“栈分配”,因为空间存储在goroutine栈中。
  • 堆分配
  • 由于Go语言编译器无法确定其生存期,因此不能以这种方式分配内存的Go语言值被称为逃到堆中。“堆”可以被认为是内存分配的一个万能工具。用于Go语言的值需要被放置的时候。在堆上分配内存的行为通常被称为“动态内存分配”。因为编译器和运行时都很少会假设内存是如何使用的以及何时可以清理。这就是GC的用武之地:它是一个专门识别和清理动态内存分配的系统。

Go语言的值需要逃到堆中的原因有很多,其中一个原因可能是它的大小是动态确定的,例如,一个切片的后备数组的初始大小是由一个变量而不是一个常量决定的,注意,逃到堆中也必须是可传递的:如果一个Go语言值的引用被写入到另一个已经被确定要转义的Go语言值中,那么这个值也必须转义。

逃逸分析

至于如何从Go语言编译器的转义分析中获取信息,最简单的方法是通过Go语言编译器支持的debug标志,这个标志以文本格式描述了Go语言编译器对某个包进行过或没有进行过的所有优化,包括值是否转义,可以尝试下面的命令,其中[package]是Go语言包的路径。

$ go build -gcflags=-m=3 [package]

特定于实施的优化

Go语言的GC对动态内存的使用情况很敏感,因为对象和指针的复杂图形既限制了并行性,又增加了GC的工作量。因此,GC包含了一些针对特定公共结构的优化。下面列出了对性能优化最直接有用的优化。

  • 无指针值与其他值隔离。

结果,从并不严格需要指针的数据结构中消除指针可能是有利的,因为这减少了GC施加在程序上该高速缓存压力。结果,依赖于指针值上的索引的数据结构,虽然不是良好类型化的,只有当对象图很复杂并且GC花费大量时间标记和扫描时,才值得这样做。

  • GC将在值中的最后一个指针处停止扫描值。

因此,将结构类型值中的指针字段分组在值的开头可能是有利的。只有当应用程序显然花费了大量时间进行标记和扫描时,才值得这样做。(理论上,编译器可以自动这样做,但还没有实现,结构字段是按照源代码中所写的那样排列的。)

相关问题