“Önbellek dostu” kod nedir?


738

" Önbellek dostu olmayan kod " ile " önbellek dostu " kod arasındaki fark nedir ?

Önbellek verimli kod yazdığımdan nasıl emin olabilirim?


28

4
Ayrıca bir önbellek satırının boyutunun farkında olun. Modern işlemcilerde genellikle 64 bayttır.
John Dibling

3
İşte başka bir çok iyi makale. İlkeler herhangi bir işletim sistemindeki (Linux, MaxOS veya Windows) C / C ++ programları için geçerlidir: lwn.net/Articles/255364
paulsm4


Yanıtlar:


965

Hazırlıklar

Modern bilgisayarlarda, yalnızca en düşük düzeydeki bellek yapıları ( kayıtlar ), verileri tek saat döngülerinde hareket ettirebilir. Ancak, kayıtlar çok pahalıdır ve bilgisayar çekirdeklerinin çoğunun birkaç düzine kayıttan daha azı vardır ( toplamda birkaç yüz ila belki bin bayt ). Bellek spektrumunun ( DRAM ) diğer ucunda, bellek çok ucuzdur (yani kelimenin tam anlamıyla milyonlarca kez daha ucuz ), ancak verileri alma isteğinden sonra yüzlerce döngü alır. Süper hızlı ve pahalı ve süper yavaş ve ucuz arasındaki bu boşluğu kapatmak için önbellek anıları, azalan hız ve maliyette L1, L2, L3 olarak adlandırılmıştır. Fikir, yürütme kodunun çoğunun sık sık küçük bir değişken kümesine ve geri kalanının (çok daha büyük değişkenler kümesi) sık sık vurmasıdır. İşlemci verileri L1 önbelleğinde bulamazsa, L2 önbelleğine bakar. Orada değilse, L3 önbelleği ve orada değilse ana bellek. Bu "özlülerin" her biri zaman pahalıdır.

(Sistem belleği çok sabit disk depolama alanı olduğu için önbellek sistem belleğidir. Sabit disk depolama süper ucuz ama çok yavaştır).

Önbellekleme, gecikmenin etkisini azaltmak için ana yöntemlerden biridir . Herb Sutter'i (aşağıdaki linkler) açıklamak için: bant genişliğini artırmak kolaydır, ancak gecikmeden çıkış yolumuzu alamayız .

Veriler her zaman bellek hiyerarşisiyle alınır (en küçük == en hızlıdan en yavaşına). Bir önbellek isabet / özledim genellikle CPU'nun en yüksek önbellek düzeyinde bir isabet / özledim anlamına gelir - en yüksek düzeyde en büyük == en yavaş anlamına gelir. Önbellek isabet oranı performans için çok önemlidir, çünkü her önbellek kaçırması çok fazla zaman alan (RAM için yüzlerce döngü, HDD için on milyonlarca döngü ) RAM'den veri almanıza (veya daha kötüsü ...) neden olur . Buna karşılık, (en üst düzey) önbellekten veri okumak tipik olarak sadece bir avuç döngü alır.

