Отпишусь пожалуй, чтобы не забыть по горячим следам и потом проще было документацию писать.
Поехали
Что это такое
Набор хелперов для competition performance tests, работающий поверх единственного более-менее правильного .net-фреймворка для бенчмарков — BenchmarkDotNet. В CodeJam лежит в проекте CodeJam.Main-Tests.Performance.
Говоря русским языком, эта штука позволяет написать набор реализаций-кандидатов, дополнить их образцом (baseline test), оформить в виде юнит-теста, и дальше CI-сервер за вас сам проверяет, что все реализации укладываются в заданные лимиты по производительности. Лимиты записываются в виде коэффициентов — производительность относительно baseline.
Абсолютные значения тут неприменимы по очевидным причинам: тесты будут выполняться на разном железе/в разном окружении и абсолютное время может легко различаться в разы.
Главная и уникальная киллфича проекта: нет необходимости самому размечать лимиты по перфомансу для каждого из тестов, это делается автоматически.
Чтоб было понятно о чём речь: простейший тест, который гарантирует, что sum += i выполняется примерно в полтора раза быстрее, чем sum += i + 1 выглядит
вот так:
public class ProofsSensitivityBenchmark
{
[Test]
public void BenchmarkSensitivity() => CompetitionBenchmarkRunner.Run(this, AssemblyWideConfig.RunConfig);
public const int Count = 100 * 1000;
// baseline
[CompetitionBaseline)]
public int Test00Baseline()
{
var sum = 0;
for (var i = 0; i < Count; i++)
{
sum += i;
}
return sum;
}
// не быстрее, чем 1.55 от baseline
// не медленнее, чем 1.65 от baseline
[CompetitionBenchmark(1.55, 1.65)]
public int Test01PlusOne()
{
var sum = 0;
for (var i = 0; i < Count; i++)
{
sum += i + 1;
}
return sum;
}
}
Минимальный/максимальный коэффициенты в [CompetitionBenchmark(1.55, 1.65)] заполнены автоматически. После того, как режим автозаполнения отключен, тест упадёт, если Test01PlusOne будет выполняться быстрее/медленнее заданных значений.
Зачем это нужно
Во-первых, чтобы принимать обоснованные решения о выборе конкретной реализации и чтобы узнать, что текущая реализация перестала быть оптимальной при смене компилятора/фреймворка.
Пример — см папку Arithmetic с перфтестами для Operators<T>.
Во-вторых, для регрессионных юнит-тестов и для документирования perf-контракта.
В каком оно состоянии
Pre-alpha. Проблема в следующем: BenchmarkDotNet не затачивался под запуск в виде юнит-тестов, всю доработку напильником приходится делать самому.
К сожалению, не всё можно обойти самостоятельно и часть изменений надо продавить в виде pull requests в сам BenchmarkDotNet.
Сюрпрайз №2: похоже, очень мало кто занимается перфтестами, поэтому помимо "что надо поменять" нужно ещё обосновать "зачем это надо" даже в очевидных (мне ) моментах.
Это ещё больше замедляет дело, так что точных сроков когда я назвать пока не могу.
Проблемные моменты:
1. В текущей реализации каждый метод-кандидат собирается как отдельный бинарник и запускается в собственном процессе. В лучшем случае каждый тест занимает 5-10 секунд, бонусом идёт 2..20 мб мусора в папке bin. Это ключевой затык, тикет с проверенно рабочим решением заведён, жду ответа от команды Bench.NET.
2. Нет возможности задать лимиты по аллокациям/сборкам мусора. Точнее, она есть, но ждёт реализации п.1.
Как использовать:
На сегодня — только внутри самого CodeJam. Я намеренно не выпускаю отдельный пакет, т.к. в процессе догфудинга время от времени удаётся заметно облегчить написание/использование юнит-тестов. В частности, с вероятностью в 100% будут поправлены имена классов/методов в самих перфтестах, текущий вариант неудачный.
Если есть желание добавить свой перфест, на примере кода выше:
1. Написать метод-образец, пометить как [CompetitionBaseline]
2. Написать методы-кандидаты, пометить их как [CompetitionBenchmark]. В параметрах конструктора атрибута можно указать лимиты по времени в виде мин/макс коэффициентов относительно Baseline-метода. Значения коэффициентов <0 обрабатываются как "не учитывать". Итого:
[CompetitionBenchmark] // падает всегда, т.к. не заданы границы.
[CompetitionBenchmark(1.5, 3)] // падает, если быстрее, чем 1.5x и медленнее, чем 3x.
[CompetitionBenchmark(1.5, -1)] // падает, если быстрее, чем 1.5x.
[CompetitionBenchmark(-1, 3)] // падает, если медленнее, чем 3x.
[CompetitionBenchmark(3)] // падает, если медленнее, чем 3x.
[CompetitionBenchmark(-2, -3)] // не падает вообще.
[Benchmark] // не падает вообще.
3. Добавить запуск бенчмарка:
[Test]
public void TestName() => CompetitionBenchmarkRunner.Run(this, AssemblyWideConfig.RunConfig);
поддерживается любой из юнит-тест-фреймворков. Главное, чтоб он распознавал исключения как ошибки теста и перенаправлял вывод на консоль в output теста.
4.2. Запустить тест вручную через VS test explorer. Раннер решарпера (по крайней мере в EAP) пока не поддерживается.
* В режиме автоаннотации тест выполняется в несколько проходов (до 10), пока не начнёт укладываться в свежесобранные лимиты. Т.е. если обычных десяти секунд тест выполняется полторы минуты — это нормально.
После завершения работы теста значения min/max в [CompetitionBenchmark] будут заполнены актуальными лимитами.
5. После того, как лимиты расставлены, запустить тест. Если он зелёный — вы угадали с лимитами. Если он красный — или лимиты слишком жёсткие, или реализация одного из методов поменялась и вы нашли баг
Подробнее — см сообщения теста и таблицу с результатами в output.
6. Для динамически генерируемых бенчмарков лимиты можно задавать в виде xml-файла. Пример см в NumOperatorsPerfTest.generated.cs. Для этого надо:
6.1. Добавить xml-файл рядом с файлом с кодом бенчмарка, build action — embedded resource. Имя файла должно различаться только расширением (.xml вместо .cs). Содержимое файла:
[CompetitionMetadata("CodeJam.Arithmetic.NumOperatorsPerfTest.generated.xml")] // имя ресурсаpublic class NumOperatorsPerfTest
Параметр атрибута — имя ресурса с xml-файлом.
6.3. В коде не указывать лимиты в виде параметров [CompetitionBenchmark] — они будут игнорироваться.
6.4. Запустить перфтест в режиме авто-аннотирования.
Вопросы / предложения? Велкам
P.S. Будет обновляться по мере появления вопросов/прогресса/наличия свободного времени.
Здравствуйте, AndrewVK, Вы писали:
AVK>Разогревающие прогоны делаются или сразу измерения начинаются?
За сам запуск тестов отвечает benchmarkDotNet, от меня только обвязка чтоб оно дружило с студией/юниттестами.
Авторы benchmarkDotNet пошли по пути прогонов мало не бывает. У них очень гибкие настройки, вариант по умолчанию выдаёт довольно стабильные результаты, мне пришлось долго подбирать варианты чтоб получить приближенный к реальным условиям разброс.
Без этого было безумно тяжело подобрать лимиты по времени так, чтобы перфтест в них укладывался на других машинах. Как результат, тесты становились бесполезными, т.к. работали только у автора.
Сами параметры не захардкожены, по-прежнему можно подсунуть любой конфиг.
AVK>Почему то стабильно в режиме аннотации на первый тест орет что source file not found.
Это решарперовский тест раннер. С ним ещё не разбирался, предварительный диагноз — он pdb-шки тырит. Из-под стандартного студийного всё ок.
AVK>Еще такой вопрос: зачем указывать нижнюю границу коэффициента и будет ли считаться тест непрошедшим, если он выполнится быстрее нижней границы
Можно указать нижнюю в 0. Но по хорошему нижнюю надо тоже указывать. Иначе, если тест внезапно стал выполняться в n раз быстрее, то мы об этом не узнаем и оставим старый перф контракт. Из опыта — так очень легко упустить баги из разряда "упс, часть функционала отвалилась"
Если сценарий без нижней границы важен — допилю api, чтоб с ним было удобнее работать.
Здравствуйте, Sinix, Вы писали:
S>Если сценарий без нижней границы важен — допилю api, чтоб с ним было удобнее работать.
Да фик его знает. Я просто попробовал тестик написать, что показалось неочевидным написал.
Еще с правками при автоаннотации какие то странности есть — почему то не аннотируются тесты DoubleXxx. Причем оно вроде файл обновляет, но такое ощущение что правит его не там где нужно, у других тестов.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>
Еще надо подумать над сценарием, когда исходники тестов генерятся, а не руками пишутся. В этом случае удобнее было не исходники аннотировать, а положить рядом отдельный файлик.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>
AVK>Да фик его знает. Я просто попробовал тестик написать, что показалось неочевидным написал.
Задачу понял, добавлю в атрибут перегрузку только с max + допилю аннотатор, чтоб в таких атрибутах мин не проставлял. Несложно, так что пусть будет.
AVK>Еще с правками при автоаннотации какие то странности есть — почему то не аннотируются тесты DoubleXxx. Причем оно вроде файл обновляет, но такое ощущение что правит его не там где нужно, у других тестов.
Буду смотреть, походу PDB-шка старая попалась, надо чексуммы сверять. Там код дубовый как не знаю кто: берёт номер строки + имя файла из pdb, перебирает все строки вверх, заменяет значение атрибута. Перебор прерывается, если встретился "class", "///", ";" или "}". До этого работал с первого раза, зарраза.
Ещё вопрос: а как у тебя в T4 прикручена поддержка c#6? Её ж там нет
---
AVK>Да, вот еще такой вопрос — у тебя методы возвращают значения. Что с этими значениями делается? Они сравниваются?
Сейчас игнорятся. Без правок bench.Net не поправить, запрос на это дело на прошлой неделе завёл.
--- AVK>Еще надо подумать над сценарием, когда исходники тестов генерятся, а не руками пишутся. В этом случае удобнее было не исходники аннотировать, а положить рядом отдельный файлик.
Здравствуйте, Sinix, Вы писали:
S>Ещё вопрос: а как у тебя в T4 прикручена поддержка c#6? Её ж там нет
Там следующем же ответом написано, что в update 2 — есть.
Здравствуйте, Jack128, Вы писали:
S>>Ещё вопрос: а как у тебя в T4 прикручена поддержка c#6? Её ж там нет J>Там следующем же ответом написано, что в update 2 — есть.
Тьфублин, слона не заметил
На upd2 пока переползать не тянет, подождём пока оно само в updates студии не приедет.
Здравствуйте, AndrewVK, Вы писали:
AVK>Еще надо подумать над сценарием, когда исходники тестов генерятся, а не руками пишутся. В этом случае удобнее было не исходники аннотировать, а положить рядом отдельный файлик.
Done, см NumOperatorsPerfTest.generated.xml
Чтобы файл подхватился, в атрибуте надо указать [CompetitionBenchmark(MinMaxFromResource = true)]
С именем xml-файла проблемы, опишу текущий вариант, есть варианты как сделать лучше — велкам
1. Файл должен быть приаттачен как внедрённый ресурс, т.к. в общем случае раннер не должен требовать, чтобы исходники лежали рядом с бинарником перфтестов.
2. Имя файла ресурса взято как resourceType.FullName + ".generated.xml";
resourceType — тип, в котором объявлены бенчмарки. Если тип вложенный — берётся его родитель.
Вот тут у нас затык: получается, что путь к файлу с ресурсом должен совпадать с namespace класса с перфтестами. Чтобы эту связь поломать, достаточно сменить root namespace проекта/переименовать папку _и_ не переименовать сам тип.
3. Режим автоаннотации требует, чтобы xml-файл назывался так же, как файл с исходниками, только расширение было не .cs, а xml.
В принципе, всё это можно решить вот так:
1. xml-файл должен называться так же, как файл с исходниками, только расширение было не .cs, а xml
2. На resourceType обязательно должен быть навешен кастомный атрибут с именем ресурса. Атрибута нет — xml вообще не проверяется.
3. [CompetitionBenchmark(MinMaxFromResource = true)] больше не нужен, используется [CompetitionBenchmark], как раньше.
Здравствуйте, Sinix, Вы писали:
S>В принципе, всё это можно решить вот так: S>1. xml-файл должен называться так же, как файл с исходниками, только расширение было не .cs, а xml S>2. На resourceType обязательно должен быть навешен кастомный атрибут с именем ресурса. Атрибута нет — xml вообще не проверяется. S>3. [CompetitionBenchmark(MinMaxFromResource = true)] больше не нужен, используется [CompetitionBenchmark], как раньше. S>Пойдёт?
А вот это нормально, я границы подправлю чуть пожже. Замерял на ноуте, там результаты из-за агрессивного speed step легко процентов на 20 скачут. Пока лучше пометить тесты как Explicit, чтоб на них не отвлекаться.
Здравствуйте, AndrewVK, Вы писали:
AVK>Да.
Ок, вечером скину, если время будет.
AVK>ХЗ.
Значит заменяю. Я не думаю, что кто-то в здравом уме будет пихать в один файл два разных класса с одинаковыми именами.
Здравствуйте, AndrewVK, Вы писали:
AVK>Еще такой вопрос: зачем указывать нижнюю границу коэффициента и будет ли считаться тест непрошедшим, если он выполнится быстрее нижней границы?
Добавил.
Бонусом — значения <0 обрабатываются как "не учитывать". Итого:
[CompetitionBenchmark] // падает всегда, т.к. не заданы границы.
[CompetitionBenchmark(1.5, 3)] // падает, если быстрее, чем 1.5x и медленнее, чем 3x.
[CompetitionBenchmark(1.5, -1)] // падает, если быстрее, чем 1.5x.
[CompetitionBenchmark(-1, 3)] // падает, если медленнее, чем 3x.
[CompetitionBenchmark(3)] // падает, если медленнее, чем 3x.
[CompetitionBenchmark(-2, -3)] // не падает вообще.
[Benchmark] // не падает вообще.
Здравствуйте, Sinix, Вы писали:
S>А вот это нормально, я границы подправлю чуть пожже. Замерял на ноуте, там результаты из-за агрессивного speed step легко процентов на 20 скачут. Пока лучше пометить тесты как Explicit, чтоб на них не отвлекаться.
Видимо, придется так и сделать — там уже и 0.77 об baseline проскакивают. Как то оно архинестабильно.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>
Здравствуйте, AndrewVK, Вы писали:
AVK>Видимо, придется так и сделать — там уже и 0.77 об baseline проскакивают. Как то оно архинестабильно.
Ну вот как-то так вот. Как будет время, попробую отказаться от хранения результата в поле (Storage) в тестах. Есть у меня мысля, что оно в итоге в память упирается.
Должно вылечиться с переездом на in-process бенчмарки, там фоновой нагрузки из-за запуска процесса нет.
Вариант 2 — определить какой-нибудь символ для сборки на CI-сервере и если он включён, настраивать FastRunConfig на большее количество прогонов.
Здравствуйте, AndrewVK, Вы писали:
AVK>Видимо, придется так и сделать — там уже и 0.77 об baseline проскакивают. Как то оно архинестабильно.
Починил вроде.
Изначально идея была в том, что запись результата в поле добавит разброса результатам, чтобы не возиться с подгоном границ теста под разные машины — в общем оно сработало чересчур удачно
Здравствуйте, Sinix, Вы писали:
S>Изначально идея была в том, что запись результата в поле добавит разброса результатам, чтобы не возиться с подгоном границ теста под разные машины — в общем оно сработало чересчур удачно
Оно прежде всего оптимизацию вырубило, так что разница между baseline и операторами стала крошечной. А с локальной переменной разница в 6-7 раз.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>
Здравствуйте, AndrewVK, Вы писали:
S>>Изначально идея была в том, что запись результата в поле добавит разброса результатам, чтобы не возиться с подгоном границ теста под разные машины — в общем оно сработало чересчур удачно AVK>Оно прежде всего оптимизацию вырубило, так что разница между baseline и операторами стала крошечной. А с локальной переменной разница в 6-7 раз.
Ок, чего делаем? Оставляем текущий вариант, откатываем на поле, делаем больше прогонов, чтобы результат не так скакал?.
UPD У меня разброс не так велик, например для BitwiseAndOperator MinRatio="2.24" MaxRatio="2.87". Завтра на нормальной машине проверю.
Здравствуйте, Sinix, Вы писали:
S>Ок, чего делаем? Оставляем текущий вариант, откатываем на поле, делаем больше прогонов, чтобы результат не так скакал?.
ХЗ.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>