Неправильно ли мой метод измерения времени работы?

Извините, он длинный, но я просто объясняю ход своих мыслей, пока анализирую это. Вопросы в конце.

Я понимаю, что входит в измерение времени выполнения кода. Это' 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. (запуск и остановка таймера просто запрашивает таймер с высоким разрешением) Также любое происходящее переключение контекста неважно для меня, так как оно в любом случае является частью нормального выполнения.

В какой-то момент я заставил поток уступить в цикле, чтобы убедиться, что ему дается шанс в какой-то момент в удобное время (если тестовый код привязан к процессору и не т блок вообще). Меня не слишком беспокоят запущенные процессы, которые могут изменить кеш в худшую сторону, так как я бы все равно запускал эти тесты в одиночку. Однако я пришел к выводу, что в данном конкретном случае в этом нет необходимости. Хотя я мог бы включить его в окончательную окончательную версию, если она окажется полезной в целом. Возможно, в качестве альтернативного алгоритма для определенного кода.


Теперь мои вопросы:

  • Я сделал правильный выбор? Некоторые неправильные?
  • Сделал ли я неправильные предположения о целях в моем мыслительном процессе?
  • Действительно ли минимальное или максимальное время выполнения будет полезной информацией или это бесперспективное дело?
  • Если да, какой подход был бы лучше в целом? Время, идущее в цикле (подход 1)? Или время выполнения только рассматриваемого кода (подход 2)?
  • Можно ли будет использовать мой гибридный подход в целом?
  • Должен ли уступить (по причинам, объясненным в последнем абзаце), или это больше навредить времени, чем необходимо?
  • Есть ли более предпочтительный способ сделать это, о котором я не упоминал?

Чтобы внести ясность, я не ищу универсальный, использовать где угодно, точный таймер. Я просто хочу знать алгоритм, который мне следует использовать, когда я хочу быстро реализовать достаточно точный таймер для измерения кода, когда библиотека или другие сторонние инструменты недоступны.

Я склонен писать все свои тесты код в таком виде не должно быть возражений:

// 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
}

За награду, В идеале я хотел бы получить ответы на все вышеперечисленные вопросы. Я надеюсь на хорошее объяснение того, были ли мои мысли, которые повлияли на код здесь, хорошо обоснованы (и, возможно, мысли о том, как его улучшить, если они неоптимальные), или если я ошибся с точкой, объясните, почему это неправильно и / или ненужно, и если применимо, предложите лучшую альтернативу.

Подводя итог важным вопросам и моим мыслям по поводу принятых решений:

  1. Хорошо ли вообще получать информацию о времени выполнения каждой отдельной итерации?
    Со временем для каждой При индивидуальной итерации я могу рассчитать дополнительную статистическую информацию, такую ​​как минимальное и максимальное время работы, а также стандартное отклонение. Так что я могу увидеть, есть ли такие факторы, как кеширование или другие неизвестные факторы, которые могут искажать результаты. Это привело к моей «гибридной» версии.
  2. Хорошо ли иметь небольшой цикл прогонов до фактического начала отсчета времени?
    Судя по моему ответу на мысль Сэма Саффрона о цикле, это должно увеличить вероятность того, что постоянно доступная память будет кешировано. Таким образом, я измеряю время только тогда, когда все кэшируется, а не в некоторых случаях, когда доступ к памяти не кэшируется.
  3. Будет ли принудительный Thread.Yield () внутри цикла помочь или повредить тайминги тестовых примеров с привязкой к ЦП?
    Если процесс был привязан к ЦП, планировщик ОС снизил бы приоритет этой задачи, что потенциально увеличило бы время из-за нехватки времени ЦП. Если это не связано с процессором, я бы пропустил уступку.

Основываясь на ответах здесь, я ' Я буду писать свои тестовые функции, используя окончательную реализацию, без отдельных таймингов для общего случая. Если мне нужны другие статистические данные, я бы снова ввел их в тестовую функцию, а также применил другие вещи, упомянутые здесь.

16
задан Community 23 May 2017 в 10:33
поделиться