Veri Odaklı Tasarım - 1-2'den fazla yapıda “üyeler” ile pratik değil mi?


23

Veri Odaklı Tasarımın genel örneği, Top yapısı iledir:

struct Ball
{
  float Radius;
  float XYZ[3];
};

ve sonra bir std::vector<Ball>vektörü yineleyen bazı algoritmalar yaparlar .

Sonra size aynı şeyi veriyorlar, ancak Veri Odaklı Tasarımda uygulandılar:

struct Balls
{
  std::vector<float> Radiuses;
  std::vector<XYZ[3]> XYZs;
};

Hangi tüm iyi ya da eğer ilk önce tüm yarıçaplar, sonra tüm pozisyonlar ve benzeri yinelenirseniz. Ancak, topları vektör içinde nasıl hareket ettirirsiniz? Orijinal versiyonda eğer varsa, std::vector<Ball> BallsAllherhangi bir BallsAll[x]kişiye taşıyabilirsiniz BallsAll[y].

Ancak, Veri Yönelimli sürümü için bunu yapmak için, her özellik için aynı şeyi yapmanız gerekir (Ball - radius ve pozisyon durumunda 2 kez). Ancak, çok daha fazla özellik varsa, daha da kötüye gider. Her "top" için bir indeks tutmanız ve hareket ettirmeye çalıştığınızda, her özellik vektöründe hareketi yapmanız gerekir.

Bu, Veri Odaklı Tasarım'ın performans avantajını ortadan kaldırmaz mı?

Yanıtlar:


23

Başka bir cevap , sıraya dayalı depolamayı nasıl güzel bir şekilde kapladığınız ve daha iyi bir görüntü verdiğiniz konusunda mükemmel bir genel bakış sağladı. Ama aynı zamanda performans hakkında soru sorduğun için, şunu söylememe izin ver: SoA düzeni gümüş bir mermi değil . Oldukça iyi bir varsayılan (önbellek kullanımı için; çoğu dilde uygulama kolaylığı için çok fazla değil), ancak veri odaklı tasarımda bile (hepsi ne anlama gelirse) hepsi bu kadar değil. Okuduğunuz bazı tanıtımların yazarlarının bu noktayı kaçırması ve yalnızca SoA düzenini sunması olasıdır çünkü DOD'un tüm noktası budur. Yanlış olurlar ve neyse ki herkes bu tuzağa düşmez .

Muhtemelen zaten fark ettiğiniz gibi, her ilkel veri parçası kendi dizilimine dahil edilmekten fayda görmez. SoA düzeni, ayrı dizilere ayırdığınız bileşenlere genellikle ayrı olarak erişildiğinde avantaj sağlar. Ancak her küçük parçaya izolasyonla erişilmez, örneğin bir konum vektörü neredeyse her zaman toptan okunur ve güncellenir, bu nedenle doğal olarak onu ayırmazsınız. Aslında, örneğiniz de bunu yapmadı! Aynı şekilde, genellikle bir Topun tüm özelliklerine birlikte erişirseniz , zamanınızın çoğunu top koleksiyonunuzda topların yerini değiştirmek için harcadığınız için, toplarınızı ayırmanın hiçbir anlamı yoktur.

Ancak, DOD'un ikinci bir tarafı var. Tüm önbellek ve organizasyon avantajlarını yalnızca bellek düzeninizi 90 ° döndürerek ve elde edilen derleme hatalarını düzeltmek için en iyisini yaparak elde edemezsiniz. Bu bayrak altında öğretilen başka ortak numaralar da var. Örneğin "varlık tabanlı işleme": Topları sık sık devre dışı bırakırsanız ve yeniden etkinleştirirseniz, top nesnesine bir bayrak eklemeyin ve güncelleme döngüsünün bayrağı yanlış olarak ayarlanmış topları görmezden getirmesini sağlayın. Topu "aktif" bir koleksiyondan "etkin olmayan" bir koleksiyona getirin ve güncelleme döngüsünün sadece "aktif" koleksiyonu incelemesini sağlayın.

Daha da önemlisi ve örneğinizle alakalı olarak: Toplar dizisini karıştırmak için çok zaman harcıyorsanız, belki yanlış bir şey yapıyorsunuzdur. Sipariş neden önemli? Önemli değil misin? Öyleyse, birkaç avantaj elde edersiniz:

  • Koleksiyona karıştırmanız gerekmez (en hızlı kod hiçbir kod değildir).
  • Daha kolay ve verimli bir şekilde ekleyebilir ve silebilirsiniz (sonuna kadar değiştirin, en son bırakın).
  • Kalan kod daha fazla optimizasyon için uygun hale gelebilir (odaklandığınız düzen değişikliği gibi).

