Dinamik Bellek Ayırma ve Bellek Yönetimi


17

Ortalama bir oyunda, sahnede yüzlerce veya belki de binlerce nesne vardır. Silah atışları (madde işaretleri) dahil tüm nesneler için varsayılan olarak new () yoluyla dinamik olarak bellek ayırmak doğru mu?

Dinamik ayırma için herhangi bir bellek havuzu oluşturmalı mıyım yoksa bununla uğraşmanıza gerek yok mu? Hedef platform mobil cihazlarsa ne olur?

Mobil oyunda bir hafıza yöneticisine ihtiyaç var mı , lütfen? Teşekkür ederim.

Kullanılan Dil: C ++; Şu anda Windows altında geliştirildi, ancak daha sonra taşınması planlandı.


Hangi dil?
Kylotan

@Kylotan: kullanılan dil: C ++ şu anda Windows altında geliştirildi, ancak daha sonra taşınması planlandı.
Bunkai.Satori

Yanıtlar:


23

Ortalama bir oyunda, sahnede yüzlerce veya belki de binlerce obje vardır. Silah atışları (mermiler) dahil olmak üzere tüm nesneler için varsayılan olarak new () yoluyla dinamik olarak bellek ayırmak doğru mu?

Bu gerçekten ne demek istediğini "doğru" olarak belirler. Terimi tam anlamıyla alırsanız (ve ima edilen tasarımın herhangi bir doğruluk kavramını göz ardı ederseniz) evet, mükemmel kabul edilebilir. Programınız iyi derlenecek ve çalışacaktır.

En iyi şekilde performans gösterebilir, ancak yine de sevk edilebilir, eğlenceli bir oyun olacak kadar iyi performans gösterebilir.

Dinamik ayırma için herhangi bir bellek havuzu oluşturmalı mıyım yoksa bununla uğraşmanıza gerek yok mu? Hedef platform mobil cihazlarsa ne olur?

Profil ve görün. Örneğin, C ++ 'da, öbek üzerinde dinamik olarak ayırma genellikle "yavaş" bir işlemdir (uygun boyutta bir blok arayan öbek boyunca yürümeyi içerir). C # 'da, genellikle bir artıştan biraz daha fazlasını içerdiğinden, son derece hızlı bir işlemdir. Farklı dil uygulamaları bellek tahsisi, serbest bırakıldığında parçalanma, vb. Bakımından farklı performans özelliklerine sahiptir.

Bir bellek havuzu sistemi uygulamak kesinlikle performans kazanımları sağlayabilir - ve mobil sistemler genellikle masaüstü sistemlere göre düşük güçte olduğundan, belirli bir mobil platformda masaüstünde olduğundan daha fazla kazanç görebilirsiniz. Ancak yine, profil yapmanız ve görmeniz gerekecek - şu anda oyununuz yavaşsa, ancak bellek ayırma / serbest bırakma profillerde etkin nokta olarak görünmüyorsa, bellek tahsisini ve erişimi muhtemelen en iyi şekilde kazanacak altyapıyı uygular ' Paran için çok fazla patlama yapmam.

Mobil oyunda bir hafıza yöneticisine ihtiyaç var mı, lütfen? Teşekkür ederim.

Tekrar, profil ve görün. Oyununuz şimdi iyi çalışıyor mu? O zaman endişelenmenize gerek olmayabilir.

Tüm bu dikkatli konuşma bir yana, her şey için dinamik ayırma kullanmak kesinlikle gerekli değildir ve bu nedenle hem potansiyel performans kazanımları hem de izleyip sonunda bırakmanız gereken bellek tahsisi nedeniyle bundan kaçınmak avantajlı olabilir. kodunuzu karmaşıklaştıracak şekilde izlemeniz ve sonunda yayınlamanız gerektiği anlamına gelir.

