Apa itu kode "ramah-cache"?

739

Apa perbedaan antara " cache code yang tidak ramah " dan kode " cache friendly "?

Bagaimana saya memastikan saya menulis kode efisien-cache?

Noah Roth
sumber
28
Ini mungkin memberi Anda petunjuk: stackoverflow.com/questions/9936132/…
Robert Martin
4
Perhatikan juga ukuran garis cache. Pada prosesor modern, seringkali 64 byte.
John Dibling
3
Ini artikel lain yang sangat bagus. Prinsip-prinsip ini berlaku untuk program C / C ++ pada OS apa saja (Linux, MaxOS atau Windows): lwn.net/Articles/255364
paulsm4
4
Pertanyaan terkait: stackoverflow.com/questions/8469427/…
Matt
stackoverflow.com/questions/763262/…
Ciro Santilli 郝海东 冠状 病 六四 事件 事件 法轮功

Jawaban:

966

Persiapan

Pada komputer modern, hanya struktur memori level terendah ( register ) yang dapat memindahkan data dalam satu siklus jam tunggal. Namun, register sangat mahal dan kebanyakan inti komputer memiliki kurang dari beberapa lusin register (beberapa ratus hingga mungkin total seribu byte ). Di ujung lain dari spektrum memori ( DRAM ), memori sangat murah (yaitu jutaan kali lebih murah ) tetapi membutuhkan ratusan siklus setelah permintaan untuk menerima data. Untuk menjembatani kesenjangan antara ini super cepat dan mahal dan super lambat dan murah adalah memori cache, bernama L1, L2, L3 dalam mengurangi kecepatan dan biaya. Idenya adalah bahwa sebagian besar kode pelaksana akan sering memukul set variabel kecil, dan sisanya (set variabel yang jauh lebih besar) jarang terjadi. Jika prosesor tidak dapat menemukan data dalam cache L1, maka itu terlihat dalam cache L2. Jika tidak ada, maka L3 cache, dan jika tidak ada, memori utama. Masing-masing "kehilangan" ini mahal pada waktunya.

(Analoginya adalah memori cache adalah memori sistem, karena memori sistem penyimpanan hard disk terlalu. Penyimpanan hard disk super murah tapi sangat lambat).

Caching adalah salah satu metode utama untuk mengurangi dampak latensi . Mengutip Herb Sutter (lihat tautan di bawah): meningkatkan bandwidth itu mudah, tapi kami tidak bisa membeli jalan keluar dari latensi .

Data selalu diambil melalui hierarki memori (terkecil == tercepat ke paling lambat). Sebuah hit cache / miss biasanya mengacu pada hit / miss di tingkat tertinggi cache CPU - oleh tingkat tertinggi Maksudku terbesar == paling lambat. Cache hit rate sangat penting untuk kinerja karena setiap cache yang hilang menghasilkan pengambilan data dari RAM (atau lebih buruk ...) yang membutuhkan banyak waktu (ratusan siklus untuk RAM, puluhan juta siklus untuk HDD). Sebagai perbandingan, membaca data dari cache (level tertinggi) biasanya hanya membutuhkan beberapa siklus.

Dalam arsitektur komputer modern, hambatan kinerja membuat CPU mati (misalnya mengakses RAM atau lebih tinggi). Ini hanya akan bertambah buruk seiring waktu. Peningkatan frekuensi prosesor saat ini tidak lagi relevan untuk meningkatkan kinerja. Masalahnya adalah akses memori. Oleh karena itu upaya desain perangkat keras dalam CPU saat ini sangat berfokus pada mengoptimalkan cache, prefetching, saluran pipa, dan konkurensi. Sebagai contoh, CPU modern menghabiskan sekitar 85% dari die pada cache dan hingga 99% untuk menyimpan / memindahkan data!

Ada banyak yang bisa dikatakan tentang masalah ini. Berikut adalah beberapa referensi hebat tentang cache, hierarki memori dan pemrograman yang tepat:

Konsep utama untuk kode ramah-cache

Aspek yang sangat penting dari kode cache-friendly adalah semua tentang prinsip lokalitas , yang tujuannya adalah untuk menempatkan data terkait dalam memori untuk memungkinkan caching yang efisien. Dalam hal cache CPU, penting untuk mengetahui garis cache untuk memahami cara kerjanya: Bagaimana cara kerja baris cache?

