Très brève question. J'ai un grand tableau de chaîne trié au hasard (100K + entrées) où je veux trouver la première occurrence d'une chaîne désirée. J'ai deux solutions.
Après avoir lu ce que je peux deviner, je suppose que la boucle «for» donnera actuellement des performances légèrement meilleures (mais cette marge pourrait toujours changer), mais je trouve également la version linq beaucoup plus lisible. Dans l’ensemble, quelle méthode est généralement considérée comme la meilleure pratique de codage actuelle et pourquoi?
string matchString = "dsf897sdf78";
int matchIndex = -1;
for(int i=0; i<array.length; i++)
{
if(array[i]==matchString)
{
matchIndex = i;
break;
}
}
ou
int matchIndex = array.Select((r, i) => new { value = r, index = i })
.Where(t => t.value == matchString)
.Select(s => s.index).First();
La meilleure pratique dépend de ce dont vous avez besoin:
LINQ ralentit vraiment les choses avec toute l’indirection. Ne vous inquiétez pas, 99% de votre code n'influe pas sur les performances de l'utilisateur final.
J'ai commencé avec le C++ et j'ai vraiment appris à optimiser un morceau de code. LINQ n'est pas conçu pour tirer le meilleur parti de votre processeur. Donc, si vous mesurez une requête LINQ comme un problème, il suffit de la laisser tomber. Mais alors seulement.
Pour votre exemple de code, j’estimerais un ralentissement de 3x. Les allocations (et les GC subséquentes!) Et les indirections à travers les lambdas font vraiment mal.
Légèrement meilleure performance? Une boucle donnera de meilleures performances!
Considérons le code ci-dessous. Sur mon système pour une version RELEASE (pas de débogage), cela donne:
Found via loop at index 999999 in 00:00:00.2782047
Found via linq at index 999999 in 00:00:02.5864703
Loop was 9.29700432810805 times faster than linq.
Le code est délibérément configuré de manière à ce que l'élément à trouver se trouve à la fin. Si c'était juste au début, les choses seraient très différentes.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Demo
{
public static class Program
{
private static void Main(string[] args)
{
string[] a = new string[1000000];
for (int i = 0; i < a.Length; ++i)
{
a[i] = "Won't be found";
}
string matchString = "Will be found";
a[a.Length - 1] = "Will be found";
const int COUNT = 100;
var sw = Stopwatch.StartNew();
int matchIndex = -1;
for (int outer = 0; outer < COUNT; ++outer)
{
for (int i = 0; i < a.Length; i++)
{
if (a[i] == matchString)
{
matchIndex = i;
break;
}
}
}
sw.Stop();
Console.WriteLine("Found via loop at index " + matchIndex + " in " + sw.Elapsed);
double loopTime = sw.Elapsed.TotalSeconds;
sw.Restart();
for (int outer = 0; outer < COUNT; ++outer)
{
matchIndex = a.Select((r, i) => new { value = r, index = i })
.Where(t => t.value == matchString)
.Select(s => s.index).First();
}
sw.Stop();
Console.WriteLine("Found via linq at index " + matchIndex + " in " + sw.Elapsed);
double linqTime = sw.Elapsed.TotalSeconds;
Console.WriteLine("Loop was {0} times faster than linq.", linqTime/loopTime);
}
}
}
LINQ, selon le paradigme déclaratif, exprime la logique d'un calcul sans décrire son flux de contrôle. La requête est orientée vers l’objectif, auto-descriptive et donc facile à analyser et à comprendre. Est également concis. De plus, en utilisant LINQ, on dépend beaucoup de l’abstraction de la structure de données. Cela implique un taux élevé de maintenance et de réutilisation.
L'approche d'itération répond au paradigme impératif. Il donne un contrôle fin, facilitant ainsi l'obtention de meilleures performances. Le code est également plus simple à déboguer. Parfois, une itération bien construite est plus lisible qu'une requête.
Il y a toujours un dilemme entre performance et maintenabilité. Et généralement (s’il n’ya pas d’exigences spécifiques en matière de performances), la facilité de maintenance doit être gagnante. Si vous rencontrez des problèmes de performances, vous devez profiler l'application, trouver la source du problème et améliorer ses performances (en réduisant la maintenabilité en même temps, c'est le monde dans lequel nous vivons).
A propos de votre échantillon. Linq n’est pas une très bonne solution ici, car il n’ajoute pas de maintenabilité dans votre code. En fait, projeter, filtrer et projeter à nouveau est encore pire qu'une simple boucle. Ce dont vous avez besoin ici est un simple Array.IndexOf, qui est plus facile à gérer, qu'une boucle et qui a presque les mêmes performances:
Array.IndexOf(array, matchString)
Eh bien, vous avez vous-même répondu à votre question.
Choisissez une boucle For
si vous voulez obtenir les meilleures performances ou Linq
si vous voulez de la lisibilité.
Gardez peut-être aussi à l’esprit la possibilité d’utiliser Parallel.Foreach () qui tirerait profit des expressions lambda en ligne (donc plus proches de Linq), ce qui est beaucoup plus lisible que la parallélisation "manuellement".
Je ne pense pas que ce soit l'une des meilleures pratiques, certaines personnes préfèrent regarder LINQ et d'autres non.
Si les performances sont un problème, je profilerais les deux bits de code pour votre scénario. Si la différence est négligeable, choisissez celui avec lequel vous vous sentez le plus conforme. Après tout, ce sera probablement à vous de gérer le code.
Avez-vous aussi pensé à utiliser PLINQ ou à faire tourner la boucle en parallèle?
La meilleure option consiste à utiliser la méthode IndexOf de la classe Array. Comme il est spécialisé dans les tableaux, il sera beaucoup plus rapide que Linq et For Loop .
using System;
using System.Diagnostics;
using System.Linq;
namespace PerformanceConsoleApp
{
public class LinqVsFor
{
private static void Main(string[] args)
{
string[] a = new string[1000000];
for (int i = 0; i < a.Length; ++i)
{
a[i] = "Won't be found";
}
string matchString = "Will be found";
a[a.Length - 1] = "Will be found";
const int COUNT = 100;
var sw = Stopwatch.StartNew();
Loop(a, matchString, COUNT, sw);
First(a, matchString, COUNT, sw);
Where(a, matchString, COUNT, sw);
IndexOf(a, sw, matchString, COUNT);
Console.ReadLine();
}
private static void Loop(string[] a, string matchString, int COUNT, Stopwatch sw)
{
int matchIndex = -1;
for (int outer = 0; outer < COUNT; ++outer)
{
for (int i = 0; i < a.Length; i++)
{
if (a[i] == matchString)
{
matchIndex = i;
break;
}
}
}
sw.Stop();
Console.WriteLine("Found via loop at index " + matchIndex + " in " + sw.Elapsed);
}
private static void IndexOf(string[] a, Stopwatch sw, string matchString, int COUNT)
{
int matchIndex = -1;
sw.Restart();
for (int outer = 0; outer < COUNT; ++outer)
{
matchIndex = Array.IndexOf(a, matchString);
}
sw.Stop();
Console.WriteLine("Found via IndexOf at index " + matchIndex + " in " + sw.Elapsed);
}
private static void First(string[] a, string matchString, int COUNT, Stopwatch sw)
{
sw.Restart();
string str = "";
for (int outer = 0; outer < COUNT; ++outer)
{
str = a.First(t => t == matchString);
}
sw.Stop();
Console.WriteLine("Found via linq First at index " + Array.IndexOf(a, str) + " in " + sw.Elapsed);
}
private static void Where(string[] a, string matchString, int COUNT, Stopwatch sw)
{
sw.Restart();
string str = "";
for (int outer = 0; outer < COUNT; ++outer)
{
str = a.Where(t => t == matchString).First();
}
sw.Stop();
Console.WriteLine("Found via linq Where at index " + Array.IndexOf(a, str) + " in " + sw.Elapsed);
}
}
}
Sortie:
Found via loop at index 999999 in 00:00:01.1528531
Found via linq First at index 999999 in 00:00:02.0876573
Found via linq Where at index 999999 in 00:00:01.3313111
Found via IndexOf at index 999999 in 00:00:00.7244812