.NET Matrix — это новый открытый проект, который сравнивает .NET-библиотеки внутри одной категории по трём аспектам: возможности, скорость и использование памяти. Все сравнения воспроизводимы: сценарии, тесты сценариев, отчёты и параметры окружения лежат в репозитории, а полный прогон запускается одной командой.
Принцип проекта — Evidence, not faith. Доказательства, а не вера.
-
Интерактивная матрица: matrix.dev-team.org
-
Репозиторий: github.com/DevTeam/dotnet-matrix (MIT)
Сейчас в матрице 6 категорий, 40 библиотек и 67 сценариев.
Статья рассчитана на .NET-разработчиков, которым:
-
нужно выбрать библиотеку и обосновать выбор чем-то кроме «у нас так принято»;
-
важно знать не только «кто быстрее», но и что именно библиотека умеет, а что не умеет;
-
интересно поучаствовать: добавить любимую или свою библиотеку, либо даже новую категорию библиотек.
Как обычно выбирают библиотеку и почему это не работает
Зачастую процесс выбора выглядит так:
-
посмотреть звёзды на GitHub и/или число загрузок на NuGet;
-
найти бенчмарки в чьём-то блоге;
-
прочитать тред, где пять человек уверенно называют пять разных библиотек;
-
выбрать то, что уже используют коллеги.
Каждый шаг имеет недостатки:
-
Популярность — инертна. Число загрузок не показатель пригодности библиотеки для вашего сценария.
-
Бенчмарки не сопоставимы между собой. Разные авторы, разное железо, разные версии рантайма, разные и часто не формализованные сценарии. Два бенчмарка одной и той же пары библиотек нередко дают противоположные выводы, и оба «правдивы» в своём окружении.
-
Бенчмарки устаревают. Пост от 2019 года ничего не знает ни о новых версиях библиотек, ни о новых рантаймах, но продолжает исправно выдаваться в поиске и служить авторитетным источником для выбора той или иной библиотеки.
-
«Поддержка фичи» проверяется по названию метода из API. В таблице стоит галочка, потому что у библиотеки есть метод с похожим по смыслу именем. Похожее имя метода — не доказательство поддержки сценария.
.NET Matrix пытается закрыть именно эти пробелы: единые сценарии для всех библиотек-участников категории, проверка поведения тестами сценариев, измерение через BenchmarkDotNet, зафиксированное окружение и воспроизводимость.
Что такое .NET Matrix
Проект вдохновлён IocPerformance — сравнением .NET IoC-контейнеров, которое Daniel Palme (palmmedia.de) начал постом ещё в 2011 году, и вёл больше десяти лет. Там были похожие сценарии Basic/Advanced Features и время однопоточного и многопоточного прогона в миллисекундах для нескольких десятков контейнеров.
На мой взгляд, идея была толковой и актуальна до сих пор, но проект остановлен: репозиторий заархивирован на GitHub, последнее обновление — 20 июля 2023 года. Версии библиотек ушли вперёд, .NET сменил несколько major-версий, а таблицы со статистикой остались без изменений.
.NET Matrix продолжает эту идею и расширяет её:
-
не только DI, а любые категории библиотек;
-
вместо галочки — контракт возможностей и тест сценария на каждую фичу;
-
вместо самодельных бенчмарков — BenchmarkDotNet;
-
добавлена статистика потребления памяти, для многих задач это важный фактор выбора;
-
интерактивное приложение вместо статической таблицы упрощает сравнение и выбор;
-
воспроизведение всей статистики одной командой на своём компьютере.
Наследие проекта IocPerformance очевидно по списку категорий и библиотек-участников Dependency Injection: Faster.Ioc, Maestro, Singularity, ZenIoc, MvvmCross, Catel, Spring, VS.MEF. Была взята большая часть библиотек, за исключением нескольких, которые давно не обновлялись и есть подозрение, что их развитие остановлено.
.NET Matrix для каждой категории библиотек отвечает на три важных вопроса.
1. Что библиотека умеет. Матрица возможностей, где у каждой пары «библиотека × фича» один из четырёх статусов:
-
реализация прошла все проверки сценария
-
у библиотеки нет нужной семантики или точки расширения
-
у фичи нет осмысленного эквивалента для этой библиотеки
-
поддержка заявлена, но проверки не прошли
2. Сколько это стоит по времени. В отчёт попадают две статистики:
-
выборочное среднее (sample mean) — среднее время одной операции по всем итерациям. Это точечная оценка истинного среднего времени, а не само истинное значение;
-
стандартная ошибка среднего (standard error of the mean, SEM) — оценка того, насколько выборочное среднее может отклоняться от истинного. Считается как
s / √n, гдеs— выборочное стандартное отклонение, аn— число итераций. В приложении она показана как±рядом со средним.
Практический смысл различия: стандартное отклонение описывает разброс отдельных измерений, а стандартная ошибка — точность полученной оценки среднего. Поэтому если два результата различаются меньше, чем на несколько стандартных ошибок, разницы, скорее всего, нет — она в пределах шума измерения. И поэтому же итерации нельзя сокращать бесконечно: SEM убывает лишь как 1 / √n.
3. Сколько это стоит по памяти. Аллокации на операцию.
Порядок важен: сначала корректность и потом уже бенчмарки. Если тесты на сценарий не прошли, бенчмарки для этого сценария и библиотеки не публикуются — в отчёт попадает только матрица возможностей с явной пометкой, что бенчмарки пропущены. Быстрая, но неверная реализация сценария не будет отображаться в статистике и участвовать в рейтинге.
Результаты сводятся в общий рейтинг: сценарии объединены в группы (например, у CSV это Read, Correctness, Throughput, Write; у DI — Basic, Advanced, Prepare), и в каждой группе первые три места получают 🥇, 🥈 и 🥉. Для некоторых категорий библиотек ручная реализация (Hand-coded) задаёт точку отсчёта (baseline), но не участвует в рейтинге.
Актуальная статистика
|
Категория |
Лидер рейтинга |
Библиотек |
Сценариев |
|---|---|---|---|
|
CSV Processing |
Sep |
3 |
10 |
|
Dependency Injection |
Pure.DI |
23 |
15 |
|
JSON Serialization |
System.Text.Json |
3 |
14 |
|
Logging |
Microsoft.Extensions.Logging |
5 |
8 |
|
Object Mapping |
Mapperly |
4 |
10 |
|
Validation |
DataAnnotations |
3 |
10 |
Цифры статистики намеренно не приводятся: они привязаны к версиям пакетов и к окружению, в котором проходили тесты, и быстро устареют. Актуальные числа, графики и матрицу возможностей смотрите в приложении: matrix.dev-team.org.
Отдельно стоит сказать про эталонные реализации (baseline). В нескольких категориях помимо библиотек участвует «нулевой вариант»: HandCoded — код, написанный руками, в Dependency Injection и Object Mapping; System.Text.Json, DataAnnotations и Microsoft.Extensions.Logging — то, что уже есть в платформе. Это довольно полезный показатель, он отвечает на вопрос, сколько вы «платите» за библиотеку, и стоит ли её использовать. И иногда ответ неожиданный: в Validation и JSON Serialization рейтинг сейчас возглавляют именно платформенные варианты — DataAnnotations и System.Text.Json.
Почему этим данным можно верить
Тесты сценариев подтверждают поддержку фич
Каждая фича категории описана заранее в контракте, отдельным Markdown-документом для каждой категории библиотек. Контракт фиксирует, что именно делает сценарий, что подаётся на вход, какой результат считается верным, что происходит до замера, а что — внутри него, и при каком условии фича считается корректно поддержанной.
На каждую фичу каждой библиотеки есть тест сценария: в тесте используется настоящее API библиотеки, а результат сверяется с эталоном из контракта: не «не упало», а точные значения. Например, в CSV-сценарии Custom Conversion поле формата sku-NNNN должно превратиться в значение доменного типа, и проверяются конкретные числа 42 и 73. А в контракте прямо написано, что не считается корректной поддержкой: разобрать строку своим кодом, а потом вызвать преобразование — нельзя; обернуть синхронный парсер в Task.Run и назвать это async API — нельзя; похожее по имени API — не доказательство поддержки сценария.
Из этого следует важное свойство: пустые позиции в матрице — это тоже результат. Фичу, которую библиотека не реализует, автор реализации обязан объявить «недоступной» с указанием причины, иначе прогон тестов сценариев падает. Забыть реализовать фичу и тихо получить прочерк не получится.
Прогон тестов сценариев — обязательный шаг перед бенчмарками и локально, и в CI.
Замеры делает BenchmarkDotNet
Время и память измеряет BenchmarkDotNet — открытая библиотека под эгидой .NET Foundation и де-факто стандарт бенчмаркинга в .NET: на ней построены в том числе замеры производительности самого .NET в репозитории dotnet/performance.
Вот почему результатам BenchmarkDotNet можно верить больше, чем Stopwatch:
-
замеры выполняются в отдельном сгенерированном процессе под нужную конфигурацию, а не внутри вашего приложения или тестового раннера;
-
перед измерением идёт прогрев, чтобы в результат не попали JIT-компиляция и «холодные» пути выполнения кода;
-
число операций в итерации подбирается предварительной пилотной стадией, а не угадывается;
-
считается статистика по итерациям: выборочное среднее и стандартная ошибка среднего с отбраковкой выбросов, поэтому у каждого числа есть известная погрешность;
-
аллокации измеряются отдельным механизмом диагностики, а не грубым измерением с использованием
GC.GetTotalMemory; -
окружение бенчмарка (ОС, версия рантайма, версия SDK, процессор, число логических ядер, версия самого инструмента) фиксируется в отчёте, поэтому числа из разных окружений не получится смешать по невнимательности.
Автор и мейнтейнер BenchmarkDotNet — Андрей Акиньшин, он же автор книги «Pro .NET Benchmarking: The Art of Performance Measurement» (Apress, 2019), инструмент написан человеком, который отдельной книгой описал методологию корректного измерения производительности в .NET — включая все те ловушки, на которых обычно и ломаются самодельные бенчмарки. В .NET Matrix сейчас используется BenchmarkDotNet версии 0.15.8.
Как этим пользоваться
Основной инструмент — интерактивное приложение matrix.dev-team.org. Оно позволяет:
-
выбрать интересующие библиотеки и убрать остальные из сравнения;
-
переключаться между обзором, матрицей возможностей, бенчмарками и параметрами окружения;
-
выбрать версию отчётов: данные загружаются из репозитория по коммиту выбранной версии, поэтому можно смотреть и опубликованный релиз, и текущее состояние ветки, и историю статистики, например, для разных версий библиотек.
README.md в репозитории — сгенерированный снимок тех же отчётов: рейтинги, графики по каждой группе сценариев, описания библиотек и список сценариев. Шкала на графиках одна и та же для README и приложения, поэтому графики разных категорий сравнимы между собой.
Рекомендации по выбору библиотеки для своего проекта:
-
Сначала матрица возможностей. Если библиотека не реализует нужную вам фичу, её место в рейтинге не имеет значения.
-
Затем группа сценариев, похожая на ваши требования к библиотеке. Общий рейтинг — это сумма медалей по группам, а вам обычно нужна одна конкретная группа: пропускная способность, подготовка конфигурации, корректность краевых случаев.
-
Потом аллокации. На «горячем пути» и в высоконагруженном сервисе память бывает важнее среднего времени выполнения кода.
-
И только после этого — статистика по времени выполнения вместе с окружением, в котором она получена.
Не верьте на слово — получите статистику самостоятельно
Полный цикл: тесты сценариев, бенчмарки и подготовка всех данных запускается одной командой:
dotnet run --project .\build -- reproduce
Она прогоняет тесты сценариев и бенчмарки всех библиотек, перегенерирует отчёты, графики, метаданные и README, а затем поднимает локальное приложение на автоматически выбранном свободном порту и открывает его в браузере. Если браузер открывать не нужно, добавьте --no-browser; остановить приложение — Ctrl+C.
Если бенчмарки прогонять не нужно и достаточно уже имеющихся отчётов:
dotnet run --project .\build -- reproduce --skip-benchmarks
Для отдельной категории есть свои команды, например для CSV:
dotnet run --project .\build -- csv-processing-validatedotnet run --project .\build -- csv-processing-benchmarks
Что нужно для полного прогона: 64-битная ОС, .NET 10 SDK в PATH, несколько гигабайт свободного места и минимум 8 ГБ RAM. И, что важнее технических требований, — незагруженная задачами машина, подключённая к сети питания, без отладчика и с фиксированным профилем производительности. Полная матрица бенчмарков считается долго, это нормально.
Отчёты и параметры окружения есть в репозитории, поэтому свою статистику можно сравнить с опубликованной и увидеть разницу.
О конфликте интересов — честно
Об этом лучше сказать первым, а не в ответ на комментарий.
.NET Matrix сделан мной. Я же разрабатываю и Pure.DI — библиотеку, которая сейчас лидирует в категории Dependency Injection. Более того, Pure.DI используется внутри самого проекта как compile-time DI. Ситуация «сам измерил, сам и победил» вызывает недоверие, и это нормальная реакция.
Поэтому проект устроен так, чтобы результат можно было проверить, а не принять на веру:
-
Правила общие и лежат в открытом виде. Контракт возможностей категории — один для всех участников, он в репозитории и был написан до реализаций.
-
Сценарии одинаковые. Никаких «особых» бенчмарков под одного участника: все реализуют один и тот же сценарий и проходят одни и те же тесты на сценарии.
-
В категории сейчас 23 участника, включая ручную реализацию, которая показывает «физический» предел — «сколько это стоило бы без библиотеки вообще».
-
Всё воспроизводится одной командой на вашем железе, а отчёты вместе с окружением закоммичены.
-
Разногласие решается pull request’ом. Если сценарий кажется вам «странным», реализация конкурента — неоптимальной, а замер — некорректным, это правится в коде и обсуждается в комментариях репозитория dotnet-matrix на GitHub.
Если вы найдёте некорректный сценарий или неэффективную реализацию, заводите issue и/или присылайте PR. Для проекта, чья ценность целиком в доверии к данным, исправленный сценарий полезнее любого хорошего рейтинга.
Как добавить библиотеку
Процесс описан в workflows/add-library.md. Коротко:
1. Прочитайте контракт категории целиком — workflows/feature-contracts/<module-id>.md. Это не формальность: там написано, что считается поддержкой каждой фичи.
2. Сопоставьте каждую фичу с реальным API библиотеки. До написания кода, а не после.
3. Добавьте один аннотированный PackageReference в проект категории. Метаданные пакета — это и есть запись о библиотеке; отдельного JSON-файла заводить не нужно:
<PackageReference Include="Sep" Version="0.15.1"> <MatrixLibraryId>Sep</MatrixLibraryId> <MatrixLibraryName>Sep</MatrixLibraryName> <MatrixCodeName>Sep</MatrixCodeName> <MatrixDescription>A modern SIMD-accelerated separated-values reader and writer with span-based conversion and async enumeration.</MatrixDescription> <MatrixDocumentationUrl>https://github.com/nietras/Sep</MatrixDocumentationUrl> <MatrixRepositoryUrl>https://github.com/nietras/Sep</MatrixRepositoryUrl> <MatrixLogo>logos/sep.svg</MatrixLogo></PackageReference>
Версия указывается точным значением — без диапазонов, звёздочек и MSBuild-свойств: сравнение должно быть привязано к конкретной версии. Если обязательных метаданных не хватает, сборка падает с сообщением об ошибке, а по этим же метаданным генерируется вспомогательный код — константы каталога библиотек.
4. Поместите логотип в metadata/<Категория>/logos/ и обновите метаданные, выполнив цель сборки generate-metadata.
5. Реализуйте по файлу на фичу — Benchmarks/<CodeName>/NN_Feature.cs. Один файл содержит и замеряемый метод, и проверку результата, то есть тот самый тест сценария. Внутри замеряемого метода — прямой типизированный вызов API библиотеки.
6. Явно объявите о том, что сценарий не реализован с указанием причины — иначе тест сценария упадёт.
7. Выполните тесты сценариев только по своей библиотеке (для экономии вашего времени):
dotnet run --project .\build -- csv-processing-validate --libraries Sep
Отдельно про то, что нельзя править руками: libraries.json, features.json, benchmarks.json, графики в reports/*/charts/, README.md и run-конфигурации в .run/ — всё это генерируется из исходников и отчётов. Ваши правки в них потеряются при следующей регенерации.
Как добавить категорию
Это уже объёмная задача, она описана в workflows/add-category.md. Порядок принципиален: сначала контракт фич, потом код.
-
Написать
workflows/feature-contracts/<module-id>.md: список фич, входные данные, ожидаемые результаты, границу между подготовкой и замером, условияSupported/Unsupported/NotApplicable, участие в рейтинге. -
Определить идентичность категории: проект
Matrix.<Category>, стабильный module ID (например,object-mapping), отображаемое имя, префикс run-конфигураций, каталог отчётов. Эти значения становятся ключами данных, поэтому меняться потом не должны. -
Создать проект-исполняемый файл со сценариями, реализациями и проверками, зарегистрировать его в
dotnet-matrix.slnxи подключить общийMatrix.Module.targets.
Дальше категория подхватывается сама, правки в билд-проект не нужны. Автоматическая цепочка выглядит так:
Matrix.<Category>.csproj -> встраивается в сборку как ресурс -> метаданные модуля и библиотек читаются из него -> сборка обнаруживает модуль -> отчёты тестов сценариев и бенчмарков -> общие рендереры: PNG, README, Web
Общая часть: модели отчётов, обнаружение модулей, фильтрация, рейтинги, графики и веб-каталог — живёт в src/Matrix, каждая категория владеет уже своей собственной семантикой (модели данных для тестов, сценарии).
Что нужно проекту прямо сейчас
Самое ценное участие — новые данные. Есть три уровня входа, от простого к сложному.
Добавить библиотеку в существующую категорию. Самый низкий порог, и польза сразу заметна: в CSV Processing, JSON Serialization и Validation сейчас всего по три библиотеки. Кандидаты лежат на поверхности: SpanJson в JSON, Validot в Validation, ещё несколько мапперов и логгеров.
Реализовать новую категорию. Идеи в roadmap:
-
Binary Serialization (MessagePack, MemoryPack, protobuf-net) — возможно, нужна поддержка размера полезной нагрузки как отдельной метрики;
-
Mediator / Message Dispatch;
-
CLI Parsing — детерминированная и без инфраструктуры;
-
Template Engines — компиляция и рендеринг;
-
Caching — in-memory;
-
далее более рискованные: HTTP Clients, Data Access / ORM, Resilience.
Оспорить сценарий или реализацию библиотекой. Если вы хорошо знаете конкретную библиотеку, самое полезное — посмотреть, реализована ли она в матрице так, как её задумывал автор. Неоптимальная реализация конкурента — это баг проекта, и правится она как баг.
Что стоит помнить, если возьмётесь: бенчмарки прогоняются локально и это долго, но тесты сценариев — быстрые. То есть довести реализацию до состояния «все фичи корректны» можно без многочасовых прогонов. А полную или выборочную матрицу по нескольким библиотекам — посчитать уже один раз в конце.
Заключение
Выбор библиотеки — это инженерное решение и обосновываться оно должно так же, как остальные инженерные решения: воспроизводимым экспериментом с описанной методикой, а не звёздами на GitHub, и не бенчмарком из блога пятилетней давности.
Задача .NET Matrix сделать эксперимент общим и всегда актуальным: единый контракт возможностей на категорию, тест сценария на каждую фичу, BenchmarkDotNet для замеров, зафиксированное окружение прогона и полное воспроизведение одной командой. Проект молодой: шесть категорий и сорок библиотек, и поэтому сейчас самое удачное время в него зайти, даже одна добавленная библиотека заметно поменяет картину.
-
Матрица: matrix.dev-team.org
-
Репозиторий: github.com/DevTeam/dotnet-matrix
-
Добавить библиотеку: workflows/add-library.md
-
Добавить категорию: workflows/add-category.md
-
Roadmap: workflows/category-roadmap.md
Если ваша библиотека умеет что-то, чего нет в контракте — это тоже повод открыть issue: возможно, в матрице не хватает целой фичи.
ссылка на оригинал статьи https://habr.com/ru/articles/1064954/