LINQ Performance pour les Grandes Collections
J'ai une grande collection de chaînes de caractères (jusqu'à 1M) triés par ordre alphabétique. J'ai expérimenté avec des requêtes LINQ contre cette collection à l'aide de HashSet, SortedDictionary, et le Dictionnaire. Je suis statique de la mise en cache de la collection, c'est jusqu'à 50 mo en taille, et je suis toujours à l'appel de la requête LINQ contre la mise en cache de la collection. Mon problème est comme suit:
Indépendamment du type de collection, les performances sont beaucoup plus pauvres que SQL (jusqu'à 200 ms). Lorsque vous faites une requête similaire contre le sous-jacent de tables SQL, la performance est beaucoup plus rapide ( 5-10ms). J'ai mis en œuvre mes requêtes LINQ comme suit:
public static string ReturnSomething(string query, int limit)
{
StringBuilder sb = new StringBuilder();
foreach (var stringitem in MyCollection.Where(
x => x.StartsWith(query) && x.Length > q.Length).Take(limit))
{
sb.Append(stringitem);
}
return sb.ToString();
}
C'est ma compréhension que le HashSet, Dictionnaire, etc. mettre en œuvre des recherches à l'aide d'arbres binaires de recherche au lieu de la norme de l'énumération. Quelles sont mes options pour la haute performance des requêtes LINQ dans l'avancée des types de collection?
Vous devez vous connecter pour publier un commentaire.
Dans votre code actuel, vous ne faites pas usage de l'une des caractéristiques particulières de l'
Dictionary
/SortedDictionary
/HashSet
collections, vous utilisez la même manière que vous utiliseriez unList
. C'est pourquoi vous ne verrez pas de différence dans la performance.Si vous utilisez un dictionnaire, index, où les premiers caractères de la chaîne est la clé et une liste de chaînes de valeur, vous pouvez à partir de la chaîne de recherche choisir une petite partie de l'ensemble de la collection de chaînes de caractères qui a des correspondances possibles.
J'ai écrit la catégorie ci-dessous pour tester cela. Si je le remplir avec un million de cordes et de recherche avec une chaîne de huit caractères, il déchire toutes les correspondances possibles dans environ 3 ms. Recherche une chaîne de caractères est le pire des cas, mais il trouve les 1000 premiers matches sur 4 mme. Trouver tous les matches pour une seule des chaînes de caractères prend environ 25 ms.
La classe crée des index pour 1, 2, 4 et 8 touches de caractères. Si vous regardez vos données et ce que vous recherchez, vous devriez être en mesure de sélectionner quels de créer des index pour optimiser vos conditions.
Je parie que vous avez un index sur la colonne SQL server peut faire la comparaison en O(log(n)) opérations plutôt que de O(n). Pour imiter le comportement de SQL server, utilisez une collection triée et trouver toutes les chaînes de s tel que s >= requête et puis regarder les valeurs jusqu'à ce que vous trouver une valeur qui ne commence pas par s et ensuite faire un filtre supplémentaire sur les valeurs. C'est ce qu'on appelle une analyse de plage (Oracle) ou une recherche d'index (SQL server).
C'est un exemple de code qui est très susceptible d'aller dans une boucle infinie ou ont one-off des erreurs parce que je n'ai pas fait de test, mais vous devez obtenir l'idée.
Si vous êtes en train de faire une "commence par", vous ne se soucient ordinale des comparaisons, et vous pouvez avoir la collection triée (encore une fois dans l'ordinal de l'ordre), alors je vous suggère d'avoir les valeurs dans une liste. Vous pouvez ensuite binaire de recherche pour trouver la première valeur qui commence avec le bon préfixe, puis allez en bas de la liste de façon linéaire donne des résultats jusqu'à ce que la première valeur qui n'est pas commencer avec le bon préfixe.
En fait, vous pourriez probablement faire une autre recherche binaire pour la première valeur qui ne commence pas par le préfixe, alors vous pourriez avoir un début et une fin. Ensuite, vous avez juste besoin d'appliquer la longueur critère pour que la partie correspondante. (J'espère que si il est judicieux de données, la mise en correspondance de préfixe va se débarrasser de la plupart des candidats de valeurs.) Le chemin pour trouver la première valeur qui ne commence pas par le préfixe est à la recherche de la lexicographiquement-première valeur qui n'est pas - par exemple avec un préfixe "ABC", recherche pour "ABD".
Rien de tout cela utilise LINQ, et c'est très spécifique à votre cas particulier, mais il devrait fonctionner. Laissez-moi savoir si ce n'est pas logique.
Si vous essayez d'optimiser la recherche d'une liste de chaînes de caractères avec un préfixe que vous voudrez peut-être jeter un oeil à la mise en œuvre d'un Trie (à ne pas confondre avec un arbre) structure de données en C#.
Essaie offre très rapide préfixe recherches et ont une très faible charge de la mémoire par rapport à d'autres structures de données pour ce genre d'opération.
Sur LINQ to Objects en général. Il n'est pas rare d'avoir une réduction de la vitesse par rapport à SQL. Le net est jonché d'articles l'analyse de ses performances.
Simplement en regardant ton code, je dirais que vous devez réorganiser la comparaison de prendre avantage de court-circuit lors de l'utilisation d'opérateurs booléens:
La comparaison de la longueur est toujours va être un O(1) opération (comme la longueur est stockée comme une partie de la chaîne, il ne compte pas chaque personnage à chaque fois), alors que l'appel à StartsWith va être un O(N), où N est la longueur de la requête (ou de la longueur de la chaîne, selon le moindre des deux).
En plaçant la comparaison de la longueur avant l'appel à StartsWith, si la comparaison échoue, vous vous sauver de quelques cycles supplémentaires qui pourraient ajouter jusqu'à lors du traitement d'un grand nombre d'éléments.
Je ne pense pas qu'une table de recherche va vous aider ici, comme les tables de recherche sont bons quand vous comparez l'ensemble des clés, et non pas les parties de la clé, comme vous le faites avec l'appel à StartsWith.
Plutôt, vous pourriez être mieux à l'aide d'une structure de l'arbre qui est coupé basé sur les lettres dans les mots de la liste.
Toutefois, à ce stade, vous êtes vraiment juste recréer ce que SQL Server est en train de faire (dans le cas de l'index) et qui serait juste une duplication d'effort de votre part.
Je pense que le problème est que Linq n'a aucun moyen d'utiliser le fait que votre séquence est déjà triée. En particulier, il ne peut pas savoir, que l'application de la
StartsWith
fonction conserve l'ordre.Je vous suggérons d'utiliser le
List.BinarySearch
méthode avec unIComparer<string>
qui ne fait que la comparaison de la première requête caractères (cela peut être difficile, car il n'est pas clair, si la chaîne de requête sera toujours le premier ou le deuxième paramètre à()
).Vous pourriez même utiliser le standard de comparaison de chaîne, depuis BinarySearch retourne un nombre négatif que vous pouvez compléter (à l'aide de ~) afin d'obtenir l'indice du premier élément est plus grand que votre requête.
Vous avez ensuite commencer à partir de l'index renvoyées (dans les deux sens!) pour trouver tous les éléments correspondant à votre chaîne de requête.