Administra tu Blog

¡Crea tu Blog Ya! Fácil y Gratis

Debería funcionar
Divulgación y, sobretodo, divagación sobre computación, mayormente práctico.

16/01/2008 GMT 1

Programando en pequeñito (I)

fermat @ 00:12

Muchos de nosotros (por no decir todos, que suena un poco prepotente) hemos aprendido a programar como buenamente hemos podido, pero delante de ordenadores hechos y derechos, sin dar mayor importancia (o ninguna) a circunstancias como el uso comedido de dispositivos (memoria, procesador, disco...).

Claro que tampoco tiene mucho sentido pensar en ello cuando no existe tal limitación. El problema viene cuando nuestro programa no va a utilizarse en un ordenador de sobremesa clónico, o en un portátil con 2 procesadores y más espacio en RAM que los primeros Pentiums en disco, sino cuando se utilizará un teléfono móvil para ello, o una libreta electrónica, o una webcam avanzada (y demás dispositivos limitados).

Por lo general, es necesario dar una vuelta de tuerca al diseño del software que se desarrollará para estos sistemas que, normalmente, no pueden utilizar más de 1MB de espacio, ya sea en disco o en memoria, enfocando, así, el diseño en la mayor limitación que padecemos. Además, como no somos los primeros que nos enfrentamos a estos diseños, tenemos la suerte de poder echar mano de soluciones funcionales y bien probadas para los problemas más comunes que surgen en este tipo de software. Estas soluciones probadas son lo que se conocen como patrones de diseño.

Algunas técnicas definidas en esos patrones son:

  1. Uso de estructuras de datos pequeñas/simples (para que no ocupen mucho)
  2. Diferentes enfoques en cuanto a la asignación de memoria
  3. Uso de compresión: el caso más sangrante es el de las demos de 4ks de las parties (tipo Euskal Party), que ese es el máximo que ocupan en disco (aunque luego se expande esa información en memoria hasta que deja a todo el público boquiabierto)
  4. Uso de almacenes secundarios de la información (como tarjetas sim)
  5. Cambios en los estados de los programas para permitir un máximo de ellos ejecutándose al mismo tiempo

En el primer caso, se pueden utilizar métodos de enmascaramiento de los datos para que ocupen menos. Un ejemplo podría ser la forma de almacenar una dirección IP. Lo primero que nos viene a la cabeza (o no xD) es el uso de una cadena de caracteres para almacenar cada número (y cada punto) tal como son leídos por el ser humano (ej. 127.0.0.1). La segunda posibilidad es almacenarlo como un conjunto de bytes, que es, además, cómo trabajan los sistemas operativos con estos datos. Si la forma en que dicho dato se almacenase nos diese igual, podríamos demostrar que es mejor la segunda opción por lo siguiente:

  • Un carácter ocupa 1 byte de memoria, lo que unido a 9 caracteres que forman la IP de localhost, nos dan 9 bytes utilizados (en IPv6 se complica un poco más)
  • Un entero largo, que es dónde se almacena la dirección IP en forma numérica, ocupa 8 bytes de memoria (el doble que un entero normal). De esta forma, estaríamos usando 1 byte menos que en la primera forma, pudiendo ahorrar hasta 7 bytes (pensad en una estructura repetida muchas veces y multiplicad el ahorro).

Otra recomendación, extendida a todo tipo de programas, aunque muy aconsejada en este tipo de sistemas, es el uso de clases contenedoras de datos (conocidas como DTO (Data Transfer Object)). Igualmente, se aconsejan diseños que fomenten el uso compartido de información, con el fin de evitar duplicados (aunque podría traer problemas mayores ante concurrencia en el acceso a dichos datos... para lo que utilizaríamos otro patrón de diseño conocido que evitaría inconsistencias).

Pasando al siguiente punto, la asignación dinámica de memoria (malloc) es uno de los grandes progresos en la codificación de programas porque permite variar su tamaño en tiempo de ejecución, pudiendo ampliar o reducir su contenido, y así dar flexibilidad a dichos programas. Pero esta flexibilidad tiene un precio, pagado (como casi siempre) en variaciones del rendimiento de nuestros programas.

En este aspecto, quizás lo más importante sea la predicción del uso de memoria. Es decir, tener en mente un rango de uso de memoria del que nunca va a salir nuestros programas. Para ello, hay diferentes aproximaciones, entre las que se podría destacar el evitar llamar a malloc cada vez que se necesite. Las razones son variadas. Por un lado, podría darse el caso de que no tuviésemos suficiente memoria para completar todo el programa. Para resolverlo, se podría asignar toda la memoria que se pretende utilizar al principio del programa (en base a una matriz de objetos o structs), y así no se ejecutará ningún proceso hasta que se esté seguro de tener espacio para ello. Por otro lado, la asignación dinámica no tiene un tiempo definido para realizar dicha asignación (depende del núcleo del sistema operativo y del recolector de basura), algo inconcebible en sistemas de tiempo real. En estos casos, quizás se debería realizar una preasignación que será utilizada de manera transparente durante el programa (haces como que asignas, pero internamente reutiliza un espacio ya asignado). El inconveniente es que se asigna toda la memoria que será utilizada, posiblemente en tiempos diferentes, en todo el programa. Por último, están 2 formas de asignación similares a las anteriores: pool de asignaciones (con nodos en forma de árbol) y asignación estática (no variable, y a la que no afecta el recolector de basura).

Durante la asignación dinámica tiene especial relevancia una sección del espacio asignado en memoria para la ejecución del programa llamada heap, lugar donde se almacena la información asignada. La forma en que construimos el proceso de asignación de memoria de nuestro programa es muy importante ya que los pequeños espacios asignados pueden fragmentar espacios mayores que se asignen posteriormente (lo que provocaría problemas de rendimiento). La idea para resolver esto sería asignar primeramente los espacios mayores, pero tarde o temprano estos espacios se librarán y volverá a surgir dicho problema. Si ésta parece una circunstancia inevitable (asignar y desasignar continuamente), quizás habría que pensar en crear grupos, o varios heaps (a nivel de usuario), con el fin de destruir dichos grupos (eliminando varios espacios del mismo grupo de una sola vez), o incluso optimizar los espacios en memoria.

El tercer punto que comentaba era el de la compresión. Aunque es difícil de implementar, creo que se gana mucho con ello. Dicha compresión puede ir enfocada a los datos (como la codificación de Huffman) o a tablas (en casos de codificaciones de caracteres extrañas, como el cirílico o el chino).

Otra forma de reducir el uso de memoria es mediante el procesamiento secuencial de la información, que se puede conseguir mediante pipes (tuberías). También suele ser interesante cargar las bibliotecas, con funciones externas a nuestro programa, de forma dinámica (es decir, en el momento que vayamos a usarlas), con el fin de reducir el tiempo que permanecen accesibles (y ocupando) en memoria.

Y por último, y para no aburrir más, recordar que estamos jugando con fuego cuando tenemos pocos recursos, donde puede ocurrir que nuestro programa sea interrumpido inesperadamente por el sistema operativo (si es que no estamos programando el sistema operativo xDD) con el fin de salvarse a sí mismo. En estos casos, deberíamos reducir los daños colaterales que se producirían (inconsistencias, etc.), e incluso pensar en una política de recuperación ante problemas (de qué prescindir en el rearranque, ...). Si estamos programando el gestor de memoria del sistema operativo, sería necesario dar a cada proceso en ejecución una prioridad, una importancia, con el fin de clasificarlos y así sacrificar el menos importante/necesario.

Comentarios

No hay Comentarios »

Dejar un Comentario


<a href> <em> <blockquote> <strong> <cite> <code> <ul> <li> <dl> <dt> <dd>

Contactar con la autora o autor | Archivo | ¡Crea tu Blog Ya! Fácil y Gratis