Alocação de pilha contra pilha de estruturas no Go e como elas se relacionam com a coleta de lixo

165

Eu sou novo no Go e estou experimentando um pouco de dissonância congitiva entre a programação baseada em pilha no estilo C, em que variáveis ​​automáticas vivem na pilha e alocam a vida útil da memória no heap e a programação baseada em pilha no estilo Python, em que o A única coisa que fica na pilha são referências / ponteiros para objetos na pilha.

Tanto quanto posso dizer, as duas funções a seguir fornecem a mesma saída:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

ou seja, aloque uma nova estrutura e retorne-a.

Se eu tivesse escrito isso em C, o primeiro teria colocado um objeto na pilha e o segundo o teria colocado na pilha. O primeiro retornaria um ponteiro para a pilha, o segundo retornaria um ponteiro para a pilha, que teria evaporado no momento em que a função retornasse, o que seria uma Coisa Ruim.

Se eu tivesse escrito em Python (ou em muitas outras linguagens modernas, exceto C #), o exemplo 2 não seria possível.

Entendo que o Go go coleta os dois valores, portanto, os dois formulários acima estão corretos.

Citar:

Observe que, ao contrário de C, é perfeitamente bom retornar o endereço de uma variável local; o armazenamento associado à variável sobrevive após o retorno da função. De fato, pegar o endereço de um literal composto aloca uma nova instância toda vez que é avaliada, para que possamos combinar essas duas últimas linhas.

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

Mas isso levanta algumas questões.

1 - No exemplo 1, a estrutura é declarada na pilha. E o exemplo 2? Isso é declarado na pilha da mesma maneira que seria em C ou também vai para a pilha?

2 - Se o exemplo 2 é declarado na pilha, como fica disponível após o retorno da função?

3 - Se o exemplo 2 é realmente declarado no heap, como as estruturas são passadas por valor e não por referência? Qual é o sentido dos ponteiros nesse caso?

Joe
fonte

Respostas:

170

Vale a pena notar que as palavras "empilhar" e "heap" não aparecem em nenhum lugar nas especificações de idioma. Sua pergunta está escrita com "... é declarada na pilha" e "... declarada na pilha", mas observe que a sintaxe da declaração Go não diz nada sobre pilha ou pilha.

Isso tecnicamente torna dependente a resposta para todas as suas perguntas. Na realidade, é claro, há uma pilha (por goroutine!) E uma pilha e algumas coisas vão para a pilha e outras para a pilha. Em alguns casos, o compilador segue regras rígidas (como " newsempre aloca na pilha") e, em outros, o compilador "escapa da análise" para decidir se um objeto pode viver na pilha ou se deve ser alocado na pilha.

No exemplo 2, a análise de escape mostraria o ponteiro para a estrutura escapando e, portanto, o compilador teria que alocar a estrutura. Eu acho que a implementação atual do Go segue uma regra rígida neste caso, no entanto, que é que, se o endereço for tomado de qualquer parte de uma estrutura, a estrutura continuará na pilha.

Para a pergunta 3, corremos o risco de ficar confusos sobre a terminologia. Tudo no Go é passado por valor, não há passe por referência. Aqui você está retornando um valor de ponteiro. Qual o sentido dos ponteiros? Considere a seguinte modificação do seu exemplo:

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
}

Modifiquei myFunction2 para retornar a estrutura em vez do endereço da estrutura. Compare a saída de montagem de myFunction1 e myFunction2 agora,

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

Não se preocupe que a saída myFunction1 aqui seja diferente da resposta (excelente) de peterSO. Obviamente, estamos executando diferentes compiladores. Caso contrário, veja que modifiquei myFunction2 para retornar myStructType em vez de * myStructType. A chamada para runtime.new se foi, o que em alguns casos seria uma coisa boa. Espere, aqui está o 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     ,

Ainda não há chamada para runtime.new, e sim, realmente funciona para retornar um objeto de 8 MB por valor. Funciona, mas você geralmente não gostaria. O objetivo de um ponteiro aqui seria evitar empurrar objetos de 8 MB.

Sonia
fonte
9
Excelente obrigado. Eu não estava realmente perguntando "qual é o sentido dos ponteiros", era mais como "qual é o sentido dos ponteiros quando os valores parecem se comportar como ponteiros", e esse caso é discutido pela sua resposta de qualquer maneira.
Joe
25
Uma breve explicação da montagem seria apreciada.
elefent
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
}

Nos dois casos, as implementações atuais do Go alocariam memória para um structtipo MyStructTypeem um heap e retornariam seu endereço. As funções são equivalentes; a origem do compilador asm é a mesma.

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

Chamadas

Em uma chamada de função, o valor e os argumentos da função são avaliados na ordem usual. Após serem avaliados, os parâmetros da chamada são transmitidos por valor para a função e a função chamada inicia a execução. Os parâmetros de retorno da função são passados ​​por valor de volta à função de chamada quando a função retorna.

Todas as funções e parâmetros de retorno são passados ​​por valor. O valor do parâmetro de retorno com o tipo *MyStructTypeé um endereço.

peterSO
fonte
Muito obrigado! Voto positivo, mas estou aceitando o de Sonia por causa da parte da análise de escape.
Joe
1
peterSo, como você e a @Sonia estão produzindo essa montagem? Vocês dois têm a mesma formatação. Eu não posso produzi-lo, independentemente do comando / sinalizadores, tendo tentado objdump, go tool, otool.
10
3
Ah, entendi - gcflags.
10 cls
30

De acordo com a FAQ da Go :

se o compilador não puder provar que a variável não é referenciada após o retorno da função, o compilador deve alocar a variável no heap coletado pelo lixo para evitar erros de ponteiro pendentes.

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Função1 e Função2 podem ser funções embutidas. E a variável de retorno não escapará. Não é necessário alocar variável na pilha.

Meu código de exemplo:

 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  }

De acordo com a saída do cmd:

go run -gcflags -m test.go

resultado:

# 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

Se o compilador for inteligente o suficiente, F1 () F2 () F3 () não poderá ser chamado. Porque não faz nenhum meio.

Não se preocupe se uma variável está alocada na pilha ou pilha, apenas use-a. Proteja-o por mutex ou canal, se necessário.

g10guang
fonte