Özellikle, orijinal örneğinizde, sık sık yaratılan ve imha edilen bir şey olan "mermi" den bahsettiniz - çünkü birçok oyun çok sayıda mermi içerir ve mermiler hızlı hareket eder ve böylece ömrünün sonuna (ve sıklıkla) şiddetle!). Bu nedenle, onlar için bir havuz ayırıcısı uygulamak ve onlar gibi nesneler (bir parçacık sistemindeki parçacıklar gibi) genellikle verimlilik artışlarına neden olabilir ve muhtemelen havuz tahsisini kullanmaya başlamanın ilk yeri olacaktır.

Bir bellek havuzu uygulamasını bir "bellek yöneticisi" nden farklı olarak görüyorsanız net değilim - bellek havuzu nispeten iyi tanımlanmış bir kavramdır, bu yüzden bunları uygularsanız bir fayda olabileceğini kesin olarak söyleyebilirim . Bir "bellek yöneticisi" sorumluluğu açısından biraz daha belirsizdir, bu yüzden bir kişinin gerekli olup olmadığının "bellek yöneticisi" nin ne yapacağını düşündüğünüze bağlı olduğunu söylemeliyim.

Örneğin, bir bellek yöneticisini yeni / delete / free / malloc / neye telefon etmeyi kesen ve ne kadar bellek ayırdığınıza, ne sızdırdığınıza dair tanı sağlayan bir şey olarak görüyorsanız - o zaman bu yararlı olabilir oyun geliştirilirken sızıntıları ayıklamanıza ve en uygun bellek havuzu boyutlarınızı ayarlamanıza yardımcı olacak bir araç.


Kabul. Daha sonra işleri değiştirmenize izin verecek şekilde kodlayın. Şüpheniz varsa, kıyaslama veya profil.
axel22

@ Josh: Mükemmel cevap için +1. Muhtemelen sahip olmam gereken dinamik ayırma, statik ayırma ve bellek havuzlarının birleşimidir. Ancak, oyunun performansı bana bu üçünün uygun karışımında rehberlik edecek. Bu sorum için Kabul Edilen Cevap için açık bir aday . Ancak, başkalarının nelere katkıda bulunacağını görmek için soruyu bir süre açık tutmak istiyorum.
Bunkai.Satori

+1. Mükemmel detaylandırma. Neredeyse her performans sorusunun cevabı her zaman "profil ve gör" dür. Donanım bugünlerde ilk prensiplerden elde edilen performans hakkında akıl yürütmek için çok karmaşık. Verilere ihtiyacınız var.
Münih

@Munificent: Yorumunuz için teşekkürler. Yani amaç oyunun çalışmasını ve durmaktır. Gelişimin ortasında performans hakkında çok fazla endişelenmenize gerek yok. Oyun tamamlandıktan sonra hepsi düzeltilebilir ve düzeltilecektir.
Bunkai.Satori

Bu C # tahsis süresi haksız bir temsili olduğunu düşünüyorum - örneğin, her C # tahsis de bir senkronizasyon bloğu, Nesne tahsisi vb. .
DeadMG

7

Josh'un mükemmel cevabına eklemek için fazla bir şeyim yok, ama bu konuda yorum yapacağım:

Dinamik ayırma için herhangi bir bellek havuzu oluşturmalı mıyım yoksa bununla uğraşmanıza gerek yok mu?

Bellek havuzları newile her bir ayırmayı çağırmak arasında bir orta yol vardır . Örneğin, bir dizideki belirli sayıda nesneyi ayırabilir, daha sonra üzerlerinde `` yok etmek '' için bir bayrak ayarlayabilirsiniz. Daha fazla ayırmanız gerektiğinde, yok edilen bayrak ayarlı olanların üzerine yazabilirsiniz. Bu tür şeylerin kullanımı sadece yeni / sil komutundan biraz daha karmaşıktır (bu amaçla 2 yeni fonksiyona sahip olacağınız gibi), ancak yazması kolaydır ve size büyük kazançlar sağlayabilir.


