domingo, 31 de mayo de 2015

Recapitulando: año 2

Repasando los artículos del blog escritos a lo largo de este segundo año de existencia, puedes descubrir:


Si has leído alguno, muchas gracias y espero que te haya gustado o que te haya resultado útil.

Y recordar que los comentarios son bienvenidos.

Si quieres ver la recopilación del año anterior, sigue este enlace:



(Actualizado 31/05/2015)



jueves, 28 de mayo de 2015

Más sobre Recursividad Terminal (Tail Recursion)

Introducción


En el artículo anterior sobre Recursividad Terminal incluí la siguiente conclusión:
Algo muy interesante de la Recursividad Terminal es que no es necesario almacenar las llamadas recursivas en la pila de ejecución, lo que evita su desbordamiento. Además, en teoría, podría permitir realizar una implementación recursiva en un lenguaje que no tenga soporte para ello.
¿Te has preguntado si es posible utilizar Recursividad Terminal si la función requiere varias invocaciones recursivas?

A continuación intentaré demostrar empíricamente la citada conclusión a la vez que demostraré que sí es posible usar Recursividad Terminal aunque se necesiten varias invocaciones recursivas.

La sucesión de Fibonacci


La sucesión de Fibonacci es una sucesión matemática infinita de números naturales en la que cada valor se calcula como la suma de los dos valores anteriores. Unas fuentes la definen a partir de 0 y 1 mientras que otras lo hacen a partir de 1 y 2. Me voy a quedar con la que empieza en 0.

N0001020304050607080910
Valor011235813213455

Se define recursivamente con dos invocaciones a sí misma:

fibonacci(N)     si N es 0 entonces 0//Caso base 1.
si N es 1 entonces 1 //Caso base 2.
en otro caso, fibonacci(N-1) + fibonacci(N-2) //Doble llamada recursiva.

Es un caso muy conocido que servirá para ilustrar el uso de Recursividad Terminal con varias invocaciones recursivas.

Implementación recursiva natural


Empecemos por la implementación recursiva natural. En este caso he utilizado C++, más adelante explicaré por qué.

double fibonacci_rec(int n) {
    double f;
    
    if (n <= 0) {
        f= 0;
    }
    else if (n == 1) {
        f= 1;
    }
    else {
//        f= fibonacci_rec(n - 1) + fibonacci_rec(n - 2);
        double one= fibonacci_rec(n - 1);
        double two= fibonacci_rec(n - 2);
        f= one + two;
    }
    
    return f;
}

He utilizado un estilo de programación en el que he tratado de desglosar todas las operaciones de alto nivel, para que sea más fácil comparar las diferentes implementaciones.

He llamado one al resultado de la invocación con N-1 y two al resultado de la invocación con N-2.

He utilizado double simplemente porque la función crece muy rápido y usando un int el resultado se desborda fácilmente.

Implementación recursiva terminal


Recuérdese que la Recursividad Terminal se basa en la idea de calcular todo lo necesario antes de realizar las llamadas recursivas, pasando ese cálculo intermedio a la invocación mediante argumentos.

En este caso, en teoría se necesitarían 2 llamadas recursivas, por lo que hay que realizar 2 cálculos intermedios y pasar 2 argumentos extra.

double fibonacci_tail_inner(int n, double one, double two) {
    double f;

    if (n <= 0) {
        f= 0;
    }
    else if (n == 1) {
        f= one;
    }
    else {
//        f= fibonacci_tail_inner(n - 1, one + two, one);
        double tmp= one + two;
        two= one;
        one= tmp;
        n= n - 1;

        f= fibonacci_tail_inner(n, one, two);
    }

    return f;
}

double fibonacci_tail(int n) {
    return fibonacci_tail_inner(n, 1, 0);
}

En este caso, he llamado one al acumulador de la invocación N-1 y two al acumulador de la invocación N-2. Los cálculos que hay que realizar en cada llamada son:
  • el one que se pasa a la llamada recursiva es la suma del one y el two actuales.
  • el two que se pasa a la llamada recursiva es el one actual.
