我在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的地址是完全可以的 局部变量;与该变量关联的存储保存下来 在函数返回之后。实际上,取一个合成对象的地址 每次求值时,Literal都会分配一个新实例,因此我们 可以结合这最后两行。 http://golang.org/doc/effective_go.html#functions

但它提出了几个问题。

在例1中,该结构在堆上声明。例2呢?它在堆栈上的声明和在C语言中一样吗?还是它也在堆上? 如果例子2是在堆栈上声明的,它如何在函数返回后保持可用? 如果例子2实际上是在堆上声明的,为什么结构体是按值而不是按引用传递的?在这种情况下指针的意义是什么?


当前回答

根据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
}

在这两种情况下,当前的Go实现将为堆上类型为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的参数值是一个地址。

值得注意的是,语言规范中没有出现“堆栈”和“堆”这两个词。是在堆栈上声明的,”和“…在堆上声明的”,但请注意,Go声明语法没有提到堆栈或堆。

从技术上讲,这使得所有问题的答案都依赖于实现。实际上,当然有一个堆栈(每个goroutine!)和一个堆,一些东西放在堆栈上,一些放在堆上。在某些情况下,编译器会遵循严格的规则(比如“new总是在堆上分配”),而在其他情况下,编译器会进行“逃逸分析”,以决定一个对象是否可以驻留在堆栈上,或者是否必须在堆上分配。

在例2中,转义分析将显示指向转义结构的指针,因此编译器必须分配该结构。然而,我认为当前的Go实现在这种情况下遵循了一个严格的规则,即如果一个结构体的任何部分的地址被取走,该结构体就会被放到堆上。

对于问题3,我们可能会对术语感到困惑。围棋中的所有东西都是通过值传递的,没有引用传递。这里返回一个指针值。指针的意义是什么?考虑对您的示例进行以下修改:

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。对运行时的调用。新事物消失了,这在某些情况下是件好事。等等,这是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     ,

仍然没有调用运行时。new,是的,它确实可以按值返回一个8MB的对象。它是有效的,但你通常不想这样做。这里使用指针的目的是避免推入8MB的对象。

根据Go的常见问题解答:

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

下面是Go垃圾收集器指南中关于堆栈堆和GC的另一个讨论

价值观存在的地方

stack allocation non-pointer Go values stored in local variables will likely not be managed by the Go GC at all, and Go will instead arrange for memory to be allocated that's tied to the lexical scope in which it's created. In general, this is more efficient than relying on the GC, because the Go compiler is able to predetermine when that memory may be freed and emit machine instructions that clean up. Typically, we refer to allocating memory for Go values this way as "stack allocation," because the space is stored on the goroutine stack. heap allocation Go values whose memory cannot be allocated this way, because the Go compiler cannot determine its lifetime, are said to escape to the heap. "The heap" can be thought of as a catch-all for memory allocation, for when Go values need to be placed somewhere. The act of allocating memory on the heap is typically referred to as "dynamic memory allocation" because both the compiler and the runtime can make very few assumptions as to how this memory is used and when it can be cleaned up. That's where a GC comes in: it's a system that specifically identifies and cleans up dynamic memory allocations.

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


逸出分析

至于如何从Go编译器的转义分析中访问信息,最简单的方法是通过Go编译器支持的调试标志,该标志以文本格式描述了它应用或未应用于某些包的所有优化。这包括值是否转义。尝试以下命令,其中[package]是Go包路径。

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

特定于实现的优化

Go GC对活动内存的人口统计非常敏感,因为对象和指针的复杂图既限制了并行性,又为GC生成了更多的工作。因此,GC包含一些针对特定公共结构的优化。下面列出了对性能优化最直接有用的方法。

Pointer-free values are segregated from other values. As a result, it may be advantageous to eliminate pointers from data structures that do not strictly need them, as this reduces the cache pressure the GC exerts on the program. As a result, data structures that rely on indices over pointer values, while less well-typed, may perform better. This is only worth doing if it's clear that the object graph is complex and the GC is spending a lot of time marking and scanning. The GC will stop scanning values at the last pointer in the value. As a result, it may be advantageous to group pointer fields in struct-typed values at the beginning of the value. This is only worth doing if it's clear the application spends a lot of its time marking and scanning. (In theory the compiler can do this automatically, but it is not yet implemented, and struct fields are arranged as written in the source code.)

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1和Function2可以是内联函数。return变量不会转义。没有必要在堆上分配变量。

我的示例代码:

   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()可能不会被调用。因为它没有任何意义。

不要关心变量是分配在堆上还是堆栈上,只需使用它。如果需要,可以通过互斥或通道来保护它。