tutorial smart remix programador online español curso recursion f# tail-recursion

recursion - remix - smart contracts ethereum



¿Por qué F#impone un límite bajo en el tamaño de la pila? (6)

En teoría, todo es posible. Podría escribir un compilador que use el montón para administrar lo que tradicionalmente es ''apilar''.

En la práctica, el rendimiento (especialmente para fundamentos como ''llamadas a funciones'') es importante. Tenemos medio siglo de hardware y sistema operativo adaptados / optimizados para el modelo de memoria de pila finita.

¿Este es el tipo de cosas que el compilador no debería hacerme preocupar en este día y edad?

Meh. La recolección de basura es una gran ganancia; administrar toda tu memoria es una tarea que en su mayor parte es innecesaria, y muchas aplicaciones pueden sacrificar algo de rendimiento para la productividad del programador aquí. Pero creo que pocas personas sienten que el manejo humano de la pila / recursión es un gran problema, incluso en el lenguaje funcional, por lo que el valor de dejar al programador descolgado aquí es, IMO, marginal.

Tenga en cuenta que en F # específicamente, puede usar un flujo de trabajo CPS que transformará un poco de pila en pila y / o llamadas finales, con un cambio relativamente menor en el estilo / sintaxis de programación, si desea ir allí (consulte, por ejemplo, aquí )

Me gustaría saber si existe una razón fundamental para limitar la profundidad de la recursión en F # a 10000 más o menos, e idealmente cómo evitar ese límite. Creo que es perfectamente razonable escribir código que use O (n) espacio de pila, y agradecería que alguien que no esté de acuerdo pueda explicar por qué lo haga. Muchas gracias. Explico mi pensamiento a continuación.

No veo que haya ninguna razón para no permitir que la pila crezca hasta que se agote toda la memoria disponible. Significaría que la recursión infinita tomaría más tiempo en notarse, pero no es como si ya no pudiéramos escribir programas que consumen una cantidad infinita de memoria. Soy consciente de que es posible reducir el uso de la pila a O (1) usando continuaciones y recursión final, pero no veo en particular cómo me es bueno tener que hacerlo todo el tiempo. Tampoco veo cómo ayuda tener que saber cuándo es probable que una función deba procesar una entrada "grande" (bueno, según los estándares de un microcontrolador de 8 bits).

Creo que esto es fundamentalmente diferente de tener que, por ejemplo, usar parámetros de acumulación para evitar el comportamiento de tiempo cuadrático. Si bien eso también implica preocuparse por los detalles de implementación, y no es necesario hacerlo para entradas "pequeñas", también es muy diferente en el sentido de que el compilador no puede eliminar trivialmente el problema por sí mismo. Además, es diferente en ese código O (n) ligeramente complicado que habría sido O (n ^ 2) si se escribe ingenuamente es mucho más útil que la versión simple, lenta y fácil de leer. Por el contrario, el código de estilo de continuación tiene exactamente la misma complejidad de memoria que la versión ingenua correspondiente, pero solo usa un tipo diferente de memoria. ¿Este es el tipo de cosas que el compilador no debería hacerme preocupar en este día y edad?

Si bien "preferiría" una razón teórica de por qué no es posible tener una pila profunda, también podríamos debatir aspectos prácticos. Me parece que una pila es una forma algo más eficiente de administrar la memoria que la pila, ya que no requiere recolección de basura y se libera fácilmente. No estoy seguro de poder ver que haya un costo para permitir apilamientos profundos. Es cierto que el sistema operativo debe reservar suficiente espacio virtual para contener toda la memoria que desee utilizar de una vez en todo el programa para la pila de cada hilo. Y qué. ¿No es probable que nos quedemos sin el límite actualmente común de 48 bits al hacer eso, o que los fabricantes de hardware no puedan aumentar ese límite a 64 bits de manera trivial?

No hay mucho específico para F # aquí. Espero que se aplique la misma restricción en C #, y no veo que sea más necesario allí, aunque obviamente es mucho menos doloroso cuando se programa con un estilo imperativo.

