Apakah array array yang berdampingan tampil?


12

Dalam C #, ketika pengguna membuat List<byte>dan menambahkan byte ke dalamnya, ada kemungkinan kehabisan ruang dan perlu mengalokasikan lebih banyak ruang. Ini mengalokasikan dua kali lipat (atau beberapa pengganda lainnya) ukuran array sebelumnya, menyalin byte lebih dan membuang referensi ke array lama. Saya tahu bahwa daftar tumbuh secara eksponensial karena setiap alokasi mahal dan ini membatasi O(log n)alokasi, di mana hanya menambahkan 10item tambahan setiap kali akan menghasilkan O(n)alokasi.

Namun untuk ukuran array besar bisa ada banyak ruang yang terbuang, mungkin hampir setengah dari array. Untuk mengurangi memori saya menulis kelas serupa NonContiguousArrayListyang digunakan List<byte>sebagai backing store jika ada kurang dari 4MB dalam daftar, maka akan mengalokasikan tambahan byte array 4MB seiring NonContiguousArrayListbertambahnya ukuran.

Tidak seperti List<byte>array ini tidak bersebelahan sehingga tidak ada penyalinan data di sekitar, hanya alokasi 4M tambahan. Ketika suatu item dilihat, indeks dibagi dengan 4M untuk mendapatkan indeks array yang mengandung item tersebut, kemudian modulo 4M untuk mendapatkan indeks dalam array.

Bisakah Anda menunjukkan masalah dengan pendekatan ini? Ini daftar saya:

  • 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.
  • Mengakses item tidak sesederhana itu, ada tingkat tipuan ekstra. Apakah ini akan dioptimalkan? Apakah itu menyebabkan masalah cache?
  • 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.

8
Anda telah kehabisan teori (memperhitungkan cache, membahas kerumitan asimptotik), yang tersisa hanyalah memasukkan parameter (di sini, 4 juta item per sublist) dan mungkin mengoptimalkan mikro. Sekarang saatnya untuk melakukan benchmark, karena tanpa memperbaiki perangkat keras dan implementasinya, ada terlalu sedikit data untuk membahas kinerja lebih lanjut.

3
Jika Anda bekerja dengan lebih dari 4 juta elemen dalam satu koleksi, saya berharap bahwa optimasi mikro kontainer adalah yang paling tidak menjadi perhatian kinerja Anda.
Telastyn

2
Apa yang Anda gambarkan mirip dengan daftar tertaut yang tidak terbuka (dengan simpul sangat besar). Pernyataan Anda bahwa mereka tidak memiliki lokalitas cache sedikit salah. Hanya banyak array yang cocok di dalam satu baris cache; katakanlah 64 byte. Jadi, setiap 64 byte Anda akan mengalami cache miss. Sekarang pertimbangkan daftar tertaut yang belum dibaca yang simpulnya tepat beberapa kelipatan 64 byte besar (termasuk header objek untuk pengumpulan sampah). Anda masih hanya mendapatkan satu cache miss setiap 64 byte, dan bahkan tidak masalah bahwa node tidak bersebelahan dalam memori.
Doval

@Doval Ini sebenarnya bukan daftar tertaut yang terbuka, karena potongan 4M disimpan dalam array sendiri, jadi mengakses elemen apa pun adalah O (1) bukan O (n / B) di mana B adalah ukuran blok.

2
@ user2313838 Jika ada 1000MB memori dan 350MB array, memori yang diperlukan untuk menumbuhkan array akan 1050MB, lebih besar dari apa yang tersedia, itulah masalah utama, batas efektif Anda adalah 1/3 ruang total Anda. TrimExcesshanya akan membantu ketika daftar sudah dibuat, dan itupun masih membutuhkan ruang yang cukup untuk menyalin.
noisecapella

Jawaban:


5

Pada skala yang Anda sebutkan, kekhawatiran sama sekali berbeda dari yang Anda sebutkan.