Güzel ekleme için +1. Evet, haklısınız, mermiler, parçacıklar, efektler gibi daha basit oyun öğelerini yönetmenin iyi bir yolu. Özellikle olanlar için dinamik olarak bellek ayırmaya gerek kalmayacaktı.
Bunkai.Satori

3

Silah atışları (madde işaretleri) dahil tüm nesneler için varsayılan olarak new () aracılığıyla dinamik olarak bellek ayırmak doğru mu?

Hayır tabii değil. Tüm nesneler için bellek ayırma işlemi doğru değildir . new () operatörü dinamik ayırma içindir, yani nesnenin ömrü dinamik olduğu veya nesnenin türü dinamik olduğu için ayırmanın dinamik olması gerektiğinde uygundur. Nesnenin türü ve ömrü statik olarak biliniyorsa, nesneyi statik olarak ayırmalısınız.

Tabii ki, ayırma düzeniniz hakkında ne kadar fazla bilgiye sahipseniz, bu ayırmalar o kadar hızlı nesne havuzları gibi uzman ayırıcılar aracılığıyla yapılabilir. Ancak, bunlar optimizasyonlardır ve bunları yalnızca gerekli oldukları biliniyorsa yapmalısınız.


İyi yanıt için +1. Genelleştirmek için doğru yaklaşım şu olacaktır: gelişimin başlangıcında, hangi nesnelerin statik olarak tahsis edilebileceğini planlamak. Geliştirme sırasında, yalnızca dinamik olarak tahsis edilmesi gereken nesneleri dinamik olarak ayırmak için. Sonunda, olası bellek ayırma performansı sorunlarını profillemek ve ayarlamak için.
Bunkai.Satori

0

Bir çeşit Kylotan'ın önerisini yankılanıyor, ancak bunu mümkünse veri yapısı düzeyinde çözmenizi tavsiye ederim, eğer yardımcı olabilirseniz düşük allocator seviyesinde değil.

Aşağıda Foos, öğeleri birbirine bağlı deliklere sahip bir dizi kullanarak tekrar tekrar ayırmayı ve serbest bırakmayı önleyebileceğiniz basit bir örnek (bunu "ayırıcı" düzeyi yerine "kapsayıcı" düzeyinde çözme):

struct FooNode
{
    explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}

    // Stores a 'Foo'.
    Foo element;

    // Points to the next foo available; either the
    // next used foo or the next deleted foo. Can
    // use SoA and hoist this out if Foo doesn't 
    // have 32-bit alignment.
    int next;
};

struct Foos
{
    // Stores all the Foo nodes.
    vector<FooNode> nodes;

    // Points to the first used node.
    int first_node;

    // Points to the first free node.
    int free_node;

    Foos(): first_node(-1), free_node(-1)
    {
    }

    const FooNode& operator[](int n) const
    {
         return data[n];
    }

    void insert(const Foo& element)
    {
         int index = free_node;
         if (index != -1)
         {
              // If there's a free node available,
              // pop it from the free list, overwrite it,
              // and push it to the used list.
              free_node = data[index].next;
              data[index].next = first_node;
              data[index].element = element;
              first_node = index;
         }
         else
         {
              // If there's no free node available, add a 
              // new node and push it to the used list.
              FooNode new_node(element);
              new_node.next = first_node;
              first_node = data.size() - 1;
              data.push_back(new_node);
         }
    }

    void erase(int n)
    {
         // If the node being removed is the first used
         // node, pop it from the used list.
         if (first_node == n)
              first_node = data[n].next;

         // Push the node to the free list.
         data[n].next = free_node;
         free_node = n;
    }
};

