"Cosa" de Negro
febrero 18, 2005
 
Optimizacion II - Arrays
Enlazando con el comentario anterior, Optimizacion I, hoy vamos a ver los arrays, donde la modificación del largo de un array es una de las tareas más pesadas para el sistema, asi que hay que evitarlas dentro de lo posible, en especial cuando se trate de arrays grandes.

Los arrays están optimizados para ser accedidos rápidamente, no para ser modificados rápidamente.
La estructura de un array consiste en una porción de memoria contigua que permita almacenar las estructuras de todos los datos contenidos.

La estructura de los datos es la estructura HB_ITEM, y un array consta de una cabecera (estructura BASEARRAY) y un porcion de memoria de largo sizeof( HB_ITEM ) * largo_del_array, esto hace que cada vez que se agrega un dato al array, hay que pedirle al sistema operativo más memoria contigua para el array, en caso de no tenerla, el sistema operativo copia todo a un nuevo lugar en la memoria con espacio suficiente para almacenar en forma contigua el nuevo array.
sizeof( HB_ITEM ) es 24, asi que un array de 100000 posiciones necesita un espacio contiguo de memoria de 2.4 millones de bytes, aprox 2.29 Mb

Imagínense que tienen un estante lleno de libros y al querer guardar uno más no hay espacio, pero no quieren ponerlo en otro estante, asi que reordenan un estante más grande y llevan todos los libros a ese nuevo estante, y en ese proceso de reordenar otro estante seguramente se apilaran libros en el suelo.
Lo mismo pasa dentro de una PC, se compacta memoria de otros programas y si fuera necesario, parte de ests se envía a disco.
Mientras dure el proceso de mover los datos (o los libros), el array ocupa de espacio la suma de los dos arrays (el nuevo y el viejo), lo cual es para tener en cuenta con arrays muy grandes.

Pero aqui no solo vengo a hacer advertencias y problemas, también a mostrar posibles soluciones.

Tomemos el siguiente ejemplo muy comunmente usado para llenar un array con los datos de una tabla.

aArray := {}
Do While !Eof()
...aadd(aArray, xxxxx)
...Skip()
Enddo


Por lo dicho arriba, ya sabemos que este es altamente ineficiente y hasta problemático para arrays grandes.

La forma más simple y antigua para no usar AADD, es reservar una cantidad de memoria suficientemente alta como para no necesitar agrandar el array, o como decía en el comentario anterior, que si conocemos el largo del array antes de comenzar a llenarlo, dimensionar el array a ese tamaño, pero suponiendo que en este caso no lo sabemos, sería algo así:

aArray := array(100000)
nLen := 1
Do While !Eof()
...aArray[nLen] := xxxxxx
...nLen++
...DbSkip()
Enddo
Asize( aArray, nLen-1 )

Esta es la forma más rápida de llenar un array, incluso más rápida que otras que veremos a continuación, pero no siempre es la más acorde para todos los casos y tiene varios problemas que la hace no apta para aplicar en un programa serio.

1. Sobredimiensionado para algunos casos, porque podria ser que nuestros arrays más comunes son de 20, pero en algunos casos pueden llegar a 50000.

2. Subdimensionado para otros casos, "justo tengo un cliente que acaba de superar los 100000 productos"

Para solucionar los problemas anteriores, existen 2 alternativas que producen un código un poco mayor y una carga de trabajo mayor o menor dependiendo del esquema usado, pero que resuelve los 2 problemas anteriores.

Una solución se basa en ajuste lineal y la otra en ajuste exponencial.

El ajuste lineal indica que el array debe arrancar con un valor determinado e ir ajustandose en incrementos constantes a medida que se llega al máximo valor del array.

El ajuste lineal es el más usado para arrays chicos y para arrays que crecen y decrecen, ya que es más sencillo el cálculo de ajuste.

#define K_AJUSTE 100
aArray := Array( K_AJUSTE )
nLen := 1
nMax := K_AJUSTE

Do While !Eof()
....Do While !Eof() .and. nLen <= nMax
........aArray[nLen] := xxxxxx
........nLen++
........DbSkip()
....Enddo
....If nLen > nMax .and. !Eof()
........nMax += K_AJUSTE
........aSize( aArray, nMax )
....Endif
Enddo
Asize( aArray, nLen-1 )

El ajuste exponencial es mejor para grandes cantidades porque a pesar de que las cantidades a reservar se van acrecentando de forma considerable, tambien se va reduciendo la necesidad de reajustes en forma considerable, justo en los momentos en los cuales es más costoso hacer un redimensionamiento.

aArray := Array( 100 )
nLen := 1
nMax := 100

Do While !Eof()
....Do While !Eof() .and. nLen <= nMax
........aArray[nLen] := xxxxxx
........nLen++
........DbSkip()
....Enddo
....If nLen > nMax .and. !Eof()
........nMax *= 2
........aSize( aArray, nMax )
....Endif
Enddo

Asize( aArray, nLen-1 )


(Los puntitos están para dejar la sangría y hacer más entendible el texto)
Comments:
Walter,

Gracias por tan buena explicación.

Si intentamos simplificar el ejemplo del ajuste lineal haciendo las acumulaciones inline y situando una de las condiciones WHILE sobre un IF, ¿ crees que se perderá mucho rendimiento por este IF ?

#define K_AJUSTE 100
aArray := Array( K_AJUSTE )
nLen := 0
nMax := K_AJUSTE

Do While !Eof()
....If ++nLen > nMax
........aSize( aArray, (nMax += K_AJUSTE) )
....Endif
....aArray[nLen] := xxxxxx
....DbSkip()
Enddo
Asize( aArray, nLen )

Saludos
 
Walter,

muy bueno el artículo, sí señor. Espero que muchos de los que usan (usamos) AAdd() se lo piensen 2 veces antes de hacerlo. A pesar de lo cómodo que es.

Yo llevo ya bastante tiempo dándole vueltas a este tema, y se me ocurrió una cosa, aunque por falta de tiempo no he podido ni intentar implementarlo. ¿Por qué no se implementa el ajuste lineal a nivel del propio xHarbour? Me explico:

En la estructura BASEARRAY se podría añadir un miembro ulTrueLength o algo así, que contenga la longitud real del array, entendiendo como tal el tamaño que ocupa actualmente en memoria. Por otro lado, el miembro ulLen tendría el tamaño lógico del array, es decir, los elementos que actualmente se están usando.

De esta forma, la función Len() podría seguir funcionando como hasta ahora. Y sólo habría que hacer unos pequeños ajustes en otras funciones de manejo de arrays, como AAdd(), ASize(), AIns(), etc., para que tengan en cuenta esta característica.

Por otro lado, habría que modificar la función Array(), para que tenga un segundo parámetro que indique el número de elementos que se deben de añadir en bloque (lo que tu has definido como K_AJUSTE), y que por defecto sería 1. Así, el código existente no se vería afectado en absoluto, mientras que quien quiera optimizarlo podría poner, p.ej. aArray := Array( , 100 ).

Las ventajas son obvias, como te puedes imaginar, y al implementarlo directamente en C el impacto en la velocidad sería prácticamente indetectable. Es más, incluso se podría poner por defecto K_AJUSTE a 16 ó 32, lo que sería muy beneficioso incluso para el código existente, y su impacto en el consumo de memoria, por término medio, sería muy pequeño, salvo claro está, que alguna aplicación crease muchísimos arrays muy pequeños, pero no suele ser el caso.

Un saludo.
 
Juan:

Muy buena optimización de código, haciendo uso de asignaciones inline y operadores unarios.

La única sobrecarga entre este código y el otro, es un JMP extra generado por el IF, que es totalmente despreciable, teniendo en cuenta que se va a ejecutar una cantidad de veces reducida y se obtiene una importante reducción de código.
 
Jose:

Como tu dices, es totalmente posible hacer ese cambio en la estructura de los arrays.
De hecho, los HASH de xHarbour trabajan de esa forma y la variable de ajuste es de 16.

Voy a anotarmelo para implementarlo.

Se deberían agregar 2 miembros a la estructura.
1. ulAllocated (para usar el mismo nombre que en hash)
2. ulAdjust (la constante de ajuste, independiente de cada array)
 
Buenas Walter;
Encuentro de suma utilidad tus explicaciones, y me alegra mucho que compartas tus conocimientos.
Yendo al caso del post.
Cual es la diferencia entre el "do while" y meterlo en una "dbEval" ?

Por ejemplo
#define K_AJUSTE 100
aArray := Array( K_AJUSTE )
nLen := 0
nMax := K_AJUSTE

DbEval( { || If( ++nLen > nMax, ;
.............aSize( aArray, (nMax += K_AJUSTE) ), ), ;
.............aArray[nLen] := xxxxxx } )
Asize( aArray, nLen )


Saludos
 
Renzo:

Si bien la ejecución de un codeblock significa una recarga de tiempo en la preparación del stack antes de llamarlo y la posterior restauración del stack al estado anterior, quizas en parte se compense con la ausencia de las llamadas a EOF() y DBSKIP() desde PRG.

Sería cuestión de hacer una comparativa de velocidad.

Saludos

Walter Negro
 
Publicar un comentario

<< Home

Powered by Blogger