web-dev-qa-db-ja.com

文字列の非常に長いリストの適切な検索/取得方法は何ですか?

これはひどく珍しい質問ではありませんが、それでも選択を本当に説明する答えを見つけることができなかったようです。

文字列の非常に大きなリスト(正確には SHA-256 ハッシュのASCII表現)があり、そのリスト内に文字列が存在するかどうかを照会する必要があります。

このリストには1億を超える可能性のあるエントリがあり、エントリの存在を何度も繰り返しクエリする必要があります。

サイズを考えると、すべてをHashSet<string>に詰め込めるとは思えません。パフォーマンスを最大化するための適切な検索システムは何でしょうか?

リストを事前に並べ替えたり、SQLテーブルに入れたり、テキストファイルに入れたりすることはできますが、アプリケーションで何が本当に意味があるのか​​わかりません。

これらの中でパフォーマンス、または他の検索方法の点で明確な勝者はありますか?

65
Grant H.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;

namespace HashsetTest
{
    abstract class HashLookupBase
    {
        protected const int BucketCount = 16;

        private readonly HashAlgorithm _hasher;

        protected HashLookupBase()
        {
            _hasher = SHA256.Create();
        }

        public abstract void AddHash(byte[] data);
        public abstract bool Contains(byte[] data);

        private byte[] ComputeHash(byte[] data)
        {
            return _hasher.ComputeHash(data);
        }

        protected Data256Bit GetHashObject(byte[] data)
        {
            var hash = ComputeHash(data);
            return Data256Bit.FromBytes(hash);
        }

        public virtual void CompleteAdding() { }
    }

    class HashsetHashLookup : HashLookupBase
    {
        private readonly HashSet<Data256Bit>[] _hashSets;

        public HashsetHashLookup()
        {
            _hashSets = new HashSet<Data256Bit>[BucketCount];

            for(int i = 0; i < _hashSets.Length; i++)
                _hashSets[i] = new HashSet<Data256Bit>();
        }

        public override void AddHash(byte[] data)
        {
            var item = GetHashObject(data);
            var offset = item.GetHashCode() & 0xF;
            _hashSets[offset].Add(item);
        }

        public override bool Contains(byte[] data)
        {
            var target = GetHashObject(data);
            var offset = target.GetHashCode() & 0xF;
            return _hashSets[offset].Contains(target);
        }
    }

    class ArrayHashLookup : HashLookupBase
    {
        private Data256Bit[][] _objects;
        private int[] _offsets;
        private int _bucketCounter;

        public ArrayHashLookup(int size)
        {
            size /= BucketCount;
            _objects = new Data256Bit[BucketCount][];
            _offsets = new int[BucketCount];

            for(var i = 0; i < BucketCount; i++) _objects[i] = new Data256Bit[size + 1];

            _bucketCounter = 0;
        }

        public override void CompleteAdding()
        {
            for(int i = 0; i < BucketCount; i++) Array.Sort(_objects[i]);
        }

        public override void AddHash(byte[] data)
        {
            var hashObject = GetHashObject(data);
            _objects[_bucketCounter][_offsets[_bucketCounter]++] = hashObject;
            _bucketCounter++;
            _bucketCounter %= BucketCount;
        }

        public override bool Contains(byte[] data)
        {
            var hashObject = GetHashObject(data);
            return _objects.Any(o => Array.BinarySearch(o, hashObject) >= 0);
        }
    }

    struct Data256Bit : IEquatable<Data256Bit>, IComparable<Data256Bit>
    {
        public bool Equals(Data256Bit other)
        {
            return _u1 == other._u1 && _u2 == other._u2 && _u3 == other._u3 && _u4 == other._u4;
        }

        public int CompareTo(Data256Bit other)
        {
            var rslt = _u1.CompareTo(other._u1);    if (rslt != 0) return rslt;
            rslt = _u2.CompareTo(other._u2);        if (rslt != 0) return rslt;
            rslt = _u3.CompareTo(other._u3);        if (rslt != 0) return rslt;

            return _u4.CompareTo(other._u4);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            return obj is Data256Bit && Equals((Data256Bit) obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = _u1.GetHashCode();
                hashCode = (hashCode * 397) ^ _u2.GetHashCode();
                hashCode = (hashCode * 397) ^ _u3.GetHashCode();
                hashCode = (hashCode * 397) ^ _u4.GetHashCode();
                return hashCode;
            }
        }

