web-dev-qa-db-fra.com

Quelle est la méthode de recherche/extraction appropriée pour une très longue liste de chaînes?

Ce n'est pas une question terriblement rare, mais je n'arrive toujours pas à trouver une réponse qui explique vraiment le choix.

J'ai une très grande liste de chaînes (représentations ASCII de SHA-256 hashes, pour être exact), et j'ai besoin de demander la présence d'une chaîne dans cette liste.

Il y aura probablement plus de 100 millions d'entrées dans cette liste, et je devrai interroger à plusieurs reprises la présence d'une entrée à plusieurs reprises.

Étant donné la taille, je doute d’être capable de tout ranger dans un HashSet<string>. Quel serait un système de récupération approprié pour optimiser les performances?

Je peux pré-trier la liste, je peux le mettre dans une table SQL, je peux le mettre dans un fichier texte, mais je ne suis pas sûr de ce qui est le plus logique compte tenu de mon application.

Y at-il un gagnant clair en termes de performance parmi ceux-ci, ou d’autres méthodes de récupération?

64
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");
        }
    }
}

Les résultats sont plutôt prometteurs. Ils fonctionnent en mono-thread. La version à hachage peut atteindre un peu plus d'un million de recherches par seconde à 7,9 Go RAM utilisation. La version basée sur la baie utilise moins de RAM (4,6 Go). Les temps de démarrage entre les deux sont presque identiques (388 vs 391 secondes). Le hashset échange RAM en performances de recherche. Les deux devaient être compartimentés en raison de contraintes d'allocation de mémoire.

Performances du tableau:

Le hachage et l'addition ont pris 307408ms

Le nettoyage du hachage (le tri, en général) a pris 81892ms

30000000 éléments trouvés (30000000 prévus) à 562585ms [53k recherches par seconde]

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

Hashset performance:

Le hachage et l'addition ont pris 391105ms

Le nettoyage du hachage (le tri, en général) prend 0 ms

30000000 éléments trouvés (30000000 attendus) en 74864ms [400k recherches par seconde]

62
Bryan Boettcher

Si la liste change avec le temps, je la mettrais dans une base de données. 

Si la liste ne change pas, je la placerais dans un fichier trié et ferais une recherche binaire pour chaque requête.

Dans les deux cas, j'utiliserais un filtre de Bloom pour minimiser les E/S. Et j'arrêterais d'utiliser des chaînes et utiliserais la représentation binaire avec quatre ulongs (pour éviter le coût de référence de l'objet).

Si vous avez plus de 16 Go (2 * 64 * 4/3 * 100 M, en supposant le codage Base64 ), une option consiste à créer une chaîne et à être heureux. Bien sûr, cela correspond à moins de 7 Go si vous utilisez la représentation binaire.

La réponse de David Haney nous montre que le coût en mémoire n'est pas aussi facile à calculer.

21
Juan Lopes

Avec <gcAllowVeryLargeObjects>, vous pouvez avoir des tableaux beaucoup plus grands. Pourquoi ne pas convertir ces représentations ASCII de codes de hachage 256 bits en une structure personnalisée qui implémente IComparable<T>? Cela ressemblerait à ceci:

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);
    }
}

Vous pouvez ensuite créer un tableau de ces éléments, qui occuperait environ 3,2 Go. Vous pouvez le chercher assez facilement avec Array.BinarySearch .

Bien sûr, vous devrez convertir l'entrée de l'utilisateur de ASCII en l'une de ces structures de code de hachage, mais c'est assez simple.

En ce qui concerne les performances, cela ne sera pas aussi rapide qu'une table de hachage, mais certainement plus rapide qu'une recherche dans une base de données ou une opération sur un fichier.

En y réfléchissant, vous pourriez créer un HashSet<MyHashCode>. Vous devez remplacer la méthode Equals sur MyHashCode, mais c'est très simple. Si je me souviens bien, la HashSet coûte environ 24 octets par entrée, et vous auriez le coût supplémentaire de la plus grande structure. Figure cinq ou six gigaoctets, total, si vous utilisiez une variable HashSet. Plus de mémoire, mais toujours faisable, et vous obtenez O(1) lookup.

17
Jim Mischel

Ces réponses ne prennent pas en compte la mémoire de chaîne dans l'application. Les chaînes ne sont pas 1 caractère == 1 octet dans .NET. Chaque objet chaîne nécessite une constante de 20 octets pour les données de l'objet. Et le tampon nécessite 2 octets par caractère. Par conséquent: l'estimation de l'utilisation de la mémoire pour une instance de chaîne est de 20 + (2 * longueur) octets.