Yani yerine körlemesine her şeye SOA atma, düşünmek senin veriler hakkında ve nasıl işleyecek. Konumları ve hızları bir döngüde işlediğinizi bulursanız, kafeslerden geçin ve hedef noktaları güncelleyin, bellek düzeninizi bu üç bölüme ayırmayı deneyin. Konumun x, y, z bileşenlerine yalıtımlı olarak eriştiğini tespit ederseniz, konum vektörlerinizi SoA'ya çevirebilirsiniz. Karıştırma verilerini gerçekten yararlı bir şey yapmaktan daha fazla bulursanız, karıştırmayı kesebilirsiniz.


18

Veri Odaklı Zihniyet

Veri odaklı tasarım, SoA'ları her yere uygulamak anlamına gelmez. Basitçe veri temsiline ağırlıklı olarak odaklanan mimarileri tasarlamak - özellikle verimli bellek düzeni ve bellek erişimine odaklanmak anlamına gelir.

Muhtemelen böyle uygun olduğunda SoA temsilcilerine yol açabilir:

struct BallSoa
{
   vector<float> x;        // size n
   vector<float> y;        // size n
   vector<float> z;        // size n
   vector<float> r;        // size n
};

... bu genellikle küre merkezi vektör bileşenlerini ve yarıçapı aynı anda (dört alan aynı anda sıcak değildir) işlemeyen dikey döngü mantığı için uygundur, bunun yerine bir seferde bir tane (yarıçaptan geçen bir döngü, başka bir 3 döngü küre merkezlerinin bireysel bileşenleri aracılığıyla).

Diğer durumlarda, alanlara sık sık erişilirse (döngüsel mantığınız tek tek değil tüm top alanları boyunca yineleniyorsa) ve / veya topun rasgele erişilmesi gerekiyorsa, bir AoS kullanmak daha uygun olabilir:

struct BallAoS
{
    float x;
    float y;
    float z;
    float r;
};
vector<BallAoS> balls;        // size n

... diğer durumlarda, her iki yararı da dengeleyen bir melez kullanmak uygun olabilir:

struct BallAoSoA
{
    float x[8];
    float y[8];
    float z[8];
    float r[8];
};
vector<BallAoSoA> balls;      // size n/8

... daha fazla top alanını önbellek çizgisine / sayfaya sığdırmak için yarım yüzer kullanarak bir topun boyutunu yarıya bile sıkabilirsiniz.

struct BallAoSoA16
{
    Float16 x2[16];
    Float16 y2[16];
    Float16 z2[16];
    Float16 r2[16];
};
vector<BallAoSoA16> balls;    // size n/16

... belki de yarıçapa bile neredeyse kürenin merkezine kadar erişilmez (belki kod üssünüz sık sık onlara puanlar gibi davranır ve sadece nadiren küreler gibi davranır). Bu durumda, daha sonra sıcak / soğuk alan bölme tekniğini uygulayabilirsiniz.

struct BallAoSoA16Hot
{
    Float16 x2[16];
    Float16 y2[16];
    Float16 z2[16];
};
vector<BallAoSoA16Hot> balls;     // size n/16: hot fields
vector<Float16> ball_radiuses;    // size n: cold fields

Veri odaklı tasarımın anahtarı, tasarım kararlarınızı vermeden önce bu tür bütün temsilleri göz önünde bulundurmak, kendinizi arkasında kamuya açık bir arayüze sahip alt-optimal bir gösterime hapsetmemek.

Hafıza erişim düzenlerine ve beraberindeki düzenlere ışık tutar ve bunları normalden çok daha güçlü bir endişe haline getirir. Bir anlamda, soyutlamaları bir nebze bile parçalayabilir. Bu zihniyeti daha fazla uygulayamadığımdan std::deque, örneğin, algoritmik gereklilikleri bakımından, sahip olduğu toplanmış bitişik blokların gösterimi kadar ve bunun rasgele erişimin bellek düzeyinde nasıl çalıştığını görmeye başladım . Uygulama detaylarına biraz odaklanmak, ancak ölçeklendirilebilirliği tanımlayan algoritmik karmaşıklık kadar performans üzerinde bir etkiye veya daha fazla etkiye sahip olma eğiliminde olan uygulama detayları.

Erken Optimizasyon