Sólo ha quedado una única llamada recursiva. Hay que tener en cuenta que esto no es un detalle nimio, ya que esta implementación es muchísimo más rápida por tener un coste lineal (N), mientras que la implementación recursiva natural tiene un coste exponencial (2N-1).

Implementación sin pila de ejecución: goto


Sí sí, has leído bien, he utilizado la operacion goto para demostrar que es posible hacer una implementación recursiva terminal sin ir apilando las invocaciones. Dicha característica, la posibilidad de usar variables globales y la disponibilidad de GCC son la razones de que haya utilizado C++ .

Discusiones "gotogoto no" aparte, la operación goto no deja de ser una herramienta que en este caso me ha venido genial para realizar una implementación recursiva sin llamadas a subrutinas.

static int g_n;
static double g_one;
static double g_two;
static double g_f;
#define pseudocall_fibonacci_goto_inner() goto l_start

double fibonacci_goto_inner() {
l_start:
    if (g_n <= 0) {
        g_f= 0;
    }
    else if (g_n == 1) {
        g_f= g_one;
    }
    else {
        double tmp= g_one + g_two;
        g_two= g_one;
        g_one= tmp;
        g_n= g_n - 1;

        pseudocall_fibonacci_goto_inner(); //goto l_start;
    }

    return g_f;
}

double fibonacci_goto(int n) {
    g_one= 1;
    g_two= 0;
    g_n= n;
    g_f= -1;

    return fibonacci_goto_inner();
}

Para evitar suspicacias, he utilizado variables globales (g_n, g_one, g_two y g_f), dejando solamente la variable local tmp necesaria para poder calcular los valores de g_one y g_two para la "siguiente llamada recursiva". Al ser variables globales, sólo existen una única vez en memoria: no se instancian en la pila de ejecución cuando se necesitan, sino que están en el bloque de memoria estática del programa.

Al comienzo de la función fibonacci_goto_inner está la etiqueta l_start, que es el punto al que se salta cada vez que se desea simular una llamada recursiva, es decir, la "llamada recursiva" consiste saltar al comienzo de la función. Por estética, se puede "ocultar" la sentencia goto con una macro.

No hay ninguna llamada a subrutina, por lo que no se apila nada en la pila de ejecución: las únicas posiciones de memoria ocupadas son las de las variables globales y la variable local tmp.

Se puede comprobar visualmente que esta solución es prácticamente idéntica a la versión anterior, salvo por el uso de variables globales y la simulación de la llamada recursiva.

Optimización


Gracias a Avelino Herrera Morales sé que el GCC soporta Tail Call Optimization utilizando la opción -foptimize-sibling-calls, incluida en las opciones -O2 y -O3 (aparentemente sola no funciona, necesita acompañarse de -O1). Aparte de este comentario, utilizando el comando objdump me enseñó que el código generado con dicho flag no genera ninguna instrucción de llamada a subrutina (call o similar).

Probablemente, la solución que aplique el compilador sea muy similar a la que yo he expuesto aquí (pero con variables locales claro), es decir, simplemente sustituir la llamada recursiva por un simple salto al comienzo de la función sin apilar ni desapilar nada en la pila de ejecución.

Pruebas


En este enlace puedes ver un pequeño programa que incluye todas las implementaciones de la función Fibonacci de este artículo y una versión interativa.


Variando la variable MAX de la función main se puede comprobar fácilmente que la implementación recursiva natural es lentísima, volviéndose desesperante a partir de 50.

Con valores grandes de MAX (en mi caso, un valor cercano a 33333) se puede provocar que se desborde la pila en la implementación recursiva terminal sin optimizar sin que se desborde la pila en la implementación recursiva terminal con goto. Si se activa la optimización, se puede comprobar que no se desborda en ninguno de esos dos casos. En teoría, la versión recursiva natural también se desbordaría, pero en la práctica es tan lenta que no se puede comprobar (hay que desactivar esa parte del código para hacer pruebas de fuerza bruta).

