´

C# - Cuando la precisión que da el StopWatch no es suficiente

Puedes ver el articulo original en  mi blog:

http://juank.black-byte.com/c-medir-nanosegundos/

 ---

Medir tiempo en nanosegundos

Hola!

He observado que es muy frecuente cuando alguien quiere hacer una prueba de rendimiento (sobre todo a nivel académico) que la resolución que da el objeto StopWatch (System.Diagnostics.Stopwatch) de Milisegundos resulta no ser siempre suficiente.

En esos casos lo mejor es recurrir a las funciones de la API para crear algo más de acuerdo a nuestras necesidades, de tal forma que podamos medir el tiempo transcurrido con una precisión mayor a la que da  - por alguna razón – el objeto StopWatch, así que creare algo sencillo que permitirá lograr la precisión deseada, pero primero – como siempre –algo de teoría al respecto.

 

CÓMO CALCULAR EL TIEMPO

Para calcular el tiempo dentro de un computador debemos valernos de la información que nos brinda el procesador, como todos sabemos el procesador posee un atributo llamado frecuencia, el cual nos indica cuantos ciclos de reloj realiza el procesador cada segundo. De esta forma encontramos que hay procesadores 1 Ghz (un millon de ciclos de reloj por segundo) y hay de muchos diferentes valores.

Por otro lado un procesador posee un contador de ciclos es decir un registro el cual informa de cuantos ciclos ha procesado.

Así que tenemos dos fuentes de información que utilizaremos para calcular el tiempo transcurrido ya que si dividimos la cantidad de ciclos que han pasado de un momento a otro entre la frecuencia, obtendremos el tiempo transcurrido con una precisión bastante grande (double).

 

Tenemos:

Frecuencia= ciclos por segundo

Ticks= ciclos procesados de un instante a otro

Tiempo = Ticks / Frecuencia (   ciclos / ciclos por segundo  )

De tal forma que las unidades resultantes serán: segundos.

 

Que!!! segundos? si pero esos segundos están expresados con una gran precisión decimal por lo cual podemos llegar a la precisión de nanosegundos tan solo multiplicando por 1.000‘000.000 (mil millones), y con un tipo de dato double tenemos espacio mas que suficiente para manejar estas cifras.

 

DE DONDE OBTENEMOS LOS DATOS?

Para ello utilizaremos dos funciones de la API de Windows:

  • QueryPerformanceCounter: Retorna el valor almacenado en el registro contador de ciclos del procesador en un momento dado
  • QueryPerformanceFrequency: Retorna la velocidad del procesador

 

Como ven ya esta todo lo necesario, ahora la implementación.

 

IMPLEMENTACIÓN

Lo primero es poder hacer uso de las funciones API  para lo cual nos ayudaremos con DllImport, ya saben la mejor fuente para saber como hacer declaraciones de la API de manera rápida es: http://www.pinvoke.net/. Todo esto lo hare dentro de la clase NanoTemporizador

using System;
using System.Runtime.InteropServices;

public class Temporizador
{
    /// 
    /// Obtiene la frecuencia del procesador 
    /// 
    /// variable donde retorna la frecuencia
    /// True si el procesador tiene contador de frecuencia, false sino
    [DllImport"kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceFrequency(out long frequency);

    /// 
    /// Obtiene l evalor actual del contador de alto rendimiento del ptrocesador
    /// 
    /// variable donde retorna el valor
    /// True si todo salio OK, false sino
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceCounter(out long lpPerformanceCount);
}

Ahora en el constructor de nuestra clase no haremos nada :). Vale la pena recordar que siempre se deben crear componentes eficientes – según yo :P – por lo que es mejor que tengamos un constructor estático ya que realmente la frecuencia del procesador no cambiara nunca, así que solo es necesario calcularla una sola vez para todas las instancias.

    /// Almacena la frecuencia del contador de alto rendimiento
    private static long _frecuencia;

    static NanoTemporizador()
    {
        if (!QueryPerformanceFrequency(out _frecuencia))
            throw new NullReferenceException(
                "Este componente se hizo para utilizar contadores de alto rendimiento. Como no los hay mejor utiliza StopWatch"
             );
    }

Para que funcione realmente como un contador de tiempo necesitamos poder establecer si el contador esta andando o no, para lo cual crearemos una propiedad. Adicionalmente en el método Start del temporizador vams a calcular el valor de contador actual y a cambiar el valor de nuestro indicador a true:

    /// Almacena el valor de conteo inicial
    private long _conteoInicial;
    /// Indica si ya se ha inicializado el timer
    private bool _isRunning = false;
    /// Indica si ya se ha inicializado el timer
    public bool IsRunning { get { return _isRunning; } }

    public void Start()
    {
        if (!_isRunning)
        {
            QueryPerformanceCounter(out _conteoInicial);
            _isRunning = true;
        }
    }

De igual forma se establece el método Stop:

    /// Almacena el valor de conteo final
    private long _conteoFinal;

    public void Stop()
    {
        if (_isRunning)
        {
            QueryPerformanceCounter(out _conteoFinal);
            _isRunning = false;
        }
    }

Finalmente se crea una propiedad a travez de la cual podamos hallar el valor en nanosegundos:

    ///Retorna la cantidad de nanosegundos contados
    public double ElapsedNanoseconds
    {
        get
        {
            return (_conteoFinal - _conteoInicial) * 1000000000L
                   / (double)_frecuencia;
        }
    }

 

Perfecto, eso es todo. Esta es la versión completa:

 
using System;
using System.Runtime.InteropServices;

public class NanoTemporizador
{
    /// 
    /// Obtiene la frecuencia del procesador 
    /// 
    /// variable donde retorna la frecuencia
    /// True si el procesador tiene contador de frecuencia, false sino
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceFrequency(out long frequency);

    /// 
    /// Obtiene l evalor actual del contador de alto rendimiento del ptrocesador
    /// 
    /// variable donde retorna el valor
    /// True si todo salio OK, false sino
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

    /// Almacena la frecuencia del contador de alto rendimiento
    private static long _frecuencia;
    
    /// Almacena el valor de conteo inicial
    private long _conteoInicial;
    /// Almacena el valor de conteo final
    private long _conteoFinal;

    /// Indica si ya se ha inicializado el timer
    private bool _isRunning = false;
    /// Indica si ya se ha inicializado el timer
    public bool IsRunning { get { return _isRunning; } }

    /// Valor por el cual se multiplican segundos para pasarlos a nanosegundos
    private const long NANOSEGUNDOS = 1000000000L;

    /// Valor por el cual se multiplican segundos para pasarlos a milisegundos
    private const long MILISEGUNDOS = 1000L;
    
    static NanoTemporizador()
    {
        if (!QueryPerformanceFrequency(out _frecuencia))
            throw new NullReferenceException(
               "Este componente se hizo para utilizar contadores de alto rendimiento. Como no los hay mejor utiliza StopWatch."
            );
    }

    /// Inicia el conteo del temporizador
    public void Start()
    {
        if (!_isRunning)
        {
            QueryPerformanceCounter(out _conteoInicial);
            _isRunning = true;
        }
    }
    
    /// Detiene el conteo del temporizador
    public void Stop()
    {
        if (_isRunning)
        {
            QueryPerformanceCounter(out _conteoFinal);
            _isRunning = false;
        }
    }

    ///Retorna la cantidad de nanosegundos contados
    public double ElapsedNanoseconds
    {
        get
        {
            return (_conteoFinal - _conteoInicial) * NANOSEGUNDOS
                   / (double)_frecuencia;
        }
    }

    ///Retorna la cantidad de milisegundos contados
    public double ElapsedMilliseconds
    {
        get
        {
            return (_conteoFinal - _conteoInicial) * MILISEGUNDOS
                   / (double)_frecuencia;
        }
    }

    ///Retorna la cantidad de segundos contados
    public double ElapsedSeconds
    {
        get
        {
            return (_conteoFinal - _conteoInicial) / (double)_frecuencia;
        }
    }
}

CÓMO USARLO?

Bien este es un ejemplo tontisimo, pero muy ilustrativo:


using System;

namespace Prueba
{
    class Program
    {
        static void Main(string[] args)
        {
            NanoTemporizador temporizador = new NanoTemporizador();

            Probador(temporizador, 1000);
            Probador(temporizador, 1000);
            Probador(temporizador, 5000);
            Probador(temporizador, 2358);
            Probador(temporizador, 3541);
            Probador(temporizador, 10000);

            Console.ReadLine();
        }

        private static void Probador(NanoTemporizador temporizador, int espera )
        {
            temporizador.Start();
            System.Threading.Thread.Sleep(espera);
            temporizador.Stop();

            Console.WriteLine("Tiempo transcurrido: {0} ns> ", temporizador.ElapsedNanoseconds);
            Console.WriteLine("Tiempo transcurrido: {0} ms> ", temporizador.ElapsedMilliseconds);
            Console.WriteLine("Tiempo transcurrido: {0} sg> ", temporizador.ElapsedSeconds);
            Console.WriteLine("=====================================================");
        }
    }
}

Hasta pronto.

Posted: 18/8/2009 15:57 por Juan Carlos Ruiz Pacheco | con 4 comment(s) |
Archivado en: ,,,
Comparte este post:

Comentarios

Pablo Alarcón García ha opinado:

Me haces dudar, ahora no puedo comprobarlo pero juraría que...

- StopWatch es el wrapper manejado de las 2 APIs de QueryPerformanceXXX. Según la MSDN:

"The Stopwatch class assists the manipulation of timing-related performance counters within managed code. Specifically, the Frequency field and GetTimestamp method can be used in place of the unmanaged Win32 APIs QueryPerformanceFrequency and QueryPerformanceCounter." . Podrías evitar PINVOKE y seguir haciendo el mismo cálculo.

- Si quieres más precisión que Milisegundos puedes sacar los ticks y calcular los Nanosegundos, cada Tick son 100 nanosegundos...

Aunque no es intuitivo( entiendo que no es frecuente en .NET bajar más allá del Milisegundo ) creo que StopWatch provee de todo lo necesario.

# August 19, 2009 9:26 AM

Juan Carlos Ruiz Pacheco ha opinado:

Hola Pablo, en efecto 'por debajo' StopWatch llama a esas dos apis también a traves de DllImport.

El problema es que, por alguna razón decidieron no dejar StopWatch hasta milisegundos pudiendo dejarlo hasta nanosegundos o incluso dejarlo abierto hasta mucho más allá.

un Tick no son 100 nanosegundos, de hecho un Tick es un milisegundo... aprox eso de que equivale a 100 nano es un mito de internet...de hecho un Tick... puede ser relativo al procesador que se este usando... y no se necesita un contador de alto rendimiento de la APi para obtenerlos por lo que son imprecisos.

puedes comprobarlo tu mismo.

       static void Main(string[] args)

       {

           int inicialTick, finalTick;

           NanoTemporizador nt = new NanoTemporizador();

           nt.Start();

           System.Threading.Thread.Sleep(1000);

           nt.Stop();

           Console.WriteLine("Tiempo transcurrido Nano:"+nt.ElapsedNanoseconds.ToString());

           Console.WriteLine("Tiempo transcurrido Mili:" + nt.ElapsedMilliseconds.ToString());

           inicialTick = System.Environment.TickCount;

           System.Threading.Thread.Sleep(1000);

           finalTick = System.Environment.TickCount;

           Console.WriteLine("Tiempo transcurrido:" + (finalTick-inicialTick).ToString());

           Console.ReadLine();

       }

saludos.

# August 19, 2009 3:25 PM

Pablo Alarcón García ha opinado:

La afirmación de que la StopWatch tiene precisión hasta el milisegundo no la encuentro en la MSDN.

Los ticks a nivel de procesador si mal no recuerdo dependen de la velocidad del procesador, por eso para calcular la velocidad le pides también la frecuencia... pero los ticks en .NET, representados en un TimeSpan están normalizados a 100 nanosegundos según la documentación:

"The value of a TimeSpan object is the number of ticks that equal the represented time interval. A tick is equal to 100 nanoseconds, and the value of a TimeSpan object can range from MinValue to MaxValue."

Lo que quería decir con todo ésto es que creo que usando StopWatch.Elapsed puedes calcular con la precisión que quieres.

# August 20, 2009 12:17 PM

Juan Carlos Ruiz Pacheco ha opinado:

No es necesario que te vayas hasta msdn, nada más trata de usarlo por tu propia cuenta y veras como no hay manera de sacar una medida precisa más alla de precisión de milisegundos, salvo con el timestamp que te da una precisión de 10^-6.

Un nanosegundo es una milmillonésima de segundo, mientras que la propiedad ticks del timespan te da una millonesima de segundo.

Los ticks son muy abstractos, si te fijas los Tick del timespan tienen mas resolucion que el TickCount de system.environment. pero más alla de ello el valor de los ticks en la implementacion de Stopwatch depende de si hay o no un contador de alta resolución... y de la presicion de los calculos que hayan utilizado... que de por si son un poco locos como veras más adelante en el ejemplo...

para que te hagas una idea nada mas prueba con este ejemplo completo y veras las diferencias inclusive entre dos medidas de Ticks provistas por StopWatch.

       static void Main(string[] args)

       {

           int inicialTick, finalTick;

           NanoTemporizador nt = new NanoTemporizador();

           Stopwatch sw = new Stopwatch();

           nt.Start();

           System.Threading.Thread.Sleep(1000);

           nt.Stop();

           Console.WriteLine("Nanotemporizador Nano:"+nt.ElapsedNanoseconds.ToString());

           Console.WriteLine("Nanotemporizador Mili:" + nt.ElapsedMilliseconds.ToString());

           inicialTick = System.Environment.TickCount;

           System.Threading.Thread.Sleep(1000);

           finalTick = System.Environment.TickCount;

           Console.WriteLine("Enviroment.TickCount:" + (finalTick-inicialTick).ToString());

           sw.Start();

           System.Threading.Thread.Sleep(1000);

           sw.Stop();

           //.ElapsedTicks

           Console.WriteLine("StopWatch.ElapsedTicks :" + sw.ElapsedTicks);

           Console.WriteLine("StopWatch.Elapsed.Ticks :" + sw.Elapsed.Ticks);

           Console.ReadLine();

       }

   }

# August 20, 2009 3:43 PM