2

私は文字列を受け取り、それらを文字列 -> 整数ハッシュ マップに挿入することで、一意の文字列の出現回数をカウントする基本的なプログラムを作成しました。

ストレージには std::tr1::unordered_map を使用し、カスタム ハッシュ関数とカスタム等値関数用にテンプレート化します。キーの種類は、実際にchar*は too-slow ではなくですstd::string

次に、同じコードを変更して、非常に単純なハッシュ テーブル (実際には、ハッシュによってインデックス付けされた {key, value} 構造体の配列) を使用し、2 のべき乗のサイズと衝突の線形プローブを使用しました。プログラムは 33% 速くなりました。

tr1::unordered_map を使用していたときにハッシュ テーブルのサイズを事前に設定して、サイズが大きくならないようにし、まったく同じハッシュ ルーチンと比較ルーチンを使用していたことを考えると、tr1::unordered_map の動作が 50% 遅くなる理由は次のとおりです。想像できる最も基本的なハッシュマップと比較して?

ここで「単純」と話しているハッシュ マップ タイプのコードは次のとおりです。

typedef struct dataitem {
    char* item;
    size_t count;
} dataitem_t;

dataitem_t hashtable[HASHTABLE_SIZE] = {{NULL,0}}; // Start off with empty table

void insert(char* item) {
    size_t hash = generate_hash(item);
    size_t firsthash = hash;
    while (true) {
        hash &= HASHTABLE_SIZE_MASK; // Bitmasking effect is hash %= HASHTABLE_SIZE
        if (hashtable[hash].item == NULL) { // Free bucket
            hashtable[hash].item = item;
            hashtable[hash].count = 1;
            break;
        }
        if (strcmp(hashtable[hash].item, item) == 0) { // Not hash collision; same item
            hashtable[hash].count += 1;
            break;
        }
        hash++; // Hash collision.  Move to next bucket (linear probing)
        if (hash == firsthash) {
            // Table is full.  This does not happen because the presizing is correct.
            exit(1);
        }
    }
}
4

4 に答える 4

12

@AProgrammer の回答を拡張したいと思います。

ハッシュ マップは、ニーズに合わせてカスタム調整されているため、シンプルです。一方でstd::tr1::unordered_map、多くの異なるタスクを実行する必要があり、すべての場合にうまく機能します。これには、すべての場合で平均的なパフォーマンスのアプローチが必要であるため、特定の領域で優れていることは決してありません.

ハッシュ コンテナーは、それらを実装する多くの方法があるという点で非常に特殊です。標準では実装者にバケット アプローチを強制する一方で、あなたは Open-Addressing を選択しました。どちらにも異なるトレードオフがあります。これが、今回標準が実際に特定の実装を強制した理由の 1 つです。つまり、あるライブラリから別のライブラリに切り替えたときにパフォーマンスが劇的に変化しないようにするためです。Big-O の複雑さ / 償却後の複雑さを指定するだけでは、ここでは十分ではありません。

決勝の要素数を指示したとのことunordered_mapですが、負荷率は変えましたか?チェインは、衝突の場合に (メモリの局所性がないため) 「悪い」ことで有名であり、より小さな負荷係数を使用すると、要素を分散させるのに有利になります。

最後に、1 つの違いを指摘します: ハッシュ マップのサイズを変更するとどうなりますか? 連鎖を使用することにより、unordered_mapはメモリ内の要素を移動しません。

  • それらへの参照は引き続き有効です (イテレータが無効化される場合でも)
  • 大きなオブジェクトまたは複雑なオブジェクトの場合、コピー コンストラクターの呼び出しはありません。

これは、コピーが発生する単純な実装とは対照的O(N)です (線形再ハッシュを使用して作業を分散させない限り、これは単純ではありません)。

したがって、 の選択は、より遅い平均挿入を犠牲にして、スパイクunordered_mapを滑らかにすることだったようです。

ただし、できることがあります:カスタム allocatorを提供します。ユースケース用の特定のアロケーターを作成し、すべてのメモリを一度に割り当てます (挿入されるオブジェクトの数がわかっているため、アロケーターにノードのメモリ量を報告させることができます)。次に、スタックのような方法でノードを割り当てます (単純なポインターの増加)。これにより、パフォーマンスが (多少) 向上するはずです。

于 2011-04-16T13:14:19.160 に答える
6

あなたの「自家製のハッシュマップ」はハッシュマップではなく、侵入型のハッシュセットです。そしてそれがより速い理由です。そのような単純な。

実際、侵入型ハッシュ セットも正確ではありませんが、最も近い一致です。

于 2011-04-16T06:19:25.760 に答える
4

一般に、同じ仕様でビルドされていないコンポーネントの速度を比較するのは公平ではありません。

何を測定したか、つまり、どの負荷係数でどの操作をどのように組み合わせ、現在/不在のデータをどのように組み合わせたかを正確に知らなければ、違いがどこから来るのかを説明することは困難です。

g++ の TR1 は、連鎖によって衝突を解決します。これは動的割り当てを意味します。ただし、これにより、高負荷レベルでのパフォーマンスも向上します。

于 2011-04-16T06:16:48.253 に答える
1

あなた自身が言ったように、自家製のハッシュマップは「シンプル」であり、ハッシュテーブルがいっぱいかどうかのチェックを処理しないため、 「自家製」のハッシュマップはより高速です。そして、操作する前にチェックしていない多くのことがあるかもしれません。これが、ハッシュ マップが よりも高速な理由である可能性があります。std::tr1::unordered_mapstd::tr1::unordered_map

また、 のパフォーマンスはstd::tr1::unordered_map実装によって定義されるため、実装が異なれば速度的にも異なるパフォーマンスを発揮します。その実装を見て、それをあなたのものと比較することができます。それが最初にできることであり、私はそれがあなたの質問にもある程度答えると信じています。

1. 私はあなたの主張が正しいと仮定し、それに基づいて上記のことを言いました。

于 2011-04-16T05:49:44.453 に答える