Modern bilgisayar mimarilerinde performans darboğazı CPU kalıbını terk ediyor (örn. RAM'a erişim veya daha yüksek). Bu zamanla daha da kötüleşir. İşlemci frekansındaki artış şu anda performansı artırmakla ilgili değildir. Sorun bellek erişimidir. Bu nedenle CPU'lardaki donanım tasarım çabaları şu anda ağırlıklı olarak önbellekleri, ön getirmeyi, boru hatlarını ve eşzamanlılığı optimize etmeye odaklanıyor. Örneğin, modern CPU'lar kalıbın yaklaşık% 85'ini önbelleklere ve% 99'a kadar veri depolamak / taşımak için harcıyor!

Konu hakkında söylenecek çok şey var. İşte önbellekler, bellek hiyerarşileri ve uygun programlama hakkında birkaç harika referans:

Önbellek dostu kod için temel kavramlar

Önbellek dostu kodun çok önemli bir yönü, etkili önbellekleme sağlamak için ilgili verileri belleğe yakın yerleştirmek olan yerellik ilkesiyle ilgilidir. CPU önbelleği açısından, bunun nasıl çalıştığını anlamak için önbellek satırlarının farkında olmak önemlidir: Önbellek satırları nasıl çalışır?

Önbelleği optimize etmek için aşağıdaki özel hususlar çok önemlidir:

  1. Geçici yerellik : belirli bir hafıza konumuna erişildiğinde, yakın gelecekte aynı konuma tekrar erişilmesi olasıdır. İdeal olarak, bu bilgi hala bu noktada önbelleğe alınır.
  2. Konumsal konum : Bu, ilgili verilerin birbirine yakın yerleştirilmesini ifade eder. Önbellekleme yalnızca CPU'da değil, birçok düzeyde gerçekleşir. Örneğin, RAM'den okuduğunuzda, genellikle özel olarak istenenden daha büyük bir bellek yığını getirilir, çünkü program genellikle bu verileri yakında gerektirecektir. HDD önbellekleri aynı düşünce tarzını izler. Özellikle CPU önbellekleri için önbellek çizgileri kavramı önemlidir.

Uygun kullanın konteynerler

Önbellek dostu ve önbellek dostu olmayanların basit bir örneği 's std::vectorkarşı std::list. A öğelerinin öğeleri std::vectorbitişik bellekte depolanır ve bu nedenle onlara erişmek , içeriğini her yerde depolayan a öğesindeki öğelere erişmekten çok daha önbellek dostudur std::list. Bu mekansal yerellikten kaynaklanmaktadır.

Bu çok güzel bir örnek bu youtube klibi Bjarne Stroustrup tarafından verilir (bağlantı için @Mohammad Ali Baydoun sayesinde!).

Veri yapısı ve algoritma tasarımında önbelleği ihmal etmeyin

Mümkün olduğunda, veri yapılarınızı ve hesaplama sırasınızı önbelleğin maksimum kullanımına izin verecek şekilde uyarlamaya çalışın. Bu konuda yaygın bir teknik, yüksek performanslı bilgi işlemde (cfr. Örneğin ATLAS ) çok önemli olan önbellek engellemedir (Archive.org sürümü ).

Verinin örtük yapısını bilir ve kullanır

Alandaki birçok insanın bazen unutabileceği bir başka basit örnek, sütun-büyüktür (ör. ,) ile satır içi sıralama (ör. ,) iki boyutlu dizileri saklamak için kullanılır. Örneğin, aşağıdaki matrisi düşünün:

1 2
3 4

Satır-büyük sıralamasında, bu bellekte 1 2 3 4; sütun ana sıralamasında bu olarak depolanır 1 3 2 4. Bu sıralamayı kullanmayan uygulamaların hızlı bir şekilde (kolayca önlenebilir!) Önbellek sorunlarıyla karşılaşacağını görmek kolaydır. Ne yazık ki, böyle şeyleri görmek çok alanım (makine öğrenme) genellikle. @MatteoItalia bu örneği cevabında daha ayrıntılı olarak gösterdi.

Bir matrisin belirli bir öğesini bellekten alırken, yakınındaki öğeler de alınacak ve bir önbellek satırında saklanacaktır. Sıralamadan yararlanılırsa, bu daha az bellek erişimi sağlar (çünkü sonraki hesaplamalar için gereken sonraki birkaç değer zaten bir önbellek satırındadır).

Basit olması için, önbelleğin 2 matris öğesi içerebilen tek bir önbellek satırı içerdiğini ve belirli bir öğenin bellekten getirildiğinde, bir sonrakinin de olduğunu varsayalım. Yukarıdaki örnek 2x2 matrisindeki tüm öğelerin toplamını almak istediğimizi düşünelim (diyelim M)

Sıralamadan yararlanma (örn. ):

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

Sıralamadan yararlanmama (örn. ):

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

Bu basit örnekte, sıralamayı kullanmak yaklaşık olarak yürütme hızını iki katına çıkarır (çünkü bellek erişimi toplamları hesaplamaktan çok daha fazla döngü gerektirir). Uygulamada, performans farkı çok daha büyük olabilir.

Öngörülemeyen dallardan kaçının

Modern mimariler, boru hatlarına sahiptir ve derleyiciler, bellek erişimi nedeniyle gecikmeleri en aza indirmek için kodu yeniden sıralamada çok iyi hale gelmektedir. Kritik kodunuz (öngörülemeyen) dallar içerdiğinde, verileri önceden almak zor veya imkansızdır. Bu dolaylı olarak daha fazla önbellek kaybına yol açacaktır.

Bu burada çok iyi açıklanmıştır (bağlantı için @ 0x90 sayesinde): Sıralı bir diziyi neden sıralanmamış bir diziyi işlemekten daha hızlı işliyor?

Sanal işlevlerden kaçının

Bağlamında , virtualyöntemler önbellek hatalarıyla ilgili tartışmalı bir konudur (performans açısından mümkün olduğunda kaçınılması gerektiği konusunda genel bir fikir birliği vardır). Sanal fonksiyonlar görünüm yukarı sırasında önbellek isabetsizlik tetikleyebilir, ancak bu yalnızca olur eğer bu bazıları tarafından olmayan bir konu olarak kabul edilmektedir, böylece belirli bir işlev genellikle çağrılmaz (aksi takdirde büyük olasılıkla önbelleğe olacaktır). Bu sorunla ilgili başvuru için, şunlara bakın: C ++ sınıfında sanal bir yönteme sahip olmanın performans maliyeti nedir?

Sık karşılaşılan sorunlar

Çok işlemcili önbelleklere sahip modern mimarilerde yaygın bir soruna yanlış paylaşım denir . Bu, her bir işlemci başka bir bellek bölgesinde veri kullanmaya çalıştığında ve aynı önbellek satırında depolamaya çalıştığında oluşur . Bu, başka bir işlemcinin kullanabileceği verileri içeren önbellek satırının üzerine tekrar tekrar yazılmasına neden olur. Etkili olarak, farklı iş parçacıkları bu durumda önbellek hatalarını tetikleyerek birbirlerini bekletir. Ayrıca bakınız (bağlantı için @Matt sayesinde): Önbellek satır boyutuna nasıl ve ne zaman hizalanmalı?

RAM belleğinde zayıf önbelleklemenin aşırı bir belirtisi (muhtemelen bu bağlamda kastettiğiniz şey değildir) daralma olarak adlandırılır . Bu, işlem sürekli olarak disk erişimi gerektiren sayfa hataları (örneğin geçerli sayfada olmayan belleğe erişir) oluşturduğunda oluşur.


27
belki de -multithreaded kod-verilerinin de çok yerel (örneğin yanlış paylaşım) olabileceğini açıklayarak cevabı biraz genişletebilirsiniz
TemplateRex

2
Çip tasarımcılarının yararlı olduğunu düşündüğü kadar önbellek seviyesi olabilir. Genellikle hızı ve boyutu dengeliyorlar. L1 önbelleğinizi L5 kadar büyük ve en hızlı şekilde yapabilseydiniz, sadece L1'e ihtiyacınız olurdu.
Rafael Baptista

24
StackOverflow üzerinde boş anlaşma mesajlarının onaylanmadığını anlıyorum, ancak bu şimdiye kadar gördüğüm en açık, en iyi cevap. Mükemmel iş, Marc.
Jack Aidley

2
@ JackAidley övgün için teşekkürler! Bu sorunun dikkatini çektiğimde, birçok insanın biraz kapsamlı bir açıklama ile ilgilenebileceğini düşündüm. Yararlı olduğuna sevindim.
Marc Claesen

1
Bahsetmediğiniz şey, önbellek dostu veri yapılarının bir önbellek hattına sığacak şekilde tasarlanmış ve önbellek hatlarını en iyi şekilde kullanmak için belleğe hizalanmış olmasıdır. Büyük cevap olsa! müthiş.
Matt

140

@Marc Claesen'in cevabına ek olarak, önbellek dostu olmayan kodun öğretici bir klasik örneğinin satır bilge yerine bir C bidimensional dizisini (örneğin bir bitmap görüntüsü) tararken kod olduğunu düşünüyorum.

Bir sıraya bitişik olan elemanlar da hafızaya bitişiktir, bu nedenle bunlara sırayla erişmek, artan hafıza düzeninde bunlara erişmek anlamına gelir; önbellek bitişik bellek bloklarını önceden alma eğiliminde olduğu için bu önbellek dostudur.

Bunun yerine, aynı sütun üzerindeki öğeler birbirinden bellekte uzak olduğundan (özellikle, mesafeleri satırın boyutuna eşittir), bu nedenle sütun bazında bu öğelere erişmek önbellek dostu değildir, bu nedenle bu erişim desenini kullandığınızda hafızada zıplıyor, potansiyel olarak hafızadaki yakındaki öğeleri alma önbelleğinin çabasını boşa harcıyor.

Ve performansı mahvetmek için gereken tek şey

// 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] ...
    }
}

için

// 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] ...
    }
}

Bu etki, küçük önbellekleri olan ve / veya büyük dizilerle çalışan sistemlerde (örneğin mevcut makinelerde 10+ megapiksel 24 bpp görüntüler) oldukça dramatik olabilir (hızda birkaç büyüklük sırası); Bu nedenle, çok sayıda dikey tarama yapmanız gerekiyorsa, genellikle önbelleği düşmanca kodunu yalnızca döndürmeyle sınırlandırarak, önce görüntüyü 90 derece döndürmek ve daha sonra çeşitli analizleri yapmak daha iyidir.


Hata, bu x <genişlik olmalı mı?
mowwwalker

13
Modern görüntü editörleri dahili depolama alanı olarak döşemeler kullanır, örneğin 64x64 piksel bloklar. Bu, yerel işlemler için çok daha önbellek dostudur (bir dab yerleştirme, bir bulanıklık filtresi çalıştırma), çünkü komşu pikseller her zaman her iki yönde de belleğe yakındır.
maxy

Makinemde benzer bir örneği zamanlamayı denedim ve zamanların aynı olduğunu gördüm. Başka kimse zamanlamayı denedi mi?
gsingh2011

@ I3arnon: hayır, birincisi önbellek dostudur, çünkü normalde C dizileri satır-büyük düzende saklanır (tabii ki herhangi bir nedenle görüntünüz sütun-büyük sırada depolanırsa tersi doğrudur).
Matteo Italia