Faisons des maths.

  • 100 000 000 chaînes UNIQUE
  • SHA256 = 32 octets (256 bits)
  • taille de chaque chaîne = 20 + (2 * 32 octets) = 84 octets
  • Mémoire totale requise: 8 400 000 000 octets = 8,01 gigaoctets

Il est possible de le faire, mais cela ne se stockera pas bien dans la mémoire .NET. Votre objectif devrait être de charger toutes ces données dans un formulaire accessible/paginé sans tout conserver en mémoire en même temps. Pour cela, j’utilise Lucene.net qui stockera vos données sur disque et les recherchera intelligemment. Ecrivez chaque chaîne en tant que recherche dans un index, puis recherchez-la dans l'index. Maintenant, vous avez une application évolutive qui peut gérer ce problème; votre seule limitation sera l’espace disque (et il faudrait beaucoup de chaîne pour remplir un lecteur de téraoctet). Vous pouvez également placer ces enregistrements dans une base de données et interroger celle-ci. C'est pourquoi les bases de données existent: pour conserver des choses en dehors de la RAM. :)

15
Haney

Pour une vitesse maximale, conservez-les dans la RAM. Cela ne représente qu'environ 3 Go de données, plus les frais généraux de votre structure de données. Un HashSet<byte[]> devrait fonctionner correctement. Si vous souhaitez réduire les frais généraux et la pression GC, activez <gcAllowVeryLargeObjects> , utilisez un seul byte[] et un HashSet<int> avec un comparateur personnalisé pour y indexer.

Pour une utilisation rapide et rapide de la mémoire, stockez-les dans une table de hachage basée sur disque. Par souci de simplicité, stockez-les dans une base de données.

Quoi que vous fassiez, vous devriez les stocker sous forme de données binaires simples, et non de chaînes.

8
Cory Nelson

Un hachage divise vos données en compartiments (tableaux). Sur un système 64 bits, la taille maximale d'un groupe est de 2 Go , ce qui correspond à environ 2 000 000 000 octets.

Étant donné qu'une chaîne est un type de référence et qu'une référence prenant huit octets (en supposant un système 64 bits), chaque compartiment peut contenir environ 250 000 000 (250 millions) de références à des chaînes. Cela semble être bien plus que ce dont vous avez besoin.

Cela étant dit, comme Tim S. l'a fait remarquer, il est fort peu probable que vous disposiez de la mémoire nécessaire pour tenir les chaînes elles-mêmes, même si les références s'inscrivent dans le hachage. Une base de données me conviendrait beaucoup mieux pour cela.

8
dcastro

Vous devez faire attention dans ce genre de situation car la plupart des collections dans la plupart des langues ne sont pas vraiment conçues ou optimisées pour ce type d'échelle. Comme vous avez déjà identifié l'utilisation de la mémoire sera également un problème.

Le gagnant évident est d'utiliser une forme de base de données. Soit une base de données SQL, soit un certain nombre de bases NoSQL qui conviendraient.

Le serveur SQL est déjà conçu et optimisé pour garder trace de grandes quantités de données, les indexer, ainsi que pour rechercher et interroger ces index. Il est conçu pour faire exactement ce que vous essayez de faire, ce serait vraiment la meilleure voie à suivre.

Pour des performances optimales, vous pouvez envisager d'utiliser une base de données intégrée qui s'exécutera dans votre processus et économisera le temps système de communication qui en résulte. Pour Java, je pourrais recommander une base de données Derby à cet effet. Je ne connais pas suffisamment les équivalents C # pour pouvoir faire une recommandation, mais j'imagine que des bases de données appropriées existent.

7
Tim B

Cela peut prendre un certain temps (1) pour vider tous les enregistrements d'une table (indexée par cluster) (utilisez de préférence leurs valeurs, pas leur représentation sous forme de chaîne (2)) et laissez SQL faire la recherche. Il gérera la recherche binaire pour vous, la mise en cache pour vous et c’est probablement la chose la plus facile à utiliser si vous devez modifier la liste. Et je suis à peu près sûr que poser des questions sera aussi rapide (ou plus rapide) que de créer les vôtres.

(1): Pour charger les données, jetez un coup d'œil à l'objet SqlBulkCopy. Des choses comme ADO.NET ou Entity Framework vont être trop lentes car elles chargent les données ligne par ligne.