        public static bool operator ==(Data256Bit left, Data256Bit right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Data256Bit left, Data256Bit right)
        {
            return !left.Equals(right);
        }

        private readonly long _u1;
        private readonly long _u2;
        private readonly long _u3;
        private readonly long _u4;

        private Data256Bit(long u1, long u2, long u3, long u4)
        {
            _u1 = u1;
            _u2 = u2;
            _u3 = u3;
            _u4 = u4;
        }

        public static Data256Bit FromBytes(byte[] data)
        {
            return new Data256Bit(
                BitConverter.ToInt64(data, 0),
                BitConverter.ToInt64(data, 8),
                BitConverter.ToInt64(data, 16),
                BitConverter.ToInt64(data, 24)
            );
        }
    }

    class Program
    {
        private const int TestSize = 150000000;

        static void Main(string[] args)
        {
            GC.Collect(3);
            GC.WaitForPendingFinalizers();

            {
                var arrayHashLookup = new ArrayHashLookup(TestSize);
                PerformBenchmark(arrayHashLookup, TestSize);
            }

            GC.Collect(3);
            GC.WaitForPendingFinalizers();

            {
                var hashsetHashLookup = new HashsetHashLookup();
                PerformBenchmark(hashsetHashLookup, TestSize);
            }

            Console.ReadLine();
        }

        private static void PerformBenchmark(HashLookupBase hashClass, int size)
        {
            var sw = Stopwatch.StartNew();

            for (int i = 0; i < size; i++)
                hashClass.AddHash(BitConverter.GetBytes(i * 2));

            Console.WriteLine("Hashing and addition took " + sw.ElapsedMilliseconds + "ms");

            sw.Restart();
            hashClass.CompleteAdding();
            Console.WriteLine("Hash cleanup (sorting, usually) took " + sw.ElapsedMilliseconds + "ms");

            sw.Restart();
            var found = 0;

            for (int i = 0; i < size * 2; i += 10)
            {
                found += hashClass.Contains(BitConverter.GetBytes(i)) ? 1 : 0;
            }

            Console.WriteLine("Found " + found + " elements (expected " + (size / 5) + ") in " + sw.ElapsedMilliseconds + "ms");
        }
    }
}

結果はかなり有望です。それらはシングルスレッドで実行されます。ハッシュセットバージョンは、7.9GB RAM使用量で1秒あたり100万回を少し超えるルックアップに達する可能性があります。アレイベースバージョンは、使用量が少ないRAM(4.6GB)です。 2つの間の起動時間はほぼ同じです(388秒と391秒)。ハッシュセットはルックアップパフォーマンスのためにRAM)トレードします。メモリ割り当ての制約のため、両方をバケット化する必要がありました。

アレイのパフォーマンス:

ハッシュと追加には307408msかかりました

ハッシュクリーンアップ(通常、並べ替え)には81892msかかりました

562585msで30000000要素(予想される30000000)が見つかりました[1秒あたり53k検索]

======================================

ハッシュセットのパフォーマンス:

ハッシュと追加には391105msかかりました

ハッシュクリーンアップ(通常、並べ替え)には0ミリ秒かかりました

74864msで30000000要素(予想される30000000)が見つかりました[毎秒400k検索]

62
Bryan Boettcher

リストが時間の経過とともに変化する場合は、データベースに入れます。

リストが変更されない場合は、ソートされたファイルに入れて、すべてのクエリに対してバイナリ検索を実行します。

どちらの場合も、I/Oを最小限に抑えるために ブルームフィルター を使用します。そして、文字列の使用をやめ、4つのulongを使用したバイナリ表現を使用します(オブジェクト参照コストを回避するため)。

16 GB(2 * 64 * 4/3 * 100M、 Base64 エンコーディングを想定)を超える余裕がある場合は、Set&ltstring>を作成して満足させることもできます。もちろん、バイナリ表現を使用する場合は7GB未満に収まります。

David Haneyの回答は、メモリコストがそれほど簡単に計算されないことを示しています。

21
Juan Lopes

