Извините, это длинный, но я просто объясняю свой ход мысли, когда я анализирую это. Вопросы в конце.
У меня есть понимание того, что входит в измерение времени работы кода. Он запускается несколько раз, чтобы получить среднее время выполнения для учета различий за прогон, а также для получения времени, когда кеш использовался лучше.
В попытке измерить время выполнения для кого-то, я придумал этот код после нескольких ревизий.
В конце концов я закончил с этим кодом, который дал результаты, которые я собирался захватить, не вводя в заблуждение цифры:
// implementation C
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
Console.WriteLine(testName);
Console.WriteLine("Iterations: {0}", iterations);
var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
var timer = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < results.Count; i++)
{
results[i].Start();
test();
results[i].Stop();
}
timer.Stop();
Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), timer.ElapsedMilliseconds);
Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), timer.ElapsedTicks);
Console.WriteLine();
}
Из всего кода, который я видел, что время выполнения мер, они обычно были в форме:
// approach 1 pseudocode start timer; loop N times: run testing code (directly or via function); stop timer; report results;
Это было хорошо на мой взгляд, поскольку с числами у меня есть общее время работы и вы можете легко выработать среднее время работы и иметь хорошую локальность кэша.
Но один набор значений, которые, как я думал, были важны, - это минимальное и максимальное время работы итерации. Это не может быть рассчитано с использованием вышеуказанной формы. Поэтому, когда я написал свой тестовый код, я написал их в этой форме:
// approach 2 pseudocode loop N times: start timer; run testing code (directly or via function); stop timer; store results; report results;
Это хорошо, потому что я мог бы найти минимальное, максимальное и среднее время, числа, которые меня интересовали. До сих пор я понял, что это может потенциально исказить результаты, поскольку кэш потенциально может быть затронут, поскольку цикл wasn ' t очень плотно, давая мне менее оптимальные результаты.
То, как я написал тестовый код (используя LINQ), добавило дополнительные накладные расходы, о которых я знал, но проигнорировал, так как я просто измерял текущий код, а не накладные расходы. Вот моя первая версия:
// implementation A
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
Console.WriteLine(testName);
var results = Enumerable.Repeat(0, iterations).Select(i =>
{
var timer = System.Diagnostics.Stopwatch.StartNew();
test();
timer.Stop();
return timer;
}).ToList();
Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds));
Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks));
Console.WriteLine();
}
Здесь я думал, что все в порядке, так как я измеряю только время, необходимое для запуска тестовой функции. Накладные расходы, связанные с LINQ, не включаются в время работы. Чтобы уменьшить накладные расходы на создание объектов таймера в цикле, я сделал модификацию.
// implementation B
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
Console.WriteLine(testName);
Console.WriteLine("Iterations: {0}", iterations);
var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
results.ForEach(t =>
{
t.Start();
test();
t.Stop();
});
Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), results.Sum(t => t.ElapsedMilliseconds));
Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), results.Sum(t => t.ElapsedTicks));
Console.WriteLine();
}
Это улучшило общее время, но вызвало небольшую проблему. Я добавил общее время работы в отчете, добавив каждое время итерации, но дал вводящие в заблуждение цифры, так как времена были короткими и не отражали фактическое время работы (что обычно было намного дольше). Мне нужно было измерить время всего цикла, поэтому я отошел от LINQ и получил код, который у меня сейчас наверху. Этот гибрид получает время, которое, как мне кажется, важно при минимальных накладных расходах AFAIK. (запуск и остановка таймера задает только таймер с высоким разрешением). Также любое изменение контекста для меня не имеет значения, поскольку оно является частью нормального выполнения в любом случае.
В какой-то момент я заставил поток выполнить в цикле, чтобы удостовериться, что ему предоставляется шанс в какой-то момент в удобное время (если тестовый код связан с ЦП и вообще не блокируется). Я не слишком обеспокоен запущенными процессами, которые могут изменить кэш в худшую сторону, так как я буду использовать эти тесты в любом случае. Однако я пришел к выводу, что для этого конкретного случая нет необходимости. Хотя я мог бы включить его в окончательную окончательную версию, если это окажется полезным в целом. Возможно, в качестве альтернативного алгоритма для определенного кода.
Теперь мои вопросы:
- Я сделал правильный выбор? Некоторые неправильные?
- Я сделал неправильные предположения о целях в процессе моей мысли?
- Будет ли минимальное или максимальное время работы действительно полезной информацией, чтобы иметь или это потерянное дело?
- Если да, то какой подход будет лучше вообще? Время работы в цикле (подход 1)? Или время работает только на рассматриваемом коде (подход 2)?
- Можно ли использовать мой гибридный подход в целом?
- Должен ли я уступить (по причинам, указанным в последнем абзаце), или это больше вреда для времени, чем необходимо?
- Есть ли более предпочтительный способ сделать это, о котором я не упоминал?
Просто, чтобы быть ясным, я не, ищущий универсальный, использующий везде, точный таймер. Я просто хочу знать об алгоритме, который я должен использовать, когда хочу быстро реализовать, достаточно точный таймер для измерения кода, когда библиотека или другие сторонние инструменты недоступны.
Я склонен писать весь свой тестовый код в этой форме, если не будет возражений:
// final implementation
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
// print header
var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
for (int i = 0; i < 100; i++) // warm up the cache
{
test();
}
var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process
for (int i = 0; i < results.Count; i++)
{
results[i].Start(); // time individual process
test();
results[i].Stop();
}
timer.Stop();
// report results
}
Для щедрости, я бы идеально хотел, чтобы на все вышеперечисленные вопросы ответили. Я надеюсь на хорошее объяснение того, что мои мысли, которые повлияли на код здесь, были оправданы (и, возможно, мысли о том, как улучшить его, если они субоптимальны), или если я ошибался с точкой, объясните, почему это неправильно и/или не нужно, и если применимы, предлагают лучшую альтернативу.
Подводя итог важным вопросам и моим мыслям о принятых решениях:
- Получает ли время работы каждой отдельной итерации вообще хорошее дело?
Со временем для каждой отдельной итерации я могу рассчитать дополнительную статистическую информацию, такую как минимальное и максимальное время работы, а также стандартное отклонение. Поэтому я вижу, есть ли такие факторы, как кеширование или другие неизвестные, могут искажать результаты. Это приводит к моей "гибридной" версии. - Имеет небольшой цикл прогонов до того, как фактическое время начала тоже хорошо?
Из моего ответа на Сам Шаффрон подумал о цикле, это увеличивает вероятность того, что постоянно доступ к памяти будет кэшироваться. Таким образом, я измеряю время только тогда, когда все кэшируется, а не в некоторых случаях, когда доступ к памяти не кэшируется. - Помогло ли принудительное
Thread.Yield()
в цикле или повредило тайминги тестовых случаев, связанных с CPU?
Если процесс был связан с ЦП, планировщик ОС снизил бы приоритет этой задачи, увеличивая время из-за нехватки времени на ЦП. Если он не связан с ЦП, я бы опустил урожай.
Основываясь на ответах здесь, я буду писать свои тестовые функции, используя окончательную реализацию без индивидуальных таймингов для общего случая. Если бы я хотел иметь другие статистические данные, я бы снова ввел его обратно в тестовую функцию, а также применил другие вещи, упомянутые здесь.