Skip to content

Latest commit

 

History

History
171 lines (136 loc) · 10 KB

File metadata and controls

171 lines (136 loc) · 10 KB

LinqOrderProof

Замер того, какие вызовы после 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 здесь — предварительная сборка. К релизу числа могут измениться.


Ссылки