Jadi bukankah ini overhead memori?
Ya Tidak mungkin?
Ini adalah pertanyaan yang aneh karena bayangkan rentang pengalamatan memori pada mesin, dan perangkat lunak yang perlu terus melacak di mana segala sesuatu berada dalam memori dengan cara yang tidak dapat diikat ke tumpukan.
Misalnya, bayangkan pemutar musik di mana file musik dimuat pada tombol yang ditekan oleh pengguna dan dibongkar dari memori yang tidak stabil ketika pengguna mencoba memuat file musik lain.
Bagaimana kita melacak di mana data audio disimpan? Kami membutuhkan alamat memori untuk itu. Program ini tidak hanya perlu melacak potongan data audio dalam memori tetapi juga di mana itu berada dalam memori. Jadi kita perlu menyimpan alamat memori (yaitu, pointer). Dan ukuran penyimpanan yang diperlukan untuk alamat memori akan cocok dengan rentang pengalamatan mesin (mis: pointer 64-bit untuk rentang pengalamatan 64-bit).
Jadi itu semacam "ya", itu memang membutuhkan penyimpanan untuk melacak alamat memori, tetapi tidak seperti kita dapat menghindarinya untuk memori yang dialokasikan secara dinamis semacam ini.
Bagaimana ini dikompensasi?
Berbicara tentang ukuran pointer itu sendiri, Anda dapat menghindari biaya dalam beberapa kasus dengan menggunakan stack, misalnya Dalam kasus itu, kompiler dapat menghasilkan instruksi yang secara efektif meng-hard-code alamat memori relatif, menghindari biaya pointer. Namun ini membuat Anda rentan untuk menumpuk luapan jika Anda melakukan ini untuk alokasi yang besar dan berukuran variabel, dan juga cenderung tidak praktis (jika bukan tidak mungkin dilakukan) untuk serangkaian cabang kompleks yang digerakkan oleh input pengguna (seperti dalam contoh audio) atas).
Cara lain adalah dengan menggunakan struktur data yang lebih berdekatan. Misalnya, urutan berbasis array dapat digunakan sebagai pengganti daftar dua kali lipat yang membutuhkan dua pointer per node. Kita juga dapat menggunakan gabungan keduanya seperti daftar yang tidak terbuka yang menyimpan hanya pointer di antara setiap kelompok elemen N yang berdekatan.
Apakah pointer digunakan dalam aplikasi kritis memori rendah waktu?
Ya, sangat umum demikian, karena banyak aplikasi kinerja kritis ditulis dalam C atau C ++ yang didominasi oleh penggunaan pointer (mereka mungkin berada di belakang smart pointer atau wadah seperti std::vector
atau std::string
, tetapi mekanika yang mendasarinya mendidih menjadi sebuah pointer yang digunakan untuk melacak alamat ke blok memori dinamis).
Sekarang kembali ke pertanyaan ini:
Bagaimana ini dikompensasi? (Bagian kedua)
Pointer biasanya sangat murah kecuali Anda menyimpan jutaan seperti itu (yang masih sangat kecil * 8 megabita pada mesin 64-bit).
* Perhatikan ketika Ben menunjukkan bahwa "sangat" 8 MB masih seukuran L3 cache. Di sini saya menggunakan "sangat" dalam arti penggunaan DRAM total dan ukuran relatif tipikal pada memori, penggunaan pointer yang sehat.
Di mana pointer menjadi mahal bukanlah pointer itu sendiri tetapi:
Alokasi memori dinamis. Alokasi memori dinamis cenderung mahal karena harus melalui struktur data yang mendasarinya (mis: buddy atau pengalokasi slab). Meskipun ini sering dioptimalkan sampai mati, mereka bertujuan umum dan dirancang untuk menangani blok berukuran variabel yang mengharuskan mereka melakukan setidaknya sedikit pekerjaan yang menyerupai "pencarian" (walaupun ringan dan mungkin bahkan waktu konstan) untuk temukan set bebas halaman yang berdekatan di memori.
Akses memori. Ini cenderung menjadi masalah besar yang perlu dikhawatirkan. Setiap kali kita mengakses memori yang dialokasikan secara dinamis untuk pertama kalinya, ada kesalahan halaman wajib serta cache kehilangan memindahkan memori ke bawah hierarki memori dan turun ke register.
Akses Memori
Akses memori adalah salah satu aspek kinerja yang paling penting di luar algoritma. Banyak bidang yang sangat kritis terhadap kinerja seperti mesin game AAA memusatkan banyak energi mereka ke optimisasi berorientasi data yang bermuara pada pola dan tata letak akses memori yang lebih efisien.
Salah satu kesulitan kinerja terbesar dari bahasa tingkat tinggi yang ingin mengalokasikan setiap jenis yang ditentukan pengguna secara terpisah melalui pengumpul sampah, misalnya, adalah mereka dapat memecah-mecah memori sedikit. Ini bisa benar terutama jika tidak semua objek dialokasikan sekaligus.
Dalam kasus tersebut, jika Anda menyimpan daftar sejuta contoh tipe objek yang ditentukan pengguna, mengakses instans-instans tersebut secara berurutan dalam satu loop mungkin cukup lambat karena dianalogikan dengan daftar sejuta petunjuk yang menunjuk ke wilayah memori yang berbeda. Dalam kasus tersebut, arsitektur ingin mengambil memori dari tingkat atas, lebih lambat, tingkat hierarki yang lebih besar pada bongkahan besar yang disejajarkan dengan harapan bahwa data di sekitar bongkahan tersebut akan diakses sebelum penggusuran. Ketika setiap objek dalam daftar tersebut dialokasikan secara terpisah, maka seringkali kita berakhir membayarnya dengan cache misses ketika setiap iterasi berikutnya mungkin harus memuat dari area yang sama sekali berbeda dalam memori tanpa objek yang berdekatan diakses sebelum penggusuran.
Banyak kompiler untuk bahasa-bahasa seperti ini melakukan pekerjaan yang sangat bagus akhir-akhir ini dalam pemilihan instruksi dan alokasi register, tetapi kurangnya lebih banyak kontrol langsung terhadap manajemen memori di sini dapat menjadi pembunuh (walaupun sering lebih sedikit rawan kesalahan) dan masih membuat bahasa seperti C dan C ++ cukup populer.
Secara tidak langsung Mengoptimalkan Akses Pointer
Dalam skenario yang paling kritis terhadap kinerja, aplikasi sering menggunakan kumpulan memori yang menyatukan memori dari potongan yang berdekatan untuk meningkatkan lokalitas referensi. Dalam kasus seperti itu, bahkan struktur yang ditautkan seperti pohon atau daftar tertaut dapat dibuat ramah-cache asalkan tata letak memori dari node-node tersebut berdekatan. Ini secara efektif membuat pointer dereferencing lebih murah, meskipun secara tidak langsung dengan meningkatkan lokalitas referensi yang terlibat ketika mendereferensi mereka.
Mengejar Pointers Sekitar
Asumsikan kita memiliki daftar yang terhubung sendiri seperti:
Foo->Bar->Baz->null
Masalahnya adalah bahwa jika kita mengalokasikan semua node secara terpisah terhadap pengalokasi tujuan umum (dan mungkin tidak semuanya sekaligus), memori sebenarnya mungkin agak tersebar seperti ini (diagram yang disederhanakan):
Ketika kita mulai mengejar pointer di sekitar dan mengakses Foo
node, kita mulai dengan kehilangan wajib (dan mungkin kesalahan halaman) memindahkan potongan dari wilayah memorinya dari daerah memori yang lebih lambat ke daerah memori yang lebih cepat, seperti:
Hal ini menyebabkan kami untuk men-cache (mungkin juga halaman) wilayah memori hanya untuk mengakses sebagian darinya dan mengusir sisanya saat kami mengejar petunjuk di sekitar daftar ini. Namun, dengan mengambil kendali atas pengalokasi memori, kami dapat mengalokasikan daftar seperti itu secara bersebelahan seperti:
... dan dengan demikian secara signifikan meningkatkan kecepatan di mana kita dapat mengubah referensi ini dan memproses poinee mereka. Jadi, meskipun sangat tidak langsung, kita dapat mempercepat akses pointer dengan cara ini. Tentu saja jika kita hanya menyimpannya secara berdekatan dalam array, kita tidak akan memiliki masalah ini di tempat pertama, tetapi pengalokasi memori di sini memberi kita kontrol eksplisit atas tata letak memori dapat menghemat hari ketika struktur terkait diperlukan.
* Catatan: ini adalah diagram dan diskusi yang sangat disederhanakan tentang hierarki memori dan lokalitas referensi, tetapi mudah-mudahan ini sesuai untuk tingkat pertanyaan.