1
@Gauthier: evet, ilk pasaj iyi olanı; Bunu yazdığımda, "Çalışan bir uygulamanın performansını mahvetmek için gereken her şey ..." ... 'dan gitmek "satırları boyunca düşündüğümü düşünüyorum
Matteo Italia

88

Önbellek kullanımını optimize etmek büyük ölçüde iki faktöre bağlıdır.

Referans Yeri

İlk faktör (başkalarının önceden bahsettiği) referans mevkisidir. Referans yerinin gerçekten iki boyutu vardır: mekan ve zaman.

  • uzaysal

Mekansal boyut da iki şeye ayrılır: ilk olarak, bilgilerimizi yoğun bir şekilde paketlemek istiyoruz, böylece bu sınırlı hafızaya daha fazla bilgi sığacak. Bu, örneğin işaretçilerin katıldığı küçük düğümlere dayanan veri yapılarını haklı göstermek için hesaplama karmaşıklığında önemli bir iyileştirmeye ihtiyacınız olduğu anlamına gelir.

İkincisi, birlikte işlenecek bilgilerin birlikte de yer almasını istiyoruz. Tipik bir önbellek "satırlarda" çalışır, yani bazı bilgilere eriştiğinizde, yakındaki adreslerdeki diğer bilgiler dokunduğumuz bölümle önbelleğe yüklenir. Örneğin, bir bayta dokunduğumda, önbellek buna yakın 128 veya 256 bayt yükleyebilir. Bundan yararlanmak için, genellikle düzenlenen verilerin aynı zamanda yüklenen diğer verileri de kullanma olasılığınızı en üst düzeye çıkarmasını istersiniz.

Gerçekten önemsiz bir örnek için, bu, doğrusal bir aramanın ikili bir arama ile beklediğinizden çok daha rekabetçi olabileceği anlamına gelebilir. Önbellek satırından bir öğe yükledikten sonra, bu önbellek satırındaki verilerin geri kalanını kullanmak neredeyse ücretsizdir. İkili arama, yalnızca veriler ikili aramanın eriştiğiniz önbellek satırlarının sayısını azaltacak kadar büyük olduğunda belirgin şekilde daha hızlı hale gelir.

  • zaman

Zaman boyutu, bazı veriler üzerinde bazı işlemler yaptığınızda (mümkün olduğunca) bu verilerdeki tüm işlemleri bir kerede yapmak istediğiniz anlamına gelir.