Lokalitas cache

  • Ada dua konsep terkait:
    1. Lokalitas, penggunaan kembali data pada baris cache yang sama (spasial lokalitas) yang baru-baru ini dikunjungi (temporal locality)
    2. Pengambilan cache sebelumnya (streaming).
  • Pada skala yang Anda sebutkan (seratus MB ke gigabytes, dalam potongan 4MB), kedua faktor tersebut lebih berkaitan dengan pola akses elemen data Anda daripada tata letak memori.
  • Prediksi saya (tidak mengerti) adalah bahwa secara statistik mungkin tidak ada banyak perbedaan kinerja daripada alokasi memori berdekatan yang besar. Tanpa untung, tidak rugi.

Pola akses elemen data

  • Artikel ini secara visual menggambarkan bagaimana pola akses memori akan mempengaruhi kinerja.
  • Singkatnya, perlu diingat bahwa jika algoritme Anda telah dihambat oleh bandwidth memori, satu-satunya cara untuk meningkatkan kinerja adalah dengan melakukan pekerjaan yang lebih bermanfaat dengan data yang sudah dimuat ke dalam cache.
  • Dengan kata lain, bahkan jika YourList[k]dan YourList[k+1]memiliki probabilitas tinggi berturut-turut (satu dari empat juta kemungkinan tidak), fakta itu tidak akan membantu kinerja jika Anda mengakses daftar Anda sepenuhnya secara acak, atau dalam langkah besar yang tidak dapat diprediksi misalnyawhile { index += random.Next(1024); DoStuff(YourList[index]); }

Interaksi dengan sistem GC

  • Menurut pendapat saya, di sinilah Anda harus paling fokus.
  • Paling tidak, pahami bagaimana desain Anda akan berinteraksi dengan:
  • Saya tidak memiliki pengetahuan tentang topik ini sehingga saya akan meninggalkan orang lain untuk berkontribusi.

Overhead perhitungan offset alamat

  • Kode C # tipikal sudah melakukan banyak perhitungan offset alamat, sehingga overhead tambahan dari skema Anda tidak akan lebih buruk daripada kode C # tipikal yang bekerja pada array tunggal.
    • Ingat bahwa kode C # juga melakukan pengecekan rentang array; dan fakta ini tidak mencegah C # mencapai kinerja pemrosesan array yang sebanding dengan kode C ++.
    • Alasannya adalah bahwa kinerja sebagian besar terhambat oleh bandwidth memori.
    • Trik untuk memaksimalkan utilitas dari bandwidth memori adalah dengan menggunakan instruksi SIMD untuk operasi baca / tulis memori. Baik C # maupun tipikal C ++ tidak melakukan ini; Anda harus menggunakan perpustakaan atau tambahan bahasa.

Untuk mengilustrasikan alasannya:

  • Lakukan perhitungan alamat
  • (Dalam kasus OP, muat alamat basis chunk (yang sudah ada dalam cache) dan kemudian lakukan lebih banyak perhitungan alamat)
  • Baca dari / tulis ke alamat elemen

Langkah terakhir masih mengambil bagian terbesar dari waktu.

