¿Envolviendo el cronometraje StopWatch con un delegado o lambda?

95

Estoy escribiendo un código como este, haciendo un poco de tiempo rápido y sucio:

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

Seguramente hay una manera de llamar a este fragmento de código de tiempo como una lambda .NET 3.0 elegante en lugar de (Dios no lo quiera) cortarlo y pegarlo varias veces y reemplazarlo DoStuff(s)con DoSomethingElse(s)?

Sé que se puede hacer como un Delegatemétodo, pero me pregunto sobre el método lambda.

Jeff Atwood
fuente

Respuestas:

129

¿Qué tal ampliar la clase de cronómetro?

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

Entonces llámalo así:

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

Podría agregar otra sobrecarga que omita el parámetro "iteraciones" y llame a esta versión con algún valor predeterminado (como 1000).

Matt Hamilton
fuente
3
Es posible que desee reemplazar sw.Start () con sw.StartNew () para evitar aumentar accidentalmente el tiempo transcurrido para cada llamada consecutiva de s.Time (), reutilizando la misma instancia de Stopwatch.
VVS
11
@Jay Estoy de acuerdo en que "foreach" con Enumerable.Range parece un poco más "moderno", pero mis pruebas muestran que es aproximadamente cuatro veces más lento que un ciclo "for" en un recuento grande. YMMV.
Matt Hamilton
2
-1: Usar una extensión de clase aquí no tiene sentido. Timese comporta como un método estático, descartando todo el estado existente en sw, por lo que introducirlo como un método de instancia parece ingenioso.
ildjarn
2
@ildjam Aprecio que haya dejado un comentario explicando su voto negativo, pero creo que está malinterpretando la idea detrás de los métodos de extensión.
Matt Hamilton
4
@Matt Hamilton: No lo creo, son para agregar (lógicamente) métodos de instancia a clases existentes. Pero, este no es más un método de instancia que Stopwatch.StartNew, que es estático por una razón. C # carece de la capacidad de agregar métodos estáticos a las clases existentes (a diferencia de F #), así que entiendo el impulso de hacer esto, pero todavía me deja mal sabor de boca.
ildjarn
31

Esto es lo que he estado usando:

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
fuente
¡Esta es la mejor solución que he visto! ¡Sin extensión (para que pueda usarse en muchas clases) y muy limpio!
Calvin
No estoy seguro de haber obtenido el ejemplo de uso correctamente. Cuando trato de usar algunos Console.WriteLine("")para probar // do stuff that I want to measure, el compilador no está contento en absoluto. ¿Se supone que debe hacer expresiones y declaraciones normales allí?
Tim
@Tim: estoy seguro de que lo resolvió, pero a la declaración de uso le faltaba un corchete
Alex
12

Puede intentar escribir un método de extensión para cualquier clase que esté usando (o cualquier clase base).

Me gustaría que la llamada se viera así:

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

Entonces el método de extensión:

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

Cualquier objeto derivado de DependencyObject ahora puede llamar a TimedFor (..). La función se puede ajustar fácilmente para proporcionar valores de retorno a través de parámetros de referencia.

-

Si no desea que la funcionalidad esté vinculada a ninguna clase / objeto, puede hacer 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;
  }
}

Entonces podrías usarlo como:

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

De lo contrario, esta respuesta parece que tiene una habilidad "genérica" ​​decente:

¿Envolviendo el cronometraje StopWatch con un delegado o lambda?

Mark Ingram
fuente
genial, pero no me importa la forma en que esto está ligado a una clase particular o clase base; ¿Se puede hacer de forma más genérica?
Jeff Atwood
¿Como en la clase MyObject para la que está escrito el método de extensión? Se puede cambiar fácilmente para extender la clase Object u otra clase en el árbol de herencia.
Mark Ingram
Estaba pensando más estático, como si no estuviera atado a NINGÚN objeto o clase en particular ... el tiempo y el tiempo son universales
Jeff Atwood
Excelente, la 2da versión es más lo que estaba pensando, +1, pero le doy aceptado a Matt cuando llegó primero.
Jeff Atwood
7

La StopWatchclase no tiene por qué ser Disposedo Stoppedpor error. Entonces, el código más simple para cronometrar alguna acción es

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

Código de llamada de muestra

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

No me gusta la idea de incluir las iteraciones en el StopWatchcódigo. Siempre puede crear otro método o extensión que maneje la ejecución de Niteraciones.

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

Código de llamada de muestra

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);
}

Aquí están las versiones del método de extensión

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);
    }
}

Y código de llamada de muestra

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

Probé los métodos estáticos y los métodos de extensión (combinando iteraciones y puntos de referencia) y el delta del tiempo de ejecución esperado y el tiempo de ejecución real es <= 1 ms.

Anthony Mastrean
fuente
Las versiones del método de extensión me hacen la boca agua. :)
bzlm
7

Escribí una clase simple de CodeProfiler hace algún tiempo que envolvió Stopwatch para perfilar fácilmente un método usando una Acción: http://www.improve.dk/blog/2008/04/16/profiling-code-the-easy-way

También le permitirá perfilar fácilmente el código multiproceso. El siguiente ejemplo perfilará la acción lambda con 1-16 subprocesos:

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
fuente
4

Suponiendo que solo necesita una sincronización rápida de una cosa, esto es 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; }
    }
}
jyoung
fuente
2

Puede sobrecargar una serie de métodos para cubrir varios casos de parámetros que tal vez desee pasar a la 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;
}

Alternativamente, puede usar el delegado Func si debe devolver un valor. También puede pasar una matriz (o más) de parámetros si cada iteración debe usar un valor único.

Morten Christiansen
fuente
2

Para mí, la extensión se siente un poco más intuitiva en int, ya no es necesario crear una instancia de un cronómetro ni preocuparse por restablecerlo.

Así que tienes:

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);
    }
}

Con el uso de muestra 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);
        }
    });

Salida de muestra:

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
fuente
1

Me gusta usar las clases CodeTimer de Vance Morrison (uno de los tipos de rendimiento de .NET).

Hizo una publicación en su blog titulada " Medir el código administrado de forma rápida y sencilla: CodeTimers ".

Incluye cosas interesantes como MultiSampleCodeTimer. Hace un cálculo automático de la desviación estándar y media y también es muy fácil imprimir sus resultados.

Davy Landman
fuente
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
fuente