Veri odaklı tasarımın ana odak odasının çoğu, en azından bir bakışta, tehlikeli durumdaki erken optimizasyona tehlikeli bir şekilde yakın görünecektir. Tecrübe genellikle bize bu tür mikro optimizasyonların en iyi şekilde ve en iyi şekilde bir profiler ile uygulandığını öğretir.

Yine de veri odaklı tasarımdan almak için güçlü bir mesaj, bu tür optimizasyonlara yer bırakmaktır. Veri odaklı bir zihniyet buna izin verebilir.

Veri odaklı tasarım, daha etkili temsiller keşfetmek için nefes alandan ayrılabilir. Bu, bir seferde bellek düzeni mükemmelliğini elde etmekle ilgili olmak zorunda değil, giderek artan şekilde en iyi sunumları mümkün kılmak için uygun düşünceleri önceden yapmakla ilgili.

Granüler Nesneye Dayalı Tasarım

Birçok veri odaklı tasarım tartışması, klasik nesne yönelimli programlama kavramlarına karşı çıkacak. Yine de, OOP'yi tamamen reddetmek kadar zor olmayan bir şeye bakmanın bir yolunu sunacağım.

Nesne yönelimli tasarımın zorluğu, bizi arabirimleri çok granüler bir seviyede modellemeye teşvik etmemiz ve bizi paralel bir zihniyet yerine, birer birer birer zihniyet ile tuzağa düşürmemize neden olması.

Abartılı bir örnek olarak, görüntünün tek bir pikseline uygulanan nesne yönelimli tasarım zihniyetini hayal edin.

class Pixel
{
public:
    // Pixel operations to blend, multiply, add, blur, etc.

private:
    Image* image;          // back pointer to access adjacent pixels
    unsigned char rgba[4];
};

Umarım kimse bunu yapmaz. Örneği gerçekten brüt yapmak için, piksel içeren görüntüye bir arka işaretçi koydum, böylece bulanıklaştırma gibi görüntü işleme algoritmaları için komşu piksellere erişebilir.

Görüntü arka göstericisi derhal bir göze çarpan ek yükü ekler, ancak dışladıysak bile (yalnızca pikselin genel arayüzünü tek bir piksele uygulanan işlemler sağlar), sadece bir pikseli temsil eden bir sınıfla sonuçlanır.

Şimdi, genel gider anlamında bir sınıfın, C ++ bağlamında, bu arka göstericinin yanı sıra hiçbir sorunu yoktur. C ++ derleyicilerini optimize etmek, inşa ettiğimiz tüm yapıyı almak ve onu smithereen'lere bölmek için mükemmeldir.

Buradaki zorluk, kapsüllenmiş bir arayüzü çok fazla piksel seviyesinde granülleştirmemizdir. Bu, bizi bu tür bir granüler tasarım ve verilerle hapsolmuş durumda bırakıyor, potansiyel olarak onları bu Pixelarayüze bağlayan çok sayıda müşteri bağımlılığı .

Çözüm: tanecikli bir pikselin nesne yönelimli yapısını ortadan kaldırın ve arabirimlerinizi çok sayıda pikselle ilgilenen daha kalın bir seviyede modellenmeye başlayın (görüntü seviyesinde).

Toplu görüntü düzeyinde modelleme yaparak, optimize etmek için önemli ölçüde daha fazla alana sahibiz. Örneğin, 64 byte önbellek çizgisine mükemmel şekilde uyan 16x16 piksel boyutunda birleştirilmiş döşemeler olarak büyük görüntüleri temsil edebiliriz, ancak tipik olarak küçük bir adımla piksellerin verimli komşu dikey erişimine izin veriyoruz (eğer çok sayıda görüntü işleme algoritması varsa). sabit veri yönelimli bir örnek olarak komşu piksellere dikey biçimde erişilmesi gerekir).

Kaba Düzeyde Tasarım

Bir görüntü seviyesindeki modelleme arayüzlerinin yukarıdaki örneği, görüntü işleme, üzerinde çalışılmış ve ölüme göre optimize edilmiş çok olgun bir alan olduğu için daha akıllıca bir örnektir. Yine de daha az belirgin olan, bir parçacık yayıcıdaki bir parçacık, bir sprite ya da bir sprite topluluğuna, bir kenar grafiğindeki bir kenar veya hatta bir insana karşı bir insan topluluğuna olabilir.

