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]
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 next
bağ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 removed
bayrak 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 next
indeksler 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 ( Foo
bazı removed
bayrağı 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_back
ekleme clear
fonksiyonu, tüm elemanları çıkarma ve swap
içeriği çıkarılan elemanlar hariç yeni, kompakt bir kapla değiştirme fonksiyonu ; bu mutasyona kadar gider.