<gcAllowVeryLargeObjects>を使用すると、はるかに大きな配列を作成できます。 256ビットハッシュコードのこれらのASCII表現を、IComparable<T>を実装するカスタム構造体に変換してみませんか?次のようになります。

struct MyHashCode: IComparable<MyHashCode>
{
    // make these readonly and provide a constructor
    ulong h1, h2, h3, h4;

    public int CompareTo(MyHashCode other)
    {
        var rslt = h1.CompareTo(other.h1);
        if (rslt != 0) return rslt;
        rslt = h2.CompareTo(other.h2);
        if (rslt != 0) return rslt;
        rslt = h3.CompareTo(other.h3);
        if (rslt != 0) return rslt;
        return h4.CompareTo(other.h4);
    }
}

次に、これらのアレイを作成できます。これは、約3.2GBを占有します。 Array.BinarySearch で簡単に検索できます。

もちろん、ユーザーの入力をASCIIからこれらのハッシュコード構造の1つに変換する必要がありますが、それは十分に簡単です。

パフォーマンスに関しては、これはハッシュテーブルほど高速ではありませんが、データベースルックアップやファイル操作よりも確かに高速になります。

考えてみると、HashSet<MyHashCode>を作成できます。 EqualsMyHashCodeメソッドをオーバーライドする必要がありますが、それは本当に簡単です。私が覚えているように、HashSetはエントリあたり24バイトのようなものであり、より大きな構造体の追加コストがかかります。図HashSetを使用する場合、合計で5ギガバイトまたは6ギガバイト。より多くのメモリがありますが、それでも実行可能であり、O(1)ルックアップを取得します。

17
Jim Mischel

これらの回答は、文字列メモリをアプリケーションに考慮していません。 文字列は1文字ではありません== .NETでは1バイトです。各文字列オブジェクトには、オブジェクトデータ用に定数20バイトが必要です。また、バッファには1文字あたり2バイトが必要です。したがって:文字列インスタンスのメモリ使用量の見積もりは20 +(2 *長さ)バイトです。

数学をやってみましょう。

  • 100,000,000個のUNIQUE文字列
  • SHA256 = 32バイト(256ビット)
  • 各文字列のサイズ= 20 +(2 * 32バイト)= 84バイト
  • 必要なメモリの合計:8,400,000,000バイト= 8.01ギガバイト

そうすることは可能ですが、これは.NETメモリにうまく保存されません。目標は、このすべてのデータを、一度にすべてのデータをメモリに保持せずにアクセス/ページングできる形式にロードすることです。そのために、私はLucene.netを使用します。これは、データをディスクに保存し、インテリジェントに検索します。各文字列を検索可能なものとしてインデックスに書き込み、インデックスで文字列を検索します。これで、この問題を処理できるスケーラブルなアプリができました。唯一の制限はディスク容量です(テラバイトのドライブをいっぱいにするのに多くの文字列が必要になります)。または、これらのレコードをデータベースに配置し、それに対してクエリを実行します。それがデータベースが存在する理由です。RAMの外で物事を永続化するためです。 :)

15
Haney

最高速度を得るには、RAMに保存してください。わずか3GB相当のデータに加えて、データ構造に必要なオーバーヘッドがあります。 HashSet<byte[]>は問題なく機能するはずです。オーバーヘッドとGC圧力を下げたい場合は、 <gcAllowVeryLargeObjects> をオンにし、単一のbyte[]と、カスタム比較子を備えたHashSet<int>を使用してインデックスを作成します。

速度とメモリ使用量を少なくするために、それらをディスクベースのハッシュテーブルに保存します。簡単にするために、それらをデータベースに保存します。

何をするにしても、文字列ではなく、プレーンなバイナリデータとして保存する必要があります。

8
Cory Nelson

ハッシュセットは、データをバケット(配列)に分割します。 64ビットシステムでは、 配列のサイズ制限は2 GB 、つまりおよそ2,000,000,000バイトです。

文字列は参照型であり、参照には8バイトが必要であるため(64ビットシステムを想定)、各バケットは文字列への約250,000,000(2億5000万)の参照を保持できます。それはあなたが必要とするものよりはるかに多いようです。

そうは言っても、Tim S.が指摘したように、参照がハッシュセットに収まるとしても、文字列自体を保持するために必要なメモリがある可能性はほとんどありません。データベースは私がこれにはるかに適しているでしょう。

