Mekanika bahasa tumpukan dan pointer

Pendahuluan


Ini adalah yang pertama dari empat artikel dalam seri yang akan memberikan wawasan tentang mekanisme dan desain pointer, tumpukan, tumpukan, analisis pelarian, dan semantik Go / pointer. Posting ini adalah tentang tumpukan dan petunjuk.

Daftar Isi:

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

pengantar


Saya tidak akan menyembunyikan - pointer sulit dimengerti. Jika digunakan secara tidak benar, pointer dapat menyebabkan kesalahan yang tidak menyenangkan dan bahkan masalah kinerja. Ini terutama benar ketika menulis program kompetitif atau multithreaded. Tidak mengherankan, banyak bahasa mencoba menyembunyikan pointer dari programmer. Namun, jika Anda menulis di Go, Anda tidak bisa lepas dari petunjuk. Tanpa pemahaman yang jelas tentang pointer, akan sulit bagi Anda untuk menulis kode yang bersih, sederhana, dan efisien.

Perbatasan bingkai


Fungsi dilakukan dalam batas bingkai yang menyediakan ruang memori terpisah untuk setiap fungsi terkait. Setiap frame memungkinkan fungsi untuk bekerja dalam konteksnya sendiri, dan juga menyediakan kontrol aliran. Suatu fungsi memiliki akses langsung ke memori di dalam frame-nya melalui sebuah pointer, tetapi akses ke memori di luar frame membutuhkan akses tidak langsung. Agar suatu fungsi dapat mengakses memori di luar bingkainya, memori ini harus digunakan bersamaan dengan fungsi ini. Mekanisme dan batasan yang ditetapkan oleh batas-batas ini harus dipahami dan dipelajari terlebih dahulu.

Ketika suatu fungsi dipanggil, transisi antara dua frame terjadi. Kode berpindah dari bingkai fungsi panggilan ke bingkai fungsi yang dipanggil. Jika data diperlukan untuk memanggil fungsi, maka data ini harus ditransfer dari satu frame ke frame lainnya. Transfer data antara dua frame di Go dilakukan "berdasarkan nilai."

Keuntungan dari transmisi data “by value” adalah keterbacaan. Nilai yang Anda lihat dalam panggilan fungsi adalah apa yang disalin dan diterima di sisi lain. Itulah mengapa saya menghubungkan "pass by value" dengan WYSIWYG, karena apa yang Anda lihat adalah apa yang Anda dapatkan. Semua ini memungkinkan Anda untuk menulis kode yang tidak menyembunyikan biaya beralih antara dua fungsi. Ini membantu mempertahankan model mental yang baik tentang bagaimana setiap pemanggilan fungsi akan memengaruhi program selama transisi.

Lihatlah program kecil ini yang memanggil fungsi dengan mengirimkan data integer "berdasarkan nilai":

Daftar 1:

01 package main
02
03 func main() {
04
05    // Declare variable of type int with a value of 10.
06    count := 10
07
08    // Display the "value of" and "address of" count.
09    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
10
11    // Pass the "value of" the count.
12    increment(count)
13
14    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc int) {
19
20    // Increment the "value of" inc.
21    inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
23 }

Ketika program Go Anda diluncurkan, runtime menciptakan goroutine utama untuk mulai mengeksekusi semua kode, termasuk kode di dalam fungsi utama. Gorutin adalah jalur eksekusi yang sesuai dengan utas sistem operasi, yang akhirnya berjalan pada beberapa kernel. Dimulai dengan versi 1.8, setiap goroutine dilengkapi dengan blok awal memori kontinu berukuran 2048 byte, yang membentuk ruang stack. Ukuran tumpukan awal ini telah berubah selama bertahun-tahun dan dapat berubah di masa mendatang.

Tumpukan ini penting karena memberikan ruang memori fisik untuk batas bingkai yang diberikan ke masing-masing fungsi individu. Pada saat goroutine utama menjalankan fungsi utama pada Listing 1, tumpukan program (pada level yang sangat tinggi) akan terlihat seperti ini:

Gambar 1:



Pada Gambar 1, Anda dapat melihat bahwa bagian dari tumpukan "dibingkai" untuk fungsi utama. Bagian ini disebut " stack frame ", dan inilah frame yang menunjukkan batas fungsi utama pada stack. Frame diatur sebagai bagian dari kode yang dieksekusi ketika fungsi dipanggil. Anda juga dapat melihat bahwa memori untuk variabel jumlah dialokasikan pada 0x10429fa4 di dalam bingkai untuk utama.

