web-dev-qa-db-ja.com

C ++での冗長文字列割り当ての最適化

パフォーマンスが問題となっているかなり複雑なC++コンポーネントがあります。プロファイリングは、ほとんどの実行時間がstd::stringsのメモリの割り当てに費やされていることを示しています。

これらの文字列には多くの冗長性があることを知っています。一握りの値が非常に頻繁に繰り返されますが、固有の値もたくさんあります。文字列は通常かなり短いです。

私は今、これらの頻繁な割り当てを何らかの方法で再利用することが理にかなっているのかどうか考えています。 1000の別個の「foobar」値への1000ポインターの代わりに、1つの「foobar」値への1000ポインターを持つことができます。これによりメモリ効率が向上するという事実は素晴らしいボーナスですが、ここでは主に遅延について心配しています。

すでに割り当てられた値のある種のレジストリを維持するのが1つのオプションだと思いますが、レジストリの検索を冗長なメモリ割り当てよりも高速にすることは可能ですか?これは実行可能なアプローチですか?

10
Muton

Basileが示唆するように、私はインターンされた文字列に大きく依存しています。文字列ルックアップは、保存および比較する32ビットのインデックスに変換されます。これは私の場合に便利です。たとえば、「x」という名前のプロパティを持つコンポーネントが数十万から数百万ある場合があるためです。たとえば、スクリプト作成者が名前でアクセスすることが多いため、ユーザーフレンドリーな文字列名である必要があります。

私はルックアップにトライを使用します(unordered_mapでも実験しましたが、メモリプールに裏打ちされた調整済みトライは、少なくともパフォーマンスが向上し始め、構造体にアクセスするたびにロックするだけでスレッドセーフにすることが容易になりました)。 std::stringの作成ほど高速ではありません。ポイントは、文字列が等しいかどうかのチェックなど、後続の操作を高速化することです。私の場合、2つの整数が等しいかどうかをチェックし、メモリ使用量を大幅に削減します。

既に割り当てられた値のある種のレジストリを維持するのが1つのオプションだと思いますが、レジストリの検索を冗長なメモリ割り当てよりも高速にすることは可能ですか?

これは、単一のmallocよりもはるかに高速にデータ構造全体を検索するのは困難です。たとえば、ファイルのような外部入力から文字列のボートロードを読み取るケースがある場合、可能であればシーケンシャルアロケーターを使用するのが私の誘惑です。これには、個々の文字列のメモリを解放できないという欠点があります。アロケータによってプールされたすべてのメモリは、一度に解放されるか、まったく解放されない必要があります。しかし、シーケンシャルアロケーターは、可変サイズの小さなメモリチャンクのボートロードをストレートシーケンシャル方式で割り当てる必要がある場合に便利です。それがあなたのケースに当てはまるかどうかはわかりませんが、当てはまる場合、それは頻繁な小さなメモリ割り当てに関連するホットスポットを修正する簡単な方法です(キャッシュミスとページフォールトは、根本的なものよりも関係があるかもしれません)たとえば、mallocが使用するアルゴリズム)。

固定サイズの割り当ては、特定のメモリチャンクを解放して後で再利用できないようにするシーケンシャルアロケーターの制約がなければ、スピードアップが簡単です。しかし、可変サイズの割り当てをデフォルトのアロケーターよりも速くするのはかなり難しいです。基本的に、mallocより高速なあらゆる種類のメモリアロケータを作成することは、その適用範囲を狭める制約を適用しない場合、一般的に非常に困難です。 1つの解決策は、たとえば、ボートロードがある場合に8バイト以下のすべての文字列に固定サイズのアロケーターを使用することです。長い文字列はまれなケースです(デフォルトのアロケーターをそのまま使用できます)。つまり、1バイトの文字列では7バイトが浪費されますが、割り当てに関連するホットスポットが排除されるはずです。たとえば、95%の確率で、文字列が非常に短い場合です。

ちょうど私に起こった別の解決策は、狂ったように聞こえるかもしれないが私から聞こえるかもしれない展開されたリンクされたリストを使うことです。

enter image description here

ここでの考え方は、展開された各ノードを可変サイズではなく固定サイズにすることです。これを行うと、メモリをプールする非常に高速な固定サイズのチャンクアロケータを使用して、リンクされた可変サイズの文字列に固定サイズのチャンクを割り当てることができます。これはメモリ使用量を削減しませんが、リンクのコストのために追加される傾向がありますが、展開されたサイズで遊んで、ニーズに適したバランスを見つけることができます。ちょっと変わったアイデアですが、かさばる連続したブロックに既に割り当てられているメモリを効果的にプールでき、文字列を個別に解放できるという利点があるため、メモリ関連のホットスポットを排除する必要があります。これは私が書いた簡単な古い固定アロケータ(私が他の誰かのために作成したもので、プロダクション関連の綿毛がないもの)で、自由に使用できます。

#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;
}
3
user204677

インターンされた文字列 機構が必要になる場合があります(ただし、文字列は不変である必要があるため、const std::string- sを使用してください)。 symbols が必要かもしれません。 スマートポインター を調べてみてください(例 std :: shared_ptr )。またはC++ 17では std :: string_view .

メモリ割り当てと実際に使用されるメモリの両方がパフォーマンスの低下にどのように関連しているかに注意してください。

もちろん、実際にメモリを割り当てるコストは非常に高くなります。したがって、std :: stringは小さい文字列に対して既にインプレース割り当てを使用している可能性があり、実際の割り当て量は最初に想定するよりも少ない可能性があります。このバッファのサイズが十分に大きくない場合は、たとえば、割り当て前に内部で23文字を使用するFacebookの文字列クラス( https://github.com/facebook/folly/blob/master/folly/FBString.h )。

大量のメモリをusingするコストにも注目に値します。これはおそらく最大の違反者です:マシンにはたくさんのRAMがあるかもしれませんが、キャッシュサイズはまだ十分に小さいため、まだキャッシュされていないメモリにアクセスするとパフォーマンスが低下します。これについては、ここで読むことができます: https://en.wikipedia.org/wiki/Locality_of_reference

0
asger

文字列操作を高速化する代わりに、文字列操作の数を減らす方法もあります。たとえば、文字列を列挙型に置き換えることはできますか?

Cocoaでは、もう1つの便利なアプローチが使用されています。数百または数千の辞書があり、ほとんどが同じキーを使用している場合があります。そのため、ディクショナリキーのセットであるオブジェクトを作成でき、そのようなオブジェクトを引数として取るディクショナリコンストラクターがあります。ディクショナリは他のディクショナリと同じように動作しますが、キーと値のペアをそのキーセットのキーと一緒に追加すると、キーは複製されず、キーセットのキーへのポインターのみが保存されます。したがって、これらの何千もの辞書は、そのセット内の各キー文字列のコピーを1つだけ必要とします。

0
gnasher729

昔々、コンパイラの構築では、データチェアと呼ばれるものを使用していました(データバンクではなく、DBの口語的なドイツ語の翻訳)。これは単に文字列のハッシュを作成し、それを割り当てに使用しただけです。そのため、どの文字列もヒープ/スタック上の一部のメモリではなく、このデータチェアへのハッシュコードでした。 Stringをそのようなクラスで置き換えることができます。ただし、かなりのコードの修正が必要です。そしてもちろん、これはr/o文字列でのみ使用できます。

0
qwerty_so