Mostrando entradas con la etiqueta source code. Mostrar todas las entradas
Mostrando entradas con la etiqueta source code. Mostrar todas las entradas

martes, 31 de mayo de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 3

En este artículo consiste en un pequeño programa que permite comprobar algunas de las cosas que he comentado sobre el Event Dispatch Thread de Java Swing en artículos anteriores.

Haciendo pruebas con el Event Dispatch Thread


El pequeño programa EventDispatchThreadTest (ver código fuente completo aquí) ilustrar fácilmente algunos de los efectos que se producen al bloquear el EDT. Se compone de 2 ventanas (JFrame) llamadas One y Two:
  • En la ventana One hay un botón llamado Long-running task que ejecuta una tarea larga que bloquea el EDT.
  • En la ventana Two se deberían mostrar los efectos de la pulsación de dicho botón.
El botón Long-running task simplemente imprime por consola una línea cada cierto número de iteraciones del bucle. De este modo es una tarea medianamente intensiva en operaciones de entrada/salida y no sólo en consumo de procesador (no conviene hacer pruebas de bloqueo con un simple bucle vacío porque se consumiría el procesador sin facilitar la conmutación entre varios hilos de ejecución).

Al comienzo del oyente del botón se establece el texto del JLabel de la ventana Two a "Left button pressed. Task running...". Sin embargo, jamás se llega a ver ese texto porque hasta que no termina el oyente del botón, no se repinta y al final del evento, se ejecuta el método labelAutoText() para pintar el número de veces que se ha realizado al tarea en ese mismo JLabel.

Además, durante todo ese tiempo, el botón parece pulsado, aunque se haya soltado el botón del ratón y se esté haciendo otras cosas con él.

Otra cosa que se puede comprobar es que el resto de componentes de la GUI no responde mientras la tarea está en ejecución. Por ejemplo, no se puede pulsar el botón Nothing.

Sí se puede mover, maximizar/minimizar y cambiar el tamaño de ambas ventanas. Si se agranda cualquiera de ellas, se verá como el nuevo área queda sin pintar hasta que finaliza la tarea. En este caso, el tamaño, posición y estado (minimizado/maximizado) de las ventanas es controlado por el gestor de ventanas del sistema operativo, pero el repintado no se puede realizar hasta que termine el oyente del botón.

Otra de las cosas que permite comprobar el programa es que mientras la tarea se está ejecutando, no se reciben eventos (y por tanto no se podrán disparar otros oyentes). En especial, si se minimiza y se vuelve a maximizar una de las ventanas mientras se está ejecutando la tarea, los mensajes respectivos se verán todos juntos al final, pero no se recibirán en el momento en el que ocurren.

El programa es muy sencillo y se puede retocar muy fácilmente.

Enlaces


Los enlaces de interés relacionados con esta serie de artículos sobre el EDT están aquí, en el primero de los artículos sobre este tema.


(Actualizado 31/05/2016)

Source code: EventDispatchThreadTest

Código fuente del programa EventDispatchThreadTest

Ver la entrada correspondiente en el siguiente enlace:


Java Swing: Event Dispatch Thread (EDT) - Parte 3

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.util.Date;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class EventDispatchThreadTest {

    private JFrame myOne;
    private JFrame myTwo;
    private JButton myButtonTask;
    private JButton myButtonReset;
    private JButton myButtonNothing;
    private JLabel myLabel;
    private int myClickCount;
    private int myResetCount;

    public EventDispatchThreadTest() {
        super();

        JFrame one= new JFrame("One");
        one.setName( one.getTitle() );
        one.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        one.setSize(300, 400);
        one.setLocation(0, 0);
        JButton buttonTask= new JButton("Long-running task");
        JButton buttonReset= new JButton("Reset click count");
        JPanel onePanel= new JPanel();
        onePanel.setOpaque(true);
        onePanel.setBackground(Color.BLUE);
        onePanel.add(buttonTask);
        onePanel.add(buttonReset);
        one.setContentPane(onePanel);

        WindowListener auxWindowListener= EventDispatchThreadTest.createWindowListener();
        one.addWindowListener(auxWindowListener);

        WindowStateListener auxWindowStateListener= EventDispatchThreadTest.createWindowStateListener();
        one.addWindowStateListener(auxWindowStateListener);

        MouseListener auxMouseListener= EventDispatchThreadTest.createMouseListener();
        onePanel.addMouseListener(auxMouseListener);

        MouseMotionListener auxMouseMotionListener= EventDispatchThreadTest.createMouseMotionListener();
        onePanel.addMouseMotionListener(auxMouseMotionListener);

        JFrame two= new JFrame("Two");
        two.setName( two.getTitle() );
        two.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        two.setSize(300, 400);
        two.setLocation(400, 0);
        JLabel label= new JLabel("Never pressed");
        JButton buttonNothing= new JButton("Nothing");
        JPanel twoPanel= new JPanel();
        twoPanel.setOpaque(true);
        twoPanel.setBackground(Color.BLUE);
        twoPanel.add(label);
        twoPanel.add(buttonNothing);
        two.setContentPane(twoPanel);

        buttonTask.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //This text is never shown.
                EventDispatchThreadTest.this.myLabel.setText("Left button pressed. Task running...");

                for (int i= 0; i < 99999; i++) {
                    String auxString= String.format("Working... %s %s%n", i, new Date(System.currentTimeMillis()));
                    if ((i % 2) == 0) {
                        System.out.print(auxString);
                    }
                }

                EventDispatchThreadTest.this.myClickCount++;
                EventDispatchThreadTest.this.labelAutoText();
                System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
            }
        });

        buttonReset.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                EventDispatchThreadTest.this.myClickCount= 0;
                EventDispatchThreadTest.this.myResetCount++;

                EventDispatchThreadTest.this.labelAutoText();
            }
        });

        this.myOne= one;
        this.myTwo= two;
        this.myButtonTask= buttonTask;
        this.myButtonReset= buttonReset;
        this.myButtonNothing= buttonNothing;
        this.myLabel= label;

        this.myClickCount= 0;
        this.myResetCount= 0;
        this.myOne.setVisible(true);
        this.myTwo.setVisible(true);
    }

    private void labelAutoText() {
        this.myLabel.setText(String.format("Click count: %s. Reset count= %s"
            , this.myClickCount
            , this.myResetCount
        ));
    }

    private static WindowListener createWindowListener() {
        WindowListener auxWindowListener= new WindowListener() {
            @Override
            public void windowOpened(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowClosing(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowClosed(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowIconified(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowDeiconified(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowActivated(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowDeactivated(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxWindowListener;
    }

    private static WindowStateListener createWindowStateListener() {
        WindowStateListener auxWindowStateListener= new WindowStateListener() {
            @Override
            public void windowStateChanged(WindowEvent e) {
                System.out.format("Window[State]Event (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxWindowStateListener;
    }

    private static MouseListener createMouseListener() {
        MouseListener auxMouseListener= new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mousePressed(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxMouseListener;
    }

    private static MouseMotionListener createMouseMotionListener() {
        MouseMotionListener auxMouseMotionListener= new MouseMotionListener() {

            @Override
            public void mouseDragged(MouseEvent e) {
                System.out.format("MouseMotionEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                System.out.format("MouseMotionEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxMouseMotionListener;
    }

    public static void main(String[] args) {
        System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
        //GUI code should be executed in the EDT.
        SwingUtilities.invokeLater(new Runnable()  {
            @Override
            public void run() {
                System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
                new EventDispatchThreadTest();
            }
        });
    }

}

lunes, 29 de febrero de 2016

Subrutina recursiva en Fortran 77

Introducción


Cuando empecé los artículos previos sobre Recursividad Terminal, pensé en demostrarlos en Fortran 77 por ser el lenguaje por excelencia que no soporta recursividad. Sin embargo, encontré algunos problemas y opté por otros lenguajes. En este artículo he retomado ese reto. ¿Conseguí finalmente ejecutar una subrutina recursiva en Fortran 77?

Objetivo: implementar la función Factorial recursivamente en Fortran 77


Un ejemplo típico de función recursiva cuyo valor depende de invocarse a sí misma 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.

Utilicé dicha función como ejemplo por ser muy sencilla de implementar, muy ilustrativa y también porque yo no soy ningún experto en Fortran 77.

Problema 1: Fortran 77 no soporta recursividad


Al intentar crear un pequeño programa en Fortran 77 con una función (usaré el termino subrutina en general) recursiva me topé con el primer problema: no está permitida la recursividad ni directa (invocarse a sí mismo) ni indirecta (una subrutina no puede invocar a otra que ya esté en la pila de ejecución, es decir, que una misma subrutina sólo puede estar una única vez en memoria).

El compilador (supongo que el lenguaje en sí) que utilicé resuelve la imposibilidad de que una subrutina se invoque a sí misma o a otra que indirectamente pueda invocarla a ella permitiendo invocar únicamente subrutinas ya definidas (no se permiten declaraciones por adelantado como en otros lenguajes).

Así, al estar definiendo una subrutina, su cuerpo no puede tener invocaciones a sí misma porque aún no está completamente definida y obtendremos un error de subrutina (símbolo) desconocida. De igual modo, cada subrutina sólo puede invocar subrutinas que hayan sido previamente definidas, por lo que ninguna de ellas podrá contener invocaciones a ella.

Solución al problema 1


Siguiendo las indicaciones dadas por Andrew J. Miller en su página Example 1: Recursive Routines in FORTRAN 77 (and Fortran 90), utilicé las directivas EXTERNAL y CALL para conseguir que una subrutina se invoque a sí misma. Sólo sirve para subroutine, no sirve para function.

De este modo, la subrutina recursiva recibirá como argumento el nombre de una subrutina a la que invocar, que en este caso, será ella misma.

Problema 2: en Fortran 77 se pasan los argumentos por referencia


En Fortran 77 los argumentos siempre se pasan por referencia, por lo que hay que tener cuidado de no alterar el valor de aquellos argumentos que semánticamente sean de entrada, sobre todo si hicieran falta después de la invocación recursiva.

Solución al problema 2


En el caso de la función Factorial(N) es necesario realizar llamadas recursivas con el valor (N-1). Hay que tener en cuenta que si se altera el argumento con una asignación N= N-1 se está alterando el valor de la variable original, porque se pasa por referencia. Por lo tanto, hay que tener cuidado con el sitio donde se almacena el resultado de ese cálculo, ya que su ubicación será traspasada a la invocación recurisva.

Problema 3: subrutinas sin almacenar variables en la pila


En Fortran 77 el valor una una variable local se considera indefinido una vez que se sale de su ámbito. Sin embargo, es opcional, mediante la directiva SAVE, que se salvaguarde el valor de las variables locales de una subrutina entre una invocación y otra. En otras palabras, se solicita al compilador que almacene la variable local de la subrutina de forma estática.

Suena bastante chocante, pero después de todo, no es una idea tan disparatada dado que el lenguaje no permite que una misma subrutina esté dos veces en memoria, y por tanto, es irrelevante que sus variables locales estuviesen almacenadas estáticamente, ahorrando el coste computacional que conlleva mantener la pila de ejecución. Más discutible es que se programase usando esa característica como truco para reaprovechar el valor entre invocaciones.

Al parecer, ese comportamiento se hizo tan común que algunos compiladores antiguos (especialmente conocido el de DEC) se saltaban el estándar y siempre almacenaban las variables locales de forma estática sin que se pusiese explícitamente SAVE, y muchos programadores se acostumbraron a programar así.

El compilador que utilicé, el g77, tiene una opción para compilar en ese modo: -fno-automatic.

Al utilizar la directiva SAVE, la variable local V de la subrutina S ocupa siempre la misma posición de memoria M. En caso de que S se invocase recursivamente, se estaría sobreescribiendo constantemente la misma posición de memoria M, por lo que es muy probable que al acceder a V después de una invocación recursiva su valor difiera del valor que tenía antes de la invocación.

Solución al problema 3


Yo he forzado que aparezca este problema utilizando la directiva SAVE, demostrando que se puede eludir mediante una implementación basada en Recursividad Terminal, puesto que tras la invocación ya se tiene el resultado y no hay que realizar ninguna operación más (siendo por tanto irrelevante el valor de las variables locales en ese punto).

Demostración


Es muy importante tener en cuenta utilizar un compilador estricto para Fortran 77, ya que a partir de Fortran 90 ya no existen tantas restricciones (soporta recursión). Para las pruebas he utilizado:
Hoy día me costó encontrar compiladores de Fortran 77, así que realicé las pruebas sólo con ese compilador.

A continuación se puede ver el programa en Fortran 77 que escribí para este artículo, con una implementación recursiva de la función matemática Factorial. Se compone de la función factorial(num) que es un simple envoltorio (wrapper) para la subrutina recursiva factorial_inner(self, input, output), donde:
  • self es el nombre de la subrutina a invocar con CALL (ella misma en este caso).
  • input es el número cuyo Factorial se desea calcular (argumento de entrada).
  • output es la variable acumulador del resultado (argumento de entrada/salida).
Hay que prestar especial atención a las líneas 3, 10, 11 y 12.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
      subroutine factorial_inner(self, input, output)
          integer :: input, output, tmp
          save tmp
          external self

      
          if (input .GT. 0) then
c Con cualquier otro orden de estas 3 sentencias el programa
c fallará si se usa la directiva SAVE sobre la variable tmp.
              output= input * output
              tmp= input - 1
              call self(self, tmp, output)
          endif

          return
      end

      integer function factorial(num)
          integer :: num, output
          external factorial_inner

          output= 1
          call factorial_inner(factorial_inner, num, output)
          factorial= output

          return
      end


      program demo_factorial
      integer :: input, output
      integer :: factorial

      write (*,*) 'Input number:'
      read (*,*) input

      output= factorial(input)
      write (*,*) 'Factorial= ', output

      stop
      end

He puesto la variable tmp para evitar que el compilador cree variables temporales debidas a cálculos intermedios (expresiones) y para demostrar el funcionamiento de la directiva SAVE.

Si se elimina la directiva SAVE de la línea 3, el programa funcionará perfectamente aunque se reordenen las sentencias 10, 11 y 12 (con sentido claro), porque la variable tmp estará almacenada en la pila. Sin embargo, con dicha directiva activada, sólo fucionará correctamente en ese orden.

Obsérvese que, con la directiva SAVE activada, desde factorial_inner sólo se accede a la verdadera variable input (el argumento input de factorial_inner apunta al argumento num de factorial que a su vez apunta a la variable input del programa principal) en la primera invocación, de ahí en adelante se está trabajando con una referencia a la variable tmp (a la que se le ha asignado una posición fija en memoria), y por tanto, en el resto de invocaciones el argumento input y la variable local tmp apuntan a la misma posición de memoria. Si se intercambian las instrucciones 10 y 11, output estará mal calculado puesto que en la primera iteración input ya habrá sido decrementado. De hecho, ese intercambio de instrucciones provoca que el resultado siempre sea 0 porque al llegar a la invocación correspondiente al Factorial de 1 se realizaría la multiplicación output*input = output*0 = 0.

Conclusión


En respuesta a la pregunta que planteé al comienzo del artículo, sí ha sido posible implementar una subrutina recursiva en Fortran 77, con ciertas restricciones impuestas por el propio lenguaje, aunque lo más interesante ha sido aprender sobre lo útil que es la pila de ejecución y cómo la Recursividad Terminal puede ser de ayuda en ciertas condiciones.

Enlaces


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

(Actualizado 03/03/2016)

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;
}