Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Midiendo el tiempo con precisión

Tenemos que comparar la velocidad de dos algoritmos. ¿Cómo medimos el tiempo que la aplicación tarda en superar un bloque de código? He aquí una primera aproximación:

var
   T0: TDateTime;
begin
   T0 := Now;
   Algoritmo;
   ShowMessage(TimeToStr(Now - T0));
end;

No es una buena idea, sin embargo. En primer lugar, por la poca precisión de la función Now. Una sencilla prueba nos permitiría comprobar que el intervalo mínimo que permite registrar Now es de cerca de 12-13 milisegundos. Si alguna sección de código consume menos tiempo, Now lo interpretaría como un cero.

La razón anterior es la principal, pero además, siento inquina personal hacia Now. Su implementación llama a la función GetLocalTime, del API de Windows, que deja el resultado en una variable de tipo record. Delphi tiene entonces que transformar ese valor en un TDateTime, que como sabemos utiliza un algoritmo "juliano" muy simplificado... y para ello utiliza la función EncodeDate, una de las funciones más estúpidamente programadas de Delphi. Esta instrucción es la que me hace rabiar:

for I := 1 to Month - 1 do Inc(Day, DayTable^[I]);

¡Cada vez que se ejecuta EncodeDate, Delphi tiene que entrar en un bucle para conocer los días transcurridos desde el principio del año hasta el principio del mes! DayTable es una variable de tipo vector que contiene los días de cada mes. Lo más eficiente, sin embargo, habría sido utilizar una segunda variable inicializada que ya contuviese la suma requerida.


Digamos, en favor de Borland, que este tipo de chapuzas eran justificables en la época del Windows de 16 bits. En aquellos tiempos, el segmento de datos, donde se colocaban las constantes inicializadas, estaba severamente limitado, y era preferible gastar un poco más de tiempo de ejecución antes que duplicar el espacio requerido por un algoritmo.

Hay otra función del API de Windows que puede servir como "reloj":

function GetTickCount: DWORD; stdcall;

GetTickCount devuelve el número de milisegundos transcurridos desde el arranque del sistema operativo. Con ella, podríamos retocar el código inicial de este modo:

var
   T0: Cardinal;
begin
   T0 := GetTickCount;
   Algoritmo;
   ShowMessage(IntToStr(GetTickCount - T0));
end;

El problema de GetTickCount es el mismo de Now: la falta de precisión. Aparte de ello, GetTickCount tiene muchas aplicaciones. Delphi, por ejemplo, la utiliza para saber cuántos milisegundos transcurren entre tecla y tecla que pulsa el usuario. En un TDBLookupComboBox se decide de este modo si, durante una búsqueda incremental, se deben concatenar los últimos dos caracteres tecleados, o si se debe interpretar el segundo carácter de forma independiente.


Tenga mucho cuidado, si piensa utilizar GetTickCount para este propósito. Cada 49 días y fracción, el contador interno sobrepasará el límite de capacidad del tipo Cardinal. Si tenemos la mala fortuna de hacer la primera llamada antes del suceso, y la segunda después, obtendremos una diferencia exageradamente grande, suponiendo que utilizamos un Cardinal para el resultado. Si nuestro algoritmo trata sobre teclas y combos de búsqueda, no puede pasar nada "excesivamente" malo como castigo. Pero si estamos controlando algún proceso peligroso... no quiero ni pensar lo que podría suceder.

De modo que nuestro problema es la falta de precisión. ¿Hay alguna solución? Sí, si utilizamos las siguientes funciones que encontrará en la propia unidad Windows:

function QueryPerformanceCounter(
   var lpPerformanceCount: TLargeInteger): BOOL; stdcall;
function QueryPerformanceFrequency(
   var lpFrequency: TLargeInteger): BOOL; stdcall;

El tipo TLargeInteger es simplemente un sinónimo de Int64, un tipo nativo de Delphi que contiene enteros de 64 bits.

type
   TLargeInteger = Int64;

Estas funciones acceden a un reloj de alta precisión del hardware del sistema. Algunos ordenadores muy, pero muy viejos, pueden carecer del mismo... pero no he encontrado todavía ninguno perteneciente a ese grupo, por lo que puede utilizar las funciones con total tranquilidad. ¿Qué precisión tiene ese reloj? En teoría, depende del hardware específico. Por este motivo, debemos ejecutar QueryPerformanceFrequency para saber cuántos tics marca nuestro reloj en un segundo:

var
   Frecuencia: Int64;
begin
   QueryPerformanceFrequency(Frecuencia);
   ShowMessage(IntToStr(Frecuencia));
end;

En mi sistema (en el momento en que escribo, un AMD XP+ 2400), el código anterior devuelve 3.579.545. Es decir, que entre tic y tic transcurre aproximadamente la tercera parte de una millonésima de segundo. Impresionante, ¿verdad?

Por su parte, QueryPerformanceCounter devuelve un número de tics, probablemente desde el momento de inicio del sistema. Para comprobar si el contador está correctamente calibrado (¡y de paso, si no me he equivocado al explicar estas funciones!), puede probar las siguientes instrucciones:

var
   Frecuencia, X, Y: Int64;
begin
   QueryPerformanceFrequency(Frecuencia);
   QueryPerformanceCounter(X);
   Sleep(1000);
   QueryPerformanceCounter(Y);
   ShowMessage(IntToStr(Y - X) + '/' + IntToStr(Frecuencia));
end;

El primer valor mostrado debe ser similar al segundo, porque Sleep fuerza al hilo activo para que pause durante un segundo. Por ejemplo:

3579704/3579545

¿Volvemos al punto de partida? Si queremos una buena medida, en millonésimas de segundo, del tiempo consumido por un algoritmo, podemos utilizar las funciones presentadas de esta manera:

const
  UnMillon = 1000000;
var
   Frecuencia, X, Y: Int64;
begin
   QueryPerformanceFrequency(Frecuencia);
   QueryPerformanceCounter(X);
   Algoritmo;
   QueryPerformanceCounter(Y);
   ShowMessage(FormatFloat('0,', (Y - X) * UnMillon div Frecuencia));
end;