Замер того, какие вызовы после OrderBy проходят набор один раз, а какие
приводят к полной сортировке.
Один аргумент внутри First меняет способ вычисления: без условия вызов
проходит набор один раз, с условием сортирует его полностью. У зеркального
Last этого не происходит.
Проект к статье «Бенчмаркая LINQ: подстава с OrderBy — одно условие и полная сортировка».
Обращения к компаратору на наборе из 1000 элементов, .NET 10. Числа одинаковы на всех четырёх машинах.
| Вызов | Сравнений | Ключей |
|---|---|---|
OrderBy().First() |
999 | 1000 |
OrderBy().Last() |
999 | 1000 |
OrderBy().ElementAt(0) |
999 | 1000 |
OrderBy().Last(условие) |
500 | 501 |
OrderBy().ElementAt(последний) |
1610 | 1000 |
OrderBy().Take(10).Last() |
1748 | 1000 |
OrderBy().ElementAt(середина) |
3334 | 1000 |
OrderBy().First(условие) |
11 081 | 1000 |
OrderBy().Min() |
11 081 | 1000 |
OrderBy().ToArray()[0] |
11 081 | 1000 |
Один проход по 1000 элементам — 999 сравнений. Полная сортировка того же набора — 11 081.
Что это даёт по времени на наборе из 100 000 элементов, .NET 10:
First с условием занимает 8349,20–14 520,81 микросекунды против
85,65–221,83 у First без условия, то есть больше в 65,46–97,48 раза.
Нужны рантаймы 8, 9, 10 и 11. Одиннадцатый обязателен: замеры идут сразу на четыре цели одним прогоном.
all.bat
Он снимает замеры BenchmarkDotNet и все четыре отчёта — calls, growth,
checks и timing — на каждом из четырёх рантаймов.
Отдельные шаги:
dotnet run -c Release -f net10.0 все замеры, четыре рантайма
dotnet run -c Release -f net10.0 -- calls сравнений и ключей на запись
dotnet run -c Release -f net10.0 -- growth рост сравнений по размеру набора
dotnet run -c Release -f net10.0 -- checks сверка ответов
dotnet run -c Release -f net10.0 -- timing замер без BenchmarkDotNet
Считается не время, а обращения к компаратору и к селектору ключа. По ним способ вычисления виден сразу, а от машины они не зависят: числа совпали на всех четырёх.
Набор — псевдослучайные числа с постоянным начальным значением, один и тот же в каждом прогоне и на каждой машине.
Четыре класса замеров, наборы из 1000 и 100 000 элементов.
TerminalBench— способы получить результат изOrderBy:First,Last, те же два с условием,FirstOrDefaultс условием,Min,Max,ElementAtпо трём позициям,TakeиToArray, плюс пара с условиями наоборот;ShapeBench— один оператор междуOrderByиFirst:ThenBy,OrderByDescending,Select,Distinct,Take,Skip,Reverse;WorkaroundBench— четыре записи одной задачи, три записи одного кода с разными компараторами и запись с условием без компаратора;ItemBench— те же записи на элементе-классе с ключом в свойстве.
Замеры идут одним прогоном сразу на четырёх рантаймах. BenchmarkDotNet
0.15.8 не знает про net11.0: запуск под ним падает на проверке
с NotImplementedException. Поэтому BenchmarkConfig задаёт цели строками
через CsProjCoreToolchain, а сам прогон запускается под net10.0.
Опорное задание — .NET 10.0.
Что закрыто замером, а не словами:
- Записи одной группы возвращают один ответ. Сверяет отчёт
checks: каждый ответ сравнивается с эталоном, полученным полной сортировкой. При расхождении прогон останавливается. Отчётcallsпечатает верность ответа рядом с числом сравнений. - Способ вычисления виден числом, а не по времени. Отчёт
callsсчитает обращения к компаратору и к селектору ключа. - Полная сортировка отличена от частичной выборки. Отчёт
growthснимает те же числа на 1000, 10 000 и 100 000 элементов: уFirstс условием и уMinони совпадают сToArrayна всех трёх размерах, уSkipиElementAtдержатся на 11,4–13,1 процента от неё. - Счётчик не меняет то, что мерит. В
WorkaroundBenchодна и та же запись снята трижды: с компаратором-счётчиком, с обычным и без компаратора вовсе. - Дело не в размере набора. Каждый замер идёт на 1000 и на 100 000 элементов.
- Дело не в том, в какую сторону смотрит условие. Пара
First_WhereBelowиLast_WhereAboveзадаёт условия наоборот. - Дело не в примитивах.
ItemBenchработает с элементом-классом, у которого ключ лежит в свойстве. - Ноль сравнений и прочерк — разное. Если компаратор в запись
не передан, отчёт
callsставит прочерк: ноль означал бы, что сравнений не было, а здесь их просто не видно счётчику. - Не артефакт BenchmarkDotNet. Отчёт
timingмеряет то же самое наStopwatchиз отдельного процесса, тремя проходами с печатью разброса. - Не следствие динамического профиля.
timingснимается ещё раз сDOTNET_TieredPGO=0. - Не следствие сборщика.
timingснимается ещё раз на серверном сборщике.
LinqOrderProof.csproj net8.0;net9.0;net10.0 (+net11.0 при SDK 11)
LinqOrderProof.slnx
Program.cs точка входа и отчёты
BenchmarkConfig.cs четыре рантайма одним прогоном
Subjects.cs все измеряемые записи
all.bat весь прогон
Counting/ счётчик обращений, два компаратора и элемент-класс
Benchmarks/ четыре класса замеров
Diagnostics/ отчёты вне BenchmarkDotNet
Results/
Comp_1 Intel Core i9-10900KF 3.70GHz, 10 ядер, Windows 10 22H2
Comp_2 AMD Ryzen 9 5950X 3.39GHz, 16 ядер, Windows 10 1809
Comp_3 Intel Xeon W-2255 3.70GHz, 10 ядер, Windows Server 2022
Comp_4 Intel Xeon Silver 4314 2.40GHz, 2 CPU, 32 ядра, Windows Server 2022
В каждой папке машины:
Bdn/results/ отчёты BenchmarkDotNet: csv, md, html
calls_netN.0.txt сравнений и ключей на запись
growth_netN.0.txt рост сравнений на 1000, 10 000 и 100 000
checks_netN.0.txt сверка ответов
timing_netN.0.txt замер на Stopwatch
timing_netN.0_nopgo.txt то же без динамического профиля
timing_netN.0_servergc.txt то же на серверном сборщике
Рантаймы в приложенных прогонах: 8.0.11–8.0.30, 9.0.4–9.0.19, 10.0.1–10.0.11, 11.0.0 preview. BenchmarkDotNet 0.15.8.
.NET 11 здесь — предварительная сборка. К релизу числа могут измениться.