Envolvendo o tempo de StopWatch com um delegado ou lambda?

95

Estou escrevendo um código assim, fazendo um pouco de tempo rápido e sujo:

var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1000; i++)
{
    b = DoStuff(s);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);

Certamente há uma maneira de chamar esse código de temporização de lambda do .NET 3.0 sofisticado, em vez de (Deus me livre) recortá-lo e colá-lo algumas vezes e substituí-lo DoStuff(s)por DoSomethingElse(s)?

Eu sei que pode ser feito como um, Delegatemas estou me perguntando sobre o caminho lambda.

Jeff Atwood
fonte

Respostas:

129

Que tal estender a classe Stopwatch?

public static class StopwatchExtensions
{
    public static long Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Reset();
        sw.Start(); 
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.ElapsedMilliseconds;
    }
}

Em seguida, chame-o assim:

var s = new Stopwatch();
Console.WriteLine(s.Time(() => DoStuff(), 1000));

Você poderia adicionar outra sobrecarga que omite o parâmetro "iterações" e chame essa versão com algum valor padrão (como 1000).

Matt Hamilton
fonte
3
Você pode querer substituir sw.Start () por sw.StartNew () para evitar o aumento acidental do tempo decorrido para cada chamada consecutiva de s.Time (), reutilizando a mesma instância do Stopwatch.
VVS
11
@Jay Concordo que "foreach" com Enumerable.Range parece um pouco mais "moderno", mas meus testes mostram que é cerca de quatro vezes mais lento do que um loop "for" em uma grande contagem. YMMV.
Matt Hamilton,
2
-1: Usar uma extensão de classe aqui não faz sentido. Timese comporta como um método estático, descartando todos os estados existentes em sw, portanto, apresentá-lo como um método de instância parece enganoso.
ildjarn
2
@ildjam Agradeço que você tenha deixado um comentário explicando seu voto negativo, mas acho que você está entendendo mal a ideia por trás dos métodos de extensão.
Matt Hamilton
4
@Matt Hamilton: Acho que não - eles servem para adicionar (logicamente) métodos de instância a classes existentes. Mas, este não é mais um método de instância do que Stopwatch.StartNew, o que é estático por uma razão. C # não tem a capacidade de adicionar métodos estáticos às classes existentes (ao contrário do F #), então eu entendo o impulso de fazer isso, mas ainda deixa um gosto ruim na minha boca.
ildjarn,
31

Aqui está o que tenho usado:

public class DisposableStopwatch: IDisposable {
    private readonly Stopwatch sw;
    private readonly Action<TimeSpan> f;

    public DisposableStopwatch(Action<TimeSpan> f) {
        this.f = f;
        sw = Stopwatch.StartNew();
    }

    public void Dispose() {
        sw.Stop();
        f(sw.Elapsed);
    }
}

Uso:

using (new DisposableStopwatch(t => Console.WriteLine("{0} elapsed", t))) {
  // do stuff that I want to measure
}
Mauricio Scheffer
fonte
Esta é a melhor solução que já vi! Sem extensão (para que possa ser usado em muitas classes) e muito limpo!
Calvin
Não tenho certeza se entendi o exemplo de uso corretamente. Quando tento usar alguns Console.WriteLine("")para testes em // do stuff that I want to measure, o compilador não fica nada satisfeito. Você deveria fazer expressões e declarações normais lá?
Tim
@Tim - Tenho certeza de que você entendeu, mas a instrução using faltou um colchete
Alex
12

Você pode tentar escrever um método de extensão para qualquer classe que estiver usando (ou qualquer classe base).

Eu gostaria que a ligação se parecesse com:

Stopwatch sw = MyObject.TimedFor(1000, () => DoStuff(s));

Então, o método de extensão:

public static Stopwatch TimedFor(this DependencyObject source, Int32 loops, Action action)
{
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < loops; ++i)
{
    action.Invoke();
}
sw.Stop();

return sw;
}

Qualquer objeto derivado de DependencyObject agora pode chamar TimedFor (..). A função pode ser facilmente ajustada para fornecer valores de retorno por meio de parâmetros ref.

-

Se você não quiser que a funcionalidade seja vinculada a qualquer classe / objeto, você pode fazer algo como:

public class Timing
{
  public static Stopwatch TimedFor(Action action, Int32 loops)
  {
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < loops; ++i)
    {
      action.Invoke();
    }
    sw.Stop();

    return sw;
  }
}

Então você pode usá-lo como:

Stopwatch sw = Timing.TimedFor(() => DoStuff(s), 1000);

Caso contrário, esta resposta parece ter alguma habilidade "genérica" ​​decente:

Envolvendo o tempo de StopWatch com um delegado ou lambda?

Mark Ingram
fonte
legal, mas não me importo com a maneira como isso está vinculado a uma determinada classe ou classe base; pode ser feito de forma mais genérica?
Jeff Atwood,
Como na classe MyObject para a qual o método de extensão foi escrito? Ele pode ser facilmente alterado para estender a classe Object ou outra classe na árvore de herança.
Mark Ingram
Eu estava pensando mais estático, como não estar vinculado a QUALQUER objeto ou classe em particular ... o tempo e o momento são universais
Jeff Atwood
Excelente, a 2ª versão é mais o que eu estava pensando, 1, mas aceito o Matt quando ele chegou a ela primeiro.
Jeff Atwood,
7

A StopWatchclasse não precisa estar Disposedou Stoppedcom erro. Portanto, o código mais simples para cronometrar alguma ação é

public partial class With
{
    public static long Benchmark(Action action)
    {
        var stopwatch = Stopwatch.StartNew();
        action();
        stopwatch.Stop();
        return stopwatch.ElapsedMilliseconds;
    }
}

Amostra de código de chamada

public void Execute(Action action)
{
    var time = With.Benchmark(action);
    log.DebugFormat(“Did action in {0} ms.”, time);
}

Não gosto da ideia de incluir as iterações no StopWatchcódigo. Você sempre pode criar outro método ou extensão que controle a execução de Niterações.

public partial class With
{
    public static void Iterations(int n, Action action)
    {
        for(int count = 0; count < n; count++)
            action();
    }
}

Amostra de código de chamada

public void Execute(Action action, int n)
{
    var time = With.Benchmark(With.Iterations(n, action));
    log.DebugFormat(“Did action {0} times in {1} ms.”, n, time);
}

Aqui estão as versões do método de extensão

public static class Extensions
{
    public static long Benchmark(this Action action)
    {
        return With.Benchmark(action);
    }

    public static Action Iterations(this Action action, int n)
    {
        return () => With.Iterations(n, action);
    }
}

E um exemplo de código de chamada

public void Execute(Action action, int n)
{
    var time = action.Iterations(n).Benchmark()
    log.DebugFormat(“Did action {0} times in {1} ms.”, n, time);
}

Testei os métodos estáticos e os métodos de extensão (combinando iterações e benchmark) e o delta do tempo de execução esperado e do tempo real de execução é <= 1 ms.

Anthony Mastrean
fonte
As versões do método de extensão me dão água na boca. :)
bzlm
7

Eu escrevi uma classe CodeProfiler simples há algum tempo que envolveu o cronômetro para facilmente criar o perfil de um método usando uma ação: http://www.improve.dk/blog/2008/04/16/profiling-code-the-easy-way

Ele também permitirá facilmente que você crie o perfil do código multithread. O exemplo a seguir criará o perfil da ação lambda com 1-16 threads:

static void Main(string[] args)
{
    Action action = () =>
    {
        for (int i = 0; i < 10000000; i++)
            Math.Sqrt(i);
    };

    for(int i=1; i<=16; i++)
        Console.WriteLine(i + " thread(s):\t" + 
            CodeProfiler.ProfileAction(action, 100, i));

    Console.Read();
}
Mark S. Rasmussen
fonte
4

Supondo que você só precisa de um tempo rápido de uma coisa, isso é fácil de usar.

  public static class Test {
    public static void Invoke() {
        using( SingleTimer.Start )
            Thread.Sleep( 200 );
        Console.WriteLine( SingleTimer.Elapsed );

        using( SingleTimer.Start ) {
            Thread.Sleep( 300 );
        }
        Console.WriteLine( SingleTimer.Elapsed );
    }
}

public class SingleTimer :IDisposable {
    private Stopwatch stopwatch = new Stopwatch();

    public static readonly SingleTimer timer = new SingleTimer();
    public static SingleTimer Start {
        get {
            timer.stopwatch.Reset();
            timer.stopwatch.Start();
            return timer;
        }
    }

    public void Stop() {
        stopwatch.Stop();
    }
    public void Dispose() {
        stopwatch.Stop();
    }

    public static TimeSpan Elapsed {
        get { return timer.stopwatch.Elapsed; }
    }
}
jovem
fonte
2

Você pode sobrecarregar vários métodos para cobrir vários casos de parâmetros que você pode querer passar para o lambda:

public static Stopwatch MeasureTime<T>(int iterations, Action<T> action, T param)
{
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < iterations; i++)
    {
        action.Invoke(param);
    }
    sw.Stop();

    return sw;
}

public static Stopwatch MeasureTime<T, K>(int iterations, Action<T, K> action, T param1, K param2)
{
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < iterations; i++)
    {
        action.Invoke(param1, param2);
    }
    sw.Stop();

    return sw;
}

Como alternativa, você pode usar o delegado Func se eles devem retornar um valor. Você também pode passar uma matriz (ou mais) de parâmetros se cada iteração deve usar um valor exclusivo.

Morten Christiansen
fonte
2

Para mim, a extensão parece um pouco mais intuitiva no int, você não precisa mais instanciar um cronômetro ou se preocupar em redefini-lo.

Então você tem:

static class BenchmarkExtension {

    public static void Times(this int times, string description, Action action) {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < times; i++) {
            action();
        }
        watch.Stop();
        Console.WriteLine("{0} ... Total time: {1}ms ({2} iterations)", 
            description,  
            watch.ElapsedMilliseconds,
            times);
    }
}

Com o uso de amostra de:

var randomStrings = Enumerable.Range(0, 10000)
    .Select(_ => Guid.NewGuid().ToString())
    .ToArray();

50.Times("Add 10,000 random strings to a Dictionary", 
    () => {
        var dict = new Dictionary<string, object>();
        foreach (var str in randomStrings) {
            dict.Add(str, null);
        }
    });

50.Times("Add 10,000 random strings to a SortedList",
    () => {
        var list = new SortedList<string, object>();
        foreach (var str in randomStrings) {
            list.Add(str, null);
        }
    });

Saída de amostra:

Add 10,000 random strings to a Dictionary ... Total time: 144ms (50 iterations)
Add 10,000 random strings to a SortedList ... Total time: 4088ms (50 iterations)
Sam Saffron
fonte
1

Eu gosto de usar as classes CodeTimer de Vance Morrison (um dos caras performáticos do .NET).

Ele postou em seu blog com o título " Medindo código gerenciado com rapidez e facilidade: CodeTimers ".

Inclui coisas legais como um MultiSampleCodeTimer. Ele faz o cálculo automático da média e do desvio padrão e também é muito fácil de imprimir seus resultados.

Davy Landman
fonte
0
public static class StopWatchExtensions
{
    public static async Task<TimeSpan> LogElapsedMillisecondsAsync(
        this Stopwatch stopwatch,
        ILogger logger,
        string actionName,
        Func<Task> action)
    {
        stopwatch.Reset();
        stopwatch.Start();

        await action();

        stopwatch.Stop();

        logger.LogDebug(string.Format(actionName + " completed in {0}.", stopwatch.Elapsed.ToString("hh\\:mm\\:ss")));

        return stopwatch.Elapsed;
    }
}
Alper Ebicoglu
fonte