Bu etkiye sahip bir şey: ücretsiz listeye sahip tek bağlantılı bir dizin listesi. Dizin bağlantıları, kaldırılan öğelerin üzerinden atlamanıza, öğeleri sabit zamanda kaldırmanıza ve ayrıca sabit zamanlı ekleme ile serbest öğeleri geri almanıza / yeniden kullanmanıza / üzerine yazmanıza olanak tanır. Yapıyı yinelemek için şöyle bir şey yaparsınız:

for (int index = foos.first_node; index != -1; index = foos[index].next)
    // do something with foos[index]

resim açıklamasını buraya girin

Ve yukarıdaki tür "bağlantılı delikler dizisi" veri yapısını, kopya atama gereksinimini önlemek için yeni ve manuel dtor çağırma yerleşimlerini kullanarak genelleştirebilirsiniz, öğeler kaldırıldığında yıkıcıları çağırmasını sağlayın, ileri bir yineleyici sağlayın, vb. konsepti daha net bir şekilde göstermek için örneği C-benzeri tutmayı ve ayrıca çok tembel olmamı tercih etti.

Bununla birlikte, bu yapı, ortadan çok şey kaldırıp yerleştirdikten sonra uzamsal yörede bozulma eğilimi gösterir. Bu noktada nextbağlantılar, vektör boyunca ileri geri yürütebilir, daha önce aynı sıralı geçiş içinde bir önbellek satırından tahliye edilen verileri yeniden yükleyebilir (bu, geri kazanırken öğeleri karıştırmadan sabit zamanlı olarak kaldırmaya izin veren herhangi bir veri yapısı veya ayırıcı ile kaçınılmazdır) ortadan sabit zamanlı yerleştirme ile ve paralel bitset veya removedbayrak gibi bir şey kullanmadan boşluklar ). Önbelleğe uygunluğu geri yüklemek için, şu şekilde bir kopya ctor ve takas yöntemi uygulayabilirsiniz:

Foos(const Foos& other)
{
    for (int index = other.first_node; index != -1; index = other[index].next)
        insert(foos[index].element);
}

void Foos::swap(Foos& other)
{
     nodes.swap(other.nodes):
     std::swap(first_node, other.first_node);
     std::swap(free_node, other.free_node);
}

// ... then just copy and swap:
Foos(foos).swap(foos);

Şimdi yeni sürüm tekrar önbellek dostu. Başka bir yöntem de yapılara ayrı bir indeks listesi depolamak ve periyodik olarak sıralamaktır. Bir diğeri, hangi indekslerin kullanıldığını belirtmek için bir bit kümesi kullanmaktır. Bu, her zaman bit kümesini sırayla geçirmenizi sağlayacaktır (bunu verimli bir şekilde yapmak için, bir seferde 64 biti kontrol edin, örneğin FFS / FFZ kullanarak). Bitset en verimli ve müdahaleci olmayan, 32 bit nextindeksler yerine hangilerinin kullanıldığını ve hangilerinin kaldırıldığını belirtmek için öğe başına sadece paralel bir bit gerektirir , ancak iyi yazmak için en fazla zaman alıcıdır ( bir seferde bir biti kontrol ediyorsanız geçiş için hızlı olun - işgal edilen endekslerin aralıklarını hızlı bir şekilde belirlemek için bir seferde 32+ bit arasında bir set veya unset biti bulmak için FFS / FFZ'ye ihtiyacınız vardır).

Bu bağlantılı çözüm genellikle uygulanması en kolay ve müdahaleci olmayan ( Foobazı removedbayrağı saklamak için değişiklik gerektirmez ), bu kapsayıcıyı herhangi bir veri türüyle çalışmak için genelleştirmek istiyorsanız yararlıdır. eleman başına ek yük.

Dinamik ayırma için herhangi bir bellek havuzu oluşturmalı mıyım yoksa bununla uğraşmanıza gerek yok mu? Hedef platform mobil cihazlarsa ne olur?

ihtiyaç güçlü bir kelimedir ve ışın izleme, görüntü işleme, parçacık simülasyonları ve örgü işleme gibi çok kritik performans gösteren alanlarda önyargılıyım, ancak mermiler gibi çok hafif işleme için kullanılan ufacık nesnelerin tahsis edilmesi ve serbest bırakılması nispeten çok pahalı ve genel amaçlı, değişken boyutlu bir bellek ayırıcısına karşı ayrı ayrı parçacıklar. İstediğiniz herhangi bir şeyi saklamak için yukarıdaki veri yapısını bir veya iki gün içinde genelleştirebilmeniz gerektiğine göre, bu tür yığın tahsis / dağıtma maliyetlerini her ufacık şey için ödenmekten tamamen ortadan kaldırmanın değerli bir değişim olacağını düşünüyorum. Tahsis / yeniden yerleştirme maliyetlerini azaltmanın yanı sıra, sonuçlara göre daha iyi referans konumu elde edersiniz (daha az önbellek gözden kaçırması ve sayfa hatası vb.).

Josh'un GC hakkında söylediklerine gelince, C # 'in GC uygulamasını Java'nınki kadar yakından incelemedim, ancak GC ayırıcılarının genellikle ilk tahsisi varbu çok hızlı çünkü ortada belleği serbest bırakamayan sıralı bir ayırıcı kullanıyor (neredeyse bir yığın gibi, ortadaki şeyleri silemezsiniz). Daha sonra, bellek kopyalayarak ve önceden tahsis edilen belleği bir bütün olarak temizleyerek tek tek nesnelerin ayrı bir iş parçacığında çıkarılmasına izin vermek için pahalı maliyetler öder (verileri, bağlı bir yapı gibi bir şeye kopyalarken bir kerede tüm yığını yok etmek gibi), ancak ayrı bir iş parçacığında yapıldığı için, uygulamanızın iş parçacıklarını çok fazla durdurmaz. Bununla birlikte, bu, ek bir dolaylama seviyesinin ve bir ilk GC döngüsünün ardından genel LOR kaybının çok önemli bir gizli maliyetini taşır. Yine de tahsisi hızlandırmak için başka bir stratejidir - çağıran evrede daha ucuz hale getirin ve sonra pahalı bir işi başka bir işte yapın. Bunun için, nesnelerinize başvurmak için iki yerine dolaylama düzeyine ihtiyacınız vardır, çünkü başlangıçta ayırdığınız zamanla ilk döngüden sonra bellekte karıştırılırlar.

C ++ 'da uygulanması biraz daha kolay olan benzer bir damardaki başka bir strateji, nesnelerinizi ana iş parçacıklarınızda serbest bırakmak için zahmet etmeyin. Sadece bir şeyleri ortadan kaldırmak için izin vermeyen bir veri yapısının sonuna ekleme ve ekleme ve ekleme yapmaya devam edin. Ancak, kaldırılması gereken şeyleri işaretleyin. Daha sonra ayrı bir iş parçacığı, kaldırılan öğeler olmadan yeni bir veri yapısı oluşturma pahalı işiyle ilgilenebilir ve daha sonra yenisini eskisiyle değiştirebilir, örneğin, hem ayırma hem de serbestleştirme öğelerinin maliyetinin büyük bir kısmı, Eğer bir elemanın çıkarılması talebinin hemen yerine getirilmesinin gerekmediğini varsayabilirsiniz. Bu, serbest bırakmayı sadece iş parçacıklarınız açısından daha ucuz hale getirmekle kalmaz, aynı zamanda tahsisi daha ucuz hale getirir, çünkü kaldırma kasalarını ortadan kaldırmak zorunda kalmayacak kadar basit ve dolambaçlı bir veri yapısı kullanabilirsiniz. Sadece bir konteynere ihtiyacı olanpush_backekleme clearfonksiyonu, tüm elemanları çıkarma ve swapiçeriği çıkarılan elemanlar hariç yeni, kompakt bir kapla değiştirme fonksiyonu ; bu mutasyona kadar gider.

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.