Benim sorum şu, bu gibi durumlarda bir seferde doğrusal olarak bitişik bir diziyi yinelemediğim için, bileşenleri bu şekilde ayırmaktan elde edilen performans kazanımlarını hemen feda ediyor muyum?
Şansını söylemek gerekirse, "yatay" değişken boyutlu bir blokta bir varlığa bağlı bileşenleri bir araya getirmek yerine bileşen türü başına ayrı "dikey" dizilerle toplamda daha az önbellek özlemi elde etme şansınız vardır.
Bunun nedeni, ilk olarak "dikey" gösterimin daha az bellek kullanma eğiliminde olmasıdır. Bitişik olarak ayrılmış homojen diziler için hizalama konusunda endişelenmenize gerek yoktur. Bir bellek havuzuna ayrılmış homojen olmayan türlerle, dizideki ilk öğenin ikincisinden tamamen farklı boyut ve hizalama gereksinimleri olabileceğinden hizalama konusunda endişelenmeniz gerekir. Sonuç olarak, basit bir örnek gibi, genellikle dolgu eklemeniz gerekir:
// Assuming 8-bit chars and 64-bit doubles.
struct Foo
{
// 1 byte
char a;
// 1 byte
char b;
};
struct Bar
{
// 8 bytes
double opacity;
// 8 bytes
double radius;
};
Diyelim ki serpiştirmek Foo
ve Bar
bunları yan yana bellekte saklamak istiyoruz :
// Assuming 8-bit chars and 64-bit doubles.
struct FooBar
{
// 1 byte
char a;
// 1 byte
char b;
// 6 bytes padding for 64-bit alignment of 'opacity'
// 8 bytes
double opacity;
// 8 bytes
double radius;
};
Şimdi Foo ve Bar'ı ayrı bellek bölgelerinde saklamak için 18 bayt almak yerine, onları birleştirmek 24 bayt alır. Siparişi değiştirip değiştirmemeniz önemli değil:
// Assuming 8-bit chars and 64-bit doubles.
struct BarFoo
{
// 8 bytes
double opacity;
// 8 bytes
double radius;
// 1 byte
char a;
// 1 byte
char b;
// 6 bytes padding for 64-bit alignment of 'opacity'
};
Erişim kalıplarını önemli ölçüde iyileştirmeden sıralı erişim bağlamında daha fazla bellek alırsanız, genellikle daha fazla önbellek özlemine maruz kalırsınız. Bunun ötesinde, bir varlıktan bir sonraki artışa ve değişken bir boyuta ulaşmak için adım atarak, hangi bileşenlere sahip olduğunuzu görmek için bir varlıktan diğerine geçmek için bellekte değişken boyutlu sıçramalar yapmanız gerekir. ' yeniden ilgileniyorum.
Dolayısıyla, bileşen türlerini depolarken yaptığınız gibi "dikey" bir temsili kullanmak aslında "yatay" alternatiflerden daha uygun olabilir. Bununla birlikte, dikey temsille önbellek özledim sorunu burada örneklenebilir:
Oklar basitçe varlığın bir bileşene "sahip olduğunu" gösterir. Her ikisine de sahip varlıkların tüm hareketlerine ve oluşturma bileşenlerine erişmeye çalışacak olursak, sonunda hafızadaki her yere atlıyoruz. Bu tür düzensiz erişim deseni, örneğin bir hareket bileşenine erişmek için verileri bir önbellek satırına yüklemenizi, ardından daha fazla bileşene erişmenizi ve eski verilerin çıkarılmasını sağlayabilir; bileşen. Bu, bileşenlerin bir listesine erişmek ve erişmek için aynı bellek bölgelerini bir kereden fazla bir önbellek hattına yüklemek çok israf olabilir.
Daha net görebilmek için bu karışıklığı biraz temizleyelim:
Bu tür bir senaryo ile karşılaşırsanız, genellikle oyunun başlamasından çok sonra, birçok bileşen ve varlık eklendikten ve kaldırıldıktan sonra olduğunu unutmayın. Genel olarak oyun başladığında, tüm varlıkları ve ilgili bileşenleri bir araya getirebilirsiniz, bu noktada iyi bir mekansal konuma sahip çok düzenli, sıralı bir erişim örüntüsüne sahip olabilirler. Çok sayıda kaldırma ve eklemeden sonra, yukarıdaki karışıklık gibi bir şey elde edebilirsiniz.
Bu durumu iyileştirmenin çok kolay bir yolu, bileşenlerinizi sahip oldukları varlık kimliğine / dizinine göre basitçe sıralamaktır. Bu noktada şöyle bir şey elde edersiniz:
Ve bu çok daha önbellek dostu bir erişim modeli. Mükemmel değil, çünkü burada ve orada bazı oluşturma ve hareket bileşenlerini atlamamız gerektiğini görebiliyoruz, çünkü sistemimiz sadece ikisine birden sahip olan varlıklarla ilgileniyor ve bazı varlıkların yalnızca bir hareket bileşeni ve bazılarının yalnızca bir oluşturma bileşeni var , ancak en azından bazı bitişik bileşenleri işleyebileceksiniz (uygulamada daha çok, tipik olarak, genellikle ilgili ilgili bileşenleri ekleyeceksiniz, örneğin sisteminizde belki de bir hareket bileşenine sahip daha fazla varlık bir oluşturma bileşenine sahip olacak değil).
En önemlisi, bunları sıraladıktan sonra, verileri bir bellek bölgesini önbellek satırına yüklemezsiniz, ancak daha sonra tek bir döngüde yeniden yüklersiniz.
Ve bu son derece karmaşık bir tasarım gerektirmez, sadece arada bir doğrusal zaman yarıçapı sıralama geçişi, belki de belirli bir bileşen türü için bir grup bileşeni ekleyip çıkardıktan sonra, bu noktada sıralanması gerekiyor. Makul şekilde uygulanan bir radyus sıralaması (hatta paralelleştirebilirsiniz, ki ben bunu paralelleştirebilirsiniz), burada gösterildiği gibi dört çekirdekli i7'de yaklaşık 6 ms'de bir milyon öğe sıralayabilir:
Sorting 1000000 elements 32 times...
mt_sort_int: {0.203000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_sort: {1.248000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_radix_sort: {0.202000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
std::sort: {1.810000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
qsort: {2.777000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
Yukarıdaki, bir milyon unsuru 32 kez memcpy
sıralamaktır (sıralamadan önce ve sonra sonuçlara kadar geçen süre dahil ). Çoğu zaman aslında sıralamak için bir milyondan fazla bileşene sahip olmayacağınızı varsayıyorum, bu yüzden şimdi ve orada dikkat çekici bir kare hızı kekemesine neden olmadan bunu kolayca gizleyebilmelisiniz.