´

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.

Leave a Comment

(required) 

(required) 

(optional)
 

(required) 

If you can't read this number refresh your screen
Enter the numbers above: