Go to content Go to navigation Go to search

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

February 2nd, 2010 by JuanK

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
{
    /// <summary>
    /// Obtiene la frecuencia del procesador 
    /// </summary>
    /// <param name="frequency" />variable donde retorna la frecuencia
    /// <returns>True si el procesador tiene contador de frecuencia, false sino</returns>
    [DllImport"kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceFrequency(out long frequency);
 
    /// <summary>
    /// Obtiene l evalor actual del contador de alto rendimiento del ptrocesador
    /// </summary>
    /// <param name="lpPerformanceCount" />variable donde retorna el valor
    /// <returns>True si todo salio OK, false sino</returns>
    [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.

/// <summary>Almacena la frecuencia del contador de alto rendimiento</summary>
    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:

/// <summary>Almacena el valor de conteo inicial</summary>
    private long _conteoInicial;
    /// <summary>Indica si ya se ha inicializado el timer</summary>
    private bool _isRunning = false;
    /// <summary>Indica si ya se ha inicializado el timer</summary>
    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:

/// <summary>Almacena el valor de conteo final</summary>
    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:

///<summary>Retorna la cantidad de nanosegundos contados</summary>
    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
{
    /// <summary>
    /// Obtiene la frecuencia del procesador 
    /// </summary>
    /// <param name="frequency">variable donde retorna la frecuencia</param>
    /// <returns>True si el procesador tiene contador de frecuencia, false sino</returns>
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceFrequency(out long frequency);
 
    /// <summary>
    /// Obtiene l evalor actual del contador de alto rendimiento del ptrocesador
    /// </summary>
    /// <param name="lpPerformanceCount">variable donde retorna el valor</param>
    /// <returns>True si todo salio OK, false sino</returns>
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool QueryPerformanceCounter(out long lpPerformanceCount);
 
    /// <summary>Almacena la frecuencia del contador de alto rendimiento</summary>
    private static long _frecuencia;
    
    /// <summary>Almacena el valor de conteo inicial</summary>
    private long _conteoInicial;
    /// <summary>Almacena el valor de conteo final</summary>
    private long _conteoFinal;
 
    /// <summary>Indica si ya se ha inicializado el timer</summary>
    private bool _isRunning = false;
    /// <summary>Indica si ya se ha inicializado el timer</summary>
    public bool IsRunning { get { return _isRunning; } }
 
    /// <summary>Valor por el cual se multiplican segundos para pasarlos a nanosegundos</summary>
    private const long NANOSEGUNDOS = 1000000000L;
 
    /// <summary>Valor por el cual se multiplican segundos para pasarlos a milisegundos</summary>
    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."
            );
    }
 
    /// <summary>Inicia el conteo del temporizador</summary>
    public void Start()
    {
        if (!_isRunning)
        {
            QueryPerformanceCounter(out _conteoInicial);
            _isRunning = true;
        }
    }
    
    /// <summary>Detiene el conteo del temporizador</summary>
    public void Stop()
    {
        if (_isRunning)
        {
            QueryPerformanceCounter(out _conteoFinal);
            _isRunning = false;
        }
    }
 
    ///<summary>Retorna la cantidad de nanosegundos contados</summary>
    public double ElapsedNanoseconds
    {
        get
        {
            return (_conteoFinal - _conteoInicial) * NANOSEGUNDOS
                   / (double)_frecuencia;
        }
    }
 
    ///<summary>Retorna la cantidad de milisegundos contados</summary>
    public double ElapsedMilliseconds
    {
        get
        {
            return (_conteoFinal - _conteoInicial) * MILISEGUNDOS
                   / (double)_frecuencia;
        }
    }
 
    ///<summary>Retorna la cantidad de segundos contados</summary>
    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.

Bookmark and Share

Originally posted 2009-07-26 14:11:55.

C# – la palabra clave volatile, explicación y ejemplos

August 13th, 2009 by JuanK

La palabra clave volatile es una de esas palabras clave muy pocas veces comprendidas, la documentación presente en msdn permite concluir que hay que utilizarla siempre que se manejen hilos, pero esto no siempre es así. Sin embargo lograr identificar que es lo que hace realmente esta palabra clave es una labor complicada así que dedicaré este artículo a explorar esta funcionalidad y a crear un ejemplo práctico que permita entender su verdadera naturaleza.

 

La documentación msdn

En msdn encontramos la siguiente definición de la palabra clave volatile:

La palabra clave volatile indica que varios subprocesos que se ejecutan a la vez pueden modificar un campo. Los campos que se declaran como volatile no están sujetos a optimizaciones del compilador que suponen el acceso por un subproceso único. Esto garantiza que el valor más actualizado está en todo momento presente en el campo

 

información completa

 

Debemos resaltar dos aspectos importantes de ese texto:

  1. Se menciona que los campos volatile no son susceptibles de optimizaciones por parte del compilador. Cuales optimizaciones?
  2. Dice que esto garantiza que el valor más actualizado siempre esta presente en el campo. No se supone que esto es así siempre?

A continuación revisaremos estas dos preguntas.

 

Optimizaciones del compilador

Siempre que compilamos un programa hecho con C# el compilador se encarga de convertir ese código C# en código de lenguaje IL, bueno realmente en OpCodes de IL. Esto es así de sencillo, pero resulta que cuando compilamos nuestro código en la configuración release o más específicamente cuando se marca la casilla de Optimizar código en el proyecto (ver imagen) el  compilador realiza una revisión general del código par determinar que cosas puede hacer funcionar de una manera mejor a la que codificó el programador inicialmente o incluso como puede cambiar las cosas en el ejecutable que no están en manos del programador ni del propio lenguaje para que a la hora de ejecutarse el programa sea más eficiente.

image

Estas optimizaciones son calculadas por el compilador haciendo uso de un complejo juego de reglas algunas de las cuales dependen de la arquitectura del procesador ( x86, x64, IA64, etc) y algunas otras del modelo de memoria que se esta trabajando. Los modelos de memoria son un tema complejo incluso desde sus fundamentos si bien no es algo imposible de aprender, pero quien quiera puede profundizar un poco más al respecto puede leer el libro de Sistemas Operativos Modernos de Andrew Tanembaum y desde luego buscar referencias adicionales en internet.

 

Adicionalmente a las optimizaciones realizadas por el compilador de C# tenemos una segunda etapa de compilación y optimizaciones realizadas por el JIT al momento de ejecutarse el programa, las cuales tiene su propio conjunto de reglas algunas de las cuales también están influenciadas por la arquitectura del procesador y el modelo de memoria.

 

El Valor Actualizado

Parte de las optimizaciones realizadas por el compilador pueden evitar que en algún momento determinado los campos de memoria donde se encuentran las variables no sean actualizados con su valor más reciente, porque?

 

Cada vez que el procesador hace una operación necesita colocar el valor de las variables en uno de sus registros, de tal forma que si tenemos por ejemplo un bucle con una suma de esta manera:

int i = 0;
        while (condicion)
        {
            i = i + 1;
        }

 

El procesador enviaría el valor de “i” a uno de sus registros y el valor ”1” en otro registro, al obtener el valor de la suma el procesador debe colocar este valor de nuevo en memoria en la variable “i” de tal forma que al continuar el bucle todo el proceso se repite.

 

Esta secuencia sin embargo puede ser optimizada por el compilador ya que se hace innecesario que el valor de “i” sea copiado de los registros de la CPU a la memoria y viceversa tantas veces como dure el bucle, así que el compilador genera un código ejecutable que permite que los cálculos sean hechos en su totalidad en los registros de la CPU y solo hasta salir del bucle estos datos serian copiados de nuevo a la memoria.

Este tipo de optimizaciones que usan ”cache”  trae una mejora considerable en la ejecución de procesos intensivos a nivel matemático y funcionan bastante bien en la mayoría de los escenarios.

 

Optimizaciones de cache y el problema de los hilos

Cuando el compilador esta realizando las optimizaciones analiza (de acuerdo a los estándares) todo lo que pueda hacer referencia a las variables en el contexto de ‘subproceso’ actual ( realmente en la mayoría de sistemas operativos no se ejecutan los procesos sino los subprocesos y se le llama proceso a un grupo de subprocesos que comparten la memoria ), así que con base a lo analizado realiza las optimizaciones, por ello el problema surge cuando un hilo trata de acceder a una variable que esta siendo modificada por un objeto que se ejecuta en un hilo diferente, ya que es posible que el hilo dueño de la variable se encuentre en  medio de un proceso optimizado para funcionar por cache en los registros y al no estar verificando el valor de la variable en memoria pues no vera los cambios realizados, de hecho los sobrescribirá al finalizar. Así que en escenarios donde existan múltiples hilos lo ideal es bloquear este tipo de optimizaciones y estar muy atento a los casos donde no sea conveniente inactivarlas pues hay casos para todo.

 

El Ejemplo

Bien después de la teoría veamos la práctica, veremos ejemplos donde volatile no sirve para nada ( y creo que son la mayoría de casos en el mundo real – al menos en CPUs x86 ya que en IA64 al parecer el tema es muy diferente), y finalmente un caso que de acuerdo a la teoría que acabamos de ver funcionara perfectamente desde que lo compiles para x86.

Sobra decir que para probar los ejemplos lo debes hacer con la versión Release del ejecutable.

Ejemplo 1

Este es el ejemplo típico, donde todos usamos volatile porque así lo dice la documentación pero la realidad es que en estos casos no sirve de NADA.

using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    //volatile
    static int i = 0;
    private static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {  
        i++;
        Console.WriteLine("Valor de i="+i);
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}

 

Podríamos ponerle a “i” como volatile pero no habría diferencia puesto que básicamente el código no necesita ni tiene como ser optimizado. Así que volatile no bloquearía ninguna optimización.

Ejemplo 2

Este es el ejemplo tampoco sirve de NADA, si bien en el bucle se ve la necesidad de optimizar el rendimiento del código evitando que se copie el valor de “i” o de “stopping” de los registros de la CPU a la memoria y viceversa resulta que al convertir “i” a string estamos forzando al compilador a que no utilice el cache dado que necesita el valor mas reciente de “i” en la memoria para poder llamar el método ToString() y al método WriteLine, adicionalmente al llamar a alguno de esos métodos se requiere colocar en los registros de la CPU los valores necesarios para cambiar de contexto y para poder ejecutar las operaciones internas de dichos métodos, así que no hay manera de optimizar nada.

using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    //volatile
    private static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {  
        int i = 0;
 
        while (!stopping)
        {
            i++;
            Console.WriteLine("Valor de i=" + i.ToString()); 
        }
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}

 

Nuevamente podríamos ponerle a “i” como volatile pero no habría diferencia puesto que básicamente el código no necesita ni tiene como ser optimizado. Así que volatile no bloquearía ninguna optimización.

Ejemplo 3

En este SI que funcionan las optimizaciones, ya que el compilador optimizara el uso de “i” para que se apoye en los registros del procesador de principio a fin, eso es perfecto!, pero por otro lado “stopping” también es optimizado, trayendo como resultado que siempre este en uno de los registros y por tanto nunca se entere en que momento “stopping” cambio de valor para que saliera del bucle.

using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    private static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {  
        int i = 0;
 
        while (!stopping)
        {
            i++;
            Console.WriteLine("Valor de i=" + i.ToString()); 
        }
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}

 

Al ejecutar la version release para x86 de este programa veremos como, aunque en codigo parece que todo saldra bien, al ejecutarse el programa este nunca terminara pues el hilo nunca se entera que “stopping” cambio su valor. En pantalla veriamos perpetuamente

Main Exit

Y nada más :(

 

Ese es el ejemplo perfecto de la funcionalidad de volatile, así que podemos ahora marcar el campo “stopping” como volatile, compilar y ejecutar la versión release… y AHORA SI FUNCIONA COMO DEBERIA DE SER!!!

using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    private volatile static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {  
        int i = 0;
 
        while (!stopping)
        {
            i++;
            Console.WriteLine("Valor de i=" + i.ToString()); 
        }
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}

Ahora si en pantalla veríamos

Main Exit
EfectuarTrabajo exit xxxxxxxx

:)

Ejemplo 4

Este es el mismo ejemplo un poco mas elaborado haciendo uso de algunas operaciones de System.Math las cuales son suceptibles de optimización:

using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    //volatile
    private  static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {  
        int i = 0;
        int j = 2;
 
        while (!stopping)
        {
            i++;
            j = 100 + (int)Math.Sin((double)i * 10d);
        }
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}

Prueben quitando y poniendo volatile y verán la diferencia.

Eso es todo.

 

Hasta pronto.

Bookmark and Share

Next Entries »