Ders 09 / 12
Önbellek Dostu Veri Düzeni
Önbellek satırı, uzamsal ve zamansal yerellik, yapı dizisi ile dizi yapısı, satır ve sütun sırası, sahte paylaşım ve bir veri düzeninin kaç bitişik olmayan geçiş bıraktığının sayılması.
Yayın Güncelleme
Bu derste
- Önbellek satırının ve yerelliğin bir döngünün erişim maliyetini nasıl belirlediğini açıklar.
- Yapı dizisi ile dizi yapısı düzenlerini aynı veri için kurar ve karşılaştırır.
- Bir erişim örüntüsünde bitişik olmayan geçişleri işaretçi farkıyla sayar.
- Bir veri düzeninin hangi erişim örüntüsü için uygun olduğunu söyler.
İçindekiler
Önceki iki ders eşzamanlılığın bedelini ölçtü: kilit kaç kez alınıyor, bir yayın kaç eşgüdüm noktası istiyor. Bu ders tek bir iş parçacığına dönüyor, çünkü bellekte verinin nasıl dizildiği eşzamanlılık olmadan da bedeli değiştirir. İşlemci belleği tek tek bayt olarak değil, önbellek satırı denen sabit boyutlu bloklar halinde taşır. Bir değere dokunduğunuzda onunla birlikte çevresindeki komşu baytlar da yakın belleğe gelir. Komşularının bir kısmına yakında dokunacaksanız bu iyi bir takastır; dokunmayacaksanız taşınan baytların çoğu boşa gitmiştir. Kursun ölçtüğü soru yine aynı: düzenek bedeli nereye taşıyor? Bu dersin cevabı bellek sınıfıdır. Satır boyunu ve adresleri basmıyoruz, çünkü ikisi de makineye bağlıdır; ölçü, bir döngünün art arda dokunduğu iki değerin bellekte yan yana olup olmadığını sayar.
Yerellik
İki tür yerellik vardır. Uzamsal yerellik, bir değere dokunan programın yakında onun komşusuna da dokunacağı beklentisidir; dizinin elemanlarını sırayla gezmek bunun örneğidir. Zamansal yerellik, bir değere dokunan programın yakında aynı değere yeniden dokunacağı beklentisidir; bir döngü değişkeni ya da bir toplam bunun örneğidir. Önbellek satırı, uzamsal yerelliği ödüllendirir: satırda yan yana duran değerler tek bir taşımayla gelir.
Çok boyutlu dizilerde aynı fikir satır ve sütun sırası olarak görünür. C++’ta
int tablo[satir][sutun] satır sırasındadır: aynı satırın elemanları bitişiktir.
Satırlar boyunca gezen iç döngü uzamsal yerelliğe uyar; sütunlar boyunca gezen iç döngü
her adımda bir satır boyu atlar. Bu ayrımı aşağıda geçiş sayımıyla gösteriyoruz.
Yapı Dizisi ve Dizi Yapısı
Bir nokta kümesi üç alanla x, y ve z tutulur. İlk düzen yapı dizisidir (AoS):
her nokta bir yapıdır ve bütün noktalar bir dizide durur. Bellekte sıra
x0 y0 z0 x1 y1 z1 ... olur. İkinci düzen dizi yapısıdır (SoA): üç alanın her biri
kendi dizisinde durur. Bellekte sıra x0 x1 x2 ... y0 y1 y2 ... z0 z1 z2 ... olur.
Yalnız x alanını dolaşan bir döngü, yapı dizisinde her adımda diğer iki alanı da
atlar; bitişik olan x değerleri değil, aralarında y ve z olan değerlerdir. Dizi
yapısında x değerleri yan yanadır. Aşağıdaki başlık iki düzeni ve ölçümü kurar. Ölçü,
bir döngünün dokunduğu adresleri sırayla kaydeder ve art arda iki dokunuşun arasındaki
mesafenin tam bir int kadar olup olmadığına bakar. Adres basılmaz; yalnızca sayı çıkar.
// dizilim.hpp — iki veri duzeni ve dokunuslari kaydeden olcum
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
constexpr int NOKTA_SAYISI = 4;
struct Nokta {
int x, y, z;
};
struct NoktaDizisi {
std::array<Nokta, NOKTA_SAYISI> noktalar;
};
struct AlanDizileri {
std::array<int, NOKTA_SAYISI> x, y, z;
};
inline NoktaDizisi yapi_dizisi_kur() {
NoktaDizisi d;
for (int i = 0; i < NOKTA_SAYISI; ++i) d.noktalar[i] = Nokta{i, 10 + i, 20 + i};
return d;
}
inline AlanDizileri dizi_yapisi_kur() {
AlanDizileri d;
for (int i = 0; i < NOKTA_SAYISI; ++i) {
d.x[i] = i;
d.y[i] = 10 + i;
d.z[i] = 20 + i;
}
return d;
}
inline bool bitisik(const int* once, const int* sonra) {
auto a = reinterpret_cast<std::uintptr_t>(once);
auto b = reinterpret_cast<std::uintptr_t>(sonra);
return b - a == sizeof(int);
}
struct Gecisler {
int atlanan = 0;
int toplam = 0;
};
inline Gecisler say(const std::vector<const int*>& dokunuslar) {
Gecisler g;
for (std::size_t i = 0; i + 1 < dokunuslar.size(); ++i) {
++g.toplam;
if (!bitisik(dokunuslar[i], dokunuslar[i + 1])) ++g.atlanan;
}
return g;
}
Bitişiklik testi işaretçileri tam sayıya çevirip farkı sizeof(int) ile karşılaştırır.
İki ayrı dizinin işaretçilerini çıkarmak tanımsız olurdu; tam sayıya çevirmek bu yüzden
gereklidir. atlanan bitişik olmayan geçişleri, toplam bütün geçişleri sayar.
Satır Sırası ve Sütun Sırası: Geçiş Sayımı
Aynı fikri tek bir düz dizi üzerinde sayıya dökelim. Dört satır ve dört sütunluk bir
tablo, on altı elemanlı tek bir dizide satır sırasıyla durur: m[r * 4 + c] ifadesi
r. satırın c. elemanıdır. İki iç içe döngüyü ele alalım. Birincisinde dış döngü
satırlar, iç döngü sütunlar üzerinde döner; ardışık dokunuşlar dizide yan yanadır.
İkincisinde dış döngü sütunlar, iç döngü satırlar üzerinde döner; ardışık dokunuşlar
her adımda bir satır boyu, yani dört int atlar, sütun değişirken de geriye döner.
Dikkat edilecek nokta, on altı dokunuşun on beş geçiş ürettiğidir: geçiş, art arda
iki dokunuş arasındaki adımdır. Satır sırasında on beş geçişin hiçbiri atlamaz. Sütun
sırasında on beşinin hepsi atlar, çünkü ardışık iki dokunuş ya dört int ileridedir ya
da geridedir. Geriye atlayan geçiş de bitişik sayılmaz: bitişiklik testi farkın tam bir
int olmasını ister.
// matris.hpp — dort satir dort sutunluk duz dizide iki dolasma sirasi
#pragma once
#include <array>
#include "dizilim.hpp"
constexpr int SATIR = 4;
constexpr int SUTUN = 4;
inline Gecisler satir_sirasi_say(const std::array<int, SATIR * SUTUN>& m) {
std::vector<const int*> dokunuslar;
for (int r = 0; r < SATIR; ++r)
for (int c = 0; c < SUTUN; ++c) dokunuslar.push_back(&m[r * SUTUN + c]);
return say(dokunuslar);
}
inline Gecisler sutun_sirasi_say(const std::array<int, SATIR * SUTUN>& m) {
std::vector<const int*> dokunuslar;
for (int c = 0; c < SUTUN; ++c)
for (int r = 0; r < SATIR; ++r) dokunuslar.push_back(&m[r * SUTUN + c]);
return say(dokunuslar);
}
Bu, bir önceki bölümdeki aynı say işlevidir; yeni bir ölçü yok, yalnızca iki yeni
erişim örüntüsü var. Gerçek bir tabloda bu fark, her atlayan geçişin başka bir önbellek
satırını getirme ihtimaline dönüşür. Tablonun bir satırı önbellek satırından uzunsa
atlayan her geçiş başka bir önbellek satırına düşer, bu yüzden büyük tablolarda döngü sırasını verinin dizilişine uydurmak ilk bakılacak yerdir.
Sahte Paylaşım ve alignas
Veri düzeni yalnızca tek iş parçacığını ilgilendirmez. İki iş parçacığı birbirinden bağımsız iki değişkene yazıyorsa ama bu iki değişken aynı önbellek satırına düşmüşse, bir iş parçacığının yazısı öbürünün satırını geçersiz kılar ve iki iş parçacığı birbirinin önbelleğini sürekli boşaltır. Buna sahte paylaşım denir: değişkenler paylaşılmıyor, ama satır paylaşılıyor. Kod doğrudur, veri yarışı yoktur; yalnızca her yazı satırın sahipliğini el değiştirmeye zorlar. Bu, kilitlerin ya da atomiklerin dersindeki bekleme noktalarından farklı bir bedeldir: kodda hiçbir eşgüdüm görünmez, bedel düzenden gelir.
Çözüm, iş parçacıklarının yazdığı alanları farklı satırlara ayırmaktır. C++’ta bunu
alignas belirteci yapar: bir tipe alignas(N) verilirse o tipin nesnelerinin adresi N’in katı olur
ve tipin boyutu da N’in katına yuvarlanır. İki ardışık Hizali elemanı bu yüzden en
az N bayt arayla durur.
Burada 64 sayısı bir varsayımdır: yaygın işlemcilerde önbellek satırı bu boydadır,
ama dilin söylediği bir sabit değildir ve her donanımda doğru olmayabilir. Bu yüzden
sayıyı adlandırıp tek yerde tutuyoruz ve ölçüm, gerçek satırı değil, o varsayımı
kullanıyor. Ayrıca aşağıdaki static_assert, hizalamanın gerçekten istenen değerde
olduğunu derleme sırasında doğrular. Sayım, ardışık iki elemanın adresleri arasındaki
farkın varsayılan satır boyundan küçük olup olmadığına bakar; küçükse ikisi aynı satıra
düşme adayıdır.
// paylasim.hpp — paketli ve hizali sayaclar, ayni satira dusme adayi sayimi
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <thread>
#include <vector>
constexpr std::size_t SATIR_VARSAYIM = 64; // varsayim: yaygin islemci onbellek satiri
struct Paketli {
long deger = 0;
};
struct alignas(SATIR_VARSAYIM) Hizali {
long deger = 0;
};
static_assert(alignof(Hizali) == SATIR_VARSAYIM, "hizalama istenen degerde degil");
static_assert(sizeof(Hizali) % SATIR_VARSAYIM == 0, "boyut satira yuvarlanmadi");
template <typename T>
int ayni_satir_adayi(const std::array<T, 4>& dizi) {
int aday = 0;
for (std::size_t i = 0; i + 1 < dizi.size(); ++i) {
auto a = reinterpret_cast<std::uintptr_t>(&dizi[i]);
auto b = reinterpret_cast<std::uintptr_t>(&dizi[i + 1]);
if (b - a < SATIR_VARSAYIM) ++aday;
}
return aday;
}
template <typename T>
long dort_yazici(std::array<T, 4>& dizi) {
std::vector<std::thread> isler;
for (int t = 0; t < 4; ++t) {
isler.emplace_back([&dizi, t] {
for (int k = 0; k < 10000; ++k) ++dizi[t].deger;
});
}
for (auto& is : isler) is.join();
long toplam = 0;
for (const auto& e : dizi) toplam += e.deger;
return toplam;
}
Her iş parçacığı yalnızca kendi elemanına yazar; bu yüzden iki sürüm de doğrudur ve aynı
toplamı verir. ayni_satir_adayi bu platformda sekiz baytlık olan long elemanlar için
üç çiftin üçünü, altmış dört baytlık hizalı elemanlar için üçünün hiçbirini aday sayar. Burada sayılan şey
gerçek bir taşıma değil, düzenin yol açabileceği durumdur.
Ölçüm: Yalnız x Alanı, Tablo ve Sayaçlar
Aşağıdaki program üç ölçümü birlikte koşturur ve her birinin sayacını basar.
// dizilim_olcumu.cpp — yalniz x alani ve uc alan birlikte
#include <array>
#include <iostream>
#include <vector>
#include "dizilim.hpp"
#include "matris.hpp"
#include "paylasim.hpp"
int main() {
NoktaDizisi aos = yapi_dizisi_kur();
AlanDizileri soa = dizi_yapisi_kur();
std::vector<const int*> aos_x;
std::vector<const int*> soa_x;
int aos_toplam = 0;
int soa_toplam = 0;
for (int i = 0; i < NOKTA_SAYISI; ++i) {
aos_x.push_back(&aos.noktalar[i].x);
aos_toplam += aos.noktalar[i].x;
soa_x.push_back(&soa.x[i]);
soa_toplam += soa.x[i];
}
Gecisler a = say(aos_x);
Gecisler s = say(soa_x);
std::cout << "yalniz x alani bellek aos=" << a.atlanan << "/" << a.toplam
<< " soa=" << s.atlanan << "/" << s.toplam << "\n";
std::cout << "toplam x aos=" << aos_toplam << " soa=" << soa_toplam << "\n";
std::array<int, SATIR * SUTUN> tablo{};
for (int i = 0; i < SATIR * SUTUN; ++i) tablo[i] = i;
Gecisler satir = satir_sirasi_say(tablo);
Gecisler sutun = sutun_sirasi_say(tablo);
std::cout << "satir ve sutun sirasi bellek satir=" << satir.atlanan << "/"
<< satir.toplam << " sutun=" << sutun.atlanan << "/" << sutun.toplam << "\n";
std::array<Paketli, 4> paketli{};
std::array<Hizali, 4> hizali{};
std::cout << "sahte paylasim bellek paketli=" << ayni_satir_adayi(paketli) << "/3"
<< " hizali=" << ayni_satir_adayi(hizali) << "/3\n";
long paketli_toplam = dort_yazici(paketli);
long hizali_toplam = dort_yazici(hizali);
std::cout << "sahte paylasim sonuc paketli=" << paketli_toplam
<< " hizali=" << hizali_toplam << "\n";
std::vector<const int*> aos_uc;
std::vector<const int*> soa_uc;
for (int i = 0; i < NOKTA_SAYISI; ++i) {
aos_uc.push_back(&aos.noktalar[i].x);
aos_uc.push_back(&aos.noktalar[i].y);
aos_uc.push_back(&aos.noktalar[i].z);
soa_uc.push_back(&soa.x[i]);
soa_uc.push_back(&soa.y[i]);
soa_uc.push_back(&soa.z[i]);
}
Gecisler au = say(aos_uc);
Gecisler su = say(soa_uc);
std::cout << "uc alan birlikte bellek aos=" << au.atlanan << "/" << au.toplam
<< " soa=" << su.atlanan << "/" << su.toplam << "\n";
return 0;
}
yalniz x alani bellek aos=3/3 soa=0/3 toplam x aos=6 soa=6 satir ve sutun sirasi bellek satir=0/15 sutun=15/15 sahte paylasim bellek paketli=3/3 hizali=0/3 sahte paylasim sonuc paketli=40000 hizali=40000 uc alan birlikte bellek aos=0/11 soa=11/11
EB13. İlk satır yalnız x alanını dolaşan döngünün iki düzende dört elemanda üç
geçiş yaptığını, yapı dizisinde üçünün de bitişik olmadığını, dizi yapısında hiçbirinin
atlamadığını gösterir: aos=3/3, soa=0/3. İkinci satır iki düzenin aynı toplamı
verdiğini doğrular, yani fark sonuçta değil dokunuşların bellekteki konumundadır.
EB14. Sayım yalnızca bitişik olmayan geçişleri veriyor, satır boyunu vermiyor: dört
noktalık küçük bir küme tek bir önbellek satırına sığabilir ve atlanan her geçiş yeni
bir satır taşımasına yol açmayabilir. Bu yüzden 3/3 bir taşıma sayısı değil, düzenin
bitişikliği bozma yönüdür; gerçek taşıma sayısı küme büyüdükçe anlam kazanır ve
makineye bağlıdır.
EB15. Üçüncü satır, dört satır dört sütunluk tabloda satır sırasıyla dolaşmanın on
beş geçişin hiçbirini atlatmadığını (satir=0/15), sütun sırasıyla dolaşmanın ise
hepsini atlattığını (sutun=15/15) gösterir. İki döngü aynı on altı elemana dokundu,
yalnızca sıra farklıydı. Sütun sırasında geriye dönen geçişler de sayıya girdi, çünkü
bitişiklik testi yalnızca ileri doğru tam bir int adımı kabul eder.
EB16. Dördüncü satırda bu platformda sekiz baytlık olan long elemanlardan oluşan paketli dizi, üç
ardışık çiftin üçünü de aynı satıra düşme adayı yaptı (paketli=3/3); alignas
verilen dizi hiçbirini yapmadı (hizali=0/3). Bu sayı gerçek bir önbellek satırı
ölçümü değildir, SATIR_VARSAYIM varsayımına dayanır. Varsayım yanlışsa sayım da
yanlış olur; bu yüzden değeri adlandırdık ve static_assert ile hizalamanın istenen
değerde olduğunu doğruladık.
EB17. Beşinci satırda iki sürüm de 40000 verdi. Her iş parçacığı yalnızca kendi
elemanına yazdığı için veri yarışı yoktu ve yerleşim sonucu değiştirmedi. alignas
doğruluğu değiştirmez, bedelin nerede ödendiğini değiştirir; bu değişiklik
çıktıda görünmez ve bu derste bilinçli olarak ölçülmeyen kısımdır.
Sınırlayıcı Ölçüm
Bu dersin ölçtüğü düzen, yalnız bir alana dokunan döngüde dizi yapısının üstünlüğüdür.
Son satır aynı iki düzeni bu kez üç alanı birlikte okuyan bir döngüyle ölçer: her
nokta için x, y, z sırayla okunur. Burada tablo tersine döner: yapı dizisinde bir
noktanın üç alanı yan yanadır ve bir sonraki noktanın alanları da öyle, yani on bir
geçişin hiçbiri atlanmaz: aos=0/11. Dizi yapısında her okuma farklı bir diziye
atlar: soa=11/11.
Düzenin bitişikliğini veri belirlemiyor, erişim örüntüsü belirliyor. Aynı veri bir örüntüde en iyi, bir başkasında en kötü düzende durabilir. Bu yüzden “dizi yapısı daha hızlıdır” ya da “yapı dizisi daha doğaldır” diye bir kural yoktur. Soru, döngünün hangi alanlara birlikte dokunduğudur; alanlar birlikte okunuyorsa yapı dizisi, ayrı ayrı okunuyorsa dizi yapısı bitişikliği korur.
Özet
- Bu dersin ölçtüğü düzenek bedeli bellek sınıfına taşır: bir veri düzeni, bir döngünün art arda dokunduğu değerlerin ne kadarını bitişik bıraktığını belirler; ölçü süre değil, bitişik olmayan geçiş sayısıdır.
- Önbellek satırı belleği bloklar halinde taşır; uzamsal yerellik komşuya, zamansal yerellik aynı değere yeniden dokunmayı ödüllendirir.
- Yapı dizisi alanları birlikte, dizi yapısı alanları ayrı ayrı bitişik tutar; hangisinin uygun olduğunu erişim örüntüsü belirler.
yalniz x alaniyapı dizisindeaos=3/3, dizi yapısındasoa=0/3bıraktı;uc alan birlikteaynı iki düzendeaos=0/11vesoa=11/11bıraktı.- Satır sırasıyla dolaşmak
satir=0/15, sütun sırasıyla dolaşmaksutun=15/15atlama bıraktı; sıra, verinin dizilişine uymalıdır. - Sahte paylaşım, bağımsız değişkenlerin aynı satıra düşmesidir; çözümü iş parçacığı
başına satır ayırmaktır,
alignasile; satır boyu burada bir varsayımdır.
Sonraki Adım
Bu derste hangi düzenin hangi örüntüye uyduğunu bildiğimizi varsaydık, ama gerçek bir programda hangi döngünün sıcak olduğunu bilmeden hangi düzene dokunacağımızı bilemeyiz. Bir sonraki ders profillemeyi açıyor: bir sayaç sıcak noktayı nerede buluyor sorusuyla araçlı bir profilleyici kuracağız.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.