Ada hal menarik lainnya, diilustrasikan dalam Gambar 1. Semua memori tumpukan di bawah bingkai aktif tidak valid, tetapi memori dari bingkai aktif dan di atas valid. Anda perlu memahami batas antara bagian tumpukan yang valid dan tidak valid.

Alamat


Variabel digunakan untuk menetapkan nama ke sel memori tertentu untuk meningkatkan keterbacaan kode dan membantu Anda memahami data apa yang sedang Anda kerjakan. Jika Anda memiliki variabel, maka Anda memiliki nilai dalam memori, dan jika Anda memiliki nilai dalam memori, maka itu harus memiliki alamat. Pada baris 09, fungsi utama memanggil fungsi println built-in untuk menampilkan "nilai" dan "alamat" dari variabel count.

Listing 2:

09    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")

Menggunakan ampersand "&" untuk mendapatkan alamat lokasi variabel bukanlah hal baru, bahasa lain juga menggunakan operator ini. Output dari baris 09 akan terlihat seperti output di bawah ini jika Anda menjalankan kode pada arsitektur 32-bit seperti Go Playground:

Listing 3:

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

Panggilan fungsi


Selanjutnya, pada saluran 12, fungsi utama memanggil fungsi kenaikan.

Listing 4:

12    increment(count)

Melakukan panggilan fungsi berarti program harus membuat bagian memori yang baru pada stack. Namun, semuanya sedikit lebih rumit. Agar berhasil menyelesaikan panggilan fungsi, diharapkan bahwa data akan ditransfer melintasi batas bingkai dan ditempatkan dalam bingkai baru selama transisi. Secara khusus, nilai integer diharapkan akan disalin dan dikirim selama panggilan. Anda dapat melihat persyaratan ini dengan melihat deklarasi fungsi kenaikan pada baris 18.

Daftar 5:

18 func increment(inc int) {

Jika Anda melihat lagi pada panggilan ke fungsi kenaikan pada baris 12, Anda akan melihat bahwa kode melewati "nilai" dari jumlah variabel. Nilai ini akan disalin, ditransfer, dan ditempatkan di bingkai baru untuk fungsi kenaikan. Ingat bahwa fungsi kenaikan hanya dapat membaca dan menulis ke memori dalam bingkai sendiri, sehingga diperlukan variabel inc untuk mendapatkan, menyimpan, dan mengakses salinan nilai penghitung yang ditransmisikan sendiri.

Tepat sebelum kode di dalam fungsi increment mulai dijalankan, tumpukan program (pada level yang sangat tinggi) akan terlihat seperti ini:

Gambar 2:



Anda dapat melihat bahwa sekarang ada dua frame pada stack - satu untuk main dan satu di bawah untuk increment. Di dalam bingkai untuk peningkatan, Anda dapat melihat variabel inc yang berisi nilai 10, yang disalin dan diteruskan selama panggilan fungsi. Alamat variabel inc adalah 0x10429f98, dan kurang dalam memori karena frame didorong ke stack, yang hanya detail implementasi yang tidak berarti apa-apa. Yang penting adalah bahwa program mengambil nilai hitung dari bingkai untuk utama dan menempatkan salinan nilai ini dalam bingkai untuk meningkat menggunakan variabel inc.

Sisa kode di dalam increment increment dan menampilkan "value" dan "address" dari variabel inc.

Listing 6:

21    inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")

Output dari baris 22 di taman bermain akan terlihat seperti ini:

Listing 7:

inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]

Beginilah bentuk stack setelah mengeksekusi baris kode yang sama:

Gambar 3:



Setelah mengeksekusi baris 21 dan 22, fungsi increment berakhir dan mengembalikan kontrol ke fungsi utama. Kemudian fungsi utama lagi menampilkan "nilai" dan "alamat" dari jumlah variabel lokal pada baris 14.

Daftar 8:

14    println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")

Output penuh dari program di taman bermain akan terlihat seperti ini:

Daftar 9:

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]
count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

Nilai hitungan dalam bingkai untuk main adalah sama sebelum dan sesudah panggilan ke kenaikan.

Kembali dari fungsi


Apa yang sebenarnya terjadi pada memori pada tumpukan ketika fungsi keluar dan kontrol kembali ke fungsi panggilan? Jawaban singkatnya bukanlah apa-apa. Inilah yang tampak seperti tumpukan setelah fungsi kenaikan kembali:

