Analisis mekanika bahasa lolos

Pendahuluan


Ini adalah artikel kedua dari empat artikel yang akan memberikan wawasan tentang mekanisme dan desain pointer, tumpukan, tumpukan, analisis pelarian, dan semantik Go / Value. Posting ini tentang tumpukan dan analisis melarikan diri.

Daftar Isi:

  1. Mekanika Bahasa Di Stacks And Pointer ( terjemahan )
  2. Mekanika Bahasa Pada Analisis Escape
  3. Mekanika Bahasa Tentang Memori
  4. Desain Filosofi Pada Data Dan Semantik

pengantar


Dalam posting pertama dalam seri ini, saya berbicara tentang dasar-dasar mekanika pointer menggunakan contoh di mana nilai didistribusikan di tumpukan antara goroutine. Saya tidak menunjukkan kepada Anda apa yang terjadi ketika Anda membagi nilai pada tumpukan. Untuk memahami ini, Anda perlu mencari tahu tentang area memori lain di mana nilainya mungkin: tentang "tumpukan". Dengan pengetahuan ini, Anda dapat mulai mempelajari "analisis pelarian".
Analisis melarikan diri adalah proses yang digunakan kompiler untuk menentukan penempatan nilai yang dibuat oleh program Anda. Khususnya, kompiler melakukan analisis kode statis untuk menentukan apakah nilai dapat ditempatkan pada bingkai tumpukan untuk fungsi yang membangunnya, atau jika nilai harus "lolos" ke tumpukan. Tidak ada kata kunci atau fungsi tunggal di Go yang dapat Anda gunakan untuk memberi tahu kompiler keputusan yang harus diambil. Hanya cara Anda menulis kode secara kondisional yang memungkinkan Anda untuk memengaruhi keputusan ini.

Tumpukan


Tumpukan adalah area memori kedua, selain tumpukan, yang digunakan untuk menyimpan nilai. Tumpukannya tidak membersihkan sendiri seperti tumpukan, jadi menggunakan memori ini lebih mahal. Pertama-tama, biaya terkait dengan pengumpul sampah, yang harus menjaga daerah ini bersih. Ketika GC dimulai, ia akan menggunakan 25% daya prosesor Anda yang tersedia. Selain itu, ini berpotensi dapat membuat mikrodetik dari penundaan "hentikan dunia". Keuntungan memiliki GC adalah Anda tidak perlu khawatir mengelola memori tumpukan yang secara historis rumit dan rawan kesalahan.

Nilai-nilai di tumpukan memprovokasi alokasi memori di Go. Alokasi ini memberi tekanan pada GC karena setiap nilai di heap yang tidak lagi mengacu pada pointer harus dihapus. Semakin banyak nilai yang perlu Anda periksa dan hapus, semakin banyak pekerjaan yang harus dilakukan GC pada setiap awal. Oleh karena itu, algoritma stimulasi terus bekerja untuk menyeimbangkan ukuran tumpukan dan kecepatan eksekusi.

Berbagi tumpukan


Di Go, tidak ada goroutine yang diizinkan memiliki penunjuk yang menunjuk ke memori di tumpukan goroutine lain. Hal ini disebabkan oleh fakta bahwa memori tumpukan untuk goroutine dapat diganti dengan blok memori baru, ketika tumpukan harus meningkat atau berkurang. Jika pada saat run time Anda harus melacak pointer stack di goroutine lain, Anda harus mengelola terlalu banyak, dan penundaan "hentikan dunia" ketika memperbarui pointer ke tumpukan ini akan mengejutkan.

Berikut adalah contoh tumpukan yang diganti beberapa kali karena pertumbuhan. Lihatlah output di baris 2 dan 6. Anda akan melihat dua kali perubahan alamat dari nilai string di dalam frame stack utama.

play.golang.org/p/pxn5u4EBSI

Mekanik melarikan diri


Setiap kali nilai dibagi di luar wilayah bingkai tumpukan fungsi, itu ditempatkan (atau dialokasikan) di heap. Tugas algoritma melarikan diri analisis adalah untuk menemukan situasi seperti itu dan menjaga tingkat integritas dalam program. Integritas adalah untuk memastikan bahwa akses ke nilai apa pun selalu akurat, konsisten, dan efisien.

