Dalam bahasa pemrograman seperti C dan C ++, orang sering merujuk pada alokasi memori statis dan dinamis. Saya mengerti konsepnya tetapi frasa "Semua memori dialokasikan (dicadangkan) selama waktu kompilasi" selalu membingungkan saya.
Kompilasi, seperti yang saya mengerti, mengkonversi kode C / C ++ tingkat tinggi ke bahasa mesin dan menghasilkan file yang dapat dieksekusi. Bagaimana memori "dialokasikan" dalam file yang dikompilasi? Bukankah memori selalu dialokasikan dalam RAM dengan semua hal manajemen memori virtual?
Bukankah alokasi memori menurut definisi konsep runtime?
Jika saya membuat variabel statis 1KB yang dialokasikan dalam kode C / C ++ saya, apakah itu akan meningkatkan ukuran executable dengan jumlah yang sama?
Ini adalah salah satu halaman di mana frasa tersebut digunakan di bawah judul "Alokasi statis".
Kembali ke Dasar: Alokasi memori, jalan-jalan di sepanjang sejarah
sumber
Jawaban:
Memori yang dialokasikan pada waktu kompilasi berarti kompiler menyelesaikan pada waktu kompilasi di mana hal-hal tertentu akan dialokasikan di dalam peta memori proses.
Misalnya, pertimbangkan array global:
Compiler mengetahui pada waktu kompilasi ukuran array dan ukuran suatu
int
, sehingga ia mengetahui seluruh ukuran array pada waktu kompilasi. Juga variabel global memiliki durasi penyimpanan statis secara default: itu dialokasikan di area memori statis dari ruang memori proses (.data / .bss bagian). Mengingat informasi itu, kompilator memutuskan selama kompilasi di alamat mana dari area memori statis array .Tentu saja alamat memori itu adalah alamat virtual. Program ini mengasumsikan bahwa ia memiliki seluruh ruang memori sendiri (Dari 0x00000000 hingga 0xFFFFFFFF misalnya). Itu sebabnya kompiler dapat melakukan asumsi seperti "Oke, array akan ada di alamat 0x00A33211". Pada saat runtime, alamat diterjemahkan ke alamat real / hardware oleh MMU dan OS.
Nilai yang diinisialisasi hal-hal penyimpanan statis sedikit berbeda. Sebagai contoh:
Dalam contoh pertama kami, kompiler hanya memutuskan di mana array akan dialokasikan, menyimpan informasi itu di executable.
Dalam hal hal-hal yang diinisialisasi nilai, kompiler juga menyuntikkan nilai awal array ke dalam executable, dan menambahkan kode yang memberitahu program loader bahwa setelah alokasi array pada saat program dimulai, array harus diisi dengan nilai-nilai ini.
Berikut adalah dua contoh rakitan yang dihasilkan oleh kompiler (GCC4.8.1 dengan target x86):
Kode C ++:
Perakitan output:
Seperti yang Anda lihat, nilai langsung disuntikkan ke majelis. Dalam array
a
, kompiler menghasilkan inisialisasi nol 16 byte, karena Standar mengatakan bahwa hal-hal yang tersimpan statis harus diinisialisasi ke nol secara default:Saya selalu menyarankan orang untuk membongkar kode mereka untuk melihat apa yang sebenarnya dilakukan oleh kompiler dengan kode C ++. Ini berlaku dari kelas / durasi penyimpanan (seperti pertanyaan ini) hingga optimisasi kompiler tingkat lanjut. Anda bisa menginstruksikan kompiler Anda untuk membuat perakitan, tetapi ada alat yang bagus untuk melakukan ini di Internet dengan ramah. Favorit saya adalah GCC Explorer .
sumber
Memori yang dialokasikan pada waktu kompilasi berarti tidak akan ada alokasi lebih lanjut pada waktu berjalan - tidak ada panggilan ke malloc, baru, atau metode alokasi dinamis lainnya. Anda akan memiliki jumlah penggunaan memori yang tetap walaupun Anda tidak membutuhkan semua memori itu sepanjang waktu.
Memori tidak digunakan sebelum waktu berjalan, tetapi segera sebelum eksekusi mulai alokasi ditangani oleh sistem.
Cukup mendeklarasikan statis tidak akan meningkatkan ukuran executable Anda lebih dari beberapa byte. Mendeklarasikannya dengan nilai awal yang bukan nol akan (untuk menyimpan nilai awal itu). Sebaliknya, penghubung hanya menambahkan jumlah 1KB ini ke persyaratan memori yang dibuat oleh loader sistem untuk Anda segera sebelum eksekusi.
sumber
static int i[4] = {2 , 3 , 5 ,5 }
apakah ini akan meningkat dengan ukuran yang dapat dieksekusi sebesar 16 byte. Anda berkata "Cukup mendeklarasikan statis tidak akan meningkatkan ukuran executable Anda lebih dari beberapa byte. Mendeklarasikannya dengan nilai awal yang bukan nol akan" Mendeklarasikannya dengan nilai awal akan apa artinya.Memori yang dialokasikan dalam waktu kompilasi berarti bahwa ketika Anda memuat program, beberapa bagian dari memori akan segera dialokasikan dan ukuran dan (relatif) posisi alokasi ini ditentukan pada waktu kompilasi.
Ketiga variabel tersebut "dialokasikan pada waktu kompilasi", artinya kompiler menghitung ukurannya (yang diperbaiki) pada waktu kompilasi. Variabel
a
akan menjadi offset dalam memori, katakanlah, menunjuk ke alamat 0,b
akan menunjuk ke alamat 33 danc
di 34 (seandainya tidak ada optimasi penyelarasan). Jadi, mengalokasikan 1Kb data statis tidak akan meningkatkan ukuran kode Anda , karena itu hanya akan mengubah offset di dalamnya. Ruang aktual akan dialokasikan pada waktu pengambilan .Alokasi memori nyata selalu terjadi dalam waktu yang berjalan, karena kernel perlu melacaknya dan memperbarui struktur data internal (berapa banyak memori yang dialokasikan untuk setiap proses, halaman, dan sebagainya). Perbedaannya adalah bahwa kompiler sudah mengetahui ukuran setiap data yang akan Anda gunakan dan ini dialokasikan segera setelah program Anda dijalankan.
Ingat juga bahwa kita berbicara tentang alamat relatif . Alamat asli tempat variabel akan ditempatkan akan berbeda. Pada waktu buka kernel akan mencadangkan beberapa memori untuk proses, katakanlah di alamat
x
, dan semua alamat yang dikodekan dalam file yang dapat dieksekusi akan ditambahkan olehx
byte, sehingga variabela
dalam contoh akan di alamatx
, b di alamatx+33
dan begitu seterusnya.sumber
Menambahkan variabel pada tumpukan yang mengambil N byte tidak (tentu) meningkatkan ukuran bin oleh N byte. Itu akan, pada kenyataannya, menambah tetapi beberapa byte sebagian besar waktu.
Mari kita mulai dengan contoh bagaimana menambahkan 1000 karakter ke kode Anda akan meningkatkan ukuran bin secara linear.
Jika 1k adalah string, dari seribu karakter, yang dinyatakan seperti itu
dan kemudian
vim your_compiled_bin
, Anda benar-benar dapat melihat string di tempat sampah di suatu tempat. Dalam hal itu, ya: executable akan menjadi 1 k lebih besar, karena berisi string secara penuh.Namun, jika Anda mengalokasikan array
int
s,char
s ataulong
s pada stack dan menetapkannya dalam satu lingkaran, sesuatu di sepanjang baris inikemudian, tidak: itu tidak akan menambah nampan ... dengan
1000*sizeof(int)
Alokasi pada waktu kompilasi berarti apa yang Anda pahami sekarang (berdasarkan komentar Anda): nampan yang dikompilasi berisi informasi yang dibutuhkan sistem untuk mengetahui berapa banyak memori fungsi / blok apa yang akan dibutuhkan ketika dieksekusi, bersama dengan informasi tentang ukuran tumpukan yang dibutuhkan aplikasi Anda. Itulah yang akan dialokasikan sistem ketika menjalankan nampan Anda, dan program Anda menjadi sebuah proses (well, pengeksekusian nampan Anda adalah proses yang ... well, Anda mengerti apa yang saya katakan).
Tentu saja, saya tidak akan melukis gambar lengkapnya di sini: Tempat sampah berisi informasi tentang seberapa besar tumpukan tempat sampah yang sebenarnya akan dibutuhkan. Berdasarkan informasi ini (antara lain), sistem akan mencadangkan sejumlah memori, yang disebut stack, bahwa program mendapat semacam pemerintahan bebas. Memori tumpukan masih dialokasikan oleh sistem, ketika proses (hasil dari bin Anda dijalankan) dimulai. Proses kemudian mengelola memori tumpukan untuk Anda. Ketika suatu fungsi atau loop (semua jenis blok) dipanggil / dieksekusi, variabel-variabel lokal ke blok tersebut didorong ke stack, dan mereka dihapus (memori stack "dibebaskan" begitu saja) untuk digunakan oleh yang lain fungsi / blok. Begitu mendeklarasikan
int some_array[100]
hanya akan menambahkan beberapa byte informasi tambahan ke nampan, yang memberitahu sistem bahwa fungsi X akan membutuhkan100*sizeof(int)
+ beberapa ruang pembukuan tambahan.sumber
i
tidak "dibebaskan" atau keduanya. Jikai
berada di memori, itu hanya akan didorong ke tumpukan, sesuatu yang tidak dibebaskan dalam arti kata, mengabaikan itui
atauc
akan disimpan dalam register sepanjang waktu. Tentu saja, ini semua tergantung pada kompiler, yang berarti bukan hitam dan putih.free()
panggilan, tetapi memori tumpukan yang mereka gunakan gratis untuk digunakan oleh fungsi-fungsi lain begitu fungsi yang saya daftarkan kembali. Saya menghapus kode, karena mungkin membingungkan bagi beberapa orangPada banyak platform, semua alokasi global atau statis dalam setiap modul akan dikonsolidasikan oleh kompiler menjadi tiga atau lebih sedikit alokasi konsolidasi (satu untuk data yang tidak diinisialisasi (sering disebut "bss"), satu untuk data yang dapat diinisialisasi yang dapat ditulis (sering disebut "data" ), dan satu untuk data konstan ("const")), dan semua alokasi global atau statis dari setiap jenis dalam suatu program akan dikonsolidasikan oleh linker menjadi satu global untuk setiap jenis. Misalnya, dengan asumsi
int
empat byte, modul memiliki yang berikut ini sebagai satu-satunya alokasi statis:itu akan memberitahu linker bahwa diperlukan 208 byte untuk bss, 16 byte untuk "data", dan 28 byte untuk "const". Selanjutnya, setiap referensi ke variabel akan diganti dengan pemilih area dan offset, sehingga a, b, c, d, dan e, akan diganti oleh bss + 0, const + 0, bss + 4, const + 24, data +0, atau bss + 204, masing-masing.
Ketika suatu program dihubungkan, semua area bss dari semua modul digabung menjadi satu; demikian juga data dan area const. Untuk setiap modul, alamat variabel bss-relatif akan ditingkatkan dengan ukuran semua area bss modul sebelumnya (sekali lagi, juga dengan data dan const). Jadi, ketika penghubung selesai, program apa pun akan memiliki satu alokasi bss, satu alokasi data, dan satu alokasi konstanta.
Ketika sebuah program dimuat, satu dari empat hal umumnya akan terjadi tergantung pada platform:
Eksekusi akan menunjukkan berapa banyak byte yang dibutuhkan untuk setiap jenis data dan - untuk area data yang diinisialisasi, di mana konten awal dapat ditemukan. Ini juga akan mencakup daftar semua instruksi yang menggunakan alamat bss-, data-, atau konstanta. Sistem operasi atau loader akan mengalokasikan jumlah ruang yang sesuai untuk setiap area dan kemudian menambahkan alamat awal area tersebut ke setiap instruksi yang membutuhkannya.
Sistem operasi akan mengalokasikan sepotong memori untuk menampung ketiga jenis data, dan memberi aplikasi pointer ke potongan memori itu. Kode apa pun yang menggunakan data statis atau global akan melakukan dereferensi relatif terhadap penunjuk itu (dalam banyak kasus, penunjuk akan disimpan dalam register untuk masa berlaku suatu aplikasi).
Sistem operasi pada awalnya tidak akan mengalokasikan memori apa pun untuk aplikasi, kecuali untuk apa yang menyimpan kode binernya, tetapi hal pertama yang dilakukan aplikasi adalah meminta alokasi yang sesuai dari sistem operasi, yang selamanya disimpan dalam register.
Sistem operasi pada awalnya tidak akan mengalokasikan ruang untuk aplikasi, tetapi aplikasi akan meminta alokasi yang sesuai pada saat startup (seperti di atas). Aplikasi akan menyertakan daftar instruksi dengan alamat yang perlu diperbarui untuk mencerminkan di mana memori dialokasikan (seperti gaya pertama), tetapi alih-alih meminta aplikasi ditambal oleh pemuat OS, aplikasi akan menyertakan cukup kode untuk menambal sendiri .
Keempat pendekatan memiliki kelebihan dan kekurangan. Namun dalam setiap kasus, kompiler akan mengkonsolidasikan sejumlah variabel statis sewenang-wenang menjadi sejumlah kecil permintaan memori, dan penghubung akan mengkonsolidasikan semua itu ke dalam sejumlah kecil alokasi konsolidasi. Meskipun suatu aplikasi harus menerima sepotong memori dari sistem operasi atau loader, itu adalah kompiler dan linker yang bertanggung jawab untuk mengalokasikan masing-masing bagian dari potongan besar itu ke semua variabel individu yang membutuhkannya.
sumber
Inti dari pertanyaan Anda adalah ini: "Bagaimana memori" dialokasikan "dalam file yang dikompilasi? Bukankah memori selalu dialokasikan dalam RAM dengan semua hal manajemen memori virtual? Bukankah alokasi memori menurut definisi konsep runtime?"
Saya pikir masalahnya adalah ada dua konsep berbeda yang terlibat dalam alokasi memori. Pada dasarnya, alokasi memori adalah proses di mana kita mengatakan "item data ini disimpan dalam potongan memori khusus ini". Dalam sistem komputer modern, ini melibatkan proses dua langkah:
Proses yang terakhir adalah murni waktu berjalan, tetapi yang pertama dapat dilakukan pada waktu kompilasi, jika data memiliki ukuran yang diketahui dan jumlah tetap dari mereka diperlukan. Inilah dasarnya cara kerjanya:
Kompiler melihat file sumber yang berisi baris yang terlihat sedikit seperti ini:
Ini menghasilkan output untuk assembler yang menginstruksikan untuk menyimpan memori untuk variabel 'c'. Ini mungkin terlihat seperti ini:
Ketika assembler berjalan, ia menyimpan penghitung yang melacak offset setiap item dari awal 'segmen' memori (atau 'bagian'). Ini seperti bagian-bagian dari 'struct' yang sangat besar yang berisi semua yang ada di seluruh file yang tidak memiliki memori aktual yang dialokasikan padanya saat ini, dan bisa di mana saja. Ini mencatat dalam tabel yang
_c
memiliki offset tertentu (katakanlah 510 byte dari awal segmen) dan kemudian menambah penghitungnya dengan 4, sehingga variabel tersebut berikutnya akan berada pada (misalnya) 514 byte. Untuk kode apa pun yang memerlukan alamat_c
, itu hanya menempatkan 510 di file output, dan menambahkan catatan bahwa output membutuhkan alamat segmen yang berisi_c
menambahkannya nanti.Linker mengambil semua file output assembler, dan memeriksanya. Ini menentukan alamat untuk setiap segmen sehingga mereka tidak akan tumpang tindih, dan menambahkan offset yang diperlukan sehingga instruksi masih merujuk ke item data yang benar. Dalam kasus memori yang tidak diinisialisasi seperti yang ditempati oleh
c
(assembler diberitahu bahwa memori akan diinisialisasi oleh fakta bahwa kompiler meletakkannya di segmen '.bss', yang merupakan nama yang dicadangkan untuk memori tidak diinisialisasi), itu termasuk bidang header dalam outputnya yang memberitahu sistem operasi berapa banyak yang perlu dipesan. Ini mungkin dipindahkan (dan biasanya) tetapi biasanya dirancang untuk dimuat lebih efisien di satu alamat memori tertentu, dan OS akan mencoba memuatnya di alamat ini. Pada titik ini, kami memiliki ide yang cukup bagus tentang alamat virtual yang akan digunakanc
.Alamat fisik tidak akan benar-benar ditentukan sampai program berjalan. Namun, dari sudut pandang programmer, alamat fisik sebenarnya tidak relevan — kita bahkan tidak akan pernah tahu apa itu, karena OS biasanya tidak repot-repot memberi tahu siapa pun, alamat fisik dapat sering berubah (bahkan ketika program sedang berjalan), dan Tujuan utama dari OS adalah untuk abstrak ini.
sumber
Sebuah executable menggambarkan ruang apa yang dialokasikan untuk variabel statis. Alokasi ini dilakukan oleh sistem, saat Anda menjalankan executable. Jadi variabel statis 1kB Anda tidak akan meningkatkan ukuran executable dengan 1kB:
Kecuali tentu saja Anda menentukan penginisialisasi:
Jadi, selain 'bahasa mesin' (yaitu instruksi CPU), executable berisi deskripsi tata letak memori yang diperlukan.
sumber
Memori dapat dialokasikan dengan banyak cara:
Sekarang pertanyaan Anda adalah apa "memori yang dialokasikan pada waktu kompilasi". Jelas itu hanya ungkapan yang salah, yang seharusnya merujuk pada alokasi segmen biner atau alokasi tumpukan, atau dalam beberapa kasus bahkan ke alokasi tumpukan, tetapi dalam kasus itu alokasi disembunyikan dari mata programmer oleh panggilan konstruktor yang tidak terlihat. Atau mungkin orang yang mengatakan bahwa hanya ingin mengatakan bahwa memori tidak dialokasikan pada heap, tetapi tidak tahu tentang alokasi stack atau segmen. (Atau tidak ingin masuk ke detail seperti itu).
Tetapi dalam kebanyakan kasus orang hanya ingin mengatakan bahwa jumlah memori yang dialokasikan diketahui pada waktu kompilasi .
Ukuran biner hanya akan berubah ketika memori dicadangkan dalam kode atau segmen data aplikasi Anda.
sumber
.data
dan.bss
.Kamu benar. Memori sebenarnya dialokasikan (halaman) pada waktu pengambilan, yaitu ketika file yang dapat dieksekusi dibawa ke memori (virtual). Memori juga dapat diinisialisasi pada saat itu. Kompiler hanya membuat peta memori. [Ngomong-ngomong, ruang stack dan heap juga dialokasikan pada waktu pengambilan!]
sumber
Saya pikir Anda perlu mundur sedikit. Memori yang dialokasikan pada waktu kompilasi .... Apa artinya itu? Dapatkah ini berarti bahwa memori pada chip yang belum diproduksi, untuk komputer yang belum dirancang, entah bagaimana sedang dipesan? Tidak. Tidak, perjalanan waktu, tidak ada kompiler yang dapat memanipulasi alam semesta.
Jadi, itu harus berarti bahwa kompiler menghasilkan instruksi untuk mengalokasikan memori itu entah bagaimana pada saat runtime. Tetapi jika Anda melihatnya dari sudut kanan, kompiler menghasilkan semua instruksi, jadi apa bedanya. Perbedaannya adalah bahwa kompilator memutuskan, dan pada saat runtime, kode Anda tidak dapat mengubah atau memodifikasi keputusannya. Jika diputuskan diperlukan 50 byte pada waktu kompilasi, saat runtime, Anda tidak dapat membuatnya memutuskan untuk mengalokasikan 60 - keputusan itu telah dibuat.
sumber
Jika Anda mempelajari pemrograman rakitan, Anda akan melihat bahwa Anda harus mengukir segmen untuk data, tumpukan, dan kode, dll. Bagian data adalah tempat string dan angka Anda tinggal. Segmen kode adalah tempat kode Anda tinggal. Segmen ini dibangun ke dalam program yang dapat dieksekusi. Tentu saja ukuran tumpukan juga penting ... Anda tidak ingin tumpukan meluap !
Jadi, jika segmen data Anda adalah 500 byte, program Anda memiliki area 500 byte. Jika Anda mengubah segmen data ke 1500 byte, ukuran program akan lebih besar 1000 byte. Data dikumpulkan ke dalam program yang sebenarnya.
Inilah yang terjadi ketika Anda mengkompilasi bahasa tingkat yang lebih tinggi. Area data aktual dialokasikan ketika dikompilasi ke dalam program yang dapat dieksekusi, meningkatkan ukuran program. Program ini dapat meminta memori dengan cepat, dan ini adalah memori dinamis. Anda dapat meminta memori dari RAM dan CPU akan memberikannya kepada Anda untuk digunakan, Anda dapat melepaskannya, dan pengumpul sampah Anda akan melepaskannya kembali ke CPU. Bahkan dapat ditukar ke hard disk, jika perlu, oleh manajer memori yang baik. Fitur-fitur ini adalah apa yang disediakan bahasa tingkat tinggi bagi Anda.
sumber
Saya ingin menjelaskan konsep-konsep ini dengan bantuan beberapa diagram.
Ini benar bahwa memori tidak dapat dialokasikan pada waktu kompilasi, pasti. Tapi, lalu apa yang terjadi sebenarnya pada waktu kompilasi.
Di sinilah penjelasannya. Katakanlah, misalnya suatu program memiliki empat variabel x, y, z dan k. Sekarang, pada waktu kompilasi itu hanya membuat peta memori, di mana lokasi variabel-variabel ini terhadap satu sama lain dipastikan. Diagram ini akan menggambarkannya dengan lebih baik.
Sekarang bayangkan, tidak ada program yang berjalan di memori. Ini saya tunjukkan dengan kotak kosong besar.
Selanjutnya, instance pertama dari program ini dieksekusi. Anda dapat memvisualisasikannya sebagai berikut. Ini adalah waktu ketika sebenarnya memori dialokasikan.
Ketika instance kedua dari program ini berjalan, memori akan terlihat seperti berikut.
Dan yang ketiga ..
Begitu seterusnya dan seterusnya.
Saya berharap visualisasi ini menjelaskan konsep ini dengan baik.
sumber
Ada penjelasan yang sangat bagus diberikan dalam jawaban yang diterima. Kalau-kalau saya akan memposting tautan yang menurut saya berguna. https://www.tenouk.com/ModuleW.html
sumber