Aspek-aspek khusus berikut sangat penting untuk mengoptimalkan caching:

  1. Temporal locality : ketika lokasi memori tertentu diakses, kemungkinan lokasi yang sama diakses lagi dalam waktu dekat. Idealnya, informasi ini masih akan di-cache pada saat itu.
  2. Lokalitas spasial : ini mengacu pada menempatkan data terkait yang saling berdekatan. Caching terjadi pada banyak level, tidak hanya di CPU. Misalnya, ketika Anda membaca dari RAM, biasanya sepotong memori yang lebih besar diambil dari apa yang diminta secara khusus karena sangat sering program akan membutuhkan data itu segera. Cache HDD mengikuti garis pemikiran yang sama. Khusus untuk cache CPU, gagasan tentang garis cache adalah penting.

Gunakan sesuai wadah

Contoh sederhana dari cache-friendly versus cache-unfriendly adalah Ini std::vectordibandingkan std::list. Elemen a std::vectordisimpan dalam memori yang berdekatan, dan dengan demikian mengaksesnya jauh lebih ramah cache daripada mengakses elemen dalam a std::list, yang menyimpan kontennya di semua tempat. Ini karena lokalitas spasial.

Ilustrasi yang sangat bagus dari ini diberikan oleh Bjarne Stroustrup di klip youtube ini (terima kasih kepada @Mohammad Ali Baydoun untuk tautannya!).

Jangan abaikan cache dalam struktur data dan desain algoritma

Jika memungkinkan, cobalah untuk menyesuaikan struktur data dan urutan perhitungan Anda dengan cara yang memungkinkan penggunaan cache yang maksimal. Teknik umum dalam hal ini adalah pemblokiran cache (versi Archive.org) , yang sangat penting dalam komputasi berkinerja tinggi (cfr. Misalnya ATLAS ).

Mengetahui dan mengeksploitasi struktur data implisit

Contoh sederhana lainnya, yang terkadang dilupakan oleh banyak orang di lapangan adalah kolom-jurusan (mis. ,) vs. pemesanan baris-utama (mis. ,) untuk menyimpan array dua dimensi. Sebagai contoh, pertimbangkan matriks berikut:

1 2
3 4

Dalam pemesanan baris-utama, ini disimpan dalam memori sebagai 1 2 3 4; dalam pemesanan kolom-utama, ini akan disimpan sebagai 1 3 2 4. Sangat mudah untuk melihat bahwa implementasi yang tidak mengeksploitasi pemesanan ini akan dengan cepat mengalami masalah cache (mudah dihindari!). Sayangnya, saya melihat hal-hal seperti ini sangat sering di domain saya (pembelajaran mesin). @MatteoItalia menunjukkan contoh ini dengan lebih detail dalam jawabannya.

Saat mengambil elemen tertentu dari matriks dari memori, elemen di dekatnya akan diambil juga dan disimpan dalam garis cache. Jika pemesanan dieksploitasi, ini akan menghasilkan lebih sedikit akses memori (karena beberapa nilai berikutnya yang diperlukan untuk perhitungan selanjutnya sudah ada dalam baris cache).

Untuk kesederhanaan, anggap cache terdiri dari satu baris cache yang dapat berisi 2 elemen matriks dan ketika elemen tertentu diambil dari memori, yang berikutnya juga. Katakanlah kita ingin mengambil jumlah atas semua elemen dalam contoh matriks 2x2 di atas (sebut saja M):

Mengeksploitasi pemesanan (mis. Mengubah indeks kolom pertama di ):

M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses

Tidak mengeksploitasi pemesanan (misalnya mengubah indeks baris terlebih dahulu di ):

M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses

Dalam contoh sederhana ini, mengeksploitasi pemesanan kira-kira dua kali lipat kecepatan eksekusi (karena akses memori membutuhkan lebih banyak siklus daripada menghitung jumlahnya). Dalam praktiknya, perbedaan kinerja bisa jauh lebih besar.

Hindari cabang yang tidak terduga

Arsitektur modern memiliki fitur pipeline dan kompiler yang menjadi sangat baik dalam menyusun ulang kode untuk meminimalkan penundaan karena akses memori. Ketika kode kritis Anda berisi cabang (tidak dapat diprediksi), sulit atau tidak mungkin untuk mengambil data sebelumnya. Ini secara tidak langsung akan menyebabkan lebih banyak kesalahan cache.

