J'ai deux classes qui effectuent la récupération de données sur une plage de dates pour des jours particuliers.
public class IterationLookup<TItem>
{
private IList<Item> items = null;
public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
{
this.items = items.OrderByDescending(keySelector).ToList();
}
public TItem GetItem(DateTime day)
{
foreach(TItem i in this.items)
{
if (i.IsWithinRange(day))
{
return i;
}
}
return null;
}
}
public class LinqLookup<TItem>
{
private IList<Item> items = null;
public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
{
this.items = items.OrderByDescending(keySelector).ToList();
}
public TItem GetItem(DateTime day)
{
return this.items.FirstOrDefault(i => i.IsWithinRange(day));
}
}
Ensuite, je fais des tests de vitesse qui montrent que la version de Linq est à peu près 5 fois plus lent . Cela aurait un sens lorsque je stocke des éléments localement sans les énumérer en utilisant la fonction ToList
. Cela le rendrait beaucoup plus lent, car à chaque appel à FirstOrDefault
linq effectuerait également OrderByDescending
. Mais ce n'est pas le cas, alors je ne sais pas vraiment ce qui se passe. Linq devrait fonctionner de manière très similaire à l'itération.
Voici l'extrait de code qui mesure mes timings
IList<RangeItem> ranges = GenerateRanges(); // returns List<T>
var iterLookup = new IterationLookup<RangeItems>(ranges, r => r.Id);
var linqLookup = new LinqLookup<RangeItems>(ranges, r => r.Id);
Stopwatch timer = new Stopwatch();
timer.Start();
for(int i = 0; i < 1000000; i++)
{
iterLookup.GetItem(GetRandomDay());
}
timer.Stop();
// display elapsed time
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
linqLookup.GetItem(GetRandomDay());
}
timer.Stop();
// display elapsed time
Pourquoi je sais qu'il devrait être plus performant ? Parce que lorsque j'écris un code très similaire sans utiliser ces classes de recherche, Linq a des performances très proches de celles de foreach
itérations...
// continue from previous code block
// items used by both order as they do in classes as well
IList<RangeItem> items = ranges.OrderByDescending(r => r.Id).ToList();
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
DateTime day = GetRandomDay();
foreach(RangeItem r in items)
{
if (r.IsWithinRange(day))
{
// RangeItem result = r;
break;
}
}
}
timer.Stop();
// display elapsed time
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
DateTime day = GetRandomDay();
items.FirstOrDefault(i => i.IsWithinRange(day));
}
timer.Stop();
// display elapsed time
Il s'agit à mon avis d'un code très similaire. FirstOrDefault
pour autant que je sache, itèrent aussi seulement jusqu'à ce qu'ils atteignent un élément valide ou jusqu'à ce qu'ils atteignent la fin. Et c'est en quelque sorte la même chose que foreach
con break
.
Mais même la classe d'itération fonctionne moins bien que mon simple foreach
la boucle d'itération, qui est également un mystère car la seule surcharge qu'elle présente est l'appel à une méthode au sein d'une classe par rapport à un accès direct.
Pregunta
Qu'est-ce que je fais de mal dans ma classe LINQ pour qu'elle soit si lente ?
Qu'est-ce que je fais de mal dans ma classe Iteration pour qu'elle soit deux fois plus lente que la classe directe ? foreach
boucle ?
Quels sont les temps mesurés ?
Je fais ces étapes :
- Générer des plages (comme indiqué ci-dessous dans les résultats)
- Créer des instances d'objets pour IterationLookup, LinqLookup (et ma classe de plage de dates optimisée BitCountLookup qui ne fait pas partie de la discussion ici).
- Lancez la minuterie et exécutez 1 million de recherches sur des jours aléatoires dans la plage de dates maximale (comme indiqué dans les résultats) en utilisant la classe IterationLookup précédemment instanciée.
- Lancez une minuterie et exécutez 1 million de recherches sur des jours aléatoires dans la plage de dates maximale (comme indiqué dans les résultats) en utilisant la classe LinqLookup précédemment instanciée.
- Lancez une minuterie et exécutez 1 million de recherches (6 fois) en utilisant des boucles manuelles foreach+break et des appels Linq.
Comme vous pouvez le constater l'instanciation de l'objet n'est pas mesurée .
Annexe I : Résultats sur un million de consultations
Les plages affichées dans ces résultats ne se chevauchent pas, ce qui devrait rendre les deux approches encore plus similaires au cas où la version LINQ ne rompt pas la boucle en cas de correspondance réussie (ce qui est très probable).
Generated Ranges:
ID Range 000000000111111111122222222223300000000011111111112222222222
123456789012345678901234567890112345678901234567890123456789
09 22.01.-30.01. |-------|
08 14.01.-16.01. |-|
07 16.02.-19.02. |--|
06 15.01.-17.01. |-|
05 19.02.-23.02. |---|
04 01.01.-07.01.|-----|
03 02.01.-10.01. |-------|
02 11.01.-13.01. |-|
01 16.01.-20.01. |---|
00 29.01.-06.02. |-------|
Lookup classes...
- Iteration: 1028ms
**\- Linq: 4517ms !!! THIS IS THE PROBLEM !!!**
- BitCounter: 401ms
Manual loops...
- Iter: 786ms
- Linq: 981ms
- Iter: 787ms
- Linq: 996ms
- Iter: 787ms
- Linq: 977ms
- Iter: 783ms
- Linq: 979ms
Annexe II : code GitHub:Gist à tester par vous-même
J'ai mis en place un Gist pour que vous puissiez obtenir le code complet vous-même et voir ce qui se passe. Créez un Console demande et copie Programme.cs et ajouter d'autres fichiers qui font partie de cette liste.
Prends-le. aquí .
Annexe III : Réflexions finales et tests de mesure
La chose la plus problématique était bien sûr l'implémentation de LINQ qui était terriblement lente. Il s'avère que cela a tout à voir avec l'optimisation du compilateur de délégués. LukeH a fourni la solution la meilleure et la plus utilisable. qui m'a fait essayer différentes approches de la question. J'ai essayé plusieurs approches différentes dans le GetItem
(ou GetPointData
comme on l'appelle dans Gist) :
-
la manière habituelle que la plupart des développeurs feraient (et qui est également implémentée dans Gist et n'a pas été mise à jour après que les résultats aient révélé que ce n'est pas la meilleure manière de faire) :
return this.items.FirstOrDefault(item => item.IsWithinRange(day));
-
en définissant une variable de prédicat locale :
Func<TItem, bool> predicate = item => item.IsWithinRange(day); return this.items.FirstOrDefault(predicate);
-
constructeur local de prédicats :
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d); return this.items.FirstOrDefault(builder(day));
-
constructeur de prédicat local et variable de prédicat local :
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d); Func<TItem, bool> predicate = builder(day); return this.items.FirstOrDefault(predicate);
-
constructeur de prédicats au niveau de la classe (statique ou instance) :
return this.items.FirstOrDefault(classLevelBuilder(day));
-
prédicat défini en externe et fourni comme paramètre de la méthode
public TItem GetItem(Func<TItem, bool> predicate) { return this.items.FirstOrDefault(predicate); }
En exécutant cette méthode, j'ai également adopté deux approches :
-
prédicat fourni directement à l'appel de la méthode dans
for
boucle :for (int i = 0; i < 1000000; i++) { linqLookup.GetItem(item => item.IsWithinRange(GetRandomDay())); }
-
constructeur de prédicats défini en dehors de
for
boucle :Func<DateTime, Func<Ranger, bool>> builder = d => r => r.IsWithinRange(d); for (int i = 0; i < 1000000; i++) { linqLookup.GetItem(builder(GetRandomDay())); }
-
Résultats - ce qui est le plus performant
À titre de comparaison, l'utilisation de la classe d'itération prend environ 1,5 heure. 770ms pour exécuter 1 million de recherches sur des plages générées aléatoirement.
-
Le constructeur de prédicats locaux 3 s'avère être le mieux optimisé par le compilateur, ce qui lui permet d'être presque aussi rapide que les itérations habituelles. 800 ms .
-
6.2 constructeur de prédicats défini en dehors de l'entreprise
for
boucle : 885ms -
6.1 prédicat défini dans
for
boucle : 1525ms -
tous les autres ont pris entre 4200ms - 4360ms et sont donc considérés comme inutilisables.
Ainsi, chaque fois que vous utilisez un prédicat dans une méthode fréquemment appelée en externe, définissez un constructeur et exécutez-le. Cela donnera les meilleurs résultats.
Ce qui me surprend le plus, c'est que les délégués (ou prédicats) puissent prendre autant de temps.