Saran pribadi

  • Anda dapat memberikan CopyRangefungsi, yang akan berperilaku seperti Array.Copyfungsi tetapi akan beroperasi di antara dua instance dari Anda NonContiguousByteArray, atau antara satu instance dan normal lainnya byte[]. fungsi-fungsi ini dapat menggunakan kode SIMD (C ++ atau C #) untuk memaksimalkan pemanfaatan bandwidth memori, dan kemudian kode C # Anda dapat beroperasi pada rentang yang disalin tanpa overhead dari beberapa dereferencing atau perhitungan alamat.

Masalah kegunaan dan interoperabilitas

  • Tampaknya Anda tidak dapat menggunakan ini NonContiguousByteArraydengan pustaka C #, C ++ atau bahasa asing apa pun yang mengharapkan array byte yang berdekatan, atau array byte yang dapat disematkan.
  • Namun, jika Anda menulis pustaka percepatan C ++ Anda sendiri (dengan P / Invoke atau C ++ / CLI), Anda bisa meneruskan daftar alamat pangkalan dari beberapa blok 4MB ke dalam kode yang mendasarinya.
    • Misalnya, jika Anda perlu memberikan akses ke elemen mulai (3 * 1024 * 1024)dan berakhir pada (5 * 1024 * 1024 - 1), ini berarti akses akan menjangkau chunk[0]dan chunk[1]. Anda kemudian dapat membangun array (ukuran 2) dari byte array (ukuran 4M), pin alamat chunk ini dan meneruskannya ke kode yang mendasarinya.
  • Masalah kegunaan lain adalah bahwa Anda tidak akan dapat mengimplementasikan IList<byte>antarmuka secara efisien: Insertdan Removehanya akan memakan waktu terlalu lama untuk diproses karena mereka akan membutuhkan O(N)waktu.
    • Bahkan, sepertinya Anda tidak dapat mengimplementasikan apa pun selain IEnumerable<byte>, yaitu dapat dipindai secara berurutan dan hanya itu.

2
Anda tampaknya telah melewatkan keuntungan utama dari struktur data, yaitu memungkinkan Anda membuat daftar yang sangat besar, tanpa kehabisan memori. Saat memperluas Daftar <T>, ia membutuhkan array baru dua kali lebih besar dari yang lama, dan keduanya harus ada dalam memori pada saat yang bersamaan.
Frank Hileman

6

Perlu dicatat bahwa C ++ sudah memiliki struktur yang setara dengan Standar std::deque,. Saat ini, ini direkomendasikan sebagai pilihan default untuk memerlukan urutan akses acak.

Kenyataannya adalah bahwa memori yang bersebelahan hampir sepenuhnya tidak perlu begitu data melewati ukuran tertentu - garis cache hanya 64 byte dan ukuran halaman hanya 4-8KB (nilai khas saat ini). Setelah Anda mulai berbicara tentang beberapa MB itu benar-benar keluar jendela sebagai masalah. Hal yang sama berlaku untuk biaya alokasi. Harga pemrosesan semua data itu — bahkan hanya membacanya saja — mengecilkan harga alokasi itu.

Satu-satunya alasan lain untuk mengkhawatirkannya adalah untuk berinteraksi dengan C API. Tapi Anda tetap tidak bisa mendapatkan pointer ke buffer Daftar sehingga tidak ada masalah di sini.


Itu menarik, saya tidak tahu yang dequememiliki implementasi yang sama
noisecapella

Siapa yang saat ini merekomendasikan std :: deque? Bisakah Anda memberikan sumber? Saya selalu berpikir std :: vector adalah pilihan default yang disarankan.
Teimpz

std::dequesebenarnya sangat berkecil hati, sebagian karena implementasi perpustakaan standar MS sangat buruk.
Sebastian Redl

3

Ketika potongan memori dialokasikan pada titik waktu yang berbeda, seperti pada sub-array dalam struktur data Anda, mereka dapat ditempatkan jauh dari satu sama lain dalam memori. Apakah ini masalah atau tidak tergantung pada CPU dan sangat sulit untuk diprediksi lagi. Anda harus mengujinya.

Ini adalah ide yang bagus, dan ini sudah pernah saya gunakan di masa lalu. Tentu saja Anda hanya boleh menggunakan kekuatan dua untuk ukuran sub-array Anda dan pengalihan bit untuk divisi (dapat terjadi sebagai bagian dari optimasi). Saya menemukan jenis struktur ini sedikit lebih lambat, di mana kompiler dapat mengoptimalkan tipuan array tunggal lebih mudah. Anda harus menguji, karena jenis optimasi ini berubah setiap saat.

Keuntungan utama adalah Anda dapat berjalan lebih dekat ke batas atas memori di sistem Anda, selama Anda menggunakan jenis struktur ini secara konsisten. Selama Anda membuat struktur data Anda lebih besar, dan tidak menghasilkan sampah, Anda menghindari pengumpulan sampah tambahan yang akan terjadi untuk Daftar biasa. Untuk daftar raksasa, itu bisa membuat perbedaan besar: perbedaan antara terus berjalan, dan kehabisan memori.

Alokasi tambahan adalah masalah hanya jika potongan sub-array Anda kecil, karena ada overhead memori di setiap alokasi array.

Saya telah membuat struktur serupa untuk kamus (tabel hash). Kamus yang disediakan oleh .net framework memiliki masalah yang sama dengan Daftar. Kamus lebih sulit karena Anda harus menghindari pengulangan juga.


Seorang kolektor pemadat dapat memadatkan potongan di samping satu sama lain.
DeadMG

@DeadMG saya mengacu pada situasi di mana ini tidak dapat terjadi: ada bongkahan lain di antaranya, yang bukan sampah. Dengan Daftar <T>, Anda dijamin memiliki memori yang berdekatan untuk array Anda. Dengan daftar chunk, memori berdekatan hanya dalam chunk, kecuali Anda memiliki situasi pemadatan beruntung yang Anda sebutkan. Tapi pemadatan juga dapat membutuhkan banyak data bergerak, dan array besar pergi ke Heap Objek Besar. Itu rumit.
Frank Hileman

2

Dengan ukuran blok 4M bahkan satu blok tidak dijamin bersebelahan dalam memori fisik; ini lebih besar dari ukuran halaman VM biasa. Lokalitas tidak berarti pada skala itu.

Anda harus khawatir tentang tumpukan fragmentasi: jika alokasi terjadi sedemikian sehingga sebagian besar blok Anda tidak bersebelahan di tumpukan, maka ketika mereka direklamasi oleh GC, Anda akan berakhir dengan tumpukan yang mungkin terlalu terfragmentasi agar tidak sesuai dengan alokasi selanjutnya. Itu biasanya situasi yang lebih buruk karena kegagalan akan terjadi di tempat-tempat yang tidak terkait dan mungkin memaksa restart aplikasi.


Compacting GCs bebas fragmentasi.
DeadMG

Ini benar, tetapi pemadatan LOH hanya tersedia pada. NET 4.5 jika saya ingat dengan benar.
user2313838

Tumpukan tumpukan juga dapat menimbulkan lebih banyak overhead daripada perilaku menyalin-pada-realokasi standar List.
user2313838

Lagipula, objek yang cukup besar dan berukuran tepat bebas fragmentasi secara efektif.
DeadMG

2
@DeadMG: Perhatian sebenarnya dengan pemadatan GC (dengan skema 4MB ini) adalah bahwa mungkin menghabiskan waktu yang tidak berguna menyekop sekitar 4MB beefcakes ini. Akibatnya itu bisa menyebabkan GC jeda besar. Untuk alasan ini, ketika menggunakan skema 4MB ini, penting untuk memantau statistik GC vital untuk melihat apa yang dilakukannya, dan untuk mengambil tindakan korektif.
rwong

1

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).

masukkan deskripsi gambar di sini

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

  1. 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.
  2. Dibutuhkan sekitar dua kali lebih lama ketika mengakses elemen menggunakan pola akses acak mengingat aritmatika ekstra untuk pengindeksan dan lapisan tipuan tambahan.
  3. Akses sekuensial tidak memetakan secara efisien ke desain iterator karena iterator harus melakukan percabangan tambahan setiap kali bertambah.
  4. 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::vectorkecuali Anda memadatkannya vectoruntuk menghilangkan kelebihan kapasitas yang dihematnya. Juga saya biasanya tidak menggunakannya untuk menyimpan elemen kecil seperti itu.

Pro

  1. Akses sekuensial menggunakan for_eachfungsi 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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:

masukkan deskripsi gambar di sini

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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.