Ini dijelaskan dengan sangat baik di sini (terima kasih kepada @ 0x90 untuk tautannya): Mengapa memproses array yang diurutkan lebih cepat daripada memproses array yang tidak disortir?

Hindari fungsi virtual

Dalam konteks , virtualmetode merupakan masalah kontroversial berkenaan dengan kesalahan cache (ada konsensus umum bahwa mereka harus dihindari bila mungkin dalam hal kinerja). Fungsi virtual dapat menyebabkan kesalahan cache selama pencarian, tetapi ini hanya terjadi jika fungsi spesifik tidak sering dipanggil (jika tidak maka kemungkinan akan di-cache), jadi ini dianggap sebagai tidak masalah oleh sebagian orang. Untuk referensi tentang masalah ini, periksa: Berapa biaya kinerja memiliki metode virtual di kelas C ++?

Masalah umum

Masalah umum dalam arsitektur modern dengan cache multiprosesor disebut berbagi salah . Ini terjadi ketika setiap prosesor mencoba menggunakan data di wilayah memori lain dan mencoba untuk menyimpannya di jalur cache yang sama . Ini menyebabkan garis cache - yang berisi data yang dapat digunakan prosesor lain - ditimpa berulang kali. Secara efektif, utas berbeda membuat satu sama lain menunggu dengan menginduksi kesalahan cache dalam situasi ini. Lihat juga (terima kasih kepada @Matt untuk tautannya): Bagaimana dan kapan harus menyelaraskan ke ukuran garis cache?

Gejala ekstrim caching yang buruk dalam memori RAM (yang mungkin bukan yang Anda maksudkan dalam konteks ini) disebut thrashing . Ini terjadi ketika proses terus menerus menghasilkan kesalahan halaman (misalnya mengakses memori yang tidak ada di halaman saat ini) yang memerlukan akses disk.

Marc Claesen
sumber
27
mungkin Anda dapat memperluas jawabannya sedikit dengan juga menjelaskan bahwa, dalam -multithreaded code- data juga bisa terlalu lokal (mis. sharing salah)
TemplateRex
2
Mungkin ada banyak level cache yang menurut perancang chip berguna. Umumnya mereka menyeimbangkan kecepatan vs ukuran. Jika Anda dapat membuat cache L1 Anda sebesar L5, dan secepatnya, Anda hanya perlu L1.
Rafael Baptista
24
Saya menyadari posting kosong perjanjian tidak disetujui di StackOverflow tetapi ini sejujurnya adalah jawaban yang paling jelas, terbaik, yang pernah saya lihat sejauh ini. Kerja bagus, Marc.
Jack Aidley
2
@JackAidley terima kasih atas pujian Anda! Ketika saya melihat jumlah perhatian pertanyaan ini diterima, saya pikir banyak orang mungkin tertarik pada penjelasan yang agak luas. Saya senang ini bermanfaat.
Marc Claesen
1
Apa yang tidak Anda sebutkan adalah bahwa struktur data yang ramah cache dirancang agar sesuai dengan garis cache dan disejajarkan dengan memori untuk mengoptimalkan penggunaan jalur cache. Jawaban yang bagus! luar biasa.
Matt
140

Selain jawaban @Marc Claesen, saya berpikir bahwa contoh klasik instruktif dari kode cache-unfriendly adalah kode yang memindai array bidimensional C (misalnya gambar bitmap) kolom-bijaksana daripada baris-bijaksana.

Elemen yang berdekatan dalam satu baris juga berdekatan dalam memori, sehingga mengaksesnya secara berurutan berarti mengaksesnya dalam urutan memori yang menaik; ini ramah-cache, karena cache cenderung untuk mengambil blok memori yang berdekatan.

Alih-alih, mengakses elemen seperti kolom-bijaksana adalah cache-tidak ramah, karena elemen pada kolom yang sama berada dalam memori satu sama lain (khususnya, jaraknya sama dengan ukuran baris), jadi ketika Anda menggunakan pola akses ini Anda melompat-lompat di memori, berpotensi membuang upaya cache untuk mengambil elemen-elemen terdekat di memori.

Dan semua yang diperlukan untuk merusak kinerja adalah untuk pergi dari

// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
    for(unsigned int x=0; x<width; ++x)
    {
        ... image[y][x] ...
    }
}

untuk

// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
    for(unsigned int y=0; y<height; ++y)
    {
        ... image[y][x] ...
    }
}

Efek ini bisa sangat dramatis (beberapa urutan besarnya dalam kecepatan) dalam sistem dengan cache kecil dan / atau bekerja dengan array besar (misalnya 10+ megapiksel gambar 24 bpp pada mesin saat ini); untuk alasan ini, jika Anda harus melakukan banyak pemindaian vertikal, sering kali lebih baik memutar gambar 90 derajat terlebih dahulu dan melakukan berbagai analisis kemudian, membatasi kode cache-tidak ramah hanya untuk rotasi.

Matteo Italia
sumber
Err, haruskah itu x <lebar?
mowwwalker
13
Editor gambar modern menggunakan ubin sebagai penyimpanan internal, misalnya blok 64x64 piksel. Ini jauh lebih ramah-cache untuk operasi lokal (menempatkan setetes, menjalankan filter blur) karena piksel tetangga dekat dalam memori di kedua arah, sebagian besar waktu.
maksimal
Saya mencoba menghitung waktu contoh yang sama pada mesin saya, dan saya menemukan bahwa waktunya sama. Adakah orang lain yang mencoba mengatur waktu?
gsingh2011
@ I3arnon: tidak, yang pertama adalah cache-friendly, karena biasanya dalam array C disimpan dalam urutan baris-utama (tentu saja jika gambar Anda karena beberapa alasan disimpan dalam kolom-urutan utama, kebalikannya adalah benar).
Matteo Italia
1
@ Gauthier: ya, cuplikan pertama adalah yang bagus; Saya berpikir bahwa ketika saya menulis ini saya berpikir di sepanjang baris "Yang diperlukan [untuk merusak kinerja aplikasi yang bekerja] adalah untuk beralih dari ... ke ..."
Matteo Italia
88

Mengoptimalkan penggunaan cache sebagian besar disebabkan oleh dua faktor.

Lokalitas Referensi

Faktor pertama (yang telah disinggung orang lain) adalah lokalitas referensi. Referensi lokalitas benar-benar memiliki dua dimensi: ruang dan waktu.

  • Spasial

Dimensi spasial juga bermuara pada dua hal: pertama, kami ingin mengemas informasi kami secara padat, sehingga lebih banyak informasi yang sesuai dengan memori yang terbatas itu. Ini berarti (misalnya) bahwa Anda memerlukan peningkatan besar dalam kompleksitas komputasi untuk membenarkan struktur data berdasarkan node kecil yang bergabung dengan pointer.

Kedua, kami ingin informasi yang akan diproses bersama juga terletak bersama. Cache yang khas berfungsi di "baris", yang berarti ketika Anda mengakses beberapa informasi, informasi lain di alamat terdekat akan dimuat ke dalam cache dengan bagian yang kami sentuh. Misalnya, ketika saya menyentuh satu byte, cache mungkin memuat 128 atau 256 byte di dekat yang itu. Untuk mengambil keuntungan dari itu, Anda biasanya menginginkan data yang diatur untuk memaksimalkan kemungkinan Anda juga akan menggunakan data lain yang dimuat pada waktu yang sama.

Untuk contoh yang benar-benar sepele, ini bisa berarti bahwa pencarian linier bisa jauh lebih kompetitif dengan pencarian biner daripada yang Anda harapkan. Setelah Anda memuat satu item dari baris cache, menggunakan sisa data di baris cache itu hampir gratis. Pencarian biner menjadi terasa lebih cepat hanya ketika data cukup besar sehingga pencarian biner mengurangi jumlah garis cache yang Anda akses.

  • Waktu

Dimensi waktu berarti bahwa ketika Anda melakukan beberapa operasi pada beberapa data, Anda ingin (sebanyak mungkin) melakukan semua operasi pada data itu sekaligus.

Karena Anda telah menandai ini sebagai C ++, saya akan menunjuk contoh klasik dari desain yang relatif cache-ramah: std::valarray. valarrayoverloads operator yang paling aritmatika, jadi saya bisa (misalnya) mengatakan a = b + c + d;(di mana a, b, cdan dsemua valarrays) untuk melakukan penambahan unsur-bijaksana dari mereka array.