8
dcastro

ほとんどの言語のほとんどのコレクションは、実際にはそのような規模に合わせて設計または最適化されていないため、このような状況では注意する必要があります。すでにメモリ使用量を特定しているので、メモリ使用量も問題になります。

ここでの明確な勝者は、何らかの形式のデータベースを使用することです。 SQLデータベースか、適切なNoSQLデータベースがいくつかあります。

SQLサーバーは、大量のデータを追跡し、インデックスを作成し、それらのインデックス全体を検索およびクエリするために、すでに設計および最適化されています。それはあなたがやろうとしていることを正確に行うために設計されているので、本当に最善の方法です。

パフォーマンスのために、プロセス内で実行され、結果として生じる通信オーバーヘッドを節約する組み込みデータベースの使用を検討できます。 Javaその目的のためにDerbyデータベースを推奨できます。そこで推奨するのに十分なC#の同等物を認識していませんが、適切なデータベースが存在すると思います。

7
Tim B

(クラスター化インデックス付き)テーブル内のすべてのレコードをダンプし(できれば文字列表現ではなく値を使用する(2))、SQLに検索を実行させるには時間がかかる場合があります。バイナリ検索を処理し、キャッシュを処理します。リストに変更を加える必要がある場合は、おそらく最も簡単に操作できます。そして、私は物事をクエリすることはあなた自身を構築することと同じくらい速く(またはより速く)なるとかなり確信しています。

(1):データをロードするには、SqlBulkCopyオブジェクトを確認します。 ADO.NETEntity Framework のように、データ行をロードするときに速度が遅くなります。行ごと。

(2):SHA-256 = 256ビットなので、binary(32)で十分です。これは、現在使用している64文字の半分にすぎません。 (または、使用している場合はその4分の1 nicode numbers = P)繰り返しになりますが、現在プレーンテキストファイルの情報がある場合でも、char(64)の方法で簡単に実行できます。 bcp.exeを使用してテーブルにデータをダンプします。データベースが大きくなり、クエリが少し遅くなります(より多くのI/Oが必要になり、キャッシュは同じ量のRAMの情報の半分しか保持しないため)など...しかし、それは非常に簡単です。結果に満足できない場合でも、独自のデータベースローダーを作成できます。

7
deroby

セットが定数の場合は、ソートされた大きなハッシュリストを作成します(生の形式で、それぞれ32バイト)。すべてのハッシュをディスクセクター(4KB)に収まるように保存し、各セクターの先頭がハッシュの先頭でもあるようにします。 N番目ごとのセクターの最初のハッシュを特別なインデックスリストに保存します。これはメモリに簡単に収まります。このインデックスリストでバイナリ検索を使用して、ハッシュがあるべきセクタークラスターの開始セクターを決定してから、このセクタークラスター内の別のバイナリ検索を使用してハッシュを見つけます。値Nは、テストデータを使用した測定に基づいて決定する必要があります。

編集:別の方法は、ディスクに独自のハッシュテーブルを実装することです。テーブルは オープンアドレス法 戦略を使用する必要があり、プローブシーケンスは可能な限り同じディスクセクターに制限する必要があります。空のスロットは特別な値(たとえば、すべてゼロ)でマークする必要があるため、この特別な値は、存在を照会するときに特別に処理する必要があります。衝突を避けるために、テーブルは値で80%以上満たされている必要があります。したがって、32バイトのサイズの1億エントリの場合、テーブルには少なくとも1億/ 80%= 1億2500万のスロットがあり、サイズが必要です。 125M * 32 = 4GBの。 2 ^ 256ドメインを125Mに変換するハッシュ関数といくつかのNiceプローブシーケンスを作成するだけで済みます。

6
Dialecticus

サフィックスツリー を試すことができます、これ 質問 C#でそれを行う方法について説明します

または、そのような検索を試すことができます

var matches = list.AsParallel().Where(s => s.Contains(searchTerm)).ToList();

AsParallelは、クエリの並列化を作成するため、処理を高速化するのに役立ちます。

5
datatest
  1. ハッシュをUInt32 [8]として保存します

2a。ソートされたリストを使用します。 2つのハッシュを比較するには、最初に最初の要素を比較します。それらが等しい場合は、2番目のものを比較します。

