Tampilkan postingan dengan label eficiencia. Tampilkan semua postingan
Tampilkan postingan dengan label eficiencia. Tampilkan semua postingan

Rabu, 15 Juni 2011

De la eficiencia en cómputo

A mí me gusta Prolog, el lenguaje de programación, que a decir de la propaganda que Borland hacía de su compiladort (turbo Prolog), era "el lenguaje natural de la inteligencia artificial". Bonita frase publicitaria, nada más.

Y lo que me gusta de Prolog es su paradigma, que es totalmente diferente al que tienen los lenguajes de programación de cuarta generación como Pascal y C. En prolog actuamos en otra forma: definimos el problema y el lenguaje se encarga de darnos la solución. Suena a magia, pero no lo es.

Prolog es un lenguaje de programación declarativo y por ende, lo que tenemos que hacer es precisamente declarar las condiciones iniciales y de frontera. Por ejemplo, puedo decir:


ama(juan, ana).

Lo cual quiere decir, por ejemplo, que "juan ama a ana". Ojo, aquí es importante el orden de los términos. En prolog no es esto equivalente a lo que sigue:


ama(ana, juan).

Lo cual querría decir que "ana ama a juan", lo cual evidentemente no es equivalente.

Pero lo interesante aquí es el que declaramos, casi en el idioma que hablamos normalmente, un hecho que dentro de Prolog tiene sentido. No hablamos pues de hacer cálculos sofisticados por ejemplo, sino de declarar estas relaciones, que en los lenguajes de cuarta generación se dificulta hacerlo.

Pero pensando en esto y en los problemas que Prolog puede resolver, vía el mecanismo de la recursividad, hallé que en realidad habría que preguntarse si Prolog es un lenguaje víable para aplicaciones reales y no para meramente problemas académicos como los que ya he tratado aquí (crear crucigramas, pasear por un laberinto, resovlver sudokus, etc.). Y la realidad es que pienso que Prolog bien puede ser una herramienta víable, pero solamente para unos pocos casos en donde las simbologías son muy importantes.

Por ejemplo, pienso en un diccionario que tengo que consultar. Si mi diccionario cuenta con digamos medio  millón de palabras, y necesito saber si una palabra determinada existe en mi archivo (y considerando que las palabras están debidamente ordenadas), requiero de hacer unas 19 búsquedas, pues 2^N = 500,000 es aproximadamente 19.

Escribir un programa en Prolog que haga una búsqueda binaria no es difícil. La idea es colocarse en la palabra 250,000 inicialmente y e si la palabra que busco es la que está en esa posición. Si no es, entonces debo ver si "me pasé", es decir, la palabra está antes de la que hallé o "me quedé corto", es decir, la palabra está después de la que leí. Así, divido esa región del archivo 0 a 249,000; o 250,001 a 500,000 entre dos y vuelvo a hacer la búsqueda. En Prolog, naturalmente esto se hace de forma recursiva, lo cual implica guardar el estado del sistema, vía el stack, que es una estructura de datos LIFO (Last In, First Out), cada vez que se hace la llamada recursiva. Obviamente necesito una condición terminal o de salida para no ciclar al programa y que éste termine por quedarse sin memoria.

Si intento hacer, en cambio, una búsqueda binaria en un lenguaje de cuarta generación, hallaré que no necesito usar ninguna función recursiva y con un ciclo WHILE puedo resolver mis dificultades. Aquí no hay que llevar cuenta del stack y resulta muy eficiente en términos de memoria.

Aún así, 19, 20 búsquedas usando un stack en un algoritmo recursivo no sólo no es terriblemente ineficiente, sino que es además muy elegante. Niklaus Wirth decía: "Iteratum humanum est, recursivitum divinum est" (la iteración es humana, la recursion es divina)

Pero pensando en esto, me pregunté si quisiese hacer una búsqueda linea, es decir, buscar de la primera a la última palabra en mi diccionario. Si hago esto, en el mejor de los casos haré una búsqueda: la palabra que busco está en la primera posición. En el peor de los casos la palabra que busco estará en la última línea de mi archivo, por lo cual haré medio millón de consultas.

Si quiero hacer esto en Prolog, y además lo quiero hacer recursivo, debo prepararme para guardar en el stack medio millón de estados. Por lo menos eso en un principio. Mi esquema en Prolog sería algo así:


Si la variable I es mayor que medio millón termina.

Define la variable I como 1
busca en la iésima palabra si es la que busco
si no lo es, crea una nueva variable I1 que sea igual a I+1
ve al principio del procedimiento y vuelve a buscar con estos valores
Si es el valor, imprime el resultado y asigna a la variable el valor de medio millón + 1 y ve al procedimiento recursivo



Obviamente estoy asumiendo que quienes me leen ya han tenido contacto con Prolog, pero aquí el asunto que quiero ilustrar es la dificultad que voy a enfrentar, que es la de guardar el estado del sistema en el stack a cada llamada recursiva. Dudo que el programa soporte medio millón de llamadas guardadas en el stack. ¿Qué hacer entonces? La solución es usar la instrucción corte (cut), la cual se identifica con un signo de admiración "!" y entonces le decimos al sistema en prolog que no guarde todos estos estados temporales de la recursión, porque en el fondo no me interesa saber qué pasa cuando regreso de la recursión en sí.

Si se me ocurre hacer esto en algún lenguaje de cuarta generación como Pascal, lo que haría es:

repite
  asigna a I el valor de 1
  asigna termina = falso
  lee el registro cuyo valor es I
  Si es la palabra que busco, entonces
      asigna a la varianble termina = verdadero
      escribe resultado
  Si la palabra no es la que busco, incremento la variable I
hasta que termina = verdadero




Aquí no llevo control del stack y no tengo que preocuparme por nada de eso. Por ende, parece ser más fácil usar para este asunto en particular, un lenguaje de cuarta generación.

Pero un momento, hasta los lenguajes declarativos, que usan simbologías, no se salvan de tener que hacer procedimientos repetitivos como en los lenguajes tradicionales. Aquí la cuestión es que el programador necesita definir las cosas de manera que el sistema sea eficiente y no se quede sin memoria.

En suma, pienso que Prolog es un lenguaje comercialmente víable, pero en este caso, el programador requiere de ser más cuidadoso, mucho más, de los recursos de memoria con los que cuenta. No tener esto presente puede hacer de prolog un lenguaje por demás ineficiente y poco usable.

Senin, 23 Mei 2011

De la eficiencia en cómputo



Han pasado ya algunos años, por no decir muchos, desde que empecé a usar computadoras. Mi primera máquina fue una Apple ][ y ahí hice mis pininos, primero en el Applesoft Basic y más adelante en el UCSD Pascal. Cabe recordar que una computadora de los años ochenta tenía unos 128K de memoria RAM y unidades de discos flexibles. Ni pensar en discos duros. De hecho, me tocó ver uno, para un sistema de precios unitarios, que era cvomo de 5 megabytes y que, para usarse con la Apple ][, el disco estaba dividido en un sistema de discos flexibles virtuales, que el sistema operativo DOS 3.3, podía entender entonces.


Cuando se tenían estos sistemas, estábamos limitados a unos 70K por lado del disco. Si voltéabamos el disco flexible (floppy) y le hacíamos una muesca en un borde, podíamos duplicar la cantidad de información a guardar, aunque el fabricante de los diskettes no recomendaba esta práctica. Igualmente, al tener solamente 128K de RAM, teníamos espacio reducido para nuestros programas. Si usábamos el intérprete de Applesoft Basic entonces estaríamos hablando de unos 48K para programas, porque los 128K no podían usarse más que en dos mapas alternativos de memoria por la forma en que el procesador accedía a la memoria. Por ello, en Applesoft Basic se seguía un esquema que se usó mucho en los primeros sistemas con lenguaje BASIC, el cual lo que hacía era "tokenizar" cada palabra reservada del lenguaje, para así ahorrar bytes, que estaban tan escasos en ese tiempo. Así, la instrucción PRINT, por ejemplo, se traducía a un sólo número hexadecimal. Si queríamos ver el listado del programa en memoria, éste se desplegaba destokenizando, valga la expresión, cada token y el programador veía su código fuente.

En términos de hardware y software, por ejemplo, la Apple ][ tenía su sistema operativo en un diskette de 70K. Increíble pensar que se podía tener un sistema operativo en tan poca memoria. Pues bien, como desde luego esta pieza de software era la más importante de la Apple ][, me hice de un libro que explicaba lo que pasaba dentro del sistema: Beneath Apple DOS. Hallé, cosa notable, que los ingenieros de software de Apple habían decidido poner el sistema a la mitad del disco. Si éste tenía 36 tracks, se cargaba éste en el track 18. Sonaba extraño. ¿Para qué? la respuesta es notable: si el cargador se pone a la mitad del disco, la cabeza lectora tiene que caminar máximo la mitad de la distancia para hallar el track deseado.  Obsérvese pues cómo se buscaba optimizar hasta en estos detalles.

Con el tiempo y los megas, gigas y teras, que ya están con nosotros, las cosas cambiaron radicalmente. Cualquier computadora casera tiene al menos dos gigabytes de memoria, y ello hace que la optimización y la eficiencia pierda sentido.

Ahora nadie piensa realmente en este tema. Los compiladores ya no son tan cuidadosos para ahorrar memoria. Los procesadores de texto han hecho crecer los documentos porque ahora se les añadetodo género de tags y marcas para identificar tipos de letra, tamaños, formato, etc. Simplemente un archivo que contenga la frase "Hello world!", ocupa  24064 bytes, cosa que vendría ser la tercera parte de un diskette de Apple ][.

Ahora la eficiencia, creo yo, se considera desde otro enfoque. Ahora se trata de ser muy rápido en todo, aunque para ello necesitemos usar mucha memoria. Los programadores sabemos que no se puede tener un programa muy rápido que use poca memoria, a mayor rapidez de una cosa, más recursos para que esto ocurra. En ese sentido la computación es muy ética. No hay una relación de ganar ganar entre velocidad de proceso y uso de poca memoria.

Desde luego que a pesar de que los sistemas modernos no hacen énfasis en lo que se refiere a optimización, las herramientas de programación insisten en usar esquemas dinámicos de memoria. Apartar usando un arreglo demasiada memoria ya no es una práctica adecuada. Ahora lo que se hace es que el sistema operatiuvo otorga memoria a los procesos que la están pidiendo. Da bloques de memoria, no toda, a alguna aplicación. Si ésta requiere más, buscará darle más. De esta manera se optimizan ahora las cosas, como previniendo de antemano, por software, de hacer las cosas bien, de usar de la mejor manera losa recursos.

Como sea, creo que no estaría de más que se pensara de pronto en optimización. Aunque sobre memoria, ¿por qué no hacer las cosas un poco mejor dentro de la computadora?