(2): SHA-256 = 256 bits, donc un binaire (32) fera l'affaire; qui ne représente que la moitié des 64 caractères que vous utilisez actuellement. (Ou un quart si vous utilisez Unicode numbers = P) Ensuite, si vous avez actuellement les informations dans un fichier texte brut, vous pouvez toujours utiliser la méthode char (64) et simplement dumper les données. dans la table en utilisant bcp.exe. La base de données sera plus grande et les requêtes légèrement plus lentes (plus d’entrées/sorties sont nécessaires + le cache ne contient que la moitié des informations pour la même quantité de RAM), etc ... Mais c’est assez simple à faire, et si Si vous n'êtes pas satisfait du résultat, vous pouvez toujours écrire votre propre chargeur de base de données.

7
deroby

Si l'ensemble est constant, créez simplement une grande liste de hachage triée (au format brut, 32 octets chacun). Stockez tous les hachages de manière à ce qu’ils s’adaptent aux secteurs du disque (4 Ko) et que le début de chaque secteur soit également le début d’un hachage. Enregistrez le premier hachage de chaque Nième secteur dans une liste d'index spéciale, qui sera facilement stockée dans la mémoire. Utilisez la recherche binaire sur cette liste d'index pour déterminer le secteur de départ d'un cluster de secteurs où le hachage doit être, puis utilisez une autre recherche binaire au sein de ce cluster pour trouver votre hachage. La valeur N doit être déterminée sur la base d'une mesure avec des données de test.

EDIT: une alternative serait d’implémenter votre propre table de hachage sur le disque. La table doit utiliser adressage ouvert stratégie, et la séquence de vérification doit être limitée autant que possible au même secteur de disque. Les emplacements vides doivent être marqués avec une valeur spéciale (par exemple, tous les zéros), de sorte que cette valeur spéciale doit être spécialement gérée lors de la recherche de l'existence. Pour éviter les collisions, le tableau ne doit pas contenir moins de 80% de valeurs. Dans votre cas, avec 100 millions d'entrées de 32 octets, le tableau doit comporter au moins 100 M/80% = 125 millions d'emplacements et la taille de 125M * 32 = 4 GB. Il vous suffit de créer la fonction de hachage qui convertirait 2 ^ 256 domaines en 125 Mo et une séquence de sonde Nice.

6
Dialecticus

Vous pouvez essayer un Suffix Tree , this question explique comment le faire en C #

Ou vous pouvez essayer une recherche comme

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

AsParallel aidera à accélérer les choses car il crée une parallélisation d'une requête.

5
datatest
  1. Stockez vos hachages comme UInt32 [8]

2a. Utilisez la liste triée. Pour comparer deux hachages, comparez d’abord leurs premiers éléments; s'ils sont égaux, comparez les deuxièmes et ainsi de suite.

2b. Utiliser l'arbre de préfixe

2
Kirill Gamazkov

Tout d'abord, je vous recommande vivement d'utiliser la compression des données afin de minimiser la consommation de ressources. Le cache et la bande passante mémoire sont généralement la ressource la plus limitée d'un ordinateur moderne. Peu importe la manière dont vous l'implémentez, le plus gros goulot d'étranglement attendra les données.

Aussi, je recommanderais d'utiliser un moteur de base de données existant. Beaucoup d'entre eux ont une compression intégrée et toute base de données utiliserait le RAM dont vous disposez. Si vous avez un système d'exploitation correct, le cache système stockera autant de fichiers que possible. Mais la plupart des bases de données ont leur propre sous-système de mise en cache.

Je ne peux pas vraiment dire quel moteur de base de données sera le mieux pour vous, vous devez les essayer. Personnellement, j'utilise souvent H2, qui offre des performances correctes et peut être utilisé à la fois comme base de données en mémoire et base de données, et intégrant une compression transparente. 

Je vois que certains ont déclaré que l'importation de vos données dans une base de données et la construction de l'index de recherche pouvaient prendre plus de temps qu'une solution personnalisée. C'est peut-être vrai, mais importer est généralement quelque chose d'assez rare. Je vais supposer que vous êtes plus intéressé par les recherches rapides car elles sont probablement l'opération la plus courante.

Aussi, pourquoi les bases de données SQL sont à la fois fiables et assez rapides, vous pouvez envisager les bases de données NoSQL. Essayez quelques alternatives. La seule façon de savoir quelle solution vous donnera les meilleures performances est de les comparer.

Vous devez également vous demander si le stockage de votre liste sous forme de texte a du sens. Peut-être devriez-vous convertir la liste en valeurs numériques. Cela utilisera moins d’espace et vous donnera donc des requêtes plus rapides. L'importation de base de données peut être considérablement plus lente, mais les requêtes peuvent devenir beaucoup plus rapides.

1
user1657170