2b。プレフィックスツリーを使用する

2
Kirill Gamazkov

まず、リソースの消費を最小限に抑えるために、データ圧縮を使用することを強くお勧めします。キャッシュとメモリの帯域幅は、通常、最近のコンピュータで最も制限されているリソースです。これをどのように実装しても、最大のボトルネックはデータを待つことです。

また、既存のデータベースエンジンを使用することをお勧めします。それらの多くには圧縮機能が組み込まれており、どのデータベースでもRAM利用可能です。適切なオペレーティングシステムを使用している場合、システムキャッシュはそれと同じ量のファイルを保存します。しかし、ほとんどのデータベースには独自のキャッシングサブシステムがあります。

どのdbエンジンがあなたに最適かは本当にわかりません。試してみる必要があります。個人的には、パフォーマンスが高く、インメモリデータベースとファイルベースデータベースの両方として使用でき、透過的な圧縮が組み込まれているH2をよく使用します。

データをデータベースにインポートして検索インデックスを作成すると、カスタムソリューションよりも時間がかかる可能性があるとの意見もあります。それは本当かもしれませんが、インポートは通常非常にまれなことです。高速検索が最も一般的な操作である可能性が高いため、高速検索に関心があると想定します。

また、SQLデータベースが信頼性が高く、非常に高速である理由として、NoSQLデータベースを検討することをお勧めします。いくつかの選択肢を試してください。どのソリューションが最高のパフォーマンスを提供するかを知る唯一の方法は、それらをベンチマークすることです。

また、リストをテキストとして保存することに意味があるかどうかも検討する必要があります。おそらく、リストを数値に変換する必要があります。これにより、使用するスペースが少なくなるため、クエリが高速になります。データベースのインポートは大幅に遅くなる可能性がありますが、クエリは大幅に速くなる可能性があります。

1
user1657170

本当に高速にしたい場合で、要素が多かれ少なかれ不変であり、完全一致が必要な場合は、ウイルススキャナーのように動作するものを構築できます。エントリに関連するアルゴリズムを使用して、潜在的な要素の最小数を収集するスコープを設定します。検索条件を選択してから、それらのアイテムを繰り返し処理し、RtlCompareMemoryを使用して検索アイテムに対してテストします。アイテムがかなり連続している場合はディスクからアイテムをプルし、次のようなものを使用して比較できます。

    private Boolean CompareRegions(IntPtr hFile, long nPosition, IntPtr pCompare, UInt32 pSize)
    {
        IntPtr pBuffer = IntPtr.Zero;
        UInt32 iRead = 0;

        try
        {
            pBuffer = VirtualAlloc(IntPtr.Zero, pSize, MEM_COMMIT, PAGE_READWRITE);

            SetFilePointerEx(hFile, nPosition, IntPtr.Zero, FILE_BEGIN);
            if (ReadFile(hFile, pBuffer, pSize, ref iRead, IntPtr.Zero) == 0)
                return false;

            if (RtlCompareMemory(pCompare, pBuffer, pSize) == pSize)
                return true; // equal

            return false;
        }
        finally
        {
            if (pBuffer != IntPtr.Zero)
                VirtualFree(pBuffer, pSize, MEM_RELEASE);
        }
    }

この例を変更して、エントリでいっぱいの大きなバッファを取得し、それらをループします。しかし、マネージコードは進むべき道ではないかもしれません。最速は常に実際の作業を行う呼び出しに近いので、ストレートC上に構築されたカーネルモードアクセスを持つドライバーははるかに高速です。

1
JGU

まず、文字列は実際にはSHA256ハッシュであると言います。それを観察してください100 million * 256 bits = 3.2 gigabytesなので、メモリ効率の高いデータ構造を使用していると仮定すると、リスト全体をメモリに収めることができます。

時折の誤検知を許せば、実際にはそれよりも少ないメモリを使用できます。ブルームフィルターを参照してください http://billmill.org/bloomfilter-tutorial/

それ以外の場合は、ソートされたデータ構造を使用して高速クエリを実行します(時間計算量O(log n))。


本当にデータをメモリに保存したい場合(頻繁にクエリを実行し、高速な結果が必要なため)、Redisを試してください。 http://redis.io/