Conclusión


Tal y como dije en la introducción ha quedado demostrado:
  • que en un escenario donde no se dispone de la gestión de la pila de ejecución (al escribir en ensamblador a mano por ejemplo), es posible simular una implementación recursiva empleando un esquema de Recursividad Terminal.
  • que la Recursividad Terminal se puede utilizar con funciones que requieran varias llamadas recursivas.
Destaco que la implementación Recursiva Terminal, si está optimizada, evita los problemas de desbordamiento de pila (Stack Overflow) de la solución Recursiva Natural. De hecho, la solución implementada con goto demuestra que no se necesita ni la pila.

Enlaces


Enlaces de interés relacionados con este artículo:
(Actualizado 28/05/2015)

Source code: C++ Fibonacci Recursion Test

Código fuente del programa C++ Recursion Test

Ver la entrada correspondiente en el siguiente enlace:


Más sobre Recursividad Terminal (Tail Recursion)


#include <iostream>

using namespace std;

static int g_n;
static double g_one;
static double g_two;
static double g_f;
#define pseudocall_fibonacci_goto_inner() goto l_start

double fibonacci_goto_inner() {
l_start:
    if (g_n <= 0) {
        g_f= 0;
    }
    else if (g_n == 1) {
        g_f= g_one;
    }
    else {
        double tmp= g_one + g_two;
        g_two= g_one;
        g_one= tmp;
        g_n= g_n - 1;

        pseudocall_fibonacci_goto_inner(); //goto l_start;
    }

    return g_f;
}

double fibonacci_goto(int n) {
    g_one= 1;
    g_two= 0;
    g_n= n;
    g_f= -1;

    return fibonacci_goto_inner();
}
//------------------------------------------------------------------------------


double fibonacci_iter(int n) {
    double f;
    
    if (n <= 0) {
        f= 0;
    }
    else if (n == 1) {
        f= 1;
    }
    else {
        double two= 0;
        double one= 1;

        for (int i= 2; i <= n; i++) {
            f= one + two;
            
            two= one;
            one= f;
        }
    }
    
    return f;
}
//------------------------------------------------------------------------------


double fibonacci_rec(int n) {
    double f;
    
    if (n <= 0) {
        f= 0;
    }
    else if (n == 1) {
        f= 1;
    }
    else {
//        f= fibonacci_rec(n - 1) + fibonacci_rec(n - 2);
        double one= fibonacci_rec(n - 1);
        double two= fibonacci_rec(n - 2);
        f= one + two;
    }
    
    return f;
}
//------------------------------------------------------------------------------


double fibonacci_tail_inner(int n, double one, double two) {
    double f;

    if (n <= 0) {
        f= 0;
    }
    else if (n == 1) {
        f= one;
    }
    else {
//        f= fibonacci_tail_inner(n - 1, one + two, one);
        double tmp= one + two;
        two= one;
        one= tmp;
        n= n - 1;

        f= fibonacci_tail_inner(n, one, two);
    }

    return f;
}

double fibonacci_tail(int n) {
    return fibonacci_tail_inner(n, 1, 0);
}
//------------------------------------------------------------------------------


int main() {
    //Don't use MAX > 50 with natural recursion, it becomes extremely slowly.
    int MAX= 10;
    //Disable for big values of MAX.
    bool OUTPUT= true;
    double tmp;

    cout << "Iterativo" << endl;
    for (int i= 0; i < MAX; i++) {
        tmp= fibonacci_iter(i);
        if (OUTPUT == true) {
            cout<< "Fibonacci(" << i << ")= " << tmp << endl;
        }
    }
    cout << "Resultado(" << MAX-1 << ")= " << tmp << endl;
    cout << endl;

    cout << "GOTO" << endl;
    for (int i= 0; i < MAX; i++) {
        tmp= fibonacci_goto(i);
        if (OUTPUT == true) {
            cout<< "Fibonacci(" << i << ")= " << tmp << endl;
        }
    }
    cout << "Resultado(" << MAX-1 << ")= " << tmp << endl;
    cout << endl;
 
    cout << "Recursivo terminal" << endl;
    for (int i= 0; i < MAX; i++) {
        tmp= fibonacci_tail(i);
        if (OUTPUT == true) {
            cout<< "Fibonacci(" << i << ")= " << tmp << endl;
        }
    }
    cout << "Resultado(" << MAX-1 << ")= " << tmp << endl;
    cout << endl;

    cout << "Recursivo" << endl;
    for (int i= 0; i < MAX; i++) {
        tmp= fibonacci_rec(i);
        if (OUTPUT == true) {
            cout<< "Fibonacci(" << i << ")= " << tmp << endl;
        }
    }
    cout << "Resultado(" << MAX-1 << ")= " << tmp << endl;
    cout << endl;

    return 0;
}

