¿Por qué C nunca implementó la "extensión de pila" para permitir que las variables de pila (de tamaño dinámico) de una función de llamada sean referenciadas por la persona que llama?

Esto podría funcionar extendiendo el marco de la pila de la persona que llama para incluir las variables "devueltas dinámicamente" del marco de la pila de la persona que llama. (Podría, pero no debería, implementar esto con alloca de la persona que llama; puede que no sobreviva a la optimización).

P.ej. Si quisiera devolver la cadena de tamaño dinámico "e", la implementación podría ser:

--+---+-----+
  | a |  b  |
--+---+-----+

callee(d);

--+---+-----+---------+---+
  | a |  b  |  junk   | d |    
--+---+-----+---------+---+

char e[calculated_size];

--+---+-----+---------+---+---------+
  | a |  b  |  junk   | d |    e    |    
--+---+-----+---------+---+---------+

dynamic_return e;

--+---+-----+-------------+---------+
  | a |  b  |    waste    |    e    |
--+---+-----+-------------+---------+

("Basura" contiene la dirección de retorno y otros metadatos específicos del sistema que son invisibles para el programa).

Esto desperdiciaría un poco de espacio en la pila cuando se usa

El lado positivo es una simplificación del procesamiento de cadenas, y cualquier otra función que tenga que malloc ram actualmente, devolver punteros y esperar que la persona que llama recuerde free en el momento correcto.

Obviamente, no tiene sentido agregar tal característica a C en esta etapa de su vida, solo me interesa saber por qué no fue una buena idea.

0
fadedbee 16 ene. 2018 a las 13:37

3 respuestas

La mejor respuesta

Un nuevo objeto puede ser devuelto a través de muchas capas de software. Entonces, el espacio perdido puede ser el de docenas o incluso cientos de llamadas a funciones.

Considere también una rutina que realiza alguna tarea iterativa. En cada iteración, obtiene un objeto recientemente asignado de una subrutina, que inserta en una lista vinculada u otra estructura de datos. Dichas tareas iterativas pueden repetirse durante cientos, miles o millones de iteraciones. La pila se desbordará con espacio desperdiciado.

3
Eric Postpischil 16 ene. 2018 a las 13:11

Algunas objeciones a tu idea. Algunos ya se han mencionado en los comentarios. Algunos vienen de lo alto de mi cabeza.

  • C no tiene pilas o marcos de pila. C simplemente define los ámbitos y sus tiempos de vida y se deja a las implementaciones cómo implementar el estándar. Las pilas y los marcos de pila son realmente la forma más popular de implementar algunas semánticas en C.

  • C no tiene cadenas. C realmente no tiene matrices como tales. Bueno, tiene matrices, pero tan pronto como mencionas una matriz en una expresión (por ejemplo, una expresión de retorno), la matriz se desintegra a un puntero a su primer elemento. Devolver una "cadena" o una matriz en la pila implicaría un impacto significativo en áreas bien establecidas del idioma.

  • C tiene structs. Sin embargo, ya puede devolver un struct. No puedo decirte cómo se hace, porque es un detalle de implementación.

  • Un problema con su implementación específica es que la persona que llama tiene que saber qué tan grande es el "desperdicio". No olvide que el desperdicio incluirá el marco de la pila de la persona que llama, pero también el desperdicio de cualquier función que la persona que llama llame directa o indirectamente. La convención de devolución tendrá que incluir información sobre el tamaño de los residuos y un puntero al valor de devolución.

  • Las pilas, como regla, son bastante limitadas en comparación con la memoria de almacenamiento dinámico, particularmente en aplicaciones que usan subprocesos. En algún momento, la persona que llama deberá mover la matriz devuelta a su propio marco de pila. Si la matriz fuera simplemente un puntero al almacenamiento en el montón, esto sería mucho más eficiente, pero entonces tiene el modelo existente.

2
JeremyP 16 ene. 2018 a las 11:24

Tiene que darse cuenta de que la implementación de la pila está fuertemente dictada por la CPU y el núcleo del sistema operativo. El lenguaje no tiene mucho que decir en esto. Las limitaciones son, por ejemplo:

  • La instrucción ret de la arquitectura X86 espera la dirección de retorno en la ubicación de memoria almacenada en el puntero de la pila . Por lo tanto, no puede haber nada más en la parte superior (parte superior semántica; por lo general, esta es la dirección más baja, ya que las pilas tienden a crecer abajo ). Podría solucionar esto, por supuesto, pero eso probablemente generaría gastos generales adicionales que los programadores de C no estarán dispuestos a pagar.

  • El puntero de la pila define qué parte de la memoria de pila asignada se usa realmente. Cuando el flujo de control se cambia de forma asíncrona (interrupción de hardware), el controlador de interrupciones generalmente almacena los registros actuales de la CPU inmediatamente en las direcciones de memoria debajo del puntero de la pila. Esto puede suceder en cualquier momento, incluso en la mayor parte del código del núcleo. Cualquier dato almacenado debajo del lugar al que apunta el puntero de la pila sería golpeado por esto. (Bueno, técnicamente, eso no es completamente correcto, generalmente hay una "zona roja" debajo del puntero de la pila a la que Es posible que los manejadores de interrupciones no escriban ningún dato, pero aquí nos estamos adentrando muy firmemente en las peculiaridades del diseño arquitectónico).

  • Destruir un marco de pila es generalmente una sola adición de una constante al puntero de pila. Este es el tipo de instrucción más rápido que puede obtener, generalmente no requerirá un solo ciclo para ejecutarse (se ejecutará en paralelo a algún acceso a la memoria). Si el marco de la pila tiene un tamaño dinámico, el marco de la pila debe destruirse cargando el puntero de la pila desde la memoria, y para eso debe haberse retenido un puntero base. Es un acceso a la memoria con una latencia significativa, y otro registro que debe guardarse para ser utilizado. Nuevamente, esto es una sobrecarga que generalmente no es necesaria.

Su propuesta definitivamente sería implementable, pero requeriría algunas soluciones. Y estas soluciones generalmente costarían el rendimiento. Pequeños bits de rendimiento, pero definitivamente cantidades medibles. Eso no es lo que quieren los desarrolladores del compilador / kernel, y por una buena razón.

2
cmaster - reinstate monica 16 ene. 2018 a las 14:35
48279268