Lihatlah contoh ini untuk mempelajari mekanisme dasar analisis pelarian.

play.golang.org/p/Y_VZxYteKO

Listing 1

01 package main
02
03 type user struct {
04     name  string
05     email string
06 }
07
08 func main() {
09     u1 := createUserV1()
10     u2 := createUserV2()
11
12     println("u1", &u1, "u2", &u2)
13 }
14
15 //go:noinline
16 func createUserV1() user {
17     u := user{
18         name:  "Bill",
19         email: "bill@ardanlabs.com",
20     }
21
22     println("V1", &u)
23     return u
24 }
25
26 //go:noinline
27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

Saya menggunakan perintah go: noinline sehingga kompiler tidak menyematkan kode untuk fungsi-fungsi ini secara langsung di utama. Menanamkan akan menghapus panggilan fungsi dan memperumit contoh ini. Saya akan berbicara tentang efek samping dari penanaman di posting berikutnya.

Listing 1 menunjukkan program dengan dua fungsi berbeda yang menciptakan nilai tipe pengguna dan mengembalikannya ke pemanggil. Versi pertama dari fungsi ini menggunakan semantik nilai saat kembali.

Listing 2

16 func createUserV1() user {
17     u := user{
18         name:  "Bill",
19         email: "bill@ardanlabs.com",
20     }
21
22     println("V1", &u)
23     return u
24 }

Saya mengatakan bahwa fungsi menggunakan semantik nilai ketika kembali, karena nilai tipe pengguna yang dibuat oleh fungsi ini disalin dan diteruskan ke tumpukan panggilan. Ini berarti bahwa fungsi panggilan menerima salinan dari nilai itu sendiri.

Anda dapat melihat pembuatan nilai tipe pengguna, dieksekusi pada baris 17 hingga 20. Kemudian, pada baris 23, salinan nilai diteruskan ke tumpukan panggilan dan dikembalikan ke pemanggil. Setelah mengembalikan fungsi, tumpukan terlihat sebagai berikut.

Gambar 1



Pada Gambar 1, Anda dapat melihat bahwa nilai tipe pengguna ada di kedua frame setelah memanggil createUserV1. Dalam versi kedua dari fungsi, semantik pointer digunakan untuk kembali.

Listing 3

27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

Saya mengatakan bahwa fungsi menggunakan pointer semantik ketika kembali, karena nilai tipe pengguna yang dibuat oleh fungsi ini dibagi oleh tumpukan panggilan. Ini berarti bahwa fungsi panggilan menerima salinan alamat tempat nilai-nilai itu berada.

Anda dapat melihat struktur literal yang sama yang digunakan dalam baris 28 hingga 31 untuk membuat nilai tipe pengguna, tetapi pada baris 34 pengembalian dari fungsi berbeda. Alih-alih meneruskan salinan nilai kembali ke tumpukan panggilan, salinan alamat untuk nilai diteruskan. Berdasarkan ini, Anda mungkin berpikir bahwa setelah panggilan tumpukan terlihat seperti ini.

Gambar 2



Jika apa yang Anda lihat pada Gambar 2 benar-benar terjadi, Anda akan memiliki masalah integritas. Pointer menunjuk ke tumpukan panggilan ke memori yang tidak lagi valid. Lain kali fungsi dipanggil, memori yang ditunjukkan akan diformat ulang dan diinisialisasi ulang.

Di sinilah analisis pelarian mulai mempertahankan integritas. Dalam kasus ini, kompiler akan menentukan bahwa tidak aman untuk membuat nilai tipe pengguna di dalam bingkai stack createUserV2, jadi alih-alih itu akan membuat nilai pada heap. Ini akan terjadi segera selama konstruksi pada jalur 28.

Keterbacaan


Seperti yang Anda pelajari dari posting sebelumnya, suatu fungsi memiliki akses langsung ke memori di dalam frame-nya melalui pointer frame, tetapi akses ke memori di luar frame membutuhkan akses tidak langsung. Ini berarti bahwa akses ke nilai-nilai yang jatuh ke tumpukan juga harus dilakukan secara tidak langsung melalui pointer.

Ingat seperti apa kode createUserV2 itu.

Listing 4

27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

