Ini bisa terdengar seperti pertanyaan subyektif, tapi yang saya cari adalah contoh spesifik, yang bisa Anda temui terkait dengan ini.
Bagaimana cara membuat kode, efektif cache / ramah cache (hit cache lebih banyak, sesedikit mungkin cache gagal)? Dari kedua perspektif, cache data & cache program (cache instruksi), yaitu hal-hal apa dalam kode seseorang, yang terkait dengan struktur data dan konstruksi kode, harus dipelihara untuk membuatnya menjadi cache yang efektif.
Apakah ada struktur data tertentu yang harus digunakan / dihindari, atau adakah cara khusus untuk mengakses anggota struktur itu, dll ... untuk membuat kode cache lebih efektif.
Apakah ada konstruksi program (jika, untuk, beralih, break, kebagian, ...), aliran-kode (untuk di dalam if, jika di dalam for, dll ...) orang harus mengikuti / menghindari dalam hal ini?
Saya menantikan pengalaman individual yang terkait dengan pembuatan kode efisien cache secara umum. Ini bisa berupa bahasa pemrograman (C, C ++, Majelis, ...), semua target perangkat keras (ARM, Intel, PowerPC, ...), semua OS (Windows, Linux, S ymbian, ...), dll. .
Variasi akan membantu untuk lebih memahaminya secara mendalam.
sumber
Jawaban:
Cache ada untuk mengurangi berapa kali CPU akan berhenti menunggu permintaan memori untuk dipenuhi (menghindari latensi memori ), dan sebagai efek kedua, mungkin untuk mengurangi jumlah keseluruhan data yang perlu ditransfer (menjaga bandwidth memori ).
Teknik untuk menghindari penderitaan akibat latensi pengambilan memori biasanya adalah hal pertama yang perlu dipertimbangkan, dan terkadang membantu jauh. Bandwidth memori yang terbatas juga merupakan faktor pembatas, terutama untuk aplikasi multicores dan multithread di mana banyak thread ingin menggunakan bus memori. Serangkaian teknik yang berbeda membantu mengatasi masalah yang terakhir.
Meningkatkan lokalitas spasial berarti Anda memastikan bahwa setiap baris cache digunakan secara penuh setelah dipetakan ke cache. Ketika kita telah melihat berbagai tolok ukur standar, kita telah melihat bahwa sebagian besar yang mengejutkan dari mereka gagal menggunakan 100% dari garis cache yang diambil sebelum garis cache digusur.
Meningkatkan pemanfaatan jalur cache membantu dalam tiga hal:
Teknik umum adalah:
Kami juga harus mencatat bahwa ada cara lain untuk menyembunyikan latensi memori daripada menggunakan cache.
CPU modern: sering memiliki satu atau lebih prefetcher perangkat keras . Mereka melatih pada kesalahan dalam cache dan mencoba untuk menemukan keteraturan. Misalnya, setelah beberapa kali melewatkan baris cache selanjutnya, prefetcher hw akan mulai mengambil baris cache ke dalam cache, mengantisipasi kebutuhan aplikasi. Jika Anda memiliki pola akses reguler, prefetcher perangkat keras biasanya melakukan pekerjaan yang sangat baik. Dan jika program Anda tidak menampilkan pola akses reguler, Anda dapat meningkatkan hal-hal dengan menambahkan sendiri instruksi pengambilan sebelumnya .
Mengelompokkan kembali instruksi sedemikian rupa sehingga mereka yang selalu ketinggalan dalam cache terjadi berdekatan satu sama lain, CPU kadang-kadang dapat tumpang tindih mengambil ini sehingga aplikasi hanya mempertahankan satu latensi hit ( Memory level parallelism ).
Untuk mengurangi tekanan bus memori keseluruhan, Anda harus mulai menangani apa yang disebut temporal locality . Ini berarti bahwa Anda harus menggunakan kembali data saat itu masih belum diusir dari cache.
Menggabungkan loop yang menyentuh data yang sama ( loop fusion ), dan menggunakan teknik penulisan ulang yang dikenal sebagai ubin atau memblokir semua upaya untuk menghindari pengambilan memori ekstra.
Meskipun ada beberapa aturan praktis untuk latihan penulisan ulang ini, Anda biasanya harus mempertimbangkan dengan hati-hati dependensi data yang dibawa, untuk memastikan bahwa Anda tidak memengaruhi semantik program.
Hal-hal inilah yang benar-benar terbayar di dunia multicore, di mana Anda biasanya tidak akan melihat banyak peningkatan throughput setelah menambahkan utas kedua.
sumber
Saya tidak percaya tidak ada lagi jawaban untuk ini. Bagaimanapun, satu contoh klasik adalah untuk mengulangi array multidimensi "dalam ke luar":
Alasan ini adalah cache tidak efisien karena CPU modern akan memuat garis cache dengan alamat memori "dekat" dari memori utama ketika Anda mengakses satu alamat memori. Kami melakukan iterasi melalui baris "j" (luar) dalam array di loop dalam, jadi untuk setiap perjalanan melalui loop dalam, garis cache akan menyebabkan memerah dan dimuat dengan garis alamat yang dekat dengan [ j] [i] entri. Jika ini diubah ke yang setara:
Ini akan berjalan lebih cepat.
sumber
Aturan dasar sebenarnya cukup sederhana. Di mana itu menjadi rumit adalah bagaimana mereka berlaku untuk kode Anda.
Cache bekerja pada dua prinsip: temporal locality dan spatial locality. Yang pertama adalah gagasan bahwa jika Anda baru-baru ini menggunakan sepotong data tertentu, Anda mungkin akan membutuhkannya lagi segera. Yang terakhir berarti bahwa jika Anda baru-baru ini menggunakan data di alamat X, Anda mungkin akan segera membutuhkan alamat X + 1.
Cache mencoba untuk mengakomodasi ini dengan mengingat potongan data yang terakhir digunakan. Ini beroperasi dengan garis cache, biasanya berukuran 128 byte atau lebih, jadi bahkan jika Anda hanya membutuhkan satu byte, seluruh baris cache yang berisi itu akan ditarik ke dalam cache. Jadi jika Anda memerlukan byte berikut setelahnya, itu sudah ada dalam cache.
Dan ini berarti Anda akan selalu menginginkan kode Anda sendiri untuk mengeksploitasi dua bentuk lokalitas ini sebanyak mungkin. Jangan lompati memori. Lakukan sebanyak mungkin pekerjaan di satu area kecil, dan kemudian pindah ke yang berikutnya, dan lakukan sebanyak mungkin pekerjaan di sana.
Contoh sederhana adalah larik array 2D yang ditunjukkan oleh jawaban 1800. Jika Anda melewatinya satu per satu, Anda membaca memori secara berurutan. Jika Anda melakukannya dengan bijaksana, Anda akan membaca satu entri, lalu melompat ke lokasi yang sama sekali berbeda (awal baris berikutnya), membaca satu entri, dan melompat lagi. Dan ketika Anda akhirnya kembali ke baris pertama, itu tidak akan lagi berada di cache.
Hal yang sama berlaku untuk kode. Lompatan atau cabang berarti penggunaan cache yang kurang efisien (karena Anda tidak membaca instruksi secara berurutan, tetapi melompat ke alamat lain). Tentu saja, pernyataan if kecil mungkin tidak akan mengubah apa pun (Anda hanya melewatkan beberapa byte, sehingga Anda masih akan berakhir di dalam wilayah cache), tetapi pemanggilan fungsi biasanya menyiratkan bahwa Anda melompat ke yang benar-benar berbeda alamat yang mungkin tidak di-cache. Kecuali jika itu disebut baru-baru ini.
Instruksi penggunaan cache biasanya jauh dari masalah. Apa yang biasanya perlu Anda khawatirkan adalah data cache.
Dalam sebuah struct atau kelas, semua anggota diletakkan secara bersebelahan, yang bagus. Dalam sebuah array, semua entri ditata secara bersamaan. Dalam daftar tertaut, setiap node dialokasikan di lokasi yang sama sekali berbeda, yang buruk. Pointer secara umum cenderung mengarah ke alamat yang tidak terkait, yang mungkin akan menghasilkan cache yang hilang jika Anda merujuknya.
Dan jika Anda ingin mengeksploitasi banyak core, itu bisa menjadi sangat menarik, seperti biasanya, hanya satu CPU yang mungkin memiliki alamat yang diberikan dalam cache L1 pada suatu waktu. Jadi jika kedua core secara konstan mengakses alamat yang sama, itu akan mengakibatkan cache yang terus-menerus hilang, karena mereka berebut alamat.
sumber
Saya sarankan membaca artikel 9-bagian Apa yang harus diketahui setiap programmer tentang memori oleh Ulrich Drepper jika Anda tertarik pada bagaimana memori dan perangkat lunak berinteraksi. Ini juga tersedia dalam format 104 halaman PDF .
Bagian yang sangat relevan dengan pertanyaan ini mungkin Bagian 2 (cache CPU) dan Bagian 5 (Apa yang dapat dilakukan oleh programmer - optimasi cache).
sumber
Terlepas dari pola akses data, faktor utama dalam kode ramah-cache adalah ukuran data . Lebih sedikit data berarti lebih banyak cocok dengan cache.
Ini terutama merupakan faktor dengan struktur data yang disejajarkan dengan memori. "Konvensional" kebijaksanaan mengatakan struktur data harus disejajarkan pada batas-batas kata karena CPU hanya dapat mengakses seluruh kata, dan jika sebuah kata mengandung lebih dari satu nilai, Anda harus melakukan pekerjaan tambahan (baca-modifikasi-tulis alih-alih menulis sederhana) . Tetapi cache bisa sepenuhnya membatalkan argumen ini.
Demikian pula, array boolean Java menggunakan seluruh byte untuk setiap nilai untuk memungkinkan operasi pada nilai individual secara langsung. Anda dapat mengurangi ukuran data dengan faktor 8 jika Anda menggunakan bit aktual, tetapi kemudian akses ke nilai individual menjadi jauh lebih kompleks, membutuhkan operasi bit shift dan mask (
BitSet
kelas melakukan ini untuk Anda). Namun, karena efek cache, ini masih bisa jauh lebih cepat daripada menggunakan boolean [] ketika arraynya besar. IIRC I pernah mencapai speedup dengan faktor 2 atau 3 dengan cara ini.sumber
Struktur data yang paling efektif untuk cache adalah array. Cache berfungsi paling baik, jika struktur data Anda diletakkan secara berurutan karena CPU membaca seluruh baris cache (biasanya 32 byte atau lebih) sekaligus dari memori utama.
Algoritma apa pun yang mengakses memori secara acak mengacak cache karena selalu membutuhkan baris cache baru untuk mengakomodasi memori yang diakses secara acak. Di sisi lain suatu algoritma, yang berjalan secara berurutan melalui array adalah yang terbaik karena:
Ini memberi CPU kesempatan untuk membaca-depan, misalnya secara spekulatif memasukkan lebih banyak memori ke dalam cache, yang akan diakses nanti. Membaca-depan ini memberikan peningkatan kinerja yang sangat besar.
Menjalankan loop ketat pada array besar juga memungkinkan CPU untuk cache kode yang dieksekusi dalam loop dan dalam kebanyakan kasus memungkinkan Anda untuk mengeksekusi algoritma sepenuhnya dari memori cache tanpa harus memblokir akses memori eksternal.
sumber
Salah satu contoh yang saya lihat digunakan dalam mesin permainan adalah memindahkan data dari objek dan ke array mereka sendiri. Objek permainan yang tunduk pada fisika mungkin memiliki banyak data lain yang melekat padanya. Tetapi selama loop pembaruan fisika, semua mesin yang dipedulikan adalah data tentang posisi, kecepatan, massa, kotak pembatas, dll. Jadi semua itu ditempatkan ke dalam susunannya sendiri dan dioptimalkan sebanyak mungkin untuk SSE.
Jadi selama loop fisika, data fisika diproses dalam susunan array menggunakan matematika vektor. Objek game menggunakan ID objek mereka sebagai indeks ke berbagai array. Itu bukan pointer karena pointer bisa menjadi tidak valid jika array harus dipindahkan.
Dalam banyak hal ini melanggar pola desain berorientasi objek tetapi membuat kode jauh lebih cepat dengan menempatkan data berdekatan yang perlu dioperasikan di dalam loop yang sama.
Contoh ini mungkin kedaluwarsa karena saya berharap sebagian besar game modern menggunakan mesin fisika prebuilt seperti Havok.
sumber
Hanya satu pos yang menyentuh, tetapi masalah besar muncul saat berbagi data antar proses. Anda ingin menghindari beberapa proses yang berusaha mengubah garis cache yang sama secara bersamaan. Sesuatu yang perlu diwaspadai di sini adalah berbagi "salah", di mana dua struktur data yang berdekatan berbagi jalur cache dan modifikasi untuk satu membatalkan jalur cache untuk yang lain. Hal ini dapat menyebabkan garis cache untuk berpindah-pindah di antara cache prosesor berbagi data pada sistem multiprosesor secara tidak perlu. Cara untuk menghindarinya adalah dengan menyelaraskan dan memadatkan struktur data untuk menempatkannya pada garis yang berbeda.
sumber
Sebuah komentar untuk "contoh klasik" oleh pengguna 1800 INFORMASI (terlalu panjang untuk komentar)
Saya ingin memeriksa perbedaan waktu untuk dua pesanan iterasi ("outter" dan "inner"), jadi saya membuat percobaan sederhana dengan array 2D besar:
dan kasus kedua dengan
for
loop ditukar.Versi lebih lambat ("x pertama") adalah 0,88 dtk dan yang lebih cepat, adalah 0,06 dtk. Itulah kekuatan caching :)
Saya menggunakan
gcc -O2
dan masih loop tidak dioptimalkan. Komentar oleh Ricardo bahwa "sebagian besar kompiler modern dapat mengetahui hal ini dengan sendirinya" tidak berlakusumber
Saya dapat menjawab (2) dengan mengatakan bahwa di dunia C ++, daftar tertaut dapat dengan mudah mematikan cache CPU. Array adalah solusi yang lebih baik jika memungkinkan. Tidak ada pengalaman tentang apakah hal yang sama berlaku untuk bahasa lain, tetapi mudah untuk membayangkan masalah yang sama akan muncul.
sumber
Cache diatur dalam "baris cache" dan (nyata) memori dibaca dari dan ditulis dalam potongan ukuran ini.
Struktur data yang terkandung dalam satu cache-line karenanya lebih efisien.
Demikian pula, algoritma yang mengakses blok memori yang berdekatan akan lebih efisien daripada algoritma yang melompat melalui memori secara acak.
Sayangnya ukuran garis cache bervariasi secara dramatis antara prosesor, jadi tidak ada cara untuk menjamin bahwa struktur data yang optimal pada satu prosesor akan efisien pada yang lain.
sumber
Untuk bertanya bagaimana membuat kode, cache-cache-friendly dan sebagian besar pertanyaan lainnya, biasanya bertanya bagaimana Mengoptimalkan program, itu karena cache memiliki dampak yang sangat besar pada kinerja sehingga setiap program yang dioptimalkan adalah salah satu yang adalah cache ramah cache-efektif.
Saya sarankan membaca tentang Optimasi, ada beberapa jawaban bagus di situs ini. Dalam hal buku, saya merekomendasikan Sistem Komputer: Perspektif Programmer yang memiliki beberapa teks bagus tentang penggunaan cache yang tepat.
(btw - seburuk cache-miss bisa, ada yang lebih buruk - jika suatu program paging dari hard-drive ...)
sumber
Ada banyak jawaban pada saran umum seperti pemilihan struktur data, pola akses, dll. Di sini saya ingin menambahkan pola desain kode lain yang disebut pipeline perangkat lunak yang memanfaatkan manajemen cache aktif.
Idenya adalah meminjam dari teknik perpipaan lainnya, misalnya perpipaan instruksi CPU.
Jenis pola ini paling baik berlaku untuk prosedur itu
Mari kita ambil contoh sederhana di mana hanya ada satu sub-prosedur. Biasanya kode ingin:
Untuk memiliki kinerja yang lebih baik, Anda mungkin ingin meneruskan beberapa input ke fungsi dalam batch sehingga Anda mengamortisasi overhead panggilan fungsi dan juga meningkatkan lokalitas cache kode.
Namun, seperti yang dikatakan sebelumnya, jika pelaksanaan langkah ini kira-kira sama dengan waktu akses RAM Anda dapat lebih lanjut meningkatkan kode untuk sesuatu seperti ini:
Alur eksekusi akan terlihat seperti:
Mungkin ada lebih banyak langkah yang terlibat, maka Anda dapat merancang pipa multi-tahap selama waktu langkah-langkah dan kecocokan latensi akses memori, Anda akan mengalami sedikit kode / cache data yang hilang. Namun, proses ini perlu disesuaikan dengan banyak percobaan untuk mengetahui pengelompokan langkah yang tepat dan waktu pengambilan awal. Karena upaya yang diperlukan, ia melihat lebih banyak adopsi dalam pemrosesan data / paket aliran kinerja tinggi. Contoh kode produksi yang baik dapat ditemukan dalam desain saluran pipa DPDK QoS Enqueue: http://dpdk.org/doc/guides/prog_guide/qos_framework.html Bab 21.2.4.3. Pipa Enqueue.
Informasi lebih lanjut dapat ditemukan:
https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and
http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf
sumber
Tulis program Anda untuk mengambil ukuran minimal. Itulah mengapa tidak selalu merupakan ide yang baik untuk menggunakan optimisasi -O3 untuk GCC. Ini membutuhkan ukuran yang lebih besar. Seringkali, -O sama baiknya dengan -O2. Itu semua tergantung pada prosesor yang digunakan. YMMV.
Bekerja dengan potongan kecil data sekaligus. Itulah sebabnya algoritma pengurutan yang kurang efisien dapat berjalan lebih cepat daripada quicksort jika kumpulan data besar. Temukan cara untuk memecah kumpulan data Anda yang lebih besar menjadi yang lebih kecil. Orang lain telah menyarankan ini.
Untuk membantu Anda lebih mengeksploitasi instruksi temporal / spatial locality, Anda mungkin ingin mempelajari bagaimana kode Anda dikonversi menjadi perakitan. Sebagai contoh:
Kedua loop menghasilkan kode yang berbeda meskipun mereka hanya menguraikan array. Bagaimanapun, pertanyaan Anda sangat spesifik untuk arsitektur. Jadi, satu-satunya cara Anda untuk mengontrol penggunaan cache adalah dengan memahami cara kerja perangkat keras dan mengoptimalkan kode Anda untuk itu.
sumber
Selain menyelaraskan struktur dan bidang Anda, jika struktur Anda jika tumpukan dialokasikan, Anda mungkin ingin menggunakan pengalokasi yang mendukung alokasi yang selaras; seperti _aligned_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE); jika tidak, Anda mungkin memiliki berbagi salah secara acak; ingat bahwa di Windows, tumpukan default memiliki penyelarasan 16 byte.
sumber