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


10

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

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

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

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


6
実現可能ですか?はい、確かに-他の言語はこれを定期的に行います(例:Java-文字列インターンの検索)。ただし、考慮すべき重要な点の1つは、キャッシュされたオブジェクトは不変である必要があることですが、std :: stringはそうではありません。
ハルク、

2
この質問はより関連性があります:stackoverflow.com/q/26130941
rwong

8
アプリケーションを支配する文字列操作のタイプを分析しましたか?コピー、部分文字列抽出、連結、文字単位の操作ですか?操作のタイプごとに、異なる最適化手法が必要です。また、コンパイラと標準ライブラリの実装が「小さな文字列の最適化」をサポートしているかどうかを確認してください。最後に、文字列インターニングを使用する場合、ハッシュ関数のパフォーマンスも重要です。
rwong 2016

2
それらの弦を使って何をしていますか?それらはある種の識別子またはキーとして使用されているだけですか?または、それらを組み合わせて出力を作成しますか?もしそうなら、どのように文字列連結を行うのですか?+オペレータまたは文字列ストリームと?弦はどこから来たのですか?コード内のリテラルまたは外部入力?
エイモン

回答:


3

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

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

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

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

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

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

ここに画像の説明を入力してください

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

#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

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


コピーオンライトについてはどうでしょう。文字列を変更した場合、ハッシュを再計算して復元します。それともうまくいきませんか?
ジェリージェレミア

@JerryJeremiahそれはあなたのアプリケーションに依存します。ハッシュによって表される文字列を変更でき、ハッシュ表現を取得すると、新しい値が取得されます。コンパイラーのコンテキストでは、新しい文字列の新しいハッシュを作成します。
qwerty_so 2016年

0

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

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

大量のメモリを使用するコストにも注目に値します。これはおそらく最大の攻撃者です。マシンに十分なRAMが搭載されている可能性がありますが、まだキャッシュサイズが小さいため、まだキャッシュされていないメモリにアクセスするとパフォーマンスが低下します。これについては、https//en.wikipedia.org/wiki/Locality_of_referenceを参照してください。


0

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.