Sintaks menyembunyikan apa yang sebenarnya terjadi dalam kode ini. Variabel yang dideklarasikan pada baris 28 mewakili nilai tipe pengguna. Konstruksi di Go tidak memberi tahu Anda dengan tepat di mana nilai disimpan dalam memori, jadi sebelum pernyataan kembali pada baris 34 Anda tidak tahu bahwa nilai akan ditumpuk. Ini berarti bahwa meskipun Anda mewakili nilai dari pengguna tipe, akses ke nilai ini harus melalui sebuah pointer.

Anda dapat memvisualisasikan tumpukan yang terlihat seperti ini setelah panggilan fungsi.

Gambar 3



Variabel u dalam bingkai tumpukan untuk createUserV2 mewakili nilai pada heap, bukan pada stack. Ini berarti bahwa menggunakan u untuk mengakses suatu nilai memerlukan akses ke sebuah pointer, bukan akses langsung yang disarankan oleh sintaksis. Anda mungkin berpikir, mengapa tidak segera membuat pointer, karena mengakses nilai yang diwakilinya masih memerlukan penggunaan pointer?

Listing 5

27 func createUserV2() *user {
28     u := &user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", u)
34     return u
35 }

Jika Anda melakukannya, maka Anda akan kehilangan keterbacaan, yang tidak akan hilang dalam kode Anda. Berpindahlah dari badan fungsi sesaat dan hanya fokus pada pengembalian.

Listing 6

34     return u
35 }

Apa yang dibicarakan pengembalian ini? Yang ia katakan hanyalah bahwa salinan Anda didorong ke tumpukan panggilan. Sementara itu, apa yang dikembalikan memberitahu Anda ketika Anda menggunakan operator &?

Listing 7

34     return &u
35 }

Terima kasih kepada operator & pengembalian, sekarang memberitahu Anda bahwa Anda berbagi tumpukan panggilan dan karenanya pergi ke tumpukan. Ingat bahwa pointer dimaksudkan untuk digunakan bersama-sama dan ketika membaca kode mereka mengganti operator & dengan frasa "berbagi". Ini sangat kuat dalam hal keterbacaan. Ini adalah sesuatu yang saya tidak ingin kehilangan.

Berikut adalah contoh lain di mana membangun nilai menggunakan semantik pointer menurunkan keterbacaan.

Listing 8

01 var u *user
02 err := json.Unmarshal([]byte(r), &u)
03 return u, err

Agar kode ini berfungsi, saat Anda memanggil json.Unmarshal pada baris 02, Anda harus meneruskan pointer ke variabel pointer. Panggilan json.Unmarshal akan membuat nilai tipe pengguna dan menetapkan alamatnya ke variabel penunjuk. play.golang.org/p/koI8EjpeIx

Apa yang dikatakan kode ini:
01: Buat pointer tipe pengguna dengan nilai nol.
02: Bagikan variabel Anda dengan fungsi json.Unmarshal.
03: Kembalikan salinan variabel u ke pemanggil.

Tidak sepenuhnya jelas bahwa nilai tipe pengguna dibuat oleh fungsi json.Unmarshal diteruskan ke pemanggil.

Bagaimana perubahan keterbacaan saat menggunakan semantik nilai selama deklarasi variabel?

Listing 9

01 var u user
02 err := json.Unmarshal([]byte(r), &u)
03 return &u, err

Apa yang dikatakan kode ini:
01: Buat nilai tipe pengguna dengan nilai nol.
02: Bagikan variabel Anda dengan fungsi json.Unmarshal.
03: Bagikan variabel u dengan penelepon.

Semuanya sangat jelas. Jalur 02 membagi nilai tipe pengguna ke tumpukan panggilan di json.Unmarshal, dan jalur 03 membagi nilai tumpukan panggilan kembali ke pemanggil. Bagian ini akan menyebabkan nilai berpindah ke heap.

Gunakan semantik nilai saat membuat nilai dan manfaatkan keterbacaan operator & untuk memperjelas bagaimana nilai dipisahkan.

Pelaporan kompiler


Untuk melihat keputusan yang dibuat oleh kompiler, Anda dapat meminta kompiler untuk memberikan laporan. Yang harus Anda lakukan adalah menggunakan sakelar -gcflags dengan opsi -m saat memanggil go build.

