" Önbellek dostu olmayan kod " ile " önbellek dostu " kod arasındaki fark nedir ?
Önbellek verimli kod yazdığımdan nasıl emin olabilirim?
" Önbellek dostu olmayan kod " ile " önbellek dostu " kod arasındaki fark nedir ?
Önbellek verimli kod yazdığımdan nasıl emin olabilirim?
Yanıtlar:
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 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:
Uygun kullanın c ++ konteynerler
Önbellek dostu ve önbellek dostu olmayanların basit bir örneği c ++'s std::vector
karşı std::list
. A öğelerinin öğeleri std::vector
bitiş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. fortran,matlab) ile satır içi sıralama (ör. c,c ++) 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. c ++):
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. c ++):
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 c ++, virtual
yö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?
Ç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.
@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.
Önbellek kullanımını optimize etmek büyük ölçüde iki faktöre bağlıdır.
İlk faktör (başkalarının önceden bahsettiği) referans mevkisidir. Referans yerinin gerçekten iki boyutu vardır: mekan ve zaman.
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 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
. valarray
aşırı yükler en aritmetik operatörler, bu yüzden (örneğin) söyleyebiliriz a = b + c + d;
(burada a
, b
, c
ve d
tü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 n
ve işimiz biter til tekrarı'. 2
İ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" 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).
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 valarray
kullanıldığına bakılırsa, en azından birinin bunu yaptığını görünce en azından biraz şaşırırdım.
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ı.
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.
valarray
örneği seviyorum .
Veri Odaklı Tasarım dünyasına hoş geldiniz. Temel mantra, virtual
daha 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.
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 N
varsa, ö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:
k==;
Bunun bir yazım hatası olduğunu umuyorum?
İş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.
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.
@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 Product
saklanan adlı bir tablonun 3 özniteliği vardır int32_t key, char name[56]
ve int32_t price
bu nedenle bir demetin toplam boyutu 64
bayttır.
Product
N 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, Product
tablonun 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, Product
hafı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 64
bayt 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.
Ö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 .