Si vous voulez vraiment aller vite et que les éléments sont plus ou moins immuables et nécessitent des correspondances exactes, vous pouvez construire quelque chose qui fonctionne comme un antivirus: définissez la portée pour collecter le nombre minimum d'éléments potentiels en utilisant les algorithmes pertinents pour vos entrées et critères de recherche, puis parcourez ces éléments, testez-les par rapport à l'élément de recherche à l'aide de RtlCompareMemory .. Vous pouvez extraire les éléments du disque s'ils sont assez contigus et les comparer à l'aide de quelque chose comme ceci:

    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);
        }
    }

Je modifierais cet exemple pour récupérer un grand tampon rempli d'entrées et les parcourir en boucle. Mais le code managé n'est peut-être pas la solution. Le plus rapide est toujours plus proche des appels qui font le travail, donc un pilote avec un accès en mode noyau basé sur le droit C serait beaucoup plus rapide.

1
JGU

Premièrement, vous dites que les chaînes sont vraiment des hachages SHA256. Observez ce 100 million * 256 bits = 3.2 gigabytes afin qu'il soit possible d'ajuster toute la liste en mémoire, en supposant que vous utilisiez une structure de données économe en mémoire.

Si vous oubliez des faux positifs occasionnels, vous pouvez utiliser moins de mémoire que cela. Voir les filtres de floraison http://billmill.org/bloomfilter-tutorial/

Sinon, utilisez une structure de données triée pour une interrogation rapide (complexité temporelle O (log n)).


Si vous voulez vraiment stocker les données en mémoire (parce que vous interrogez fréquemment et avez besoin de résultats rapides), essayez Redis. http://redis.io/

Redis est un magasin avancé à clé-valeur open source, sous licence BSD. Il est souvent désigné sous le nom de serveur de structure de données car les clés peuvent contenir des chaînes, des hachages, des listes, des ensembles et des ensembles triés.

Il a un type de données set http://redis.io/topics/data-types#sets

Les ensembles Redis sont une collection non ordonnée de chaînes. Il est possible d'ajouter, de supprimer et de tester l'existence de membres dans O(1) (temps constant, quel que soit le nombre d'éléments contenus dans l'ensemble).


Sinon, utilisez une base de données qui enregistre les données sur le disque.

1
Colonel Panic

Un arbre de recherche binaire Plain Vanilla donnera d'excellentes performances de recherche sur les grandes listes. Cependant, si vous n'avez pas vraiment besoin de stocker les chaînes et que vous voulez savoir si vous êtes un simple abonnement, un filtre Bloom peut être une solution efficace. Les filtres Bloom sont une structure de données compacte que vous entraînez avec toutes les chaînes. Une fois formé, il peut rapidement vous dire s'il a déjà vu une chaîne. Il signale rarement de faux positifs, mais ne signale jamais de faux négatifs. Selon l'application, ils peuvent produire des résultats étonnants rapidement et avec relativement peu de mémoire.

0
David Cecil

J'ai développé une solution similaire à celle de Insta , mais avec quelques différences. En fait, cela ressemble beaucoup à sa solution de tableau fragmenté. Cependant, au lieu de simplement diviser les données, mon approche crée un index de morceaux et dirige la recherche uniquement vers le morceau approprié.

La manière dont l'index est construit est très similaire à une table de hachage, chaque compartiment étant un tableau trié pouvant être recherché avec une recherche binaire. Cependant, j'ai pensé qu'il était inutile de calculer un hachage d'un hachage SHA256, alors je prends plutôt un préfixe de la valeur.

La chose intéressante à propos de cette technique est que vous pouvez l’accorder en allongeant la longueur des clés d’index. Une clé plus longue signifie un index plus grand et des compartiments plus petits. Mon cas de test de 8 bits est probablement petit. 10-12 bits seraient probablement plus efficaces.

J'ai essayé de comparer cette approche, mais comme elle manquait rapidement de mémoire, je ne pouvais rien voir d'intéressant en termes de performances.

J'ai aussi écrit une implémentation en C. L’implémentation C n’a pas non plus été en mesure de traiter un ensemble de données de la taille spécifiée (la machine de test n’a que 4 Go de RAM), mais elle en a géré un peu plus. (L'ensemble de données cible ne posait pas vraiment de problème dans ce cas, c'étaient les données de test qui remplissaient la RAM.) voir ses performances testées.

Bien que j'ai aimé écrire ceci, je dirais que dans l'ensemble, cela fournit principalement des preuves en faveur de l'argument selon lequel vous ne devriez pas essayer de faire cela en mémoire avec 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