Gambar 4:



Tumpukan terlihat persis sama seperti pada Gambar 3, kecuali bahwa bingkai yang terkait dengan fungsi kenaikan sekarang dianggap sebagai memori tidak valid. Ini karena frame untuk main sekarang aktif. Memori yang dibuat untuk fungsi penambahan tetap tidak tersentuh.

Membersihkan bingkai memori dari fungsi pengembalian akan membuang-buang waktu, karena tidak diketahui apakah memori ini akan diperlukan lagi. Jadi ingatan tetap seperti itu. Selama setiap panggilan fungsi, ketika sebuah frame diambil, memori tumpukan untuk frame ini dihapus. Ini dilakukan dengan menginisialisasi nilai-nilai yang sesuai dengan bingkai. Karena semua nilai diinisialisasi sebagai "nilai nol", tumpukan dengan benar dibersihkan dengan setiap panggilan fungsi.

Berbagi Nilai


Bagaimana jika penting untuk fungsi kenaikan untuk bekerja secara langsung dengan variabel jumlah yang ada di dalam bingkai untuk main? Di sinilah saatnya untuk pointer. Pointer melayani satu tujuan - untuk berbagi nilai dengan suatu fungsi sehingga fungsi tersebut dapat membaca dan menulis nilai ini, bahkan jika nilai tersebut tidak ada langsung di dalam bingkai.

Jika Anda tidak merasa perlu "membagikan" nilainya, maka Anda tidak perlu menggunakan pointer. Saat mempelajari petunjuk, penting untuk berpikir bahwa menggunakan kamus yang bersih, bukan operator atau sintaksis. Ingat bahwa pointer dimaksudkan untuk dibagikan dan ketika membaca kode, ganti operator & dengan frasa “berbagi”.

Jenis Pointer


Untuk setiap jenis yang Anda nyatakan, atau yang dideklarasikan langsung oleh bahasa itu sendiri, Anda mendapatkan jenis penunjuk gratis yang dapat Anda gunakan untuk berbagi. Sudah ada tipe built-in yang disebut int, jadi ada tipe pointer bernama * int. Jika Anda mendeklarasikan jenis bernama Pengguna, Anda mendapatkan jenis penunjuk bernama * Pengguna gratis.

Semua jenis pointer memiliki dua karakteristik yang identik. Pertama, mereka mulai dengan karakter *. Kedua, mereka semua memiliki ukuran yang sama dalam memori dan representasi menempati 4 atau 8 byte yang mewakili alamat. Pada arsitektur 32-bit (misalnya, di taman bermain) pointer membutuhkan 4 byte memori, dan pada arsitektur 64-bit (misalnya, komputer Anda) pointer membutuhkan 8 byte memori.

Dalam spesifikasi, tipe penunjukdianggap tipe literal , yang berarti mereka adalah tipe tanpa nama yang terdiri dari tipe yang ada.

Akses memori tidak langsung


Lihatlah program kecil ini yang membuat panggilan fungsi, melewati alamat "berdasarkan nilai". Ini akan membagi variabel jumlah dari bingkai tumpukan utama dengan fungsi kenaikan:

Listing 10:

01 package main
02
03 func main() {
04
05    // Declare variable of type int with a value of 10.
06    count := 10
07
08    // Display the "value of" and "address of" count.
09    println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
10
11    // Pass the "address of" count.
12    increment(&count)
13
14    println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc *int) {
19
20    // Increment the "value of" count that the "pointer points to". (dereferencing)
21    *inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]")
23 }

Tiga perubahan menarik dilakukan pada program aslinya. Perubahan pertama ada di baris 12:

Listing 11:

12    increment(&count)

Kali ini, pada baris 12, kode tidak menyalin dan meneruskan "nilai" ke variabel jumlah, tetapi meneruskan "alamat" alih-alih variabel jumlah. Sekarang Anda dapat mengatakan: "Saya berbagi" jumlah variabel dengan kenaikan fungsi. Inilah yang dikatakan operator & - “bagikan.”

Pahami bahwa ini masih "lewat nilai," dan satu-satunya perbedaan adalah bahwa nilai yang Anda berikan adalah alamat, bukan bilangan bulat. Alamat juga merupakan nilai; ini adalah apa yang disalin dan melewati batas bingkai untuk memanggil fungsi.

Karena nilai alamat disalin dan diteruskan, Anda memerlukan variabel di dalam bingkai kenaikan untuk mendapatkan dan menyimpan alamat integer ini. Deklarasi variabel pointer integer ada di baris 18.