Veriye yönelik optimizasyonlara izin vermenin anahtarı (öngörü veya ön görüşte) genellikle arayüzleri çok daha kaba bir seviyede, toplu olarak tasarlamaya düşecektir. Tekil varlıklar için arayüzler tasarlama fikri, onları büyük oranda işlemden geçiren büyük operasyonlara sahip varlıklar koleksiyonları için tasarlamanın yerini almaktadır. Bu özellikle ve hemen her şeye erişmesi gereken ve yardım edemeyen ancak doğrusal karmaşıklığa sahip olan sıralı erişim döngülerini hedefler.

Veri odaklı tasarım, genellikle veri toplama toplu modelleme oluşturmak için veri birleştirme fikri ile başlar. Benzer bir zihniyet, ona eşlik eden arayüz tasarımlarına da yansır.

Bu, veri odaklı tasarımdan aldığım en değerli ders, çünkü bilgisayar denemesine meraklıyım çünkü ilk denememde bir şeyler için en uygun bellek düzenini bulanlar. Bu, elinizde bir profiler ile yinelendiğim bir şey haline geliyor (ve bazen işleri hızlandırmayı başaramadığım bir kaç özlemle). Yine de, veri odaklı tasarımın arayüz tasarımı, beni daha verimli veri sunumları aramaya yer bırakıyor.

Kilit nokta, arayüzleri genellikle bizim istediğimizden daha kaba bir seviyede tasarlamaktır. Bu aynı zamanda, sanal işlevlerle ilişkili dinamik gönderim ek yükünü, işlev işaretçi çağrıları, dylib çağrıları ve sıralananların yapılamaması gibi yan yararları da beraberinde getirir. Tüm bunlardan kurtulmanın ana fikri işleme (toplu halde) bakmaktır.


5

Tanımladığınız şey bir uygulama problemidir. OO tasarımı açıkça uygulamalar ile ilgili değildir .

Sütun yönelimli Top kabınızı, satır veya sütun yönelimli bir görünüm sunan bir arabirimin arkasına sarabilirsiniz. Bir Ball nesnesini , temel sütun-sütun yapısındaki ilgili değerleri yalnızca değiştiren volumeve gibi yöntemlerle uygulayabilirsiniz move. Aynı zamanda, Ball konteyneriniz verimli sütun şeklinde işlemler için bir arayüz ortaya çıkarabilir. Uygun şablonlar / türler ve akıllı bir satır içi derleyici ile bu soyutlamaları sıfır çalışma zamanı maliyetiyle kullanabilirsiniz.

Sütun veriye ne sıklıkta erişeceğinize karşılık, satır-satırda değişiklik yapmayı ne sıklıkla yapacaksınız? Sütun depolama için tipik kullanım durumlarında, sıraların sıralamasının etkisi yoktur. Ayrı bir dizin sütunu ekleyerek satırların keyfi bir permütasyonunu tanımlayabilirsiniz. Sıralamayı değiştirmek sadece indeks sütununda takas değerlerini gerektirecektir.

Elementlerin verimli bir şekilde eklenmesi / uzaklaştırılması diğer tekniklerle sağlanabilir:

  • Değişen öğeler yerine silinmiş satırların bir bitmap'ini koruyun. Çok seyrek olduğunda yapıyı sıkıştırın.
  • B-Tree benzeri bir yapıda uygun boyutta topaklara gruplandırır, böylece keyfi konumlarda takma veya çıkarma işlemi tüm yapının değiştirilmesini gerektirmez.

Müşteri kodu bir dizi Ball nesnesi, değişken bir Ball nesnesi kabı, bir radii dizisi, bir Nx3 matrisi vs. görecektir; bu karmaşık (ancak verimli) yapıların çirkin detaylarıyla ilgilenmesi gerekmez. Nesne soyutlamasının sizi satın aldığı şey budur.


Kuşkusuz o (kullanımına çirkin olur rağmen 1 AoS organizasyonu, güzel bir varlık odaklı API mükemmel iyileştirilebilir ball->do_something();karşı ball_table.do_something(ball)size sahte bir sahte-pointer aracılığıyla tutarlı bir varlık istemiyorsanız) (&ball_table, index).

1
Bir adım daha ileri gideceğim: SoA'yı kullanmanın sonucuna tamamen OO tasarım ilkelerinden ulaşılabilir. İşin püf noktası, sütunların satırlardan daha temel bir nesne olduğu bir senaryoya ihtiyacınız var. Toplar burada iyi bir örnek değil. Bunun yerine, yükseklik, toprak tipi veya yağış gibi çeşitli özelliklere sahip bir arazi düşünün. Her özellik, diğer Field nesnelerini döndürebilecek degrade () veya divergence () gibi kendi yöntemlerine sahip bir ScalarField nesnesi olarak modellenmiştir. Harita çözünürlüğü gibi şeyleri sarabilir ve arazideki farklı özellikler farklı çözünürlüklerle çalışabilir.
16807,

