Saya memutar beberapa bagian paling pusat dari basis kode saya (mesin ECS) di sekitar jenis struktur data yang Anda uraikan, meskipun menggunakan blok bersebelahan yang lebih kecil (lebih seperti 4 kilobyte daripada 4 megabyte).
Ia menggunakan daftar bebas ganda untuk mencapai penyisipan dan pemindahan waktu-konstan dengan satu daftar gratis untuk blok gratis yang siap dimasukkan (blok yang tidak penuh) dan daftar sub-bebas di dalam blok untuk indeks di blok itu siap untuk direklamasi saat penyisipan.
Saya akan membahas pro dan kontra dari struktur ini. Mari kita mulai dengan beberapa kontra karena ada beberapa di antaranya:
Cons
- Dibutuhkan sekitar 4 kali lebih lama untuk memasukkan beberapa ratus juta elemen ke struktur ini daripada
std::vector
(struktur yang bersebelahan murni). Dan saya cukup baik dalam optimasi mikro tetapi secara konseptual ada lebih banyak pekerjaan yang harus dilakukan karena kasus umum harus terlebih dahulu memeriksa blok gratis di bagian atas daftar blok bebas, kemudian mengakses blok dan mengeluarkan indeks gratis dari blok daftar bebas, tulis elemen pada posisi bebas, dan kemudian periksa apakah blok sudah penuh dan pop blok dari daftar bebas blok jika demikian. Ini masih merupakan operasi waktu konstan tetapi dengan konstanta jauh lebih besar daripada mendorong kembali ke std::vector
.
- Dibutuhkan sekitar dua kali lebih lama ketika mengakses elemen menggunakan pola akses acak mengingat aritmatika ekstra untuk pengindeksan dan lapisan tipuan tambahan.
- Akses sekuensial tidak memetakan secara efisien ke desain iterator karena iterator harus melakukan percabangan tambahan setiap kali bertambah.
- Ini memiliki sedikit overhead memori, biasanya sekitar 1 bit per elemen. 1 bit per elemen mungkin tidak terdengar banyak, tetapi jika Anda menggunakan ini untuk menyimpan sejuta bilangan bulat 16-bit, maka itu menggunakan memori 6,25% lebih banyak daripada array kompak sempurna. Namun, dalam praktiknya ini cenderung menggunakan lebih sedikit memori daripada
std::vector
kecuali Anda memadatkannya vector
untuk menghilangkan kelebihan kapasitas yang dihematnya. Juga saya biasanya tidak menggunakannya untuk menyimpan elemen kecil seperti itu.
Pro
- Akses sekuensial menggunakan
for_each
fungsi yang mengambil rentang pemrosesan elemen callback dalam blok hampir menyaingi kecepatan akses sekuensial dengan std::vector
(hanya seperti perbedaan 10%), jadi tidak jauh lebih efisien dalam kasus penggunaan yang paling kritis terhadap kinerja bagi saya ( sebagian besar waktu yang dihabiskan dalam mesin ECS berada dalam akses berurutan).
- Hal ini memungkinkan pemindahan waktu-konstan dari tengah dengan struktur blok deallocating ketika mereka benar-benar kosong. Akibatnya secara umum cukup baik untuk memastikan struktur data tidak pernah menggunakan lebih banyak memori secara signifikan daripada yang diperlukan.
- Itu tidak membatalkan indeks untuk unsur-unsur yang tidak langsung dihapus dari wadah karena hanya meninggalkan lubang di belakang menggunakan pendekatan daftar gratis untuk merebut kembali lubang-lubang tersebut pada penyisipan berikutnya.
- Anda tidak perlu terlalu khawatir kehabisan memori bahkan jika struktur ini menampung sejumlah elemen epik, karena hanya meminta blok kecil yang berdekatan yang tidak menimbulkan tantangan bagi OS untuk menemukan sejumlah besar berdekatan yang tidak digunakan yang berdekatan halaman.
- Ini cocok dengan baik untuk konkurensi dan keselamatan ulir tanpa mengunci seluruh struktur, karena operasi umumnya dilokalkan ke masing-masing blok.
Sekarang salah satu kelebihan terbesar bagi saya adalah menjadi sepele untuk membuat versi yang tidak dapat diubah dari struktur data ini, seperti ini:
Sejak saat itu, yang membuka semua jenis pintu untuk menulis lebih banyak fungsi tanpa efek samping yang membuatnya lebih mudah untuk mencapai pengecualian-keselamatan, keselamatan-thread, dll struktur data ini di belakang dan secara tidak sengaja, tetapi bisa dibilang salah satu manfaat terbaik yang didapatnya karena membuat mempertahankan basis kode jauh lebih mudah.
Array yang tidak berdampingan tidak memiliki lokalitas cache yang menghasilkan kinerja yang buruk. Namun pada ukuran blok 4M sepertinya akan ada cukup tempat untuk caching yang baik.
Lokalitas referensi bukan sesuatu yang menjadi perhatian Anda pada balok sebesar itu, apalagi 4 blok kilobita. Garis cache biasanya hanya 64 byte. Jika Anda ingin mengurangi kesalahan cache, maka hanya fokus pada menyelaraskan blok-blok itu dengan benar dan mendukung pola akses yang lebih berurutan bila memungkinkan.
Cara yang sangat cepat untuk mengubah pola memori akses-acak menjadi yang berurutan adalah dengan menggunakan bitset. Katakanlah Anda memiliki banyak indeks dan mereka berada dalam urutan acak. Anda bisa membajaknya dan menandai bit di bitset. Kemudian Anda dapat beralih melalui bitset dan memeriksa byte mana yang tidak nol, memeriksa, katakanlah, 64-bit pada suatu waktu. Setelah Anda menemukan satu set 64-bit yang setidaknya satu bit diatur, Anda dapat menggunakan instruksi FFS untuk dengan cepat menentukan bit apa yang ditetapkan. Bit memberi tahu Anda apa indeks yang harus Anda akses, kecuali sekarang Anda mendapatkan indeks diurutkan dalam urutan berurutan.
Ini memiliki beberapa overhead tetapi bisa menjadi pertukaran yang bermanfaat dalam beberapa kasus, terutama jika Anda akan berulang kali mengulangi indeks ini.
Mengakses item tidak sesederhana itu, ada tingkat tipuan ekstra. Apakah ini akan dioptimalkan? Apakah itu menyebabkan masalah cache?
Tidak, itu tidak dapat dioptimalkan jauh. Akses acak, setidaknya, akan selalu lebih mahal dengan struktur ini. Itu sering tidak akan meningkatkan cache Anda meleset sebanyak itu karena Anda akan cenderung mendapatkan lokalitas temporal tinggi dengan array pointer ke blok, terutama jika jalur eksekusi kasus umum Anda menggunakan pola akses berurutan.
Karena ada pertumbuhan linier setelah batas 4M tercapai, Anda dapat memiliki alokasi lebih banyak daripada yang biasanya (katakanlah, maks 250 alokasi untuk memori 1GB). Tidak ada memori tambahan yang disalin setelah 4M, namun saya tidak yakin apakah alokasi tambahan lebih mahal daripada menyalin potongan memori besar.
Dalam prakteknya penyalinan sering lebih cepat karena ini merupakan kasus yang jarang, hanya terjadi sesuatu seperti log(N)/log(2)
kali total sementara secara bersamaan menyederhanakan kasus umum yang murah di mana Anda hanya dapat menulis elemen ke array berkali-kali sebelum menjadi penuh dan perlu dialokasikan kembali. Jadi biasanya Anda tidak akan mendapatkan penyisipan yang lebih cepat dengan jenis struktur ini karena kerja kasus umum lebih mahal bahkan jika itu tidak harus berurusan dengan kasus langka yang mahal untuk realokasi array besar.
Daya tarik utama dari struktur ini bagi saya terlepas dari semua kontra adalah mengurangi penggunaan memori, tidak harus khawatir tentang OOM, mampu menyimpan indeks dan pointer yang tidak menjadi batal, konkurensi, dan tidak dapat diubah. Sangat menyenangkan untuk memiliki struktur data di mana Anda dapat menyisipkan dan menghapus hal-hal dalam waktu yang konstan sementara itu membersihkan sendiri untuk Anda dan tidak membatalkan pointer dan indeks ke dalam struktur.