Как измерить производительность кода в .NET?

Я делаю некоторые быстрые и грязные бенчмаркинга на одной строке кода С#, используя DateTime:

long lStart = DateTime.Now.Ticks;
// do something
long lFinish = DateTime.Now.Ticks;

Проблема заключается в результатах:

Start Time [633679466564559902]
Finish Time [633679466564559902]

Start Time [633679466564569917]
Finish Time [633679466564569917]

Start Time [633679466564579932]
Finish Time [633679466564579932]

... и т.д.

Учитывая, что время начала и окончания идентично, Ticks явно недостаточно гранулировано.

Итак, как я могу лучше измерить производительность?

Ответ 1

Класс Stopwatch, доступный с .NET 2.0, является лучшим способом для этого. Это очень высокопроизводительный счетчик с точностью до долей миллисекунды. Взгляните на документацию MSDN, что довольно ясно.

EDIT: как было предложено ранее, рекомендуется также запустить код несколько раз, чтобы получить разумное среднее время.

Ответ 2

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

Edit: Кроме того, из-за характера текущих оптимизирующих компиляторов (и виртуальных машин, таких как CLR и JVM), это может быть очень ошибочным для измерения скорости выполнения отдельных строк кода, поскольку измерение может влиять на скорость достаточно много. Гораздо лучший подход состоял бы в том, чтобы профилировать всю систему (или, по крайней мере, более крупные блоки) и проверить, где узкие места.

Ответ 3

Просто добавьте к тому, что другие уже говорили об использовании секундомера и измеряя средние значения.

Перед измерением убедитесь, что вы вызываете свой метод. В противном случае вы будете измерять время, необходимое для компиляции кода JIT. Это может значительно исказить ваши цифры.

Кроме того, убедитесь, что вы измеряете код режима выпуска, так как оптимизация отключена по умолчанию для отладочных сборников. Настройка кода отладки бессмысленна imho.

И убедитесь, что вы измеряете то, что вы на самом деле хотите измерить. Когда срабатывает оптимизация, компилятор/JIT-компилятор может переупорядочить код или полностью удалить его, чтобы вы могли измерить что-то немного отличающееся от намеченного. По крайней мере, посмотрите на сгенерированный код, чтобы убедиться, что код не был удален.

В зависимости от того, что вы пытаетесь измерить, имейте в виду, что реальная система будет подчеркивать время выполнения иначе, чем типичное тестовое приложение. Некоторые проблемы с производительностью связаны с, например, как объекты собирают мусор. Эти проблемы обычно не отображаются в простом тестовом приложении.

Собственно, лучшим советом является измерение реальных систем с реальными данными, поскольку тесты на песочнице могут оказаться весьма неточными.

Ответ 4

Я нахожу эти полезные

http://accelero.codeplex.com/SourceControl/changeset/view/22633#290971 http://accelero.codeplex.com/SourceControl/changeset/view/22633#290973 http://accelero.codeplex.com/SourceControl/changeset/view/22633#290972

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

Итак, это

var tickTimer = new TickTimer();
//call a method that takes some time
DoStuff();
tickTimer.Stop();
Debug.WriteLine("Elapsed HighResElapsedTicks " + tickTimer.HighResElapsedTicks);
Debug.WriteLine("Elapsed DateTimeElapsedTicks " + tickTimer.DateTimeElapsedTicks);
Debug.WriteLine("Elapsed ElapsedMilliseconds " + tickTimer.ElapsedMilliseconds);
Debug.WriteLine("Start Time " + new DateTime(tickTimer.DateTimeUtcStartTicks).ToLocalTime().ToLongTimeString());

выведет этот

Elapsed HighResElapsedTicks 10022886
Elapsed DateTimeElapsedTicks 41896
Elapsed ElapsedMilliseconds 4.18966178849554
Start Time 11:44:58

DebugTimer - это оболочка для TickTimer, которая будет записывать результат в Debug. (примечание: он поддерживает одноразовый шаблон)

Итак, это

using (new DebugTimer("DoStuff"))
{
    //call a method that takes some time
    DoStuff();
}

выведет это в окно отладки

DoStuff: Total 3.6299 ms

IterationDebugTimer предназначен для определения времени, необходимого для многократного запуска операции и записи результата в Debug. Он также выполнит начальный прогон, который не включен, чтобы игнорировать время запуска. (примечание: он поддерживает одноразовый шаблон)

Итак, это

int x;
using (var iterationDebugTimer = new IterationDebugTimer("Add", 100000))
{
    iterationDebugTimer.Run(() =>
    {
        x = 1+4;
    });
}

Выведет этот

Add: Iterations 100000 
Total 1.198540 ms 
Single 0.000012 ms

Ответ 5

Используйте настоящий профайлер, например dotTrace.

Ответ 6

Вы можете использовать Stopwatch, если используете .NET 2.0 или новее.

System.Diagnostics.Stopwatch.StartNew();

Класс Stopwatch также имеет общедоступное поле для чтения IsHighResolution, которое позволит вам узнать, основан ли секундомер на счетчик производительности с высоким разрешением. Если его нет, он основан на системном таймере.

Я не уверен, что нужно, чтобы секундомер был основан на счетчике производительности с высоким разрешением. Есть несколько вызовов API, но я считаю, что если секундомер не использует высокое разрешение, то API, вероятно, не существует.

Ответ 7

Образец для класса Stopwatch

    using System.Diagnostics;
    ......
    ...
    ..
    Stopwatch sw = new Stopwatch();
    sw.Start();
    //Your Code Here

    sw.Stop();
    Console.WriteLine("Elapsed={0}",sw.Elapsed);

Ответ 8

См. ответ на Является ли DateTime.Now лучшим способом измерения производительности функций? для объяснения или прочитайте мой сообщение в блоге о высокопроизводительном измерении

Проблема в том, что DateTime имеет разрешение около 15 мс, это не может быть точнее. Секундомер, однако, может.

Ответ 10

https://andreyakinshin.gitbooks.io/performancebookdotnet/content/science/microbenchmarking.html

https://github.com/PerfDotNet/BenchmarkDotNet

"Действительно, микрообъектив очень тяжелый. Если операция занимает 10-100 нс, измерение операции - большая проблема. Я предлагаю вам использовать BenchmarkDotNet для ваших тестов. Его библиотека, которая может помочь вам сделать честный тест и Разумеется, вы можете написать собственный тест без каких-либо дополнительных библиотек. В этом разделе мы расскажем о том, почему это, вероятно, плохая идея и что вы должны знать перед запуском".

Ответ 12

Другой вариант заключается в том, что таймер-код автоматически вставлен с Fody. Это делает ваш код гораздо более легким для чтения, поскольку он разделяет ваши сквозные проблемы. Я думаю, что это близко к тому, что называется Аспектно-ориентированное программирование, но выполняется в момент после компиляции.

См. https://github.com/Fody/MethodTimer для дополнения fody, который выполняет синхронизацию метода.

Цитата из Readme:

С перехватчиком, где-то в вашей сборке:

public static class MethodTimeLogger {
  public static void Log(MethodBase methodBase, long milliseconds)
  {
    //Do some logging here
  } 
}

Ваш код

public class MyClass
{
    [Time]
    public void MyMethod()
    {
        //Some code u are curious how long it takes
        Console.WriteLine("Hello");
    }
}

Скомпилировано:

public class MyClass
{
    public void MyMethod()
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            //Some code u are curious how long it takes
            Console.WriteLine("Hello");
        }
        finally
        {
            stopwatch.Stop();
            MethodTimeLogger.Log(methodof(MyClass.MyMethod), stopwatch.ElapsedMilliseconds);
        }
    }
}

Ответ 13

Одноразовый стиль Stopwatch работает лучше всего для меня.

class VWatch : IDisposable {
    Stopwatch watch = new Stopwatch();
    public VWatch() {
        this.watch.Start();
    }
    public void Dispose() {
        this.watch.Stop();
        Console.WriteLine("Finished. Elapsed={0}", this.watch.Elapsed);
    }
}

А потом:

using (new VWatch()) {
    /// do something for time measurement
}

Ответ 14

Иногда бывает лучше посмотреть, почему вам нужно время для операции? Он работает медленно? Или вам просто интересно? Первое правило оптимизации - "Не делай этого". Таким образом, в зависимости от того, что вы на самом деле измеряете, можно изменить мнение о том, какой инструмент лучше всего подходит для задачи.

Ответ 15

Самый простой способ использовать профилировщик, например ANTI Performance Profiler или один из доступных.

Ответ 16

Я сделал расширение, которое возвращает миллисекунды из тиков.

public static int GetTotalRunningTimeInMilliseconds(this DateTime start)
{
    var endTicks = DateTime.Now.Ticks - start.Ticks;
    return TimeSpan.FromTicks(endTicks).Milliseconds;
}

Использование:

 var start = DateTime.Now;

 //...your long running code here

 var endTime = start.GetTotalRunningTimeInMilliseconds();

Ответ 17

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

Для меня достаточно DateTime, но он легко адаптируется с DateTime до Stopwatch.

public static TimeSpan MeasureTime(Action action)
{
    DateTime start = DateTime.Now;

    if (action == null)
    {
        throw new ArgumentNullException("action");
    }

    try
    {
        action();
    }
    catch (Exception ex)
    {
        Debugger.Log(1, "Measuring",ex.ToString());
    }

    return DateTime.Now - start;
}

Как его использовать?:

private static void StressTest()
{
    List<TimeSpan> tss = new List<TimeSpan>();

    for (int i = 0; i < 100; i++)
    {
        // here is the measuring:
        var ts = MeasureTime(() => instance.Method("param1"));

        tss.Add(ts);
    }

    Console.WriteLine("Max: {0}", tss.Max());
    Console.WriteLine("Min: {0}", tss.Min());
    Console.WriteLine("Avg: {0}", TimeSpan.FromMilliseconds(tss.Average(i => i.TotalMilliseconds)));
}