Muchas gracias por cualquier respuesta / comentario.

EDITAR: Escribí un resumen de las respuestas a continuación.


Puedo pensar al menos en dos posibles razones:

(1) En muchas arquitecturas de computadora, es difícil aumentar el tamaño disponible para la pila en tiempo de ejecución sin moverlo a otra parte en el espacio de direcciones. Como señaló @svick en los comentarios, .NET en Windows de 32 bits limita el tamaño de la pila del subproceso principal a 1 MB. Cambiar el tamaño de pila del subproceso principal en Windows requiere un cambio en el archivo .EXE.

(2) Es LEJOS, mucho más común que un desbordamiento de pila sea causado por un error de programación que por tener realmente un código que realmente necesita exceder el tamaño de pila disponible. Un tamaño de pila limitado es un centinela muy útil para detectar errores de programación. En el 98% de los casos, si se permitía que la pila creciera a la memoria disponible, la primera vez que se enteraría de sus errores de programación sería cuando agotara la memoria disponible.



Con mucho, la razón más convincente para que F # herede las limitaciones de .NET en este contexto es la compatibilidad. Los compiladores pueden y eliminan por completo la pila, por ejemplo, el compilador SML / NJ para ML estándar transforma los programas en estilos de paso de continuación automáticamente. Las dos desventajas principales son que requiere un cambio global en la convención de llamadas que rompa la compatibilidad y que sea sustancialmente menos eficiente. Si F # hizo esto para evadir los desbordamientos de pila, entonces la interoperabilidad de C # sería mucho más difícil y F # sería mucho más lento.

Otra razón por la cual las pilas profundas son una mala idea es el recolector de basura. Las pilas se tratan especialmente con GC porque se garantiza que son locales y pueden contraerse sin necesidad de recolección. El GC .NET atraviesa todas las pilas de hilos cada vez que un hilo incurre en una colección gen0. En consecuencia, tener solo dos hilos de dormir con pilas profundas puede hacer que otro hilo funcione 10 veces más lento. Imagina lo peor que sería con montones mucho más profundos. Esto se puede resolver cambiando la forma en que el GC trata las pilas, esencialmente convirtiéndolas en montones, pero eso hace que las operaciones de pila sean mucho más lentas.


En la mayoría de los casos, la pila no será un problema si escribes tus funciones para ser recursivo de cola


Creo que esto ha tenido tantas respuestas como sea posible. Aquí hay un resumen:

i) nadie sugirió ninguna razón fundamental para un límite de pila inferior a la cantidad total de memoria disponible

ii) la respuesta que aprendí más fue la de Brian (muchas gracias). Recomiendo encarecidamente la publicación del blog que él vinculó, y el resto de su blog también. Lo encontré más informativo que cualquiera de los dos buenos libros sobre F # que tengo. (Habiendo dicho eso, probablemente deberías echar un vistazo a lo que dice acerca de cuán sencillo es lograr la recursión final en la parte 6 de las publicaciones del blog sobre catamorfismos https://lorgonblog.wordpress.com/2008/06/02/ catamorphisms-part-six / antes de tomar la palabra "marginal" que había usado a su valor nominal :-)).

EDITAR: Jon Harrop fue un segundo muy cercano. Muchas gracias.

iii) Svick sugirió una manera fácil de aumentar el límite en tres órdenes de magnitud. Muchas gracias.

iv) Delnan sugirió que la mejor práctica es simplemente usar pliegues / mapas en todas partes, y definirlos de forma recursiva. Este es ciertamente un buen consejo para las listas, pero tal vez menos aplicable cuando se atraviesan gráficos. De cualquier manera, muchas gracias por la sugerencia.

v) Joel y Brian sugirieron algunas razones prácticas de por qué el límite es una buena idea. Todos eran detalles de bajo nivel que creo que deberían estar bien ocultos por un lenguaje de alto nivel. Muchas gracias.