У меня есть два класса, которые выполняют выборку данных диапазона дат в определенные дни.
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));
}
}
Затем я делаю тесты скорости, которые показывают, что версия Linq имеет значение в 5 раз медленнее. Это имеет смысл, если я буду хранить предметы локально, не перечисляя их, используя ToList
. Это сделало бы это намного медленнее, потому что при каждом вызове FirstOrDefault
linq также выполнял бы OrderByDescending
. Но это не так, поэтому я не знаю, что происходит. Linq должен очень похож на итерацию.
Это фрагмент кода, который измеряет мои тайминги
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
Почему я знаю, что он должен работать лучше? Поскольку, когда я пишу очень похожий код без использования этих классов поиска, Linq очень похож на foreach
итерации...
// 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
Это, на мой взгляд, очень похожий код. FirstOrDefault
, насколько я знаю, итерация продолжается только до тех пор, пока не дойдет до действительного элемента или пока не достигнет конца. И это как-то так же, как foreach
с break
.
Но даже класс итераций хуже, чем мой простой цикл foreach
итерации, который также является загадкой, потому что все накладные расходы есть вызов метода внутри класса по сравнению с прямым доступом.
Вопрос
Что я делаю неправильно в своем классе LINQ, который он выполняет очень медленно?
Что я делаю неправильно в моем классе Iteration, поэтому он выполняет в два раза медленнее, чем прямой цикл foreach
?
Какое время измеряется?
Я делаю следующие шаги:
- Создание диапазонов (как показано ниже в результатах)
- Создайте экземпляры объектов для IterationLookup, LinqLookup (и мой оптимизированный класс диапазона дат BitCountLookup, который не является частью обсуждения здесь)
- Запустить таймер и выполнить 1 миллион запросов в случайные дни в пределах максимального диапазона дат (как видно из результатов) с использованием ранее созданного класса IterationLookup.
- Запустите таймер и выполните 1 миллион запросов в случайные дни в пределах максимального диапазона дат (как видно из результатов), используя ранее созданный класс LinqLookup.
- Запустите таймер и выполните 1 миллион запросов (6 раз) с помощью ручных foreach + break loops и вызовов Linq.
Как вы видите, объект не измеряется.
Приложение I: Результаты более миллиона поисковых запросов
Диапазоны, отображаемые в этих результатах, не перекрываются, что должно сделать оба подхода еще более похожими, если версия LINQ не прерывает цикл при успешном совпадении (что, скорее всего, делает).
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
Приложение II: GitHub: код Gist для проверки для себя
Я выставил Gist, чтобы вы могли получить полный код самостоятельно и посмотреть, что происходит. Создайте приложение Консоль и скопируйте Program.cs в него, добавьте другие файлы, которые являются частью этого принципа.
Захватите его здесь.
Приложение III: Заключительные мысли и измерительные тесты
Наиболее проблематичным было, конечно, LINQ implementatino, которое было ужасно медленным. Оказывается, это связано с оптимизацией компилятора делегата. LukeH предоставил лучшее и наиболее пригодное для использования решение, которое фактически заставило меня попробовать разные подходы к этому. Я пробовал различные подходы в методе GetItem
(или GetPointData
, как он назывался в Gist):
-
обычный способ, который сделает большинство разработчиков (и реализован в Gist, а также не обновлялся после того, как результаты показали, что это не лучший способ сделать это):
return this.items.FirstOrDefault(item => item.IsWithinRange(day));
-
определяя локальную предикатную переменную:
Func<TItem, bool> predicate = item => item.IsWithinRange(day); return this.items.FirstOrDefault(predicate);
-
локальный построитель предикатов:
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d); return this.items.FirstOrDefault(builder(day));
-
локальный предиктор-строитель и локальная предикатная переменная:
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d); Func<TItem, bool> predicate = builder(day); return this.items.FirstOrDefault(predicate);
-
построитель предикатов уровня (статический или экземпляр):
return this.items.FirstOrDefault(classLevelBuilder(day));
-
внешний предикат и предоставляется как параметр метода
public TItem GetItem(Func<TItem, bool> predicate) { return this.items.FirstOrDefault(predicate); }
при выполнении этого метода я также принял два подхода:
-
предикат предоставляется непосредственно при вызове метода в цикле
for
:for (int i = 0; i < 1000000; i++) { linqLookup.GetItem(item => item.IsWithinRange(GetRandomDay())); }
-
построитель предикатов, определенный вне цикла
for
:Func<DateTime, Func<Ranger, bool>> builder = d => r => r.IsWithinRange(d); for (int i = 0; i < 1000000; i++) { linqLookup.GetItem(builder(GetRandomDay())); }
-
Результаты - что лучше всего работает
Для сравнения при использовании класса итерации он занимает ок. 770 мс, чтобы выполнить 1 миллион запросов на случайно сгенерированных диапазонах.
-
3 локальный построитель предикатов оказывается лучшим оптимизированным компилятором, поэтому он выполняет почти так же быстро, как и обычные итерации. 800ms.
-
6.2 построитель предикатов, определенный вне цикла
for
: 885 мс -
6.1 предикат, определенный в цикле
for
: 1525 мс - все остальные заняли место между 4200 мс - 4360 мс и поэтому считаются непригодными.
Таким образом, всякий раз, когда вы используете предикат в способе, вызываемом извне, можно определить конструктор и выполнить его. Это даст наилучшие результаты.
Самый большой сюрприз для меня в том, что делегаты (или предикаты) могут занять много времени.