Masalah dengan ini adalah bahwa ia berjalan melalui sepasang input, menempatkan hasil sementara, berjalan melalui sepasang input lainnya, dan seterusnya. Dengan banyak data, hasil dari satu perhitungan dapat menghilang dari cache sebelum digunakan dalam perhitungan berikutnya, jadi kami akhirnya membaca (dan menulis) data berulang kali sebelum kami mendapatkan hasil akhir kami. Jika setiap elemen dari hasil akhir akan menjadi sesuatu seperti (a[n] + b[n]) * (c[n] + d[n]);, kita akan umumnya lebih memilih untuk membaca setiap a[n], b[n], c[n]dan d[n]sekali, melakukan perhitungan, menulis hasil, peningkatan ndan ulangi 'til kita sudah selesai. 2

Berbagi Garis

Faktor utama kedua adalah menghindari pembagian garis. Untuk memahami hal ini, kita mungkin perlu mencadangkan dan melihat sedikit cara mengatur cache. Bentuk cache paling sederhana dipetakan langsung. Ini berarti satu alamat di memori utama hanya dapat disimpan di satu tempat tertentu di cache. Jika kami menggunakan dua item data yang dipetakan ke tempat yang sama di cache, ia berfungsi buruk - setiap kali kami menggunakan satu item data, yang lainnya harus dihapus dari cache untuk memberikan ruang bagi yang lain. Sisa cache mungkin kosong, tetapi item itu tidak akan menggunakan bagian lain dari cache.

Untuk mencegah hal ini, kebanyakan cache adalah apa yang disebut "set associative". Misalnya, dalam cache set-asosiatif 4 arah, item apa pun dari memori utama dapat disimpan di salah satu dari 4 tempat berbeda dalam cache. Jadi, ketika cache akan memuat suatu item, ia mencari 3 item yang terakhir digunakan di antara keempat item tersebut, membuangnya ke memori utama, dan memuat item baru di tempatnya.

Masalahnya mungkin cukup jelas: untuk cache yang dipetakan langsung, dua operan yang memetakan ke lokasi cache yang sama dapat menyebabkan perilaku buruk. Cache set-asosiatif N-arah meningkatkan angka dari 2 menjadi N + 1. Mengatur cache menjadi lebih banyak "cara" membutuhkan sirkuit ekstra dan umumnya berjalan lebih lambat, jadi (misalnya) cache asosiatif 8192 cara jarang juga merupakan solusi yang baik.

Pada akhirnya, faktor ini lebih sulit untuk dikendalikan dalam kode portabel. Kontrol Anda atas tempat data Anda biasanya terbatas. Lebih buruk lagi, pemetaan yang tepat dari alamat ke cache bervariasi antara prosesor yang serupa. Namun, dalam beberapa kasus, ada baiknya melakukan hal-hal seperti mengalokasikan buffer besar, dan kemudian hanya menggunakan bagian dari apa yang Anda alokasikan untuk memastikan data tidak berbagi jalur cache yang sama (meskipun Anda mungkin perlu mendeteksi prosesor yang tepat dan bertindak sesuai untuk melakukan ini).

  • Berbagi Palsu

Ada item lain yang terkait yang disebut "berbagi salah". Ini muncul dalam multiprosesor atau sistem multicore, di mana dua (atau lebih) prosesor / core memiliki data yang terpisah, tetapi berada pada baris cache yang sama. Ini memaksa kedua prosesor / core untuk mengoordinasikan akses mereka ke data, meskipun masing-masing memiliki item data yang terpisah. Terutama jika keduanya memodifikasi data secara bergantian, ini dapat menyebabkan perlambatan besar karena data harus terus-menerus diangkut antar prosesor. Ini tidak dapat disembuhkan dengan mudah dengan mengatur cache menjadi lebih banyak "cara" atau semacamnya. Cara utama untuk mencegahnya adalah untuk memastikan bahwa dua utas jarang (lebih disukai tidak pernah) memodifikasi data yang mungkin berada di baris cache yang sama (dengan peringatan yang sama tentang kesulitan mengendalikan alamat di mana data dialokasikan).


  1. Mereka yang tahu C ++ mungkin bertanya-tanya apakah ini terbuka untuk optimasi melalui sesuatu seperti templat ekspresi. Saya cukup yakin jawabannya adalah ya, itu bisa dilakukan dan jika ya, itu mungkin akan menjadi kemenangan yang cukup besar. Saya tidak mengetahui ada yang melakukannya, dan mengingat betapa sedikit yang valarraydigunakan, saya setidaknya akan sedikit terkejut melihat ada yang melakukannya juga.

  2. Seandainya ada yang bertanya-tanya bagaimana valarray(dirancang khusus untuk kinerja) bisa menjadi ini sangat salah, ia datang ke satu hal: itu benar-benar dirancang untuk mesin seperti Crays yang lebih tua, yang menggunakan memori utama cepat dan tidak ada cache. Bagi mereka, ini benar-benar desain yang hampir ideal.

  3. Ya, saya menyederhanakan: sebagian besar cache tidak benar-benar mengukur item yang terakhir digunakan secara tepat, tetapi mereka menggunakan beberapa heuristik yang dimaksudkan untuk menjadi dekat dengan itu tanpa harus menyimpan cap waktu penuh untuk setiap akses.