Redisは、オープンソースのBSDライセンスの高度なKey-Valueストアです。キーには文字列、ハッシュ、リスト、セット、およびソートされたセットを含めることができるため、データ構造サーバーと呼ばれることがよくあります。

データ型が設定されています http://redis.io/topics/data-types#sets

Redisセットは、順序付けられていない文字列のコレクションです。 O(1)(セット内に含まれる要素の数に関係なく一定時間)のメンバーの存在を追加、削除、およびテストすることができます。


それ以外の場合は、データをディスクに保存するデータベースを使用してください。

1
Colonel Panic

プレーンなVanillaバイナリ検索ツリーは、大きなリストで優れたルックアップパフォーマンスを提供します。ただし、文字列を実際に保存する必要がなく、単純なメンバーシップが知りたい場合は、ブルームフィルターが解決策になる可能性があります。ブルームフィルターは、すべての文字列を使用してトレーニングするコンパクトなデータ構造です。トレーニングが完了すると、以前に文字列を見たことがあるかどうかがすぐにわかります。偽陽性を報告することはめったにありませんが、偽陰性を報告することはありません。アプリケーションによっては、比較的少ないメモリですばやく驚くべき結果を生み出すことができます。

0
David Cecil

Insta's アプローチに似たソリューションを開発しましたが、いくつかの違いがあります。事実上、それは彼のチャンク配列ソリューションによく似ています。ただし、私のアプローチでは、単にデータを分割するのではなく、チャンクのインデックスを作成し、適切なチャンクのみに検索を指示します。

インデックスの作成方法はハッシュテーブルと非常によく似ており、各バケットは、バイナリ検索で検索できる並べ替えられた配列です。ただし、SHA256ハッシュのハッシュを計算する意味はほとんどないと考えたため、代わりに値のプレフィックスを取得します。

この手法の興味深い点は、インデックスキーの長さを拡張することで調整できることです。キーが長いほど、インデックスが大きくなり、バケットが小さくなります。私の8ビットのテストケースはおそらく小さい側にあります。 10〜12ビットの方がおそらく効果的です。

このアプローチのベンチマークを試みましたが、すぐにメモリが不足したため、パフォーマンスの面で興味深いものを見つけることができませんでした。

また、Cの実装も作成しました。 C実装は、指定されたサイズのデータ​​セットも処理できませんでした(テストマシンには4GBのRAMしかありません)が、それよりもいくらか管理しました。 (その場合、ターゲットデータセットは実際にはそれほど問題ではありませんでした。RAMをいっぱいにしたのはテストデータでした。)実際に十分な速度でデータをスローするための良い方法を見つけることができませんでした。テストされたパフォーマンスを参照してください。

私はこれを書くのを楽しんでいましたが、全体として、C#を使用してメモリ内でこれを実行しようとしてはならないという議論を支持する証拠を提供していると思います。

public interface IKeyed
{
    int ExtractKey();
}

struct Sha256_Long : IComparable<Sha256_Long>, IKeyed
{
    private UInt64 _piece1;
    private UInt64 _piece2;
    private UInt64 _piece3;
    private UInt64 _piece4;

    public Sha256_Long(string hex)
    {
        if (hex.Length != 64)
        {
            throw new ArgumentException("Hex string must contain exactly 64 digits.");
        }
        UInt64[] pieces = new UInt64[4];
        for (int i = 0; i < 4; i++)
        {
            pieces[i] = UInt64.Parse(hex.Substring(i * 8, 1), NumberStyles.HexNumber);
        }
        _piece1 = pieces[0];
        _piece2 = pieces[1];
        _piece3 = pieces[2];
        _piece4 = pieces[3];
    }

    public Sha256_Long(byte[] bytes)
    {
        if (bytes.Length != 32)
        {
            throw new ArgumentException("Sha256 values must be exactly 32 bytes.");
        }
        _piece1 = BitConverter.ToUInt64(bytes, 0);
        _piece2 = BitConverter.ToUInt64(bytes, 8);
        _piece3 = BitConverter.ToUInt64(bytes, 16);
        _piece4 = BitConverter.ToUInt64(bytes, 24);
    }

    public override string ToString()
    {
        return String.Format("{0:X}{0:X}{0:X}{0:X}", _piece1, _piece2, _piece3, _piece4);
    }

