Alokasi vs tumpukan tumpukan struct di Go, dan bagaimana kaitannya dengan pengumpulan sampah

165

Saya baru untuk Go dan saya mengalami sedikit disonansi congitive antara pemrograman berbasis-gaya-C di mana variabel otomatis tinggal di tumpukan dan mengalokasikan memori yang hidup di heap dan dan pemrograman berbasis-tumpukan-gaya-Python di mana Satu-satunya hal yang hidup di stack adalah referensi / pointer ke objek di heap.

Sejauh yang saya tahu, dua fungsi berikut memberikan hasil yang sama:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

yaitu mengalokasikan struct baru dan mengembalikannya.

Jika saya menulisnya di C, yang pertama akan meletakkan objek di tumpukan dan yang kedua akan meletakkannya di tumpukan. Yang pertama akan mengembalikan pointer ke heap, yang kedua akan mengembalikan pointer ke stack, yang akan menguap pada saat fungsi kembali, yang akan menjadi Bad Thing.

Jika saya menulisnya dengan Python (atau banyak bahasa modern lainnya kecuali C #) contoh 2 tidak akan mungkin.

Saya mendapatkan bahwa sampah Go mengumpulkan kedua nilai, sehingga kedua bentuk di atas baik-baik saja.

Kutipan:

Perhatikan bahwa, tidak seperti dalam C, tidak apa-apa untuk mengembalikan alamat variabel lokal; penyimpanan yang terkait dengan variabel bertahan setelah fungsi kembali. Bahkan, mengambil alamat dari literal komposit mengalokasikan contoh baru setiap kali dievaluasi, sehingga kita dapat menggabungkan dua baris terakhir ini.

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

Tapi itu menimbulkan beberapa pertanyaan.

1 - Dalam contoh 1, struct dideklarasikan pada heap. Bagaimana dengan contoh 2? Apakah itu dideklarasikan pada stack dengan cara yang sama dengan di C atau apakah ia ikut tumpukan juga?

2 - Jika contoh 2 dideklarasikan pada stack, bagaimana itu tetap tersedia setelah fungsi kembali?

3 - Jika contoh 2 sebenarnya dideklarasikan di heap, bagaimana mungkin struct disahkan oleh nilai daripada dengan referensi? Apa gunanya petunjuk dalam kasus ini?

Joe
sumber

Jawaban:

170

Perlu dicatat bahwa kata-kata "tumpukan" dan "tumpukan" tidak muncul di mana pun dalam spesifikasi bahasa. Pertanyaan Anda diucapkan dengan "... dideklarasikan di stack," dan "... dideklarasikan di heap," tetapi perhatikan bahwa sintaks deklarasi Go tidak mengatakan apa pun tentang stack atau heap.

Itu secara teknis membuat jawaban untuk semua implementasi pertanyaan Anda tergantung. Sebenarnya tentu saja, ada tumpukan (per goroutine!) Dan tumpukan dan beberapa hal di tumpukan dan beberapa di tumpukan. Dalam beberapa kasus, kompiler mengikuti aturan kaku (seperti " newselalu dialokasikan pada heap") dan dalam kasus lain kompiler melakukan "escape analysis" untuk memutuskan apakah suatu objek dapat hidup di stack atau apakah ia harus dialokasikan pada heap.

Dalam contoh Anda 2, analisis melarikan diri akan menunjukkan pointer ke struct melarikan diri dan kompiler harus mengalokasikan struct. Saya pikir implementasi Go saat ini mengikuti aturan yang kaku dalam kasus ini, yaitu bahwa jika alamat diambil dari bagian mana pun dari sebuah struct, maka struct akan menjadi heap.

Untuk pertanyaan 3, kita berisiko bingung tentang terminologi. Segala sesuatu yang ada dilewatkan oleh nilai, tidak ada referensi demi referensi. Di sini Anda mengembalikan nilai pointer. Apa gunanya petunjuk? Pertimbangkan modifikasi contoh Anda berikut ini:

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
}

Saya memodifikasi myFunction2 untuk mengembalikan struct daripada alamat struct. Bandingkan output perakitan myFunction1 dan myFunction2 sekarang,

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

Jangan khawatir bahwa output myFunction1 di sini berbeda dengan jawaban peterSO (luar biasa). Kami jelas menjalankan berbagai kompiler. Kalau tidak, lihat bahwa saya memodifikasi myFunction2 untuk mengembalikan myStructType daripada * myStructType. Panggilan ke runtime.new hilang, yang dalam beberapa kasus akan menjadi hal yang baik. Tunggu sebentar, inilah 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     ,

Masih tidak ada panggilan untuk runtime.new, dan ya itu benar-benar berfungsi untuk mengembalikan objek 8MB berdasarkan nilai. Ini bekerja, tetapi Anda biasanya tidak mau. Titik penunjuk di sini adalah untuk menghindari mendorong sekitar 8MB objek.

Sonia
sumber
9
Terima kasih sangat baik Saya tidak benar-benar bertanya "apa gunanya pointer sama sekali", itu lebih seperti "apa gunanya pointer ketika nilai-nilai tampak berperilaku seperti pointer", dan kasus itu dianggap diperdebatkan oleh jawaban Anda.
Joe
25
Penjelasan singkat tentang majelis akan dihargai.
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
}

Dalam kedua kasus, implementasi Go saat ini akan mengalokasikan memori untuk structtipe MyStructTypepada heap dan mengembalikan alamatnya. Fungsinya setara; sumber asm kompiler adalah sama.

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

Panggilan

Dalam panggilan fungsi, nilai fungsi dan argumen dievaluasi dalam urutan yang biasa. Setelah dievaluasi, parameter panggilan diteruskan oleh nilai ke fungsi dan fungsi yang dipanggil memulai eksekusi. Parameter pengembalian fungsi dilewatkan oleh nilai kembali ke fungsi panggilan saat fungsi kembali.

Semua fungsi dan parameter pengembalian dilewatkan oleh nilai. Nilai parameter pengembalian dengan tipe *MyStructTypeadalah alamat.

peterSO
sumber
Terima kasih banyak! Terpilih, tetapi saya menerima Sonia karena sedikit tentang analisis pelarian.
Joe
1
peterSo, bagaimana kabar Anda dan @Sonia memproduksi perakitan itu? Anda berdua memiliki format yang sama. Saya tidak dapat memproduksinya terlepas dari perintah / flag, setelah mencoba objdump, buka tool, otool.
10 cls
3
Ah, mengerti - gcflags.
10 cls 10
30

Menurut FAQ Go :

jika kompiler tidak dapat membuktikan bahwa variabel tidak direferensikan setelah fungsi kembali, maka kompiler harus mengalokasikan variabel pada tumpukan sampah yang dikumpulkan untuk menghindari kesalahan pointer menjuntai.

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1 dan Function2 mungkin fungsi inline. Dan variabel return tidak akan lepas. Tidak perlu mengalokasikan variabel di heap.

Contoh kode saya:

 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  }

Menurut output cmd:

go run -gcflags -m test.go

keluaran:

# 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

Jika kompiler cukup pintar, F1 () F2 () F3 () mungkin tidak dipanggil. Karena tidak ada artinya.

Tidak peduli apakah suatu variabel dialokasikan pada heap atau stack, gunakan saja. Lindungi dengan mutex atau saluran jika perlu.

g10guang
sumber