web-dev-qa-db-fra.com

Efficacité de très grandes collections; itération et tri

J'ai un analyseur csv qui lit dans plus de 15 millions de lignes (avec de nombreux doublons), et une fois analysé en structures, doit être ajouté à une collection. Chaque structure a des propriétés Key (int), A (datetime) et B(int) (et d'autres qui ne sont pas pertinentes ici).

Exigence A: La collection doit appliquer l'unicité par une clé.

Exigence B: Dans une étape ultérieure, j'ai besoin de la collection triée par propriétés A(timestamp) puis B (int).

Contrainte: Les structures doivent finalement être parcourues dans l'ordre, une par une, avec des références aux voisins (une LinkedList présente ici la solution la plus propre); le point de cette opération est de partitionner l'ensemble. Veuillez supposer que c'est la première fois que le partitionnement peut se produire (c'est-à-dire qu'il ne peut pas être partitionné au stade de l'analyse).

J'ai trouvé que le SortedSet fonctionne assez bien pour l'exigence A, et il est également très performant, même si les insertions O (log n) sont beaucoup plus lentes qu'avec HashSet<T> est O (1), bien que je n'aime pas trier la clé. HashSet<T> s'embourbe lorsque la collection devient énorme, ce qui est apparemment un problème connu, tandis que SortedSet<T> ne souffre pas de cet inconvénient.

Le problème: Lorsque j'arrive à l'étape de l'exigence B, tri de la collection (un SortedSet<T> passé à une méthode en tant que IEnumerable<T>) prend un temps prohibitif (plus de 20 minutes de meulage, tout en mémoire, pas d'utilisation de fichier d'échange).

La question: Quelle (s) collection (s) est (sont) la (s) mieux adaptée (s) pour résoudre ce problème? Une idée consiste à utiliser deux collections: une pour appliquer l'unicité (comme un HashSet<int> ou SortedSet<int> des clés), et une seconde SortedSet<T> pour gérer le tri au stade de l'analyse (c'est-à-dire le plus en amont possible). Mais l'application est déjà gourmande en mémoire et les pénalités de performances liées à la nécessité du fichier d'échange sont prohibitives.
Quelles options cela me laisse-t-il pour une collection unique qui applique l'unicité par une caractéristique, mais trie par d'autres caractéristiques non liées? SortedSet<T> les usages IComparer<T> (mais pas les deux IComparer<T> et IEquitable<T>), donc s'il s'appuie sur CompareTo pour imposer l'unicité, il ne semble pas correspondre à mes exigences. Le sous-classement SortedSet est-il la voie à suivre?

Edit: Le code de tri:

SortedSet<Dto> parsedSet = {stuff};
var sortedLinkedStructs = new LinkedList<Dto>(parsedSet.OrderBy(t => t.Timestamp).ThenBy(i => i.SomeInt));

La structure:

public readonly struct Dto: IEquatable<Dto>, IComparer<Dto>, IComparable<Dto>
{
     public readonly datetime Timestamp;
     public readonly int SomeInt;
     public readonly int Key;

     ctor(ts, int, key){assigned}

     public bool Equals(Dtoother) => this.Key == other.Key;
     public override int GetHashCode() => this.Key.GetHashCode();
     public int Compare(Dto x, Dto y) =>  x.Key.CompareTo(y.Key);
     public int CompareTo(Dto other) => this.Key.CompareTo(other.Key);
}
50
Kevin Fichter

Ce n'est peut-être pas une réponse directe, mais: c'est un moyen que j'ai utilisé avec succès pour un système similaire d'échelle similaire. C'est pour le "moteur de balise" qui pilote les listes de questions ici sur Stack Overflow; Essentiellement, j'ai:

struct Question {
    // basic members - score, dates, id, etc - no text
}

et en gros un Question[] surdimensionné (en fait j'utilise un Question* dans la mémoire non gérée, mais c'est parce que je dois pouvoir le partager avec du code GPU pour raisons indépendantes). Remplir les données consiste simplement à supprimer des lignes successives dans le Question[]. Ces données ne sont jamais triées - elles sont laissées seules en tant que données source - avec juste ajout (nouvelle clé) ou écrasement (même clé); au pire nous pourrions avoir besoin de réallouer et de copier les données dans un nouveau tableau si nous atteignons la capacité maximale.

Maintenant, au lieu de trier ces données, je séparément garde un int[] (En fait int* Pour la même raison qu'auparavant , mais ... meh), où chaque valeur du int[] est l'index du réel données dans le Question[]. Donc, initialement, il peut s'agir de 0, 1, 2, 3, 4, 5, ... (Bien que je préfiltre cela, il ne contient donc que les lignes que je souhaite conserver - en supprimant "supprimé", etc.).

en utilisant soit un tri rapide parallèle modificateur (voir http://stackoverflow.com/questions/1897458/parallel-sort-algorithm ) ou un "tri introspectif" modifié ( comme ici ) - donc à la fin du tri, je pourrais avoir 0, 3, 1, 5, ....

Maintenant: pour parcourir les données, je viens de parcourir le int[], Et de l'utiliser comme une recherche pour les données réelles dans le Question[]. Cela minimise la quantité de mouvement de données pendant un tri et me permet de conserver plusieurs triés séparément (peut-être avec différents préfiltres) de manière très efficace. Il suffit de quelques millisecondes pour trier les 15 millions de données (ce qui se produit toutes les minutes environ pour introduire de nouvelles questions dans Stack Overflow ou pour noter les modifications apportées aux questions existantes).

Pour rendre le tri aussi rapide que possible, j'essaie d'écrire mon code de tri de sorte qu'un tri composite puisse être représenté par une valeur unique entière, permettant très tri efficace (utilisable par le tri introspectif). Par exemple, voici le code du tri "date de la dernière activité, puis identifiant de la question":

public override bool SupportsNaturallySortableUInt64 => true;
public override unsafe ulong GetNaturallySortableUInt64(Question* question)
{
    // compose the data (MSB) and ID (LSB)
    var val = Promote(question->LastActivityDate) << 32
        | Promote(question->Id);
    return ~val; // the same as ulong.MaxValue - val (which reverses order) but much cheaper
}

Cela fonctionne en traitant le LastActivityDate comme un entier 32 bits, décalé vers la gauche de 32 bits et en le composant avec le Id comme un entier 32 bits, ce qui signifie que nous pouvons comparer la date et l'id en une seule opération.

Ou pour "score, puis réponse score, puis id":

public override unsafe ulong GetNaturallySortableUInt64(Question* question)
{
    // compose the data
    var val = Promote(question->Score) << 48
        | Promote(question->AnswerScore) << 32
        | Promote(question->Id);
    return ~val; // the same as ulong.MaxValue - val (which reverses order) but much cheaper
}

Notez que GetNaturallySortableUInt64 N'est appelé qu'une seule fois par élément - dans une zone de travail d'un ulong[] (Oui, en fait un ulong*) De la même taille, donc initialement les deux espaces de travail sont quelque chose comme:

int[]    ulong[]
0        34243478238974
1        12319388173
2        2349245938453
...      ...

Maintenant, je peux faire tout le tri en regardant simplement un int[] Et un ulong[], De telle sorte que le vecteur ulong[] Se retrouve dans l'ordre de tri, et le int[] contient les indices des éléments à consulter.

82
Marc Gravell