Sebenarnya, Anda dapat menggunakan 4 level -m, tetapi setelah 2 level informasi itu menjadi terlalu banyak. Saya akan menggunakan 2 level -m.

Listing 10

$ go build -gcflags "-m -m"
./main.go:16: cannot inline createUserV1: marked go:noinline
./main.go:27: cannot inline createUserV2: marked go:noinline
./main.go:8: cannot inline main: non-leaf function
./main.go:22: createUserV1 &u does not escape
./main.go:34: &u escapes to heap
./main.go:34:     from ~r0 (return) at ./main.go:34
./main.go:31: moved to heap: u
./main.go:33: createUserV2 &u does not escape
./main.go:12: main &u1 does not escape
./main.go:12: main &u2 does not escape

Anda dapat melihat bahwa kompiler melaporkan keputusan untuk membuang nilai ke tumpukan. Apa yang dikatakan kompiler? Pertama, lihat kembali fungsi createUserV1 dan createUserV2 untuk menyegarkan mereka dalam memori.

Listing 13

16 func createUserV1() user {
17     u := user{
18         name:  "Bill",
19         email: "bill@ardanlabs.com",
20     }
21
22     println("V1", &u)
23     return u
24 }

27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

Mari kita mulai dengan baris ini dalam laporan.

Listing 14

./main.go:22: createUserV1 &u does not escape

Ini menunjukkan bahwa panggilan ke fungsi println di dalam fungsi createUserV1 tidak menyebabkan tipe pengguna dibuang ke heap. Kasing ini harus diperiksa karena digunakan bersama dengan fungsi println.

Selanjutnya, lihat garis-garis ini dalam laporan.

Listing 15

./main.go:34: &u escapes to heap
./main.go:34:     from ~r0 (return) at ./main.go:34
./main.go:31: moved to heap: u
./main.go:33: createUserV2 &u does not escape

Baris-baris ini mengatakan bahwa nilai tipe pengguna yang terkait dengan variabel u, yang memiliki tipe pengguna bernama dan dibuat pada baris 31, dibuang ke heap karena return on line 34. Baris terakhir mengatakan hal yang sama seperti sebelumnya, println panggilan pada baris 33 tidak mengatur ulang tipe pengguna.

Membaca laporan-laporan ini dapat membingungkan dan dapat sedikit bervariasi tergantung pada apakah jenis variabel yang dimaksud didasarkan pada jenis yang dinamai atau harfiah.

Ubah variabel u menjadi pengguna tipe literal * alih-alih pengguna tipe bernama, seperti sebelumnya.

Listing 16

27 func createUserV2() *user {
28     u := &user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", u)
34     return u
35 }

Jalankan laporan lagi.

Listing 17

./main.go:30: &user literal escapes to heap
./main.go:30:     from u (assigned) at ./main.go:28
./main.go:30:     from ~r0 (return) at ./main.go:34

Sekarang laporan mengatakan bahwa nilai tipe pengguna yang dirujuk oleh variabel u, yang memiliki tipe pengguna * literal dan dibuat pada baris 28, dibuang ke tumpukan karena pengembalian pada baris 34.

Kesimpulan


Menciptakan nilai tidak menentukan di mana ia berada. Hanya bagaimana nilai dibagi akan menentukan apa yang akan dilakukan oleh kompiler dengan nilai ini. Setiap kali Anda berbagi nilai dalam tumpukan panggilan, itu dibuang ke tumpukan. Ada alasan lain mengapa suatu nilai mungkin lolos dari tumpukan. Saya akan membicarakannya di posting selanjutnya.

Tujuan dari posting ini adalah untuk memberikan panduan dalam memilih untuk menggunakan semantik nilai atau semantik pointer untuk semua tipe yang diberikan. Setiap semantik dipasangkan dengan laba dan nilai. Semantik nilai menyimpan nilai-nilai pada tumpukan, yang mengurangi beban pada GC. Namun, ada beberapa salinan dengan nilai yang sama yang harus disimpan, dilacak, dan dipelihara. Semantik Pointer menempatkan nilai dalam tumpukan, yang dapat memberi tekanan pada GC. Namun, mereka efektif karena hanya ada satu nilai yang perlu disimpan, dilacak, dan dipelihara. Poin kuncinya adalah penggunaan setiap semantik dengan benar, konsisten dan seimbang.

All Articles