"Cosa" de Negro
febrero 27, 2005
 
Programación Concurrente
Tengo en mis manos la revista Dr.Dobbs de Marzo 2005, cuyo tema principal es la computación a 64bits.

La nota escrita por Herb Sutter titulada "Un giro fundamental hacia la concurrencia en el software" muestra un tema que se relaciona mucho con la temática de este blog.

Y el subtítulo dice "Los días de la comida gratis están por terminar" (Your free lunch will soon be over), refiriendose al fenómeno de los procesadores y el software, "No importa que tan rápidos sean los procesadores, el software constantemente encontrará nuevas formas comerse la velocidad extra". "Si tenemos un procesador 10 veces más rápido, nos daremos la libertad de hacer el software 10 veces menos eficiente".

Si tu eres un desarrollador de software, seguramente haz leido, comentado o creido que no importa que tu aplicación sea un poco lenta, los futuros procesadores serán más rápidos.

La Ley de Moore ha contribuido en gran medida a este autoconvencimiento y hemos dejado de lado la optimización de los programas.

Todo crecimiento exponencial se corta en algun momento. En el 2001 salieron los primeros procesadores de 2Ghz y según la Ley de Moore, en este año ya se debería estar anunciando un procesador de 10Ghz, o con 4 veces la potencia de aquel procesador. El lanzamiento del procesador de 4Ghz ya no tiene fecha probable de salida.
Es correcto que lo importante no es la velocidad, sino la potencia.

¿Pero cómo se logra más potencia sin aumentar la velocidad ?

Las 3 cosas que se usarán para ello serán:
  1. Hyperthreading (ya existen)
  2. Multiprocesador (Alpha y PowerPC tienen, Intel y AMD sacaran este año)
  3. Mayor cache (en constante crecimiento)

Hoy en día la mayor parte de los programas son de un solo proceso (single-thread) y sólo una de las 3 formas de mejorar la potencia de procesamiento beneficia a este tipo de programas, el aumento de cache (la principal diferencia entre los procesadores económicos y los de alta gama es una mayor cache).

Por supuesto que para poder aprovechar un multiprocesador será necesario contar con un sistema operativo acorde, WinNT, 2000 o superior, Linux, Novell o algún Unix.

De todas formas, los programas de un solo thread se beneficiarán porque el sistema operativo podrá repartir algo de carga dandole tareas al otro procesador, pero de todas formas nuestro programa no podrá ir más rápido que el caso ideal de que el programa tuviera todo el poder de un solo procesador para el solo.

Cuál es la solución ?

Como primer medida, dedicarle tiempo a la optimización, los procesadores futuros no nos darán mucha más velocidad para combatir nuestra inoperancia. De todas formas, podremos tener un software bien optimizado y aún así con problemas de performance.

Como segunda medida, la programación concurrente. Esto no es algo nuevo, pero hoy en día sólo una mínima parte de los programas lo son. Herb Sutter comenta que las verdaderas revoluciones de software no se basan en un producto determinado de una gran empresa de software, sino en tecnologías que tienen años e incluso décadas de desarrollo.

La POO surgió en los años 60 con Simula, pero no llegó a una revolución hasta los 90.

Las nuevas tecnologías pueden ser muy interesantes, e incluso beneficiosas, pero las revoluciones que cambian la forma de escribir los programas llevan años de desarrollo.

La concurrencia será la próxima gran revolución de como debemos escribir los programas.

Beneficios y costos de la concurrencia.

El primer y natural uso y beneficiario de la concurrencia es la separación de flujos de ejecución lógicamente independientes. Ej un servidor web atiende a cada solicitud de página en un hilo de ejecución separado.

El segundo uso es el de lograr una mejor performance al hacer tareas paralelas.

Sin embargo, en la ejecución, la concurrencia tiene costos.
  1. La sincronización del acceso a los recursos compartidos por medio de bloqueos.
  2. No todos los procesos pueden dividirse en tareas concurrentes o usar rutinas concurrentes o ser usados concurrentemente.
  3. Pero el gran problema es que la concurrencia no es tan sencilla de diagramar, programar y probar como la programación serial.

La programación concurrente es probablemente tanto o más dura de aprender que la programación orientada a objetos.
Es necesario aprender y comprender muchos nuevos conceptos (que es una race conditions, que es un deadlock, que construcciones actualmente serializadas se pueden ejecutar en paralelo, etc. )

La conclusión de Herb Sutter es que éste es el momento de tomarse un buen tiempo a revisar el diseño de su aplicación y determinar que operaciones pueden beneficiarse de la concurrencia.

