Извините, он длинный, но я просто объясняю ход своих мыслей, пока анализирую это. Вопросы в конце.
Я понимаю, что входит в измерение времени выполнения кода. Это' s запускаются несколько раз, чтобы получить среднее время выполнения, чтобы учесть различия за запуск, а также получить время, когда кэш использовался лучше.
В попытке измерить время выполнения для кого-то, я придумал это после нескольких ревизий.
В конце концов, я получил этот код, который дал результаты, которые я намеревался зафиксировать, без ввода вводящих в заблуждение чисел:
// implementation C
static void Test(string testName, Func 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;
Это хорошо, потому что я мог затем найти минимальное, максимальное и среднее время, числа, которые меня интересовали. До сих пор я понимал, что это может потенциально исказить результаты, поскольку кеш потенциально может это повлияло на то, что цикл был не очень плотным, что дало мне менее чем оптимальные результаты.
То, как я писал тестовый код (с использованием LINQ), добавляло дополнительных накладных расходов, о которых я знал, но игнорировал, поскольку я просто измерял выполняемый код, а не накладные расходы. Вот моя первая версия:
// implementation A
static void Test(string testName, Func 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(string testName, Func 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. (запуск и остановка таймера просто запрашивает таймер с высоким разрешением) Также любое происходящее переключение контекста неважно для меня, так как оно в любом случае является частью нормального выполнения.
В какой-то момент я заставил поток уступить в цикле, чтобы убедиться, что ему дается шанс в какой-то момент в удобное время (если тестовый код привязан к процессору и не т блок вообще). Меня не слишком беспокоят запущенные процессы, которые могут изменить кеш в худшую сторону, так как я бы все равно запускал эти тесты в одиночку. Однако я пришел к выводу, что в данном конкретном случае в этом нет необходимости. Хотя я мог бы включить его в окончательную окончательную версию, если она окажется полезной в целом. Возможно, в качестве альтернативного алгоритма для определенного кода.
Теперь мои вопросы:
Чтобы внести ясность, я не ищу универсальный, использовать где угодно, точный таймер. Я просто хочу знать алгоритм, который мне следует использовать, когда я хочу быстро реализовать достаточно точный таймер для измерения кода, когда библиотека или другие сторонние инструменты недоступны.
Я склонен писать все свои тесты код в таком виде не должно быть возражений:
// final implementation
static void Test(string testName, Func 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 ()
внутри цикла помочь или повредить тайминги тестовых примеров с привязкой к ЦП? Основываясь на ответах здесь, я ' Я буду писать свои тестовые функции, используя окончательную реализацию, без отдельных таймингов для общего случая. Если мне нужны другие статистические данные, я бы снова ввел их в тестовую функцию, а также применил другие вещи, упомянутые здесь.