Jerry Coffin
sumber
1
Saya suka potongan informasi tambahan dalam jawaban Anda, terutama valarraycontohnya.
Marc Claesen
1
+1 Akhirnya: deskripsi sederhana dari kumpulan associativity! Sunting lebih lanjut: Ini adalah salah satu jawaban paling informatif tentang SO. Terima kasih.
Insinyur
32

Selamat datang di dunia Desain Berorientasi Data. Mantra dasar adalah Menyortir, Menghilangkan Cabang, Batch, Menghilangkan virtualpanggilan - semua langkah menuju lokalitas yang lebih baik.

Karena Anda menandai pertanyaan dengan C ++, inilah tipikal C ++ omong kosong wajib . Pemrograman Berorientasi Objek dari Tony Albrecht juga merupakan pengantar yang bagus untuk subjek ini.

arul
sumber
1
apa yang Anda maksud dengan batch, orang mungkin tidak mengerti.
0x90
5
Batching: alih-alih melakukan unit kerja pada satu objek, lakukan itu pada sekelompok objek.
arul
AKA memblokir, memblokir register, memblokir cache.
0x90
1
Blocking / Non-blocking biasanya mengacu pada bagaimana benda berperilaku dalam lingkungan bersamaan.
arul
2
batching == vectorization
Amro
23

Hanya menumpuk: contoh klasik kode cache-friendly versus cache-friendly adalah "pemblokiran cache" dari matriks multiply.

Multiply matrix naif terlihat seperti:

for(i=0;i<N;i++) {
   for(j=0;j<N;j++) {
      dest[i][j] = 0;
      for( k==;k<N;i++) {
         dest[i][j] += src1[i][k] * src2[k][j];
      }
   }
}

Jika Nbesar, mis. Jika N * sizeof(elemType)lebih besar dari ukuran cache, maka setiap akses tunggal src2[k][j]akan menjadi cache miss.

Ada banyak cara untuk mengoptimalkan ini untuk cache. Berikut adalah contoh yang sangat sederhana: alih-alih membaca satu item per baris cache di loop dalam, gunakan semua item:

int itemsPerCacheLine = CacheLineSize / sizeof(elemType);