Eğer C olarak etiketlediniz beri ++, ben nispeten önbellek düşmanca tasarımı klasik bir örneğe işaret edeceğiz: std::valarray. valarrayaşırı yükler en aritmetik operatörler, bu yüzden (örneğin) söyleyebiliriz a = b + c + d;(burada a, b, cve dtüm valarrays vardır) O dizilerin öğeye göre ek yapmak.

Bununla ilgili sorun, bir çift girişten geçmesi, sonuçları geçici olarak koyması, başka bir çift girişten geçmesi vb. Çok fazla veriyle, bir hesaplamadan elde edilen sonuç bir sonraki hesaplamada kullanılmadan önce önbellekten kaybolabilir, bu nedenle nihai sonucumuzu almadan önce verileri tekrar tekrar okur (ve yazarız). Nihai sonucun her öğe gibi bir şey olacaksa (a[n] + b[n]) * (c[n] + d[n]);, genellikle her okumayı tercih ediyorum a[n], b[n], c[n]ve d[n]bir kez, hesaplama yapmak sonuç, artışı yazma nve işimiz biter til tekrarı'. 2

Hat Paylaşımı

İkinci ana faktör hat paylaşımından kaçınmaktır. Bunu anlamak için muhtemelen yedeklememiz ve önbelleklerin nasıl düzenlendiğine biraz bakmamız gerekir. En basit önbellek biçimi doğrudan eşleştirilir. Bu, ana bellekteki bir adresin önbellekteki yalnızca belirli bir noktada saklanabileceği anlamına gelir. Önbellekteki aynı noktaya eşlenen iki veri öğesi kullanırsak, kötü çalışır - bir veri öğesini her kullandığımızda, diğerine yer açmak için diğerinin önbellekten temizlenmesi gerekir. Önbelleğin geri kalanı boş olabilir, ancak bu öğeler önbelleğin diğer bölümlerini kullanmayacaktır.

Bunu önlemek için, çoğu önbellek "set ilişkisel" olarak adlandırılır. Örneğin, 4 yönlü küme ilişkilendirmeli önbellekte, ana bellekteki herhangi bir öğe önbellekteki 4 farklı yerden herhangi birinde saklanabilir. Böylece, önbellek bir öğe yükleyecek olduğunda, bu dört öğe arasında en son kullanılan 3 öğeyi arar , ana belleğe boşaltır ve yeni öğeyi yerine yükler.

Sorun muhtemelen oldukça açıktır: doğrudan eşlenen bir önbellek için, aynı önbellek konumuna eşlenen iki işlenen kötü davranışa neden olabilir. N yollu küme ilişkilendirmeli önbellek, sayıyı 2'den N + 1'e yükseltir. Bir önbelleği daha "yollara" düzenlemek, fazladan devre gerektirir ve genellikle daha yavaş çalışır, bu nedenle (örneğin) 8192 yollu set ilişkilendirilebilir önbellek de nadiren iyi bir çözümdür.

Sonuçta, bu faktörün taşınabilir kodda kontrol edilmesi daha zordur. Verilerinizin yerleştirildiği yer üzerindeki kontrolünüz genellikle oldukça sınırlıdır. Daha da kötüsü, adresden önbelleğe tam eşleme, aksi takdirde benzer işlemciler arasında değişir. Bununla birlikte, bazı durumlarda, büyük bir arabellek ayırmak ve daha sonra aynı önbellek satırlarını paylaşan veriye karşı veri sağlamak için ayırdığınız şeylerin bir kısmını kullanmak gibi şeyler yapmaya değer olabilir (muhtemelen tam işlemciyi ve buna göre hareket edin).

  • Yanlış Paylaşım