или

var ts = MeasureTime(() =>
                        {
                            // Some intensive stuff here
                            int a = 1;

                            // more here
                            int b = 2;

                            // and so on
                        });

Ответ 18

Мои предпочтения относятся к BenchmarkDotNet, упомянутому @Evgeniy. Вероятно, его ответ обойден, потому что нет фрагмента кода, но, поскольку это сложный элемент, стоит хотя бы заглянуть в библиотеку, прежде чем идти головой к чему-то особенному.

И потому, что какой-то код всегда бросается в глаза, вот пример с цитируемого сайта:

using System;
using System.Security.Cryptography;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace MyBenchmarks
{
    [ClrJob(baseline: true), CoreJob, MonoJob, CoreRtJob]
    [RPlotExporter, RankColumn]
    public class Md5VsSha256
    {
        private SHA256 sha256 = SHA256.Create();
        private MD5 md5 = MD5.Create();
        private byte[] data;

        [Params(1000, 10000)]
        public int N;

        [GlobalSetup]
        public void Setup()
        {
            data = new byte[N];
            new Random(42).NextBytes(data);
        }

        [Benchmark]
        public byte[] Sha256() => sha256.ComputeHash(data);

        [Benchmark]
        public byte[] Md5() => md5.ComputeHash(data);
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Md5VsSha256>();
        }
    }
}

Ответ 19

Чтобы измерить производительность с разницей между измерениями, я использую этот класс. Класс StopWatch не имеет метода Split.

/// <summary>
/// Stopwatch like class that keeps track of timelapses.
/// Probably low-res because of the usage of DateTime.
/// </summary>
public class ChronoMeter
{
    /// <summary>
    /// The name given when the Chronometer was constructed.
    /// </summary>
    public string Name { get; private set; }
    /// <summary>
    /// The moment in time Start was called.
    /// </summary>
    public DateTime Started { get; private set; }

    /// <summary>
    /// All time recordings are added to this list by calling Split and Stop.
    /// </summary>
    public List<ChronoRecord> Records { get; private set; }

    private readonly Stopwatch _stopWatch = new Stopwatch();

    private bool _hasBeenStopped = false;

    /// <summary>
    /// Constrcutor
    /// </summary>
    /// <param name="pName">The name is used in logging</param>
    /// <param name="pLoggingType">The type of logging appriate for the information yielded by this time recording.</param>
    public ChronoMeter(string pName)
    {
        Name = pName;
        Records = new List<ChronoRecord>();
    }

    /// <summary>
    /// Not calling Stop is bad practise. Therefore a little safety net zo the end is still recorderd.
    /// Keep in mind that the garbase collector invokes the destructor, so the moment of time probably doesn't make much sense.
    /// It is more to notify that you should have used Stop for the latest split.
    /// </summary>
    ~ChronoMeter()
    {
        if (!_hasBeenStopped)
        {
            Stop("Destructor safety net");
        }
    }

    /// <summary>
    /// TimeElapsedSinceStart of a ChronoRecord is relative to the moment ChronoMeter was started by calling this function.
    /// </summary>
    public void Start()
    {
        _stopWatch.Start();
        Started = DateTime.Now;
    }

    /// <summary>
    /// Splits the timerecording and add a record of this moment to the list of split records.
    /// </summary>
    /// <param name="pSplitName"></param>
    public void Split(string pSplitName)
    {
        _stopWatch.Stop();
        var created = Started + _stopWatch.Elapsed;
        var previousRecord = Records.LastOrDefault();
        Records.Add(new ChronoRecord(pSplitName, Started, created, previousRecord));
        _stopWatch.Start();
    }

    /// <summary>
    /// Indicates you are done and the records will be written to the log.
    /// </summary>
    public void Stop(string pSplitName)
    {
        Split(pSplitName);
        _stopWatch.Stop();
        _hasBeenStopped = true;
    }

    public class ChronoRecord
    {
        public string Name { get; private set; }
        public TimeSpan TimeElapsedSinceStart { get; private set; }
        public TimeSpan TimeElapsedSincePrevious { get; private set; }
        public DateTime Start { get; private set; }
        public DateTime Created { get; private set; }

        public ChronoRecord(string pName, DateTime pStartDateTime, DateTime pCreated, ChronoRecord pPreviousRecord=null)
        {
            if (pCreated == default(DateTime)) //Ignore DefaultDateTimeComparison
            {
                pCreated = DateTime.Now;
            }
            Created = pCreated;
            Name = pName;
            Start = pStartDateTime;

            TimeElapsedSinceStart = Created - Start;
            if (pPreviousRecord != null)
            {
                TimeElapsedSincePrevious = Created - pPreviousRecord.Created;
            }
            else
            {
                TimeElapsedSincePrevious = TimeElapsedSinceStart;
            }
        }
    }
}