4

Kısa cevap: tamamen haklısın ve bunun gibi makaleler bu noktayı tamamen kaçırıyor.

Tam cevap: Örneklerinizin "Dizilerin Yapısı" yaklaşımı, bazı işlemler için ("sütun işlemleri") ve diğer işlemler için "Yapı Dizileri" ("satır işlemleri" için performans avantajlarına sahip olabilir. ", yukarıda bahsettiğin gibi). Aynı prensip veritabanı mimarisini etkiledi , klasik sıraya dayalı veritabanlarına karşı sütun odaklı veritabanları var.

Bu nedenle, bir tasarım seçerken göz önünde bulundurmanız gereken ikinci şey, programınızda en çok ne tür işlemlere ihtiyaç duyduğunuzdur ve bunlar farklı bellek düzeninden faydalanacaksa. Bununla birlikte, göz önünde bulundurulması gereken ilk şey, gerçekten bu performansa ihtiyacınız olup olmadığıdır (bence oyun programlamada yukarıdaki makalenin sık sık bu gereksiniminiz olduğu oyun programlarıdır).

En güncel OO dilleri, nesneler ve sınıflar için bir "Yapısal Dizi" bellek düzeni kullanır. OO'nun avantajlarından yararlanmak (verileriniz için soyutlamalar oluşturmak, enkapsülasyon ve temel fonksiyonların yerel kapsamı gibi), genellikle bu tür bellek düzeniyle bağlantılıdır. Yüksek performanslı bilgi işlem yapmadığınız sürece, SoA'yı birincil yaklaşım olarak görmem.


3
DOD her zaman Dizinin Yapısı (SoA) düzeni anlamına gelmez. Yaygındır, çünkü genellikle erişim düzeniyle eşleşir, ancak başka bir düzen daha iyi çalıştığında, elbette bunu kullanın. DOD, veri yerleştirmenin belirli bir yolundan çok, bir tasarım paradigmasına benzeyen, çok daha genel (ve kuşkusuz). Eğer başvuru makale iyi kaynak uzaktır ve kusurları sahipken Ayrıca, bu yok değil SoA düzenleri reklamını. "A" lar ve "B" ler, Balltek tek floatveya vec3ler (SoA-dönüşümüne tabi olacaklar) olabileceği gibi tam özellikli olabilirler .

2
... ve bahsettiğiniz satır odaklı tasarım her zaman DOD ile kaplıdır. Buna bir dizi yapı (AoS) denir ve çoğu kaynağın "OOP yolu" (daha iyi veya wose için) olarak adlandırdığı şey arasındaki fark satır-sütun düzeninde değil, sadece bu düzenin belleğe nasıl eşleştirildiğidir (birçok küçük nesne tüm kayıtların büyük sürekli tablosu ile işaretçiler arasında bağlanır). Özet olarak -1, çünkü OP'nin yanlış anlamalarına karşı iyi puanlar toplamanıza rağmen, OP'in DOD hakkındaki anlayışını düzelmekten çok, tüm DOD cazını yanlış tanıtıyorsunuz.

@delnan: Yorumunuz için teşekkürler, muhtemelen "DOD" yerine "SoA" terimini kullanmam gerektiği konusunda haklısınız. Cevabımı buna göre düzenledim.
Doc Brown,

Çok daha iyi, olumsuz oy kaldırıldı. Kullanıcının2313838'in SoA'nın güzel "nesne" yönelimli API'lerle (soyutlamalar, kapsülleme ve "temel fonksiyonların daha fazla yerel kapsamı" anlamında) nasıl birleştirilebileceği konusundaki cevabını kontrol edin. AoS düzeni için daha doğal olarak geliyor (dizi eleman türüyle evlenmek yerine aptalca bir genel konteyner olabilir) ancak uygulanabilir.

Ve bu, SoA'dan AoS'a / AoS'den otomatik dönüşüme sahip olan github.com/BSVino/JaiPrimer/blob/master/JaiPrimer.md Örnek: reddit.com/r/rust/comments/2t6xqz/… ve daha sonra şudur: haberler. ycombinator.com/item?id=10235766
Jerry Jeremiah
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.