    public int CompareTo(Sha256_Long other)
    {
        if (this._piece1 < other._piece1) return -1;
        if (this._piece1 > other._piece1) return 1;
        if (this._piece2 < other._piece2) return -1;
        if (this._piece2 > other._piece2) return 1;
        if (this._piece3 < other._piece3) return -1;
        if (this._piece3 > other._piece3) return 1;
        if (this._piece4 < other._piece4) return -1;
        if (this._piece4 > other._piece4) return 1;
        return 0;
    }

    //-------------------------------------------------------------------
    // Implementation of key extraction

    public const int KeyBits = 8;
    private static UInt64 _keyMask;
    private static int _shiftBits;

    static Sha256_Long()
    {
        _keyMask = 0;
        for (int i = 0; i < KeyBits; i++)
        {
            _keyMask |= (UInt64)1 << i;
        }
        _shiftBits = 64 - KeyBits;
    }

    public int ExtractKey()
    {
        UInt64 keyRaw = _piece1 & _keyMask;
        return (int)(keyRaw >> _shiftBits);
    }
}

class IndexedSet<T> where T : IComparable<T>, IKeyed
{
    private T[][] _keyedSets;

    public IndexedSet(IEnumerable<T> source, int keyBits)
    {
        // Arrange elements into groups by key
        var keyedSetsInit = new Dictionary<int, List<T>>();
        foreach (T item in source)
        {
            int key = item.ExtractKey();
            List<T> vals;
            if (!keyedSetsInit.TryGetValue(key, out vals))
            {
                vals = new List<T>();
                keyedSetsInit.Add(key, vals);
            }
            vals.Add(item);
        }

        // Transform the above structure into a more efficient array-based structure
        int nKeys = 1 << keyBits;
        _keyedSets = new T[nKeys][];
        for (int key = 0; key < nKeys; key++)
        {
            List<T> vals;
            if (keyedSetsInit.TryGetValue(key, out vals))
            {
                _keyedSets[key] = vals.OrderBy(x => x).ToArray();
            }
        }
    }

    public bool Contains(T item)
    {
        int key = item.ExtractKey();
        if (_keyedSets[key] == null)
        {
            return false;
        }
        else
        {
            return Search(item, _keyedSets[key]);
        }
    }

    private bool Search(T item, T[] set)
    {
        int first = 0;
        int last = set.Length - 1;

        while (first <= last)
        {
            int midpoint = (first + last) / 2;
            int cmp = item.CompareTo(set[midpoint]);
            if (cmp == 0)
            {
                return true;
            }
            else if (cmp < 0)
            {
                last = midpoint - 1;
            }
            else
            {
                first = midpoint + 1;
            }
        }
        return false;
    }
}

class Program
{
    //private const int NTestItems = 100 * 1000 * 1000;
    private const int NTestItems = 1 * 1000 * 1000;

    private static Sha256_Long RandomHash(Random Rand)
    {
        var bytes = new byte[32];
        Rand.NextBytes(bytes);
        return new Sha256_Long(bytes);
    }

    static IEnumerable<Sha256_Long> GenerateRandomHashes(
        Random Rand, int nToGenerate)
    {
        for (int i = 0; i < nToGenerate; i++)
        {
            yield return RandomHash(Rand);
        }
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Generating test set.");

        var Rand = new Random();

        IndexedSet<Sha256_Long> set =
            new IndexedSet<Sha256_Long>(
                GenerateRandomHashes(Rand, NTestItems),
                Sha256_Long.KeyBits);

        Console.WriteLine("Testing with random input.");

        int nFound = 0;
        int nItems = NTestItems;
        int waypointDistance = 100000;
        int waypoint = 0;
        for (int i = 0; i < nItems; i++)
        {
            if (++waypoint == waypointDistance)
            {
                Console.WriteLine("Test lookups complete: " + (i + 1));
                waypoint = 0;
            }
            var item = RandomHash(Rand);
            nFound += set.Contains(item) ? 1 : 0;
        }

        Console.WriteLine("Testing complete.");
        Console.WriteLine(String.Format("Found: {0} / {0}", nFound, nItems));
        Console.ReadKey();
    }
}
0
Nate C-K