Daftar 12:

18 func increment(inc *int) {

Jika Anda melewati alamat nilai tipe Pengguna, maka variabel tersebut harus dinyatakan sebagai * Pengguna. Terlepas dari kenyataan bahwa semua variabel pointer menyimpan nilai alamat, mereka tidak dapat mengirimkan alamat apa pun, hanya alamat yang terkait dengan jenis pointer. Prinsip dasar berbagi nilai adalah bahwa fungsi penerima harus membaca atau menulis ke nilai itu. Anda memerlukan informasi tentang jenis nilai apa pun untuk membaca dan menulisnya. Compiler akan memastikan bahwa hanya nilai-nilai yang terkait dengan tipe pointer yang benar yang digunakan dengan fungsi ini.

Beginilah bentuk stack setelah memanggil fungsi increment:

Gambar 5:



Gambar 5 menunjukkan seperti apa tumpukan itu ketika "lewat nilai" dilakukan dengan menggunakan alamat sebagai nilainya. Variabel pointer di dalam frame untuk fungsi increment sekarang menunjuk ke variabel count, yang terletak di dalam frame untuk main.

Sekarang, menggunakan variabel pointer, fungsi tersebut dapat melakukan pembacaan tidak langsung dan mengubah operasi untuk variabel jumlah yang terletak di dalam bingkai untuk main.

Listing 13:

21    *inc++

Kali ini, karakter * bertindak sebagai operator dan diterapkan ke variabel pointer. Menggunakan * sebagai operator berarti "nilai yang ditunjuk oleh pointer." Variabel pointer menyediakan akses tidak langsung ke memori di luar bingkai fungsi yang menggunakannya. Kadang-kadang membaca atau menulis tidak langsung ini disebut pointer dereferencing. Fungsi kenaikan masih perlu memiliki variabel pointer di dalam bingkai, yang dapat langsung dibaca untuk melakukan akses tidak langsung.

Gambar 6 menunjukkan seperti apa tumpukan setelah baris 21.

Gambar 6:



Berikut ini adalah hasil akhir dari program ini:

Listing 14:

count:  Value Of[ 10 ]              Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 0x10429fa4 ]      Addr Of[ 0x10429f98 ]   Value Points To[ 11 ]
count:  Value Of[ 11 ]              Addr Of[ 0x10429fa4 ]

Anda mungkin memperhatikan bahwa “nilai” dari variabel penunjuk inc cocok dengan “alamat” variabel penghitung. Ini membangun hubungan berbagi yang memungkinkan akses tidak langsung ke memori di luar bingkai. Segera setelah fungsi kenaikan menulis melalui pointer, perubahan akan terlihat ke fungsi utama ketika kontrol dikembalikan ke sana.

Variabel pointer tidak spesial


Variabel pointer tidak spesial karena mereka adalah variabel yang sama dengan variabel lainnya. Mereka memiliki alokasi memori, dan mengandung makna. Kebetulan bahwa semua variabel pointer, terlepas dari jenis nilai yang mereka dapat tunjukkan, selalu memiliki ukuran dan presentasi yang sama. Yang dapat membingungkan adalah bahwa karakter * bertindak sebagai operator di dalam kode dan digunakan untuk mendeklarasikan tipe pointer. Jika Anda dapat membedakan deklarasi tipe dari operasi pointer, ini dapat membantu menghilangkan beberapa kebingungan.

Kesimpulan


Posting ini menjelaskan tujuan pointer, operasi stack, dan mekanisme pointer di Go. Ini adalah langkah pertama dalam memahami mekanisme, prinsip desain, dan teknik penggunaan yang diperlukan untuk menulis kode yang koheren dan mudah dibaca.

Pada akhirnya, inilah yang Anda pelajari:

  • Fungsi dilakukan dalam batas bingkai, yang menyediakan ruang memori terpisah untuk setiap fungsi yang sesuai.
  • Ketika suatu fungsi dipanggil, transisi antara dua frame terjadi.
  • Keuntungan dari transmisi data “by value” adalah keterbacaan.
  • Tumpukan ini penting karena memberikan ruang memori fisik untuk batas bingkai yang diberikan ke masing-masing fungsi individu.
  • Semua memori tumpukan di bawah bingkai aktif tidak valid, tetapi memori dari bingkai aktif dan di atas valid.
  • , .
  • , , .
  • — , , .
  • , , , , .
  • - , .
  • - - , , . , .

All Articles