Tarihçe
Modern CPU'lar, farklı çekirdeklerin özel L1 önbelleklerinde verileri senkronize etmek için MESI gibi önbellek uyumluluğu protokolleri kullanır. Bağımsız iş parçacıkları, aynı önbellek satırında (genellikle 64 veya 128 byte) kazara bulunan birbirinden farklı bellek konumlarına yazarsa, donanım bu işlemleri sürekli olarak geçersiz kılarak ve o satırın sahipliğini transfer ederek seri hale getirir; bu olaya false sharing denir. C++17, geliştiricilere aynı anda bilgi alışverişi yapıldığı süreyi azaltmak için değişkenleri farklı satırlarda tutma olanağı sunarak mimarinin önbellek satırı genişliğini açığa çıkaran std::hardware_destructive_interference_size'i tanıttı.
Problem
alignas(std::hardware_destructive_interference_size)'in otomatik depolama süreli bir değişkene uygulanması, nesnenin başlangıç adresinin belirli bir iş parçacığının yığın çerçevesinde önbellek satırı boyutunun katları olmasını garanti eder. Ancak, bu hizalama yalnızca iş parçacığının belleğe bakış açısına özgüdür ve fiziksel önbellek satırının tek başına işgalini garanti etmez. Nesne önbellek satırından daha küçükse, aynı yığında bitişik değişkenler veya fiziksel adreslerin satır boyutunun katları tarafından farklı olan diğer iş parçacıklarının yığınlarındaki değişkenler, aynı fiziksel önbellek satırına haritalanabilir. Sonuç olarak, donanım, başka bir iş parçacığı o aynı satırda farklı bir değişkene yazdığında hâlâ uyumlu trafikle karşılaşır, bu da alignas belirlemesini izolasyon için yetersiz hale getirir.
Çözüm
False sharing'i önlemek için verilerin önbellek satırını tamamen kaplayacak şekilde doldurulması gerekir; böylece çalışma zamanı adres düzenlemelerine bakılmaksızın hiçbir başka veri fiziksel depolama alanını paylaşmaz. Bu, hem std::hardware_destructive_interference_size'a göre hizalanmış hem de boyutlandırılmış bir yapı tanımlayarak gerçekleştirilir.
#include <new> #include <cstddef> #include <atomic> struct alignas(std::hardware_destructive_interference_size) PaddedCounter { std::atomic<int> value; // Doldurma, paylaşımı önlemek için önbellek satırının geri kalanını doldurur char padding[std::hardware_destructive_interference_size - sizeof(std::atomic<int>)]; }; // Dizi, her bir öğenin ayrı bir önbellek satırında bulunmasını garanti eder PaddedCounter thread_counters[8];
Problem açıklaması
Düşük gecikme gerektiren bir piyasa verisi işlemcisi, her biri global bir std::atomic<int> stats[8] dizisinde iş parçacığına özgü bir dakika sayacı tutan sekiz iş parçacığı kullanıyordu. Her iş parçacığı, kilit kullanmadan kendi indeksini yalnızca artırıyordu, ancak profilleme, verimliliğin teorik maksimumun bir kısmında durakladığını ve CPU sayaçlarının kullanıcı modundaki hesaplamalar yerine aşırı önbellek uyumsuzluğu döngüleri gösterdiğini ortaya koydu. Araştırma, atomik tamsayıların mantıksal olarak bağımsız olmalarına rağmen tek bir 64-byte'lık önbellek satırına bitişik olarak yerleştirildiğini, bu nedenle çekirdekler arasında yıkıcı bir etkileşim oluşturduğunu doğruladı.
Çözüm 1: Yerel hizalanmış değişkenler
Ekip, her iş parçacığının yürütme işlevinin içinde alignas(64) std::atomic<int> local_stat tanımlamayı ilk etapta denedi ve bir izleme iş parçacığına işaretçiler geçirdi. Bu yaklaşım, minimum yeniden yapılandırma gerektiriyordu ve global durumu önlüyordu. Ancak, derleyici başka otomatik değişkenleri ayrı local_stat'ın yanına yerleştirebileceğinden ve farklı iş parçacıklarının yığın tahsislerinin tam olarak 64 byte'lık katlar tarafından ayrılabileceğinden güvenilir olmadı; bu da hizalanmış değişkenlerin aynı fiziksel satıra işaret etmesine ve false sharing'i sürdürmesine neden oldu.
Çözüm 2: Ham işaretçilerle yığın tahsisi
Diğer bir düşünülmüş yaklaşım, her sayacı new std::atomic<int> ile tahsis etmekti, umarız yığın tahsisçisi tahsisleri uzak bellek adresleri arasında yayabilirdi. Bu, bazen rekabeti azalttı; ancak, küçük tahsislerin genellikle bitişik levhalardan alındığı için belirsiz bir performans ortaya çıkardı ve tahsisçi verileri aynı önbellek satırında yerleştirebilir. Ayrıca, bu, manuel bellek yönetimini gerektiriyordu ve hizalama veya doldurma için derleme zamanı garantileri sunmuyordu.
Seçilen çözüm ve sonuç
Son uygulama, yukarıda tanımlanan PaddedCounter yapısını benimsedi ve örnekleri bir statik dizide depoladı. Bu çözüm, derleme zamanı dolgu ve hizalama ile önbellek satırı ayırmasını belirleyici bir şekilde zorunlu kıldığı için seçildi ve çalışma zamanı bellek düzenlemesine bakılmaksızın donanım düzeyindeki rekabeti ortadan kaldırdı. Bellek tüketimi 32 byte'tan 512 byte'a yükseldi, bu da performans kazanımı açısından kabul edilebilirdi. Sonuç olarak, verimlilikte on iki kat artış ve gecikme varyansında bir azalma ile alt mikro saniyelik işleme gereksinimleri karşılandı.
Neden alignas(std::hardware_destructive_interference_size) küçük bir nesneye uygulandığında diğer verilerle false sharing'i önleyemez?
alignas yalnızca nesnenin başlangıç adresinin hizalamasını kontrol eder, uzantısını değil. Nesne, önbellek satırından daha küçükse (örneğin, 64-byte'lık satırda 4 byte'lık bir tamsayı), o önbellek satırının geri kalan byte'ları diğer değişkenleri tutabilir. Derleyici başka bir değişkeni aynı satıra yerleştirirse veya farklı bir iş parçacığının değişkeni fiziksel satıra haritalanırsa, false sharing meydana gelir. Gerçek izolasyon, nesnenin dolgu aracılığıyla tüm satırı kaplamasını gerektirir; yalnızca başlangıçta hizalı olması yeterli değildir.
std::hardware_destructive_interference_size ve std::hardware_constructive_interference_size arasındaki ayrım nedir ve hangi durumlarda verileri ikinci istatistikle uyumlu hale getirmek performansı artırabilir?**
std::hardware_destructive_interference_size, false sharing'den kaçınmak için gerekli minimum ayrımı ifade ederken, std::hardware_constructive_interference_size, tek bir önbellek satırında mekansal yereliteden yararlanan verilerin maksimum boyutudur. Sıklıkla erişilen ilgili alanları (örneğin, bir noktanın x, y, z koordinatları) constructive boyut içinde bir yapı içine gruplamak, bunların aynı satırda yer almasını sağlar, önbellek hit oranlarını ve ön alma etkinliğini maksimize eder; oysa destructive boyut, alakasız değişkenleri ayırmak için kullanılır.
False sharing, std::atomic işlemlerini memory_order_relaxed kullanırken nasıl etkiler ve neden gevşek bellek sıralaması performans düşüşünü çözmez?**
memory_order_relaxed ile aynı zamanda ve çevresindeki bellek işlemleri üzerinde hiçbir sıralama kısıtlaması getirmezken, atomik bir yazma yine de CPU çekirdeğinin önbellek satırının özel sahipliğini almasını gerektirir (Bir Okuma-Için-Sahiplik döngüsü). Eğer başka bir iş parçacığı o aynı satırda farklı bir değişkeni kısa süre önce değiştirdiyse, önbellek uyumluluğu protokolü satırı çekirdekler arasında geçiş ettirecektir. Bu donanım düzeyindeki senkronizasyon, C++ bellek modelinin mantıksal garantilerinden bağımsız olarak meydana gelir, bu da false sharing'in belirtilen bellek sıralamasından bağımsız olarak tam önbellek-kaçış gecikmesini beraberinde getirdiği anlamına gelir.