首页 文章

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

提问于
浏览
119

我是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,那么结构是如何通过值而不是通过引用传递的?在这种情况下,指针有什么意义?

4 回答

  • 14

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

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

    在您的示例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对象 .

  • 44
    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 的返回参数值是一个地址 .

  • 6

    根据Go's FAQ

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

  • 125

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

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

相关问题