On m'a dit que puisque .net linq est si lent, nous ne devrions pas l'utiliser et je me demandais que quelqu'un d'autre ait tiré la même conclusion, et l'exemple est:
Il a fallu 1443 ms pour faire 1000000000 de comparaisons non LINQ.
Il a fallu 4944 ms pour faire 1000000000 par rapport à LINQ.
(243% plus lent)
le code non LINQ:
for (int i = 0; i < 10000; i++)
{
foreach (MyLinqTestClass1 item in lst1) //100000 items in the list
{
if (item.Name == "9999")
{
isInGroup = true;
break;
}
}
}
Il a fallu 1443 ms pour faire 1000000000 de comparaisons non LINQ.
Code LINQ:
for (int i = 0; i < 10000; i++)
isInGroup = lst1.Cast<MyLinqTestClass1>().Any(item => item.Name == "9999");
Il a fallu 4944 ms pour faire 1000000000 par rapport à LINQ.
Je suppose qu'il est possible d'optimiser le code LINQ, mais l'idée était qu'il était facile d'obtenir du code LINQ vraiment lent et qu'il ne devrait pas être utilisé. Étant donné que LINQ est lent, il s'ensuit également que PLINQ est lent et NHibernate LINQ serait lent, donc aucun type d'instruction LINQ ne doit être utilisé.
Quelqu'un d'autre a-t-il trouvé que LINQ est si lent qu'il aurait souhaité ne jamais l'avoir utilisé, ou est-ce que je fais une conclusion trop générale basée sur des points de repère comme celui-ci?
Faut-il éviter Linq parce que c'est lent?
Non. Cela doit être évité s'il n'est pas assez rapide . Lent et pas assez rapide ne sont pas du tout la même chose!
Lent n'est pas pertinent pour vos clients, votre direction et vos parties prenantes. Pas assez rapide est extrêmement pertinent. Ne mesurez jamais la vitesse rapide de quelque chose; cela ne vous dit rien sur lequel vous pouvez fonder une décision commerciale. Mesurer à quel point il est proche d'être acceptable pour le client . Si c'est acceptable, arrêtez de dépenser de l'argent pour le rendre plus rapide; c'est déjà assez bon.
L'optimisation des performances est coûteuse . Écrire du code pour qu'il puisse être lu et maintenu par d'autres est coûteux . Ces objectifs sont souvent opposés, donc pour dépenser l'argent de votre partie prenante de manière responsable, vous devez vous assurer que vous ne consacrez que du temps et des efforts précieux à l'optimisation des performances sur des choses qui sont ) pas assez rapide .
Vous avez trouvé une situation de référence artificielle et irréaliste où le code LINQ est plus lent qu'une autre façon d'écrire le code. Je vous assure que vos clients ne se soucient pas du tout de la vitesse de votre référence irréaliste. Ils ne se soucient que si le programme que vous leur envoyez est trop lent pour eux. Et je vous assure que votre direction ne se soucie pas du tout de cela (si elle est compétente); ils se soucient de combien d'argent vous dépensez inutilement pour faire des choses qui sont assez rapides inaperçues plus rapidement, et qui rendent le code plus cher à lire, à comprendre, et maintenir dans le processus.
Pourquoi utilisez-vous Cast<T>()
? Vous ne nous avez pas donné suffisamment de code pour vraiment juger de la référence, en gros.
Oui, vous pouvez utiliser LINQ pour écrire du code lent. Devine quoi? Vous pouvez également écrire du code non LINQ lent.
LINQ grandement facilite l'expressivité du code traitant des données ... et ce n'est pas si difficile d'écrire du code qui fonctionne bien, tant que vous prenez le temps de comprendre LINQ pour commencer.
Si quelqu'un disait à moi de ne pas utiliser LINQ (en particulier LINQ to Objects) pour perç des raisons de vitesse, je leur rirais au nez. S'ils ont trouvé un goulot d'étranglement spécifique et ont dit: "Nous pouvons accélérer cela en n'utilisant pas LINQ dans cette situation, et voici les preuves", alors c'est une question très différente.
J'ai peut-être manqué quelque chose, mais je suis à peu près sûr que vos repères sont décalés.
J'ai testé avec les méthodes suivantes:
Any
("LINQ")foreach
(votre méthode "optimisée")ICollection.Contains
Any
utilisant une structure de données optimisée (HashSet<T>
)Voici le code de test:
class Program
{
static void Main(string[] args)
{
var names = Enumerable.Range(1, 10000).Select(i => i.ToString()).ToList();
var namesHash = new HashSet<string>(names);
string testName = "9999";
for (int i = 0; i < 10; i++)
{
Profiler.ReportRunningTimes(new Dictionary<string, Action>()
{
{ "Enumerable.Any", () => ExecuteContains(names, testName, ContainsAny) },
{ "ICollection.Contains", () => ExecuteContains(names, testName, ContainsCollection) },
{ "Foreach Loop", () => ExecuteContains(names, testName, ContainsLoop) },
{ "HashSet", () => ExecuteContains(namesHash, testName, ContainsCollection) }
},
(s, ts) => Console.WriteLine("{0, 20}: {1}", s, ts), 10000);
Console.WriteLine();
}
Console.ReadLine();
}
static bool ContainsAny(ICollection<string> names, string name)
{
return names.Any(s => s == name);
}
static bool ContainsCollection(ICollection<string> names, string name)
{
return names.Contains(name);
}
static bool ContainsLoop(ICollection<string> names, string name)
{
foreach (var currentName in names)
{
if (currentName == name)
return true;
}
return false;
}
static void ExecuteContains(ICollection<string> names, string name,
Func<ICollection<string>, string, bool> containsFunc)
{
if (containsFunc(names, name))
Trace.WriteLine("Found element in list.");
}
}
Ne vous inquiétez pas des éléments internes de la classe Profiler
. Il exécute simplement le Action
dans une boucle et utilise un Stopwatch
pour le chronométrer. Il s'assure également d'appeler GC.Collect()
avant chaque test pour éliminer autant de bruit que possible.
Voici les résultats:
Enumerable.Any: 00:00:03.4228475
ICollection.Contains: 00:00:01.5884240
Foreach Loop: 00:00:03.0360391
HashSet: 00:00:00.0016518
Enumerable.Any: 00:00:03.4037930
ICollection.Contains: 00:00:01.5918984
Foreach Loop: 00:00:03.0306881
HashSet: 00:00:00.0010133
Enumerable.Any: 00:00:03.4148203
ICollection.Contains: 00:00:01.5855388
Foreach Loop: 00:00:03.0279685
HashSet: 00:00:00.0010481
Enumerable.Any: 00:00:03.4101247
ICollection.Contains: 00:00:01.5842384
Foreach Loop: 00:00:03.0234608
HashSet: 00:00:00.0010258
Enumerable.Any: 00:00:03.4018359
ICollection.Contains: 00:00:01.5902487
Foreach Loop: 00:00:03.0312421
HashSet: 00:00:00.0010222
Les données sont très cohérentes et racontent l'histoire suivante:
Naïvement, utiliser la méthode d'extension Any
est environ 9% plus lent que naïvement utiliser une boucle foreach
.
Utiliser la méthode la plus appropriée (ICollection<string>.Contains
) Avec une structure de données non optimisée (List<string>
) Est environ 50% plus rapide que naïvement en utilisant une boucle foreach
.
L'utilisation d'une structure de données optimisée (HashSet<string>
) Élimine complètement toutes les autres méthodes en termes de performances.
Je n'ai aucune idée d'où vous avez obtenu 243%. Je suppose que cela a quelque chose à voir avec tout ce casting. Si vous utilisez un ArrayList
, non seulement vous utilisez une structure de données non optimisée, mais vous utilisez une structure de données largement obsolète .
Je peux prédire ce qui va suivre. "Oui, je sais que vous pouvez mieux l'optimiser, mais ce n'était qu'un exemple pour comparer les performances de LINQ et non LINQ."
Ouais, mais si vous ne pouvez même pas être minutieux dans votre exemple, comment pouvez-vous vous attendre à être aussi minutieux dans le code de production?
L'essentiel est le suivant:
La façon dont vous créez et concevez votre logiciel est exponentiellement plus importante que les outils spécifiques que vous utilisez et quand.
Si vous rencontrez des goulots d'étranglement en matière de performances - ce qui est tout aussi susceptible de se produire avec LINQ que sans -, résolvez-les. La suggestion d'Eric concernant les tests de performance automatisés est excellente; cela vous aidera à identifier les problèmes tôt afin que vous puissiez les résoudre correctement - pas en évitant un outil étonnant qui vous rend 80% plus productif mais qui entraîne une pénalité de performance <10%, mais en en fait enquêter sur le problème et proposer une vraie solution qui peut augmenter vos performances d'un facteur 2, 10 ou 100 ou plus.
Créer des applications hautes performances ne consiste pas à utiliser les bonnes bibliothèques. Il s'agit de profiler, faire de bons choix de conception et écrire un bon code.
LINQ est-il un goulot d'étranglement réel (affectant la performance globale ou perçue de l'application)?
Votre application effectuera-t-elle cette opération sur plus de 1 000 000 000 d'enregistrements dans le monde réel? Si c'est le cas - alors vous voudrez peut-être envisager des alternatives - sinon, c'est comme dire "nous ne pouvons pas acheter cette berline familiale parce qu'elle ne roule pas bien à 180 MPH +".
Si c'est "juste lent", alors ce n'est pas une très bonne raison ... par ce raisonnement, vous devriez tout écrire en asm/C/C++, et C # devrait être hors de la table pour être "trop lent".
Alors que la pessimisation prématurée est (à mon humble avis) aussi mauvaise que l'optimisation prématurée, vous ne devez pas exclure toute une technologie basée sur la vitesse absolue sans prendre en compte le contexte d'utilisation. Oui, si vous faites un calcul très lourd de nombres et c'est un goulot d'étranglement, LINQ pourrait être problématique - profile il.
Un argument que vous pourriez utiliser en faveur de LINQ est que, bien que vous puissiez probablement le surpasser avec du code manuscrit, la version LINQ pourrait probablement être plus claire et plus facile à maintenir - en plus, il y a l'avantage de PLINQ par rapport à la parallélisation manuelle complexe.
Le problème avec ce genre de comparaison, c'est que cela n'a pas de sens dans l'abstrait.
On pourrait battre l'un ou l'autre de ceux-ci si l'on commençait par hacher les objets MyLinqTestClass1 par leur propriété Name. Entre ceux-ci, si on pouvait les trier par nom et plus tard faire une recherche binaire. En effet, nous n'avons pas besoin de stocker les objets MyLinqTestClass1 pour cela, nous avons juste besoin de stocker les noms.
La taille de la mémoire est un problème? Peut-être stocker les noms dans une structure DAWG, combiner suffit et ensuite l'utiliser pour cette vérification?
La surcharge supplémentaire liée à la configuration de ces structures de données a-t-elle un sens? C'est impossible à dire.
Un autre problème est un problème différent avec le concept de LINQ, qui est son nom. C'est formidable à des fins de marketing pour MS de pouvoir dire "voici un tas de nouvelles choses intéressantes qui fonctionnent ensemble" mais moins bien quand il s'agit de personnes qui combinent des choses ensemble quand elles font le genre d'analyse où elles devraient les séparer . Vous avez un appel à Any
qui implémente fondamentalement le modèle de filtrage sur énumérable commun dans .NET2.0 jours (et non inconnu avec .NET1.1 bien qu'il soit plus difficile à écrire signifiait que c'était seulement utilisé là où ses avantages d'efficacité dans certains cas comptaient vraiment), vous avez des expressions lambda et vous avez des arbres de requêtes tous regroupés en un seul concept. Quel est le plus lent?
Je parie que la réponse ici est le lambda et non l'utilisation de Any
, mais je ne parierais pas beaucoup (par exemple, le succès d'un projet), je testerais et serais sûr. Pendant ce temps, la façon dont les expressions lambda fonctionnent avec IQueryable peut conduire à un code particulièrement efficace qu'il serait extrêmement difficile d'écrire avec une efficacité équivalente sans l'utilisation de lambdas.
N'arrive-t-il pas à être efficace quand LINQ est bon en efficacité parce qu'il a échoué à une référence artificielle? Je ne pense pas.
Utilisez LINQ là où cela a du sens.
Dans des conditions de goulot d'étranglement, puis s'éloignent ou vers LINQ bien que cela semble approprié ou inapproprié en tant qu'optimisation. N'écrivez pas d'abord du code difficile à comprendre, car vous compliquerez simplement l'optimisation réelle.
Pour moi, cela donne l'impression que vous travaillez sur un contrat et que l'employeur ne comprend pas LINQ ou ne comprend pas les goulots d'étranglement de performance du système. Si vous écrivez une application avec une interface graphique, l'impact mineur sur les performances de l'utilisation de LINQ est négligeable. Dans une interface graphique/application Web typique, les appels en mémoire représentent moins de 1% de tous les temps d'attente. Vous, ou plutôt votre employeur, essayez d'optimiser ce 1%. Est-ce vraiment bénéfique?
Cependant, si vous écrivez une application qui est scientifique ou fortement orientée vers les mathématiques, avec très peu d'accès au disque ou à la base de données, je conviens que LINQ n'est pas la voie à suivre.
BTW, le casting n'est pas nécessaire. Ce qui suit est fonctionnellement équivalent à votre premier test:
for (int i = 0; i < 10000; i++)
isInGroup = lst1.Any(item => item.Name == "9999");
Lorsque j'ai exécuté cela en utilisant une liste de test contenant 10000 objets MyLinqTestClass1, l'original a fonctionné en 2,79 secondes, le révisé en 3,43 secondes. Économiser 30% sur des opérations qui occupent probablement moins de 1% du temps processeur n'est pas une bonne utilisation de votre temps.
Peut-être que linq est lent, mais avec linq je peux paralléliser mon code très simplement.
Comme ça:
lst1.Cast<MyLinqTestClass1>().AsParallel().Any(item => item.Name == "9999");
Comment mettriez-vous en parallèle le cycle?
Voici une observation intéressante, puisque vous mentionnez que nHibernate est lent en raison de la lenteur de LINQ. Si vous utilisez LINQ to SQL (ou l'équivalent de nHibernate), votre code LINQ se traduit par une requête EXISTS sur le serveur SQL, où votre code de boucle doit d'abord récupérer toutes les lignes, puis les parcourir. Maintenant, vous pouvez facilement écrire un tel test afin que le code de boucle lise toutes les données une fois (recherche unique de base de données) pour toutes les exécutions 10K, mais le code LINQ exécute en fait des requêtes SQL 10K. Cela va probablement montrer un grand avantage de vitesse pour la version en boucle qui n'existe pas dans la réalité. En réalité, une seule requête EXISTS va surpasser à chaque fois l'analyse et la boucle de la table - même si vous n'avez pas d'index sur la colonne interrogée (ce que vous feriez probablement si cette requête est effectuée très souvent).
Je ne dis pas que c'est le cas avec votre test - nous n'avons pas assez de code pour voir - mais cela pourrait être le cas. Il se peut également qu'il y ait vraiment une différence de performances avec LINQ to Objects, mais cela peut ne pas se traduire du tout par LINQ to SQL. Vous devez savoir ce que vous mesurez et dans quelle mesure il est applicable à vos besoins réels.
"On m'a dit [par qui?] Que puisque .net linq est si lent [pour quoi?] Nous ne devrions pas l'utiliser"
D'après mon expérience, fonder des décisions telles que la technique, la bibliothèque ou le langage à utiliser uniquement sur ce que quelqu'un a une fois vous a dit que c'est une mauvaise idée.
Tout d'abord, les informations proviennent-elles d'une source de confiance? Sinon, vous pourriez faire une énorme erreur en faisant confiance à cette personne (peut-être inconnue) pour prendre vos décisions de conception. Deuxièmement, cette information est-elle toujours d'actualité? Mais d'accord, sur la base de votre benchmark simple et pas très réaliste, vous avez conclu que LINQ est plus lent que d'effectuer manuellement la même opération. La question naturelle à se poser est la suivante: les performances de ce code sont-elles essentielles? Les performances de ce code seront-elles limitées par d'autres facteurs que la vitesse d'exécution de ma requête LINQ - pensez aux requêtes de base de données, en attente d'E/S, etc. ?
Voici comment j'aime travailler:
Pour moi, cette méthode simple sert un seul but: maximiser ma productivité en minimiser le temps que je passe à améliorer du code qui est déjà parfaitement adéquat.
Oui, le jour viendra peut-être où vous constaterez que votre solution originale ne la coupe plus. Ou peut-être pas. Si c'est le cas, traitez-le sur-le-champ. Je vous suggère d'éviter de perdre votre temps à essayer de résoudre des problèmes hypothétiques (futurs).
Oui tu as raison. Il est facile d'écrire du code lent dans LINQ. Les autres ont raison aussi: il est facile d'écrire du code lent en C # sans LINQ.
J'ai écrit la même boucle que vous en C et elle a couru un certain nombre de millisecondes plus rapidement. La conclusion que j'en tire est que C # lui-même est lent.
Comme avec votre extension de boucle LINQ->, en C, il faudra plus de 5 fois plus de lignes de code pour faire la même chose, ce qui la rend plus lente à écrire, plus difficile à lire, plus susceptible d'avoir des bogues et plus difficile à trouver et corrigez-les, mais s'il est important de gagner quelques millisecondes pour chaque milliard d'itérations, c'est souvent ce qu'il faut.
Je dirais plutôt que vous devriez éviter d'essayer trop fort d'écrire le code le plus efficace, sauf que c'est obligatoire.
Étant donné que LINQ est lent, il s'ensuit également que PLINQ est lent et NHibernate LINQ serait lent, donc aucun type d'instruction LINQ ne doit être utilisé.
C'est un contexte bien différent, mais incroyablement différent. Un 1,4 contre 5 secondes pour l'ensemble du milliard d'opérations n'est pas pertinent lorsque vous parlez d'opérations d'accès aux données.
Comme vous l'avez démontré, il est possible d'écrire du code non LINQ qui fonctionne mieux que le code LINQ. Mais l'inverse est également possible. Compte tenu de l'avantage de maintenance que LINQ peut fournir, vous pouvez envisager de passer par défaut à LINQ car il est peu probable que vous rencontriez des goulots d'étranglement de performances pouvant être attribués à LINQ.
Cela dit, il existe certains scénarios dans lesquels LINQ ne fonctionnera tout simplement pas. Par exemple, si vous importez une tonne de données, vous constaterez peut-être que l'exécution d'insertions individuelles est plus lente que l'envoi des données à SQL Server par lots de XML. Dans cet exemple, ce n'est pas que l'insertion LINQ est plus rapide que l'insertion non LINQ, mais il n'est pas nécessaire d'exécuter des insertions SQL individuelles pour les importations de données en masse.
Votre scénario de test est un peu biaisé. L'opérateur ANY commencera à énumérer vos résultats et retournera true sur la première instance s'il trouve et quitte. Essayez ceci avec de simples listes de chaînes pour voir le résultat. Pour répondre à votre question sur la manière d'éviter LINQ, vous devez vraiment passer à l'utilisation de LINQ. Il rend le code plus facile à lire lors de requêtes complexes en plus de la vérification du temps de compilation. Vous n'avez pas non plus besoin d'utiliser l'opérateur Cast dans votre exemple.
string compareMe = "Success";
string notEqual = "Not Success";
List<string> headOfList = new List<string>();
List<string> midOfList = new List<string>();
List<string> endOfList = new List<string>();
//Create a list of 999,999 items
List<string> masterList = new List<string>();
masterList.AddRange(Enumerable.Repeat(notEqual, 999999));
//put the true case at the head of the list
headOfList.Add(compareMe);
headOfList.AddRange(masterList);
//insert the true case in the middle of the list
midOfList.AddRange(masterList);
midOfList.Insert(masterList.Count/2, compareMe);
//insert the true case at the tail of the list
endOfList.AddRange(masterList);
endOfList.Add(compareMe);
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
headOfList.Any(p=>p == compareMe);
stopWatch.ElapsedMilliseconds.Dump();
stopWatch.Reset();
stopWatch.Start();
midOfList.Any(p=>p == compareMe);
stopWatch.ElapsedMilliseconds.Dump();
stopWatch.Reset();
stopWatch.Start();
endOfList.Any(p=>p == compareMe);
stopWatch.ElapsedMilliseconds.Dump();
stopWatch.Stop();
Le casting de type va bien sûr ralentir votre code. Si vous vous en souciez autant, utilisez au moins un IEnumerable fortement typé pour la comparaison. J'essaye moi-même d'utiliser LINQ dans la mesure du possible. Cela rend votre code beaucoup plus concis. Il ne faut pas se soucier des détails impératifs de votre code. LINQ est un concept fonctionnel, ce qui signifie que vous expliquerez ce que vous voulez qu'il se passe sans vous soucier de la façon dont.