for(i=0;i<N;i++) {
   for(j=0;j<N;j += itemsPerCacheLine ) {
      for(jj=0;jj<itemsPerCacheLine; jj+) {
         dest[i][j+jj] = 0;
      }
      for( k=0;k<N;k++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

Jika ukuran garis cache adalah 64 byte, dan kami beroperasi pada floats 32 bit (4 byte), maka ada 16 item per baris cache. Dan jumlah cache yang hilang hanya melalui transformasi sederhana ini berkurang sekitar 16 kali lipat.

Transformasi yang lebih bagus beroperasi pada ubin 2D, optimalkan untuk banyak cache (L1, L2, TLB), dan sebagainya.

Beberapa hasil googling "pemblokiran cache":

http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf

http://software.intel.com/en-us/articles/cache-blocking-techniques

Animasi video yang bagus dari algoritma pemblokiran cache yang dioptimalkan.

http://www.youtube.com/watch?v=IFWgwGMMrh0

Ubin lingkaran sangat erat kaitannya:

http://en.wikipedia.org/wiki/Loop_tiling

Krazy Glew
sumber
7
Orang-orang yang membaca ini mungkin juga tertarik dengan artikel saya tentang perkalian matriks tempat saya menguji algoritme "cache-friendly" dan algoritma ijk yang tidak ramah dengan mengalikan dua matriks 2000x2000.
Martin Thoma
3
k==;Saya berharap ini salah ketik?
TrebledJ
13

Prosesor saat ini bekerja dengan banyak level area memori berjenjang. Jadi CPU akan memiliki banyak memori yang ada di chip CPU itu sendiri. Ini memiliki akses yang sangat cepat ke memori ini. Ada berbagai tingkat cache yang masing-masing aksesnya lebih lambat (dan lebih besar) dari yang berikutnya, sampai Anda mendapatkan memori sistem yang tidak ada di CPU dan relatif lebih lambat untuk diakses.

Secara logis, ke instruksi CPU membuat Anda cukup merujuk ke alamat memori di ruang alamat virtual raksasa. Saat Anda mengakses satu alamat memori, CPU akan mengambilnya. di masa lalu hanya akan mengambil satu alamat itu. Tapi hari ini CPU akan mengambil banyak memori di sekitar bit yang Anda minta, dan menyalinnya ke dalam cache. Diasumsikan bahwa jika Anda meminta alamat tertentu yang sangat mungkin Anda akan segera meminta alamat terdekat. Misalnya, jika Anda menyalin buffer, Anda akan membaca dan menulis dari alamat yang berurutan - satu setelah yang lainnya.

Jadi hari ini ketika Anda mengambil alamat, ia memeriksa tingkat cache pertama untuk melihat apakah sudah membaca alamat itu ke dalam cache, jika tidak menemukannya, maka ini adalah cache miss dan harus keluar ke tingkat berikutnya cache untuk menemukannya, sampai akhirnya harus keluar ke memori utama.

Kode ramah-cache mencoba untuk menjaga akses tetap berdekatan dalam memori sehingga Anda meminimalkan kesalahan cache.

Jadi contoh akan membayangkan Anda ingin menyalin tabel 2 dimensi raksasa. Ini diatur dengan baris jangkauan berturut-turut dalam memori, dan satu baris mengikuti berikutnya setelahnya.

Jika Anda menyalin elemen satu baris pada satu waktu dari kiri ke kanan - itu akan menjadi ramah cache. Jika Anda memutuskan untuk menyalin tabel satu kolom pada satu waktu, Anda akan menyalin jumlah memori yang sama persis - tetapi itu akan menjadi cache yang tidak ramah.

Rafael Baptista
sumber
4

Perlu diklarifikasi bahwa data tidak hanya ramah cache, tetapi juga penting untuk kode. Ini merupakan tambahan untuk prediksi cabang, penyusunan ulang instruksi, menghindari pembagian yang sebenarnya dan teknik lainnya.

Biasanya semakin padat kodenya, semakin sedikit garis cache yang diperlukan untuk menyimpannya. Ini menghasilkan lebih banyak baris cache yang tersedia untuk data.

Kode tidak boleh memanggil fungsi di semua tempat karena biasanya akan membutuhkan satu atau lebih baris cache sendiri, sehingga menghasilkan lebih sedikit baris cache untuk data.

Suatu fungsi harus dimulai pada alamat yang ramah baris-alignment cache. Meskipun ada switch (gcc) kompiler untuk ini perlu diketahui bahwa jika fungsinya sangat singkat mungkin akan sia-sia bagi masing-masing untuk menempati seluruh baris cache. Sebagai contoh, jika tiga dari fungsi yang paling sering digunakan masuk dalam satu baris cache 64 byte, ini kurang boros daripada jika masing-masing memiliki baris sendiri dan menghasilkan dua baris cache kurang tersedia untuk penggunaan lainnya. Nilai rata-rata penyelarasan bisa 32 atau 16.

Jadi, luangkan waktu ekstra untuk membuat kodenya padat. Uji berbagai konstruk, kompilasi dan tinjau ukuran dan profil kode yang dihasilkan.

Olof Forshell
sumber
2

Seperti @Marc Claesen menyebutkan bahwa salah satu cara untuk menulis kode ramah-cache adalah dengan mengeksploitasi struktur tempat data kita disimpan. Selain itu cara lain untuk menulis kode ramah cache adalah: ubah cara data kita disimpan; kemudian tulis kode baru untuk mengakses data yang disimpan dalam struktur baru ini.

Ini masuk akal dalam kasus bagaimana sistem basis data merumuskan tupel tabel dan menyimpannya. Ada dua cara dasar untuk menyimpan tupel tabel yaitu toko baris dan toko kolom. Di toko baris seperti namanya tupel disimpan baris bijaksana. Mari kita anggap sebuah tabel bernama Productsedang disimpan memiliki 3 atribut yaitu int32_t key, char name[56]dan int32_t price, sehingga ukuran total tupel adalah 64byte.

Kita bisa mensimulasikan eksekusi query toko baris yang sangat mendasar dalam memori utama dengan membuat array Productstruct dengan ukuran N, di mana N adalah jumlah baris dalam tabel. Layout memori seperti ini juga disebut array struct. Jadi struct untuk Produk bisa seperti:

struct Product
{
   int32_t key;
   char name[56];
   int32_t price'
}

/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */

Demikian pula kita dapat mensimulasikan eksekusi permintaan penyimpanan kolom yang sangat dasar di memori utama dengan membuat 3 array ukuran N, satu array untuk setiap atribut Producttabel. Layout memori seperti itu juga disebut struct of arrays. Jadi 3 array untuk setiap atribut Produk bisa seperti:

/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */

Sekarang setelah memuat array struct (Tata Letak Baris) dan 3 array terpisah (Tata Letak Kolom), kami memiliki penyimpanan baris dan penyimpanan kolom pada tabel kami yang Productada di memori kami.

Sekarang kita beralih ke bagian kode cache friendly. Misalkan beban kerja pada tabel kita sedemikian rupa sehingga kita memiliki kueri agregasi pada atribut harga. Seperti

SELECT SUM(price)
FROM PRODUCT

Untuk toko baris, kita dapat mengubah kueri SQL di atas

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + table[i].price;

Untuk kolom store kita dapat mengonversi query SQL di atas menjadi

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + price[i];

Kode untuk toko kolom akan lebih cepat daripada kode untuk tata letak baris dalam permintaan ini karena hanya memerlukan subset atribut dan dalam tata letak kolom kita hanya melakukan itu yaitu hanya mengakses kolom harga.

Misalkan ukuran garis cache adalah 64byte.

Dalam kasus tata letak baris ketika baris cache dibaca, nilai harga hanya 1 ( cacheline_size/product_struct_size = 64/64 = 1) tuple dibaca, karena ukuran struct kami 64 byte dan mengisi seluruh baris cache kami, jadi untuk setiap tuple cache cache terjadi jika tata letak baris.

Dalam kasus tata letak kolom ketika baris cache dibaca, nilai harga 16 ( cacheline_size/price_int_size = 64/4 = 16) tuple dibaca, karena 16 nilai harga yang berdekatan disimpan dalam memori dimasukkan ke dalam cache, jadi untuk setiap enam belas tuple cache miss ocurs dalam kasus tata letak kolom.

Jadi tata letak kolom akan lebih cepat dalam hal kueri yang diberikan, dan lebih cepat dalam kueri agregasi tersebut pada subset kolom dari tabel. Anda dapat mencoba eksperimen semacam itu untuk diri Anda sendiri menggunakan data dari TPC-H benchmark, dan membandingkan waktu menjalankan untuk kedua tata letak. The wikipedia artikel tentang sistem database berorientasi kolom juga baik.

Jadi dalam sistem basis data, jika beban kerja kueri diketahui sebelumnya, kita dapat menyimpan data dalam tata letak yang sesuai dengan kueri dalam beban kerja dan mengakses data dari tata letak ini. Dalam kasus contoh di atas kami membuat tata letak kolom dan mengubah kode kami untuk menghitung jumlah sehingga menjadi ramah cache.


sumber
1

Sadarilah bahwa cache tidak hanya menyimpan memori kontinu. Mereka memiliki beberapa baris (setidaknya 4) sehingga memori yang tidak teratur dan tumpang tindih seringkali dapat disimpan dengan efisien.

Apa yang hilang dari semua contoh di atas adalah tolok ukur yang diukur. Ada banyak mitos tentang kinerja. Kecuali Anda mengukurnya, Anda tidak tahu. Jangan menyulitkan kode Anda kecuali Anda memiliki peningkatan yang terukur .

Tuntable
sumber