"Yanlış paylaşım" adı verilen başka bir ilgili öğe daha var. Bu, iki (veya daha fazla) işlemcinin / çekirdeğin ayrı verileri olan, ancak aynı önbellek satırına düştüğü çok işlemcili veya çok çekirdekli bir sistemde ortaya çıkar. Bu, iki işlemciyi / çekirdeği, her biri ayrı bir veri öğesine sahip olsa bile verilere erişimini koordine etmeye zorlar. Özellikle ikisi dönüşümlü olarak verileri değiştirirse, verilerin işlemciler arasında sürekli olarak kapatılması gerektiğinden bu büyük bir yavaşlamaya neden olabilir. Bu, önbelleği daha "yollara" veya benzeri bir şeye organize ederek kolayca iyileştirilemez. Bunu önlemenin birincil yolu, iki iş parçacığının aynı önbellek satırında olabilecek verileri nadiren (tercihen hiçbir zaman) değiştirmemesini sağlamaktır (verilerin tahsis edildiği adresleri kontrol etme zorluğu konusunda aynı uyarılarla).


  1. C ++ bilenler bunun ifade şablonları gibi bir optimizasyona açık olup olmadığını merak edebilirler. Eminim cevap evet, yapılabilirdi ve eğer olsaydı, muhtemelen oldukça önemli bir kazanç olurdu. Bununla birlikte, bunu yapan kimsenin farkında değilim ve ne kadar az valarraykullanıldığına bakılırsa, en azından birinin bunu yaptığını görünce en azından biraz şaşırırdım.

  2. Herkesin valarray(özellikle performans için tasarlanan) bu kadar yanlış olabileceğini merak etmesi durumunda, tek bir şey söz konusudur : gerçekten eski Crays gibi hızlı ana bellek kullanan ve önbellek kullanmayan makineler için tasarlanmıştır. Onlar için, bu neredeyse ideal bir tasarımdı.

  3. Evet, basitleştiriyorum: çoğu önbellek, en son kullanılan öğeyi tam olarak ölçmez, ancak her erişim için tam bir zaman damgası tutmak zorunda kalmadan, buna yakın olması amaçlanan bir buluşsal yöntem kullanırlar.


1
Cevabınızdaki ekstra bilgileri, özellikle valarrayörneği seviyorum .
Marc Claesen

1
+1 Sonunda: küme ilişkisinin basit bir açıklaması! Daha fazla DÜZENLE: Bu, SO hakkında en bilgilendirici cevaplardan biridir. Teşekkür ederim.
Mühendis

32

Veri Odaklı Tasarım dünyasına hoş geldiniz. Temel mantra, virtualdaha iyi konum için tüm adımlar sıralamak, şubeleri ortadan kaldırmak, toplu, çağrıları ortadan kaldırmaktır .

Soruyu C ++ ile etiketlediğinizden, burada zorunlu olan tipik C ++ Bullshit var . Tony Albrecht'in Nesneye Yönelik Programlamanın Tuzakları da konuya büyük bir giriş niteliğindedir.


1
parti ile ne demek istiyorsun, kimse anlamayabilir.
0x90

5
Toplu işlem: iş birimini tek bir nesne üzerinde gerçekleştirmek yerine, bir nesne kümesinde gerçekleştirin.
arul

AKA engelleme, kayıtları engelleme, önbellek engelleme.
0x90

1
Engelleme / Engellememe genellikle nesnelerin eşzamanlı bir ortamda nasıl davrandığını ifade eder.
arul

2
toplu iş == vectorization
Amro

23

Sadece kazık: önbellek dostu kod karşı önbellek dostu kod klasik bir örnek matris çarpma "önbellek engelleme" dir.

Saf matris çarpımı şöyle görünür:

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];
      }
   }
}

Eğer Nvarsa, örneğin büyük N * sizeof(elemType)önbellek boyutu büyükse, o zaman her erişim için src2[k][j]bir önbellek bayan olacak.

Bunu bir önbellek için optimize etmenin birçok farklı yolu vardır. Çok basit bir örnek: iç döngüdeki önbellek satırı başına bir öğe okumak yerine, tüm öğeleri kullanın:

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];
         }
      }
   }
}

Önbellek satırı boyutu 64 baytsa ve 32 bit (4 bayt) kayan öğe üzerinde çalışıyorsa, önbellek satırı başına 16 öğe vardır. Ve sadece bu basit dönüşümle önbellek kaçırma sayısı yaklaşık 16 kat azalır.

Daha zengin dönüşümler 2D döşemelerde çalışır, birden çok önbellek (L1, L2, TLB) için optimize edilir.

Google önbellek engellemesinin bazı sonuçları:

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

Optimize edilmiş önbellek engelleme algoritmasının güzel bir video animasyonu.

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

Döngü döşeme çok yakından ilişkilidir:

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


7
Bunu okuyan insanlar , "önbellek dostu" ikj algoritmasını ve düşmanca ijk algoritmasını iki 2000x2000 matrisi çarparak test ettiğim matris çarpımı hakkındaki makalemle de ilgilenebilirler.
Martin Thoma

