Performans Düşmanı: False Sharing
Performans Düşmanı: False Sharing
Görsel kaynağı: https://medium.com/java-performance/performance-is-the-enemy-of-clean-code-fdd65c4c7d99
Selamlar herkese. Değerli üstadım Türkay Ürkmez’in Generic Constraintlerle alakalı bir LinkedIn paylaşımını gördüm. Burada farklı bir açıdan performansa katkı olacak bir konu hakkında yorumda bulundum O sebeple böyle bir yazı yazmak istedim. Kısacası, konu başlıktan da anlaşılacağı üzere False Sharing.
False Sharing Nedir?
False sharing, aynı CPU cache line’ında yer alan birbirinden bağımsız değişkenlere, birden fazla thread’in erişmeye çalışması durumudur. Bu işlem de performans düşüşüne yol açar. Cache line derken bunun ne olduğuna da değinmemiz gerekiyor.
Cache Line: CPU cache’lerinin bellekten veri çekerken kullandığı en küçük veri bloklarına verilen isimdir. 64 ya da 128 byte boyuta sahiptirler. CPU, veriyi bellekten tek tek okumak yerine, tüm cache line’ı çeker.
Biraz daha açıklamak gerekirse, eğer farklı thread’ler aynı cache line içinde yer alan farklı değişkenlere sıkça yazma işlemi yaparsa, CPU’lar sürekli olarak cache’in güncelliğini sağlamak için cache coherence protocol kullanarak senkronizasyonu sağlamaya çalışırlar. Bu yüzden de gereksiz bellek erişimi ve CPU yükü anlamına gelir.
Şimdi problemin olduğu noktaya gelelim. Örneğin long türünde bir değişkenimiz var bu da 8 byte demektir. Yukarıdaki bilgiye göre deCPU verileri byte byte değil de cache line’ı alarak çeker. Bu da 64–8 byte’lık bir alanın daha cache line’da gelebileceğini söyler. Bu nedenle 8'den sonra gelen diğer byte’lar da o cache line içerisinde yer alır. Ne zamana kadar? 64 ya da bazı mimarilerde 128 olana kadar.
Bunu şöyle anlatmaya çalışalım. Şöyle 2 alanımız olsun.
- sharedData[0] → 0x00000000
- sharedData[1] → 0x00000008
Bu iki değer sırasıyla thread 1 ve thread 2 tarafından güncellensin. Örneğin, sadece sharedData[0] güncellenirse, tüm CPU çekirdeklerine cache line’da değişiklik var sinyali gönderiliyor. Bu da cache invalidation dediğimiz olayı gerçekleştiriliyor. Yani cache geçersiz oluyor. Bu yüzden de tekrar cache line’ın dolması gerekiyor. Günün sonunda bu da yazının başında belirttiğimiz bellek erişimlerinin tekrar tekrar olmasını sağlar. Yani gereksiz yere bellek erişimi ortaya çıkar.
Cache Invalidation: CPU veya bellek sisteminde bir cachelenmiş bir verinin güncelliğini yitirdiğini belirten bir mekanizmadır.
False Sharing Problemine Karşı C# ile Çözüm Üretmek
Eğer C# ile yazılım geliştiriyorsanız örneklerle nasıl False Sharing oluşturacağımızı ve bunu nasıl çözeceğimizi görelim.
False Sharing Örneği
using System.Diagnostics;
const int iterations = 10_000_000;
var threadCount = Environment.ProcessorCount;
var sharedData = new long[threadCount];
var stopwatch = Stopwatch.StartNew();
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
var threadIndex = i;
threads[i] = new Thread(() =>
{
for (var j = 0; j < iterations; j++)
{
sharedData[threadIndex]++;
}
});
}
foreach (var thread in threads) thread.Start();
foreach (var thread in threads) thread.Join();
stopwatch.Stop();
Console.WriteLine($"False Sharing ile çalışan kod için geçen zaman: {stopwatch.ElapsedMilliseconds} ms");
Çıktısı: False Sharing ile çalışan kod için geçen zaman: 986ms
Yukarıdaki koda baksetığımızda masum görünen bir sharedData[threadIndex]++ ifadesi görmekteyiz. Bu ifade, aslında bütün cache line’ı güncelleten performans düşmanı bir ifadedir.
False Sharing’in Önlendiği Örnek
using System.Diagnostics;
using System.Runtime.InteropServices;
const int iterations = 10_000_000;
var threadCount = Environment.ProcessorCount;
var sharedData = new PaddedLong[threadCount];
var stopwatch = Stopwatch.StartNew();
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
var threadIndex = i;
threads[i] = new Thread(() =>
{
for (var j = 0; j < iterations; j++)
{
sharedData[threadIndex].Value++;
}
});
}
foreach (var thread in threads) thread.Start();
foreach (var thread in threads) thread.Join();
stopwatch.Stop();
Console.WriteLine($"False sharing önlerken çalışan kod için geçen zaman: {stopwatch.ElapsedMilliseconds} ms");
[StructLayout(LayoutKind.Explicit, Size = 64)]
struct PaddedLong
{
[FieldOffset(0)]
public long Value;
}
Çıktısı: False sharing önlerken çalisan kod için geçen zaman: 244 ms
Yukarıdaki kodda, PaddedLong veri türü, 64 byte’lık bir cache line’a hizalanmış durumda (alignment). Bunu Size = 64 ile belirttik. Yani yazının başındaki 64 byte’ı hatırlayacak olursak, her PaddedLong nesnesi, kendi cache line’ına sahip demek bu. FieldOffset ise, değişkenin struct içerisindeki konumunu belirtir. Eğer StructLayout tanımında LayoutKind.Explicit bir yapı kullanmamış olsaydık, derleyici değişkenler için otomatik hizalama (auto alignment) yapacaktı. Bunu önleyerek de her nesneye kendi cache line’ına sahip olma şansı verdik yani boşluk bırakmadık diyelim :)
Yazı bu kadardı. Okuduğunuz için teşekkür ederim. Umarım sizlere faydalı olabilmiştir.