Извините, это долго, но я просто объясняю свой ход мыслей, анализируя это. Вопросы в конце.
У меня есть понимание того, что входит в измерение времени выполнения кода. Он запускается несколько раз, чтобы получить среднее время выполнения для учета различий в каждом запуске, а также для получения времени, когда кэш использовался лучше.
В попытке измерить время выполнения для кого-то я придумал этот код после нескольких ревизий.
В итоге я получил этот код, который дал результаты, которые я намеревался получить, не вводя в заблуждение цифры:
// 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;
Это хорошо, потому что я мог тогда найти минимальное, максимальное и среднее время интересующих меня чисел. До сих пор я понимал, что это может потенциально исказить результаты, поскольку кэш мог потенциально пострадать, так как цикл не был ' т очень туго, давая мне результаты ниже оптимальных.
То, как я написал тестовый код (с использованием 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
}
Что касается награды, в идеале я хотел бы получить ответы на все вышеупомянутые вопросы. Я надеюсь на хорошее объяснение того, хорошо ли оправданы мои мысли, которые повлияли на код здесь (и, возможно, мысли о том, как улучшить его, если он неоптимальный), или если я ошибся с точкой зрения, объясните, почему это неправильно и / или не нужно, и если применимо, предложите лучшую альтернативу.
Суммируя важные вопросы и мои мысли о принятых решениях:
- Полезно ли получать время выполнения каждой отдельной итерации?
С учетом времени для каждой отдельной итерации я могу рассчитать дополнительную статистическую информацию, такую как минимальное и максимальное время выполнения, а также стандартное отклонение. Таким образом, я могу видеть, есть ли факторы, такие как кэширование или другие неизвестные, могут искажать результаты. Это привело к моей "гибридной" версии.
- Есть ли небольшой цикл прогонов до того, как начнется фактическое время?
Из моего ответа на мысль 1084 * Сэма Шафрона о цикле, это увеличивает вероятность того, что постоянно доступная память будет кэшироваться. Таким образом, я измеряю время только для случаев, когда все кэшируется, а не для некоторых случаев, когда доступ к памяти не кэшируется.
- Может ли принудительный
Thread.Yield()
в цикле помочь или ухудшить время тестовых случаев с привязкой к ЦП?
Если бы процесс был связан с процессором, планировщик ОС снизил бы приоритет этой задачи, потенциально увеличивая время из-за нехватки времени на процессоре. Если это не связано с процессором, я бы пропустил уступку.
Основываясь на ответах, приведенных здесь, я буду писать свои тестовые функции, используя окончательную реализацию, без индивидуальных временных интервалов для общего случая. Если бы я хотел получить другие статистические данные, я бы снова ввел их в тестовую функцию, а также применил другие вещи, упомянутые здесь.