Muy pocas aplicaciones son naturalmente concurrentes, La mayoría no lo son. Incluso cuando sepa exactamente donde está limitado por la CPU, se encontrará con dificultades para encontrar como paralelizar tales operaciones. Son todas suficientes razones para comenzar lo antes posible a pensar en la concurrencia.

Algunos compiladores podrán ayudar reordenando instrucciones y detectando zonas paralelizables, pero no debemos esperar mucho.

Nada puede ser mejor que un buen trabajo de paralelización de su programa secuencial haciendo verdaderas rutinas que se ejecuten en forma concurrente.
febrero 24, 2005
 
Optimizacion IV - Comando With Object
WITH OBJECT ... END

Esta es otro de los nuevos comandos de xHarbour que permiten ahorrar tiempo en tareas repetitivas.

Este comando realiza una cache de un objeto y junto con algunos PCODEs permite acelerar el acceso a metodos y datas de un objeto.

La sintáxis más común es:

WITH OBJECT oGet
...:Name := "Nombre"
...:VarPut( "Walter" )
END

También se puede usar la función HB_SetWith().
Al pasarle el dato, lo guarda en la cache, y al llamar a esta función sin parámetros, lo quita.
Para acceder al objeto en el cache se usa la función HB_QWith().
Hay que destacar, que tanto HB_SetWith() como HB_QWith() no son funciones reales, sino que en tiempo de compilación se transforman en los mismos PCODEs usados en WITH OBJECT.

HB_SetWith( oGet )
...HB_QWith():Name := "Nombre"

...HB_QWith():VarPut( "Walter" )

HB_SetWith()


Este código genera el mismo PCODE que el ejemplo anterior.

Es posible usar HB_QWith() dentro de WITH OBJECT, pero no es posible usar la sintáxis reducida (:Name), con HB_SetWith().

WITH OBJECT oGet
...oOtro := HB_QWith() // Correcto
END

HB_SetWith( oGet )
...? :Name // Error
HB_SetWith()

También es posible usar el WITH OBJECT dentro de una macro.

WITH OBJECT oGet
...&(":Name := 'Nombre'")
...&(":VarPut( 'Walter' )")
END

Y el HB_SetWith() también en una macro

&("HB_SetWith( oGet )")
...HB_QWith():Name := "Nombre"
...HB_QWith():VarPut( "Walter" )
&("HB_SetWith()")

WITH OBJECT
y HB_SetWith() soportan hasta 32 niveles de anidamiento.
Cada END y cada HB_SetWith() sin parámetros o con parámetro NIL, "desanidan" un nivel.

Aqui hay un ejemplo de los usos, tomado desde el CVS de xHarbour xharbour\tests\with.prg
febrero 20, 2005
 
Optimización III - ADEL, AINS, operador IN
Prosiguiendo con nuestra tarea de escribir código más óptimo, voy a nombrar algunas de las funcionalidades más útiles disponibles en xHarbour.

Función ADEL

ADEL( aArray, nPos, lAutoSize )

El parámetro lAutoSize en .T., hace la llamada a ASIZE() internamente, reduciendo la cantidad de código ejecutado.

Función AINS

AINS( aArray, nPos, xDato, lAutoSize )

El parámetro xDato asigna el dato en la posición nPos, evitando tener que hacerlo posteriormente.
El parámetro lAutoSize en .T., hace la llamada a ASIZE() internamente.
Con solo dos parámetros extras en la función se reduce la ejecución de mucho código.

Operador IN o $

Todos seguramente conocen el operador $, que sirve para saber si existe una cadena de caracteres dentro de otra cadena de caracteres.

El operador IN cumple la misma función, de hecho son sinónimos.

Pero se ha extendido el uso de estos operadores para usarlos con arrays y hashes.

La siguiente expresión

IF ASCAN( aArray, xDato ) > 0

se puede reducir a:

IF xDato IN aArray

Próximamente voy a comentar una extensión que tengo hecha asociada a este operador para recuperar la posición donde se encontró el dato.

Los animo a comparar el PCODE generado por las llamadas originales a las funciones con las llamadas optimizadas.

////////////////////////////////////////////////////////////
Function Main()
Local aArray := ARRAY( 10 )

ADEL( aArray, 2 )
ASIZE( aArray, LEN( aArray ) - 1 )

ASIZE( aArray, LEN( aArray ) + 1 )
AINS( aArray, 5 )
aArray[ 5 ] := 10

IF ASCAN( aArray, 10 ) > 0
? "Lo encontre"
ENDIF
RETURN

////////////////////////////////////////////////////////////
Function Main()
Local aArray := ARRAY( 10 )