3
k==;Bunun bir yazım hatası olduğunu umuyorum?
TrebledJ

13

İşlemciler bugün birçok basamaklı bellek alanı ile çalışmaktadır. Yani CPU, CPU yongasının kendisinde bulunan bir grup belleğe sahip olacak. Bu belleğe çok hızlı erişimi var. CPU'da olmayan ve erişilmesi nispeten daha yavaş olan sistem belleğine ulaşıncaya kadar, her biri diğerinden daha yavaş (ve daha büyük) farklı önbellek seviyeleri vardır.

Mantıksal olarak, CPU'nun talimat setine sadece dev bir sanal adres alanındaki bellek adreslerine başvurursunuz. Tek bir bellek adresine eriştiğinizde CPU onu getirecektir. eski günlerde sadece o tek adresi getirirdi. Ancak bugün CPU, istediğiniz bitin etrafında bir sürü bellek getirecek ve önbelleğe kopyalayacaktır. Belirli bir adresi talep ederseniz, yakında çok yakında bir adres isteyeceğinizi varsayar. Örneğin, bir arabelleği kopyalıyorsanız, birbiri ardına, birbirini takip eden adreslerden okur ve yazarsınız.

Bu yüzden bugün bir adres aldığınızda, bu adresi önbelleğe zaten okuyup okumadığını görmek için ilk önbellek seviyesini kontrol eder, bulamazsa, bu bir önbellek özledim ve bir sonraki seviyeye çıkması gerekir. bulmak için önbellek, sonunda ana belleğe çıkmak zorunda kalana kadar.

Önbellek dostu kod, erişimleri bellekte birbirine yakın tutmaya çalışır, böylece önbellek hatalarını en aza indirirsiniz.

Yani bir örnek dev 2 boyutlu bir tabloyu kopyalamak istediğinizi düşünebiliriz. Hafızada art arda erişim satırı ile düzenlenir ve bir satır hemen sonrakini takip eder.

Elemanları soldan sağa her seferinde bir satır kopyalarsanız, bu önbellek dostudur. Tabloyu bir kerede bir sütun kopyalamaya karar verdiyseniz, aynı miktarda belleği kopyalarsınız - ancak düşmanca önbellek olur.


4

Sadece verilerin önbellek dostu olması gerektiği değil, kod için de önemli olduğu açıklığa kavuşturulmalıdır. Bu, şube tahmini, talimatların yeniden sıralanması, gerçek bölümlerden ve diğer tekniklerden kaçınmaya ek olarak yapılır.

Tipik olarak kod ne kadar yoğun olursa, saklamak için daha az önbellek satırı gerekecektir. Bu, veriler için daha fazla önbellek satırının bulunmasına neden olur.

Kod, tipik olarak kendi başına bir veya daha fazla önbellek satırı gerektireceğinden, her yerde işlevleri çağırmamalıdır; bu da veri için daha az önbellek satırına neden olur.

Bir işlev, önbellek hizalama dostu bir adreste başlamalıdır. Bunun için (gcc) derleyici anahtarları olmasına rağmen, işlevler çok kısasa, her birinin tüm bir önbellek hattını işgal etmesinin savurgan olabileceğini unutmayın. Örneğin, en sık kullanılan işlevlerden üçü bir 64 bayt önbellek satırına sığarsa, bu, her birinin kendi satırına sahip olduğundan daha az boşa gider ve diğer kullanım için daha az kullanılabilir iki önbellek satırıyla sonuçlanır. Tipik bir hizalama değeri 32 veya 16 olabilir.

Bu yüzden kodu yoğunlaştırmak için biraz zaman ayırın. Farklı yapıları test edin, oluşturulan kod boyutunu ve profilini derleyin ve gözden geçirin.


2

@Marc Claesen'in önbellek dostu kod yazmanın yollarından birinin, verilerimizin depolandığı yapıdan faydalanmak olduğunu belirttiği gibi. Buna ek olarak, önbellek dostu kod yazmanın başka bir yolu: verilerinizin saklanma şeklini değiştirmek; ardından bu yeni yapıda depolanan verilere erişmek için yeni kod yazın.

Bu, veritabanı sistemlerinin bir tablonun örneklerini nasıl doğrusallaştırdığı ve sakladığı konusunda mantıklıdır. Bir tablonun tuples'ını depolamanın iki temel yolu vardır; yani satır deposu ve sütun deposu. Satır deposunda adından da anlaşılacağı gibi tuples satır akıllıca saklanır. Diyelim ki Productsaklanan adlı bir tablonun 3 özniteliği vardır int32_t key, char name[56]ve int32_t pricebu nedenle bir demetin toplam boyutu 64bayttır.

ProductN boyutunda bir dizi yapı oluşturarak ana bellekte çok temel bir satır deposu sorgu yürütme benzetimi yapabiliriz ; burada N tablodaki satır sayısıdır. Bu tür bellek düzenine yapı dizisi de denir. Yani Ürünün yapısı şöyle olabilir:

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. */

Benzer şekilde, Producttablonun her bir özniteliği için bir dizi N boyutunda 3 dizi oluşturarak ana bellekte çok temel bir sütun deposu sorgu yürütmesini simüle edebiliriz . Bu tür bellek yerleşimine dizilerin yapısı da denir. Dolayısıyla, Ürünün her bir özelliği için 3 dizi şöyle olabilir:

/* 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. */

Şimdi hem yapı dizisini (Satır Düzeni) hem de 3 ayrı diziyi (Sütun Düzeni) yükledikten sonra, Producthafızamızda bulunan masamıza satır deposu ve sütun deposu var .

Şimdi önbellek dostu kod bölümüne geçiyoruz. Tablomuzdaki iş yükünün, fiyat özelliğinde bir toplama sorgusuna sahip olacağımızı varsayalım. Gibi

SELECT SUM(price)
FROM PRODUCT

Satır deposu için yukarıdaki SQL sorgusunu

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

Sütun deposu için yukarıdaki SQL sorgusunu

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

Sütun deposu kodu, yalnızca bir öznitelik alt kümesi gerektirdiğinden ve sütun düzeninde, yalnızca fiyat sütununa erişirken bunu yaptığımız için bu sorgudaki satır düzeni kodundan daha hızlı olacaktır.

Önbellek satır boyutunun 64bayt olduğunu varsayalım .

Bir önbellek satırı okunduğunda satır düzeni durumunda, sadece 1 ( cacheline_size/product_struct_size = 64/64 = 1) tupleın fiyat değeri okunur, çünkü 64 bayt yapı boyutumuz ve tüm önbellek satırımızı doldurur, bu nedenle her tuple için bir önbellek kaçırma durumunda oluşur satır düzeni.

Bir önbellek satırı okunduğunda sütun düzeni durumunda, 16 ( cacheline_size/price_int_size = 64/4 = 16) tuples fiyat değeri okunur, çünkü bellekte depolanan 16 bitişik fiyat değeri önbelleğe getirilir, bu nedenle her on altıncı kümede bir önbellek sütun düzeni.

Bu nedenle, belirli bir sorgu durumunda sütun düzeni daha hızlı olur ve tablonun bir sütun alt kümesindeki bu tür toplama sorgularında daha hızlı olur. TPC-H kıyaslamasındaki verileri kullanarak bu denemeyi kendiniz deneyebilir ve her iki mizanpaj için çalışma sürelerini karşılaştırabilirsiniz. Wikipedia sütun yönelik veri tabanı sistemleri üzerinde makale de iyidir.

Dolayısıyla veritabanı sistemlerinde, sorgu iş yükü önceden biliniyorsa, verilerimizi iş yükündeki sorgulara uyacak düzenlerde saklayabilir ve bu düzenlerden verilere erişebiliriz. Yukarıdaki örnekte bir sütun düzeni oluşturduk ve kodumuzu hesaplamak için önbellek dostu olacak şekilde değiştirdik.


1

Önbelleklerin yalnızca sürekli belleği önbelleğe almadığını unutmayın. Birden fazla satıra (en az 4) sahiptir, bu nedenle hoşnutsuzluk ve üst üste binen bellek genellikle aynı derecede verimli bir şekilde saklanabilir.

Yukarıdaki örneklerin tamamında eksik olan ölçülen ölçütlerdir. Performansla ilgili birçok efsane var. Ölçmedikçe bilmiyorsun. Ölçülen bir iyileşme olmadıkça kodunuzu karmaşıklaştırmayın .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.