C ++ 'da yedek dize ayırmalarını optimize etme


10

Performansı bir sorun haline gelmiştir oldukça karmaşık bir C ++ bileşeni var. Profil oluşturma, yürütme süresinin çoğunun std::strings için bellek ayırmaya harcandığını gösterir .

Bu teller arasında çok fazla fazlalık olduğunu biliyorum. Bir avuç değer çok sık tekrarlanır, ancak birçok benzersiz değer de vardır. Dizeler tipik olarak oldukça kısadır.

Şimdi sadece bu sık tahsisleri bir şekilde yeniden kullanmanın mantıklı olup olmayacağını düşünüyorum. 1000 işaretçi ile 1000 farklı "foobar" değeri yerine, bir "foobar" değerine 1000 işaretçi alabilirim. Bunun bellekte daha verimli olması güzel bir bonus ama çoğunlukla burada gecikme konusunda endişeliyim.

Sanırım bir seçenek zaten ayrılmış değerleri bir kayıt defteri türünü korumak olurdu ama kayıt defteri yedekleri daha fazla bellek ayırma daha hızlı yapmak mümkün mü? Bu uygulanabilir bir yaklaşım mı?


6
Mümkün? Evet, kesinlikle - diğer diller bunu rutin olarak yapıyor (ör. Java - String interning'i arayın). Bununla birlikte, dikkate alınması gereken önemli bir şey, önbelleğe alınmış nesnelerin değişmez olması gerektiğidir, std :: string değildir.
Hulk

2
Bu soru daha alakalı: stackoverflow.com/q/26130941
rwong

8
Uygulamanızda ne tür dize manipülasyonlarının baskın olduğunu analiz ettiniz mi? Kopyalama, alt dize çıkarma, birleştirme, karakter karakter manipülasyonu mu? Her işlem türü farklı optimizasyon teknikleri gerektirir. Ayrıca, derleyicinizin ve standart kitaplık uygulamanızın "küçük dize optimizasyonunu" destekleyip desteklemediğini de kontrol edin. Son olarak, string interning kullanıyorsanız, hash fonksiyonunun performansı da önemlidir.
rwong

2
Bu iplerle ne yapıyorsun? Sadece bir tür tanımlayıcı veya anahtar olarak mı kullanılıyorlar? Yoksa bazı çıktılar oluşturmak için mi birleştirilirler? Öyleyse, dize birleştirmelerini nasıl yaparsınız? İle +operatör veya dize akışları ile? Teller nereden geliyor? Kodunuzdaki veya harici girişinizdeki değişmez değerler mi?
amon

Yanıtlar:


3

Ben bir dize arama depolamak ve karşılaştırmak için bir 32-bit dizine çevirir Basile önerdiği gibi yoğun stajyer dizeleri eğilir. Benim durumumda bu, bazen "x" adlı bir özelliğe sahip yüz binlerce ila milyonlarca bileşene sahip olduğum için yararlıdır, örneğin, genellikle komut dosyaları tarafından adlarına erişildiğinden, kullanıcı dostu bir dize adı olması gerekir.

Arama için bir trie kullanıyorum (aynı zamanda unordered_mapdenendi ancak bellek havuzları tarafından desteklenen ayarlanmış trie'm en azından daha iyi performans göstermeye başladı ve aynı zamanda yapıya her erişildiğinde kilitlenmeden iş parçacığı güvenli hale getirmek daha kolaydı) ama öyle değil oluşturmak için hızlı std::string. Mesele, benim durumumda, iki tamsayıyı eşitlik açısından kontrol etmek ve bellek kullanımını önemli ölçüde azaltmak için dizgi eşitliğini kontrol etmek gibi sonraki işlemleri hızlandırmaktır.

Sanırım bir seçenek zaten ayrılmış değerleri bir kayıt defteri türünü korumak olurdu ama kayıt defteri yedekleri daha fazla bellek ayırma daha hızlı yapmak mümkün mü?

Bir veri yapısı üzerinden tek bir aramadan çok daha hızlı bir arama yapmak zor olacak mallocÖrneğin, bir dosya gibi harici bir girişten bir bot yükü dizesi okuduğunuz bir durum varsa, benim cazibem mümkünse sıralı bir ayırıcı kullanmak olacaktır. Bu, tek bir dizenin hafızasını serbest bırakamayacağınız dezavantajla birlikte gelir. Ayırıcı tarafından toplanan tüm bellek bir kerede boşaltılmalı veya hiç boşaltılmamalıdır. Ancak sıralı bir ayırıcı, küçük bir değişken boyutlu bellek yığınlarını düz bir ardışık tarzda bir tekne yükü tahsis etmeniz gereken durumlarda, ancak daha sonra hepsini atmak için kullanışlı olabilir. Bunun sizin durumunuzda geçerli olup olmadığını bilmiyorum, ancak uygulanabilir olduğunda, sık sık ufacık bellek ayırmaları ile ilgili bir etkin noktayı düzeltmenin kolay bir yolu olabilir (önbellek özledikleri ve sayfa hataları ile ilgili olandan daha fazlası olabilir) tarafından kullanılan algoritma malloc.

Sabit boyutlu ayırma işlemlerinin, daha sonra yeniden kullanılmak üzere belirli bellek yığınlarını serbest bırakmanızı engelleyen sıralı ayırıcı kısıtlamaları olmadan hızlandırılması daha kolaydır. Ancak, değişken boyutlu ayırmayı varsayılan ayırıcıdan daha hızlı yapmak oldukça zordur. Temel olarak malloc, uygulanabilirliğini daraltan kısıtlamalar uygulamıyorsanız , genellikle daha hızlı olan herhangi bir bellek ayırıcı yapmak. Bir çözüm, örneğin bir tekne yükünüz varsa 8 bayt veya daha az olan tüm dizeler için sabit boyutlu bir ayırıcı kullanmaktır ve daha uzun dizeler nadir bir durumdur (yalnızca varsayılan ayırıcıyı kullanabileceğiniz). Bu, 1 baytlık dizeler için 7 baytın boşa harcandığı anlamına gelir, ancak diyelim ki zamanın% 95'i, dizeleriniz çok kısaysa, ayırmayla ilgili etkin noktaları ortadan kaldırmalıdır.

Yeni ortaya çıkan bir başka çözüm de çılgınca gelebilecek, ancak beni duyabilecek açılmamış bağlantılı listeler kullanmak.

resim açıklamasını buraya girin

Buradaki fikir, her açılmamış düğümü değişken boyutlu yerine sabit boyutlu yapmaktır. Bunu yaptığınızda, belleği birleştiren ve birbirine bağlı değişken boyutlu dizeler için sabit boyutlu parçalar ayıran son derece hızlı sabit boyutlu bir yığın ayırıcı kullanabilirsiniz. Bu, bellek kullanımını azaltmaz, bağlantıların maliyeti nedeniyle ekleme eğilimindedir, ancak ihtiyaçlarınıza uygun bir denge bulmak için kaydedilmemiş boyutla oynayabilirsiniz. Bu biraz tuhaf bir fikir ama artık hantal bitişik bloklarda zaten tahsis edilmiş belleği etkili bir şekilde havuzlayabildiğiniz ve hala dizeleri tek tek serbest bırakmanın avantajlarına sahip olabileceğiniz için bellekle ilgili etkin noktaları ortadan kaldırmalısınız. İşte özgürce kullanabileceğiniz basit bir ol 'sabit ayırıcı (üretimle ilgili tüylerden yoksun, başkası için yaptığım örnek):

#ifndef FIXED_ALLOCATOR_HPP
#define FIXED_ALLOCATOR_HPP

class FixedAllocator
{
public:
    /// Creates a fixed allocator with the specified type and block size.
    explicit FixedAllocator(int type_size, int block_size = 2048);

    /// Destroys the allocator.
    ~FixedAllocator();

    /// @return A pointer to a newly allocated chunk.
    void* allocate();

    /// Frees the specified chunk.
    void deallocate(void* mem);

private:
    struct Block;
    struct FreeElement;

    FreeElement* free_element;
    Block* head;
    int type_size;
    int num_block_elements;
};

#endif

#include "FixedAllocator.hpp"
#include <cstdlib>

struct FixedAllocator::FreeElement
{
    FreeElement* next_element;
};

struct FixedAllocator::Block
{
    Block* next;
    char* mem;
};

FixedAllocator::FixedAllocator(int type_size, int block_size): free_element(0), head(0)
{
    type_size = type_size > sizeof(FreeElement) ? type_size: sizeof(FreeElement);
    num_block_elements = block_size / type_size;
    if (num_block_elements == 0)
        num_block_elements = 1;
}

FixedAllocator::~FixedAllocator()
{
    // Free each block in the list, popping a block until the stack is empty.
    while (head)
    {
        Block* block = head;
        head = head->next;
        free(block->mem);
        free(block);
    }
    free_element = 0;
}

void* FixedAllocator::allocate()
{
    // Common case: just pop free element and return.
    if (free_element)
    {
        void* mem = free_element;
        free_element = free_element->next_element;
        return mem;
    }

    // Rare case when we're out of free elements.
    // Create new block.
    Block* new_block = static_cast<Block*>(malloc(sizeof(Block)));
    new_block->mem = malloc(type_size * num_block_elements);
    new_block->next = head;
    head = new_block;

    // Push all but one of the new block's elements to the free stack.
    char* mem = new_block->mem;
    for (int j=1; j < num_block_elements; ++j)
    {
        void* ptr = mem + j*type_size;
        FreeElement* element = static_cast<FreeElement*>(ptr);
        element->next_element = free_element;
        free_element = element;
    }
    return mem;
}

void FixedAllocator::deallocate(void* mem)
{
    // Just push a free element to the stack.
    FreeElement* element = static_cast<FreeElement*>(mem);
    element->next_element = free_element;
    free_element = element;
}


0

Bir zamanlar derleyici yapımında data-chair (veri bankası yerine DB için konuşma dili çevirisi) bir şey kullandık. Bu basitçe bir dize için bir karma oluşturdu ve bunu ayırma için kullandı. Yani herhangi bir dize, yığın / yığın üzerinde bir bellek parçası değil, bu veri koltuğuna bir karma kodu oldu. StringBöyle bir sınıfla değiştirebilirsiniz . Yine de bazı kodların yeniden işlenmesi gerekiyor. Ve elbette bu sadece r / o dizeleri için kullanılabilir.


Yazarken kopyalamaya ne dersiniz? Dizeyi değiştirirseniz, karmayı yeniden hesaplar ve geri yüklersiniz. Yoksa bu işe yaramaz mı?
Jerry Jeremiah

@JerryJeremiah Bu, uygulamanıza bağlıdır. Karma tarafından temsil edilen dizeyi değiştirebilirsiniz ve karma temsilini aldığınızda yeni değeri alırsınız. Derleyici bağlamında, yeni bir dize için yeni bir karma oluşturabilirsiniz.
qwerty_so

0

Kullanılan bellek ayırma ve gerçek belleklerin düşük performansla nasıl ilişkili olduğuna dikkat edin:

Belleği tahsis etmenin maliyeti elbette çok yüksektir. Bu nedenle std :: string, küçük dizeler için yerinde ayırmayı zaten kullanabilir ve bu nedenle gerçek ayırmaların miktarı, ilk varsaydığınızdan daha düşük olabilir. Bu arabellek boyutu yeterince büyük değilse, örneğin 23 karakter kullanan Facebook'un dize sınıfından ( https://github.com/facebook/folly/blob/master/folly/FBString.h ) ilham alabilirsiniz. tahsis etmeden önce dahili olarak.

Çok fazla bellek kullanmanın maliyeti de kayda değer. Bu belki de en büyük suçlu: Makinenizde bol miktarda RAM olabilir, ancak önbellek boyutları hala önbelleğe alınmamış belleğe erişirken performansa zarar verecek kadar küçüktür. Bununla ilgili bilgileri buradan okuyabilirsiniz: https://en.wikipedia.org/wiki/Locality_of_reference


0

Dize işlemlerini daha hızlı hale getirmek yerine, başka bir yaklaşım da dize işlemi sayısını azaltmaktır. Örneğin dizeleri bir enum ile değiştirmek mümkün müdür?

Yararlı olabilecek başka bir yaklaşım da Kakao'da kullanılır: Yüzlerce veya binlerce sözlük bulunduğunuz ve hepsi aynı tuşa sahip vakalar vardır. Böylece bir dizi sözlük anahtarı olan bir nesne oluşturmanıza izin veriyorlar ve böyle bir nesneyi argüman olarak alan bir sözlük kurucusu var. Sözlük diğer sözlüklerle aynı şekilde davranır, ancak bu anahtar kümesindeki bir anahtarla bir anahtar / değer çifti eklediğinizde anahtar çoğaltılmaz, ancak anahtar kümesindeki tuşa yalnızca bir işaretçi kaydedilir. Yani bu binlerce sözlük, o kümedeki her anahtar dizenin yalnızca bir kopyasına ihtiyaç duyar.

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.