ADEL( aArray, 2, .T. )

AINS( aArray, 5, 10, .T. )

IF 10 IN aArray
? "Lo encontre"
ENDIF
RETURN

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)
febrero 16, 2005
 
Optimizacion I
El tema optimización es muy largo, asi que iré comentando algunos procedimientos que sigo para optimizar mi propio código y el de revisiones que hago.

Pero primero qué es optimización ?
Según el diccionario de la Real Academia Española: Buscar la mejor manera de realizar una actividad.

Significa hacer un mismo trabajo con menor consumo de recursos.
La optimización es un proceso que requiere de análisis, pruebas y estadísticas.

En estos primeros comentarios voy a concentrarme en optimizaciones de velocidad de ejecución.

Seguramente seré muy repetitivo, pero así como un corredor de autos de carrera no solo tiene que saber conducir bien, tambien debe conocer y saber administrar los límites de su auto, y cuanto más conoce del funcionamiento y de los materiales y de la resistencia de los mismos, tendrá mejores oportunidades de sacar esa diferencia que lo hará ganar.

Así que una vez más le recomendaré leer el documento sobre PCODE que se encuentra en PuertoSUR en el apartado Secciones, Documentos de PuertoSUR.

La optimización más gruesa puede hacerse a nivel PRG siguiendo las siguientes reglas:

a) No evaluar funciones innecesarias en bucles FOR-NEXT, WHILE
Ej: FOR n:=1 TO LEN( aArray ) - 1
En cada iteración del bucle, se evalúa la función LEN() y la operación resta, pudiendo evitarse de la siguiente forma:
FOR n := 1 TO nLen

b) Reducir las ramas innecesarias en DO CASE o IF-ELSEIF.
DO CASE
CASE "condicion poco probable"
CASE "condicion medio probable"
CASE "condicion muy probable"
ENDCASE

es mejor hacer
DO CASE
CASE "condicion muy probable"
CASE "condicion medio probable"
CASE "condicion poco probable"
ENDCASE

c) Usar asignaciones INLINE cuando sea posible.
En lugar de hacer:
nAt := AT( "x", cText )
If nAt > 0

No tenga miedo a hacer:
If (nAt := AT( "x", cText )) > 0

d) No usar IIF() cuando se puede usar IF-ENDIF.
Un ejemplo de lo que no se debe hacer, es el comando DEFAULT de FW que genera este código.
cParam := IIF( cParam == NIL, cParam, "" )
y lo óptimo habría sido
IF cParam == NIL; cParam := ""; ENDIF

en el ejemplo no óptimo, siempre se ejecuta la asignación del datos sobre la variable cParam, incluso cuando no lo tiene que cambiar, se le asigna su propio dato.

e) Usar PROCEDURE en lugar de FUNCTION cuando sea posible.
Los procedimientos no escriben ni limpian el área de retorno, evitando gastar tiempo en algo qu no se va a tener en cuenta.

f) No usar AADD cuando el largo del array es conocido.
Si tenemos que crear un array y sabemos de que largo va a ser, no usemos AADD en forma recursiva o lineal para rellenar todo el array, en su lugar, creemos el array con ARRAY() y rellenemos sus datos con aArray[n].

g) En los IF complejos, escribir las opciones que más probabilidad tengan de definir en forma anticipada el resultado.
IF "condicion generlamente Verdadera" .and. "otra condicion"

es mejor hacer
IF "otra condicion" .and. "condicion generalmente Verdadera"

Seguramente habrá muchas formas más de optimizar el código con solo escribir un poco más ordenado.
febrero 08, 2005
 
Bienvenidos !!!
Hace tiempo que tenía intenciones de tener un espacio donde exponer y discutir acerca de lo que tanto me gusta, programar.
Mis lenguajes preferidos son el xBase, el C y el ASM.
Mi compilador preferido xHarbour.
Quiero compartir los conocimientos que he logrado gracias a pertenecer al equipo de desarrollo de Harbour y xHarbour.

Y ojala que este espacio sirva para que muchos más programadores de habla hispana puedan desarrollar extensiones, herramientas, funciones, librerías, wrappers y todos podamos beneficiarnos de el crecimiento de la comunidad de desarrolladores.

Por este medio quiero contar experiencias, problemas, soluciones, optimizaciones de código en PRG y C, guias, proyectos, etc, mayormente relacionados con xHarbour, pero no limitado.

Justamente para comenzar, tengo un texto que puede servir de guía para meterse en el corazón del compilador de xHarbour y poder hacer, en algún momento, sus propias extensiones y modificaciones.

Walter Negro

Powered by Blogger