miércoles, 29 de abril de 2015

Recursividad Terminal (Tail Recursion)

Introducción


Seguro que sabes lo que es la Recursividad: la definición de una función (o procedimiento) requiere de invocarse a sí misma con otros argumentos hasta alcanzar un caso base (situación en la que no es necesario invocarse a sí misma para obtener el resultado). El ejemplo típico es la función matemática factorial:

factorial(N)     si N es 0 entonces 1//Caso base.
en otro caso, N * factorial(N-1) //Llamada recursiva.

En una función recursiva, para poder calcular el valor de la función hay que esperar hasta haber calculado el valor de todas las llamadas recursivas (puede haber varias, como en la Sucesión de Fibonacci por ejemplo).

La programación recursiva tiene ventajas y desventajas respecto a la programación iterativa: suele simplificar mucho el algoritmo pero suele ser más lenta y sobre todo, es muy propensa al desbordamiento de la pila de ejecución (stack overflow).

Recursividad Terminal


En la Recursividad Terminal (Tail Recursion en inglés), aunque una función se invoque a sí misma, no necesita esperar al resultado de la invocación recursiva, sino que antes de invocarse a sí misma, realiza todos los cálculos necesarios, de modo que en la última llamada (la correspondiente a un caso base) ya se te tenga el resultado final.

La idea es pasar el resultado parcial a la llamada recursiva en vez de esperar a que sea ésta la que lo devuelva. En otras palabras, realizar los cálculos a medida que se desciende recursivamente en vez de hacerlo a medida que se asciende al deshacer la pila de llamadas.

Si el compilador/intérprete es suficientemente inteligente, detectará que no es necesario almacenar el frame (ventana, marco o tradúcelo como quieras) de cada invocación en la pila de ejecución, y que, al llegar al final, no es necesario deshacer la pila de llamadas. Esta optimización se conoce como Tail Call Optimization (TCO).

De ese modo, se ahorra memoria (evitando los problemas de desbordamiento de la pila de ejecución) y se aumenta muchísimo el rendimiento (se evitan todas las operaciones de manejo de la pila de ejecución), aunque el rendimiento podrá ser mejor o peor dependiendo del algoritmo en sí.

Ejemplo


El concepto es bastante extraño y complicado de explicar; es mucho mejor ver un ejemplo. Los ejemplos se han ejecutado con el intérprete GNU CLISP v2.49+ ejecutable online en el siguiente enlace:


A continuación se puede ver un programa en Common Lisp para invertir el orden de los elementos de una lista (reverse). El código fuente completo está disponible en este enlace:


a) Recursión Normal


Esta versión está escrita con recursión normal: en cada invocación, el resultado consiste en encolar el primer elemento al resultado de la invocación recursiva con el resto de elementos. El caso base es cuando se recibe una lista vacía, devolviendo una lista vacía también.

;Normal recursion
(defun my_reverse (a_list)
    (cond
        ((null a_list) nil)
        (t (append
                (my_reverse (rest a_list))
                (list (first a_list))
           ) 
        )
    )
)

(trace my_reverse)
(write (my_reverse '(1 2 3 4 5)))

A continuación se muestra la traza de ejecución:

;; Tracing function MY_REVERSE.                                                                                                                                                      
1. Trace: (MY_REVERSE '(1 2 3 4 5))                                                                                                                                                  
2. Trace: (MY_REVERSE '(2 3 4 5))                                                                                                                                                    
3. Trace: (MY_REVERSE '(3 4 5))                                                                                                                                                      
4. Trace: (MY_REVERSE '(4 5))                                                                                                                                                        
5. Trace: (MY_REVERSE '(5))                                                                                                                                                          
6. Trace: (MY_REVERSE 'NIL)                                                                                                                                                          
6. Trace: MY_REVERSE ==> NIL                                                                                                                                                         
5. Trace: MY_REVERSE ==> (5)                                                                                                                                                         
4. Trace: MY_REVERSE ==> (5 4)                                                                                                                                                       
3. Trace: MY_REVERSE ==> (5 4 3)                                                                                                                                                     
2. Trace: MY_REVERSE ==> (5 4 3 2)                                                                                                                                                   
1. Trace: MY_REVERSE ==> (5 4 3 2 1)                                                                                                                                                 
(5 4 3 2 1)  

Se puede comprobar que la lista en orden inverso se construye desde abajo hacia arriba.

b) Recursión Terminal


Ahora se muestra como quedaría el algoritmo utilizando recursividad terminal. La idea es apilar el primer elemento de in_list a out_list y pasar el resultado de eso como out_list a la invocación recursiva con el resto de in_list. Y en el caso base (in_list está vacía), ahora simplemente se devuelve el argumento out_list sin más.

Ha sido necesario cambiar la interfaz de la función para que reciba 2 argumentos en vez de uno, y algo muy importante, la primera invocación deber realizarse con el valor correcto para el argumento out_list: una lista vacía. Por eso se utiliza una función envoltorio (wrapper).

;Tail recursion
(defun my_reverse_tail_inner (in_list out_list)
    (cond
        ((null in_list) out_list)
        (t (my_reverse_tail_inner
                (rest in_list)
;Alternativa al append.
;                (cons (first in_list) out_list)
                (append
                    (list (first in_list))
                    out_list
                )
            )
        )
    )
)

(defun my_reverse_tail (a_list)
    (my_reverse_tail_inner a_list nil)
) 

(trace my_reverse_tail_inner)
(trace my_reverse_tail)
(write (my_reverse_tail '(1 2 3 4 5)))

A continuación se muestra la traza de ejecución:

;; Tracing function MY_REVERSE_TAIL_INNER.                                                                                                                                           
;; Tracing function MY_REVERSE_TAIL.                                                                                                                                                 
1. Trace: (MY_REVERSE_TAIL '(1 2 3 4 5))                                                                                                                                             
2. Trace: (MY_REVERSE_TAIL_INNER '(1 2 3 4 5) 'NIL)                                                                                                                                  
3. Trace: (MY_REVERSE_TAIL_INNER '(2 3 4 5) '(1))                                                                                                                                    
4. Trace: (MY_REVERSE_TAIL_INNER '(3 4 5) '(2 1))                                                                                                                                    
5. Trace: (MY_REVERSE_TAIL_INNER '(4 5) '(3 2 1))                                                                                                                                    
6. Trace: (MY_REVERSE_TAIL_INNER '(5) '(4 3 2 1))                                                                                                                                    
7. Trace: (MY_REVERSE_TAIL_INNER 'NIL '(5 4 3 2 1))                                                                                                                                  
7. Trace: MY_REVERSE_TAIL_INNER ==> (5 4 3 2 1)                                                                                                                                      
6. Trace: MY_REVERSE_TAIL_INNER ==> (5 4 3 2 1)                                                                                                                                      
5. Trace: MY_REVERSE_TAIL_INNER ==> (5 4 3 2 1)                                                                                                                                      
4. Trace: MY_REVERSE_TAIL_INNER ==> (5 4 3 2 1)                                                                                                                                      
3. Trace: MY_REVERSE_TAIL_INNER ==> (5 4 3 2 1)                                                                                                                                      
2. Trace: MY_REVERSE_TAIL_INNER ==> (5 4 3 2 1)                                                                                                                                      
1. Trace: MY_REVERSE_TAIL ==> (5 4 3 2 1)                                                                                                                                            
(5 4 3 2 1) 

Se puede ver que la lista en orden inverso se va construyendo desde arriba hacia abajo y que al llegar al caso base, ya está completamente construída, por lo que se deshace la pila devolviendo siempre lo mismo: el resultado final. En este caso, aparece una llamada más, la correspondiente a la función envoltorio (wrapper).

En este caso, también se podría simplificar el código utilizando la función cons para apilar un elemento al comienzo de una lista (en el caso de recursion normal hay que encolar usando la función append), simplificando mucho el código y reduciendo su coste.

Demostración de Tail Call Optimization


El intérprete indicado previamente permite aplicar Tail Call Optimization (TCO), pero para ello hay que precompilar la función utilizando compile.

Hasta donde mi conocimiento alcanza, es complicado desmostrar que se aplica TCO:
  • una vez compilada una función (optimizada o no), no se puede tracear (sólo aparecerá una invocación).
  • habría que acceder al código generado (¿es eso posible?) y además analizarlo para entenderlo...
Así que, he demostrado empíricamente que efectivamente se aplica TCO. La idea es averiguar cuán grande puede ser una lista sin que se desborde la pila al invertir el orden de sus elementos. Utilizando el intérprete indicado previamente, fui aumentando el tamaño de la lista (límite del loop) del siguiente programa:

(compile 'my_reverse)
(compile 'my_reverse_tail_inner)

;normal: se desborda la pila con menos de 35000 elementos.
(time (write (list-length (my_reverse (loop for n from 1 to 34000 by 1 collect n)))))
(write-line "")

;terminal con tantos elementos como la normal.
(time (write (list-length (my_reverse_tail (loop for n from 1 to 34000 by 1 collect n)))))
(write-line "")

;terminal: prueba de que se puede utilizar con una cantidad de elementos enorme.
(time (write (list-length (my_reverse_tail (loop for n from 1 to 3400012 by 1 collect n)))))
(write-line "")

La versión normal (my_reverse) se desborda con menos de 35000 elementos, mientras que con la versión terminal (my_reverse_tail) no conseguí desbordar la pila ni con varios millones de elementos.

Como datos orientativos, en una de las ejecuciones obtuve los siguientes resultados:

34000                                                                                                                                                                                
Real time: 38.68157 sec.                                                                                                                                                             
Run time: 38.663563 sec.                                                                                                                                                             
Space: 9248826160 Bytes                                                                                                                                                              
GC: 9157, GC time: 29.230534 sec.                                                                                                                                                    
34000                                                                                                                                                                                
Real time: 0.052549 sec.                                                                                                                                                             
Run time: 0.052638 sec.                                                                                                                                                              
Space: 1642160 Bytes                                                                                                                                                                 
GC: 1, GC time: 0.002901 sec.                                                                                                                                                        
3400012                                                                                                                                                                              
Real time: 6.269858 sec.                                                                                                                                                             
Run time: 6.099169 sec.                                                                                                                                                              
Space: 163210736 Bytes                                                                                                                                                               
GC: 21, GC time: 0.995367 sec.                                                                                                                                                       

Se puede observar una descomunal diferencia de velocidad, siendo la versión normal lentísima respecto a la terminal.

El consumo de memoria de la versión normal también es muy superior. Y se puede observar que el Garbage Collector apenas se dispara en la versión terminal.

Conclusión


Algo muy interesante de la Recursividad Terminal es que no es necesario almacenar las llamadas recursivas en la pila de ejecución, lo que evita su desbordamiento. Además, en teoría, podría permitir realizar una implementación recursiva en un lenguaje que no tenga soporte para ello.

Un punto negativo de la Recursividad Terminal está en que hay que cambiar la interfaz de la función para añadir el argumento "de salida o acumulador" y que además, hay que cerciorarse de invocarla con el valor inicial adecuado para dicho argumento; para esto último es recomendable crear una función envoltorio (wrapper) que se encargue de ello mostrando una interfaz equivalente a la versión no terminal.

Lo que a mí me parece más interesante, es que se trata una solución recursiva alternativa, que tal vez permita implementar algo recursivamente que de otro modo no se pueda. Por ejemplo, si en Common Lisp no se dispusiera de la función append (encolar), no se podría realizar la implementación normal, pero sí la terminal utilizando la función cons (apilar).

Enlaces


Enlaces de interés relacionados con este artículo:
(Actualizado 29/04/2015)

Source code: Recursividad Terminal (Tail Recursion)

Código fuente Lisp sobre Recursividad Terminal

Ver la entrada correspondiente en el siguiente enlace:


Recursividad Terminal (Tail Recursion)


;Normal recursion
(defun my_reverse (a_list)
    (cond
        ((null a_list) nil)
        (t (append
                (my_reverse (rest a_list))
                (list (first a_list))
           ) 
        )
    )
)

;(trace my_reverse)
(write (my_reverse '(1 2 3 4 5)))
(write-line "")


;Tail recursion
(defun my_reverse_tail_inner (in_list out_list)
    (cond
        ((null in_list) out_list)
        (t (my_reverse_tail_inner
                (rest in_list)
;Alternativa al append.
;                (cons (first in_list) out_list)
                (append
                    (list (first in_list))
                    out_list
                )
            )
        )
    )
)

(defun my_reverse_tail (a_list)
    (my_reverse_tail_inner a_list nil)
) 

;(compile 'my_reverse_tail_inner)
;(trace my_reverse_tail_inner)
;(trace my_reverse_tail)
(write (my_reverse_tail '(1 2 3 4 5)))
(write-line "")

(compile 'my_reverse)
(compile 'my_reverse_tail_inner)

;normal: se desborda la pila con menos de 35000 elementos.
(time (write (list-length (my_reverse (loop for n from 1 to 34000 by 1 collect n)))))
(write-line "")

;terminal con tantos elementos como la normal.
(time (write (list-length (my_reverse_tail (loop for n from 1 to 34000 by 1 collect n)))))
(write-line "")

;terminal: prueba de que se puede utilizar con una cantidad de elementos enorme.
(time (write (list-length (my_reverse_tail (loop for n from 1 to 3400012 by 1 collect n)))))
(write-line "")

martes, 31 de marzo de 2015

PostgreSQL: Interrumpir un lote de instrucciones

Complementando el artículo anterior, aquí se le puede encontrar utilidad al ejemplo tan extraño que utilicé en él (si piensas poner este artículo en práctica, no dejes de ver el anterior para que no te lleves sorpresas desagradables).

¿Y para qué voy yo a necesitar interrumpir un lote de instrucciones?


A veces hace falta lanzar de forma no interactiva un conjunto de sentencias SQL (lote) asumiendo que el conjunto se ejecutará correctamente. Para ello, se asume que en la base de datos se cumplen ciertas condiciones, que en el momento de lanzar el lote de instrucciones o durante su ejecución podrían no cumplirse.

Seguramente conozcas el concepto transacción en SQL. No siempre se puede hacer uso de esta característica, especialmente cuando el tamaño del lote produce grandes cambios en los datos (según la configuración de PostgreSQL, los ficheros WAL necesarios para la transacción podrían desbordarse).

¿Y si en medio del lote te interesa realizar alguna comprobación e interrumpir el proceso si no se dan las condiciones adecuadas?.

La solución propuesta


Cuando se produce un error, se puede detener el flujo de instrucciones (ver ON_ERROR_STOP en  psql). Por tanto, si se consigue generar un error de forma controlada, se podría utilizar para interrumpir el proceso.

Se podría crear un procedimiento almacenado que tras realizar las comprobaciones necesarias, lance una excepción cuando convenga. Sin embargo, eso requiere escribir código PL/SQL y tener privilegios para instalar procedimientos almacenados en la base de datos.

Pero hay una técnica mucho más simple y muchísimo menos intrusiva:

forzar una excepción al convertir de un tipo de dato a otro.

Por ejemplo, intentar convertir un texto no numérico en un número.

SELECT CASE WHEN (r_count = 0) THEN CAST('Nothing to process.'||r_count AS INTEGER)
            ELSE CAST(r_count AS INTEGER)
       END AS preprocessed_count
FROM (SELECT COUNT(*) AS r_count FROM preprocessed_data
     ) foo
;

Ese ejemplo permitiría interrumpir un lote si en la tabla preprocessed_data no hay nada, por ejemplo, porque acciones anteriores no han escrito en ella.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 31/03/2015)
 

sábado, 28 de febrero de 2015

PostgreSQL: Un optimizador demasiado inteligente

¿Has tenido alguna mala experiencia con algún optimizador?


Mi primera mala experiencia fue con algún compilador de C, uno de Borland creo recordar. Depurando con ejecución paso a paso, llegaba a un sitio en el que misteriosamente no se ejecutaba el código. Lo que pasaba era que por defecto el IDE estaba configurado para optimizar y el código generado no se podía depurar.

El planificador de PostgreSQL


En PostgreSQL el planificador (planner) es el módulo del sistema que interpreta el código SQL para su ejecución. Incluye optimizaciones en las que hace auténticas virguerías para que la ejecución sea eficiente.

Observa el siguiente código y trata de predecir cual es el resultado de la consulta:

CREATE TABLE test(id INTEGER, name TEXT, PRIMARY KEY (id) );

INSERT INTO test(id, name) VALUES
                (1 , 'one'  ),
                (2 , 'two'  ),
                (3 , 'three')
;

SELECT CASE WHEN (r_count < 0) THEN CAST('The concatenation avoids this whole CAST sentence being pre-evaluated.'||r_count AS INTEGER)
            ELSE CAST(r_count AS INTEGER)
       END AS demonstration
FROM (SELECT COUNT(*) AS r_count FROM test WHERE (id < 0)
     ) foo
;

No hay ningún id negativo, por lo que cabría esperar una ejecución correcta de la consulta con 0 resultados. Además, en general, la función COUNT nunca devolverá un valor negativo, por lo que es obvio que nunca se producirá el primer WHEN.

Pero no, la consulta genera un error en tiempo de compilación (considerando compilación el análisis del código previo a su ejecución) porque el planificador es "tan inteligente" que precalcula todo el código que considera constante respecto a la ejecución de la consulta. En este caso, evalua la sentencia:

CAST('This case never happens but the CAST throws an exception.' AS INTEGER)

antes de lanzar la consulta, generando un error porque la String no se puede convertir a Integer.

Desde mi ignorancia, no sé si está definido el orden correcto de evaluación o si eso es algo que se deja a disposición de la implementación. Lo que está claro, es que hay que saber estas cosas y ademas, que en este caso, no es lo más óptimo malgastar tiempo en precalcular cosas que nunca se deberían calcular dado que la consulta no va a devolver resultados.

(Nota: se ha comprobado dicho comportamiento en las versiones de PostgreSQL: 8.3.7, 8.4.8, 8.4.22, 9.1.14 y 9.2.8.)

Como evitar el problema


Para no tener ese problema, basta con hacer que el código potencialmente peligroso o costoso dependa de la consulta, es decir, que no sea precalculable. El ejemplo, podría quedar así:

SELECT CASE WHEN (r_count < 0) THEN CAST('The concatenation avoids this whole CAST sentence being pre-evaluated.'||r_count AS INTEGER)
            ELSE CAST(r_count AS INTEGER)
       END AS demonstration
FROM (SELECT COUNT(*) AS r_count FROM test WHERE (id < 0)
     ) foo
;

Destacar que raramente se puede escribir código que no dependa de la consulta que sea más costoso que la consulta en sí.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 28/02/2015)