En lenguajes de programación como C y C ++, la gente suele referirse a la asignación de memoria estática y dinámica. Entiendo el concepto, pero la frase "Toda la memoria fue asignada (reservada) durante el tiempo de compilación" siempre me confunde.
La compilación, según tengo entendido, convierte el código C / C ++ de alto nivel a lenguaje de máquina y genera un archivo ejecutable. ¿Cómo se "asigna" la memoria en un archivo compilado? ¿No se asigna siempre memoria en la RAM con todas las cosas de administración de memoria virtual?
¿No es la asignación de memoria, por definición, un concepto de tiempo de ejecución?
Si hago una variable asignada estáticamente de 1 KB en mi código C / C ++, ¿aumentará el tamaño del ejecutable en la misma cantidad?
Esta es una de las páginas donde se utiliza la frase bajo el título "Asignación estática".
Volver a lo básico: asignación de memoria, un recorrido por la historia
La memoria asignada en tiempo de compilación significa que el compilador resuelve en tiempo de compilación donde se asignarán ciertas cosas dentro del mapa de memoria del proceso.
Por ejemplo, considere una matriz global:
int array[100];
El compilador conoce en tiempo de compilación el tamaño de la matriz y el tamaño de an int
, por lo que conoce el tamaño completo de la matriz en tiempo de compilación. Además, una variable global tiene una duración de almacenamiento estática de forma predeterminada: se asigna en el área de memoria estática del espacio de memoria del proceso (sección .data / .bss). Dada esa información, el compilador decide durante la compilación en qué dirección de esa área de memoria estática estará la matriz .
Por supuesto que las direcciones de memoria son direcciones virtuales. El programa asume que tiene su propio espacio de memoria completo (de 0x00000000 a 0xFFFFFFFF por ejemplo). Es por eso que el compilador podría hacer suposiciones como "Está bien, la matriz estará en la dirección 0x00A33211". En tiempo de ejecución, la MMU y el sistema operativo traducen esas direcciones a direcciones reales / de hardware.
El valor del almacenamiento estático inicializado es un poco diferente. Por ejemplo:
int array[] = { 1 , 2 , 3 , 4 };
En nuestro primer ejemplo, el compilador solo decidió dónde se asignará la matriz, almacenando esa información en el ejecutable.
En el caso de cosas con valor inicializado, el compilador también inyecta el valor inicial de la matriz en el ejecutable y agrega código que le dice al cargador del programa que después de la asignación de la matriz al inicio del programa, la matriz debe llenarse con estos valores.
Aquí hay dos ejemplos del ensamblado generado por el compilador (GCC4.8.1 con destino x86):
Código C ++:
int a[4];
int b[] = { 1 , 2 , 3 , 4 };
int main()
{}
Montaje de salida:
a:
.zero 16
b:
.long 1
.long 2
.long 3
.long 4
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
Como puede ver, los valores se inyectan directamente en el ensamblaje. En la matriz a
, el compilador genera una inicialización cero de 16 bytes, porque el Estándar dice que las cosas almacenadas estáticas deben inicializarse a cero por defecto:
8.5.9 (Inicializadores) [Nota]:
Cada objeto de duración de almacenamiento estático se inicializa en cero al inicio del programa antes de que tenga lugar cualquier otra inicialización. En algunos casos, la inicialización adicional se realiza más tarde.
Siempre sugiero a la gente que desmonte su código para ver qué hace realmente el compilador con el código C ++. Esto se aplica desde clases de almacenamiento / duración (como esta pregunta) hasta optimizaciones avanzadas del compilador. Puede indicarle a su compilador que genere el ensamblado, pero hay herramientas maravillosas para hacerlo en Internet de una manera amigable. Mi favorito es GCC Explorer .
La memoria asignada en tiempo de compilación simplemente significa que no habrá más asignación en tiempo de ejecución - no hay llamadas a malloc
, new
u otros métodos de asignación dinámica. Tendrá una cantidad fija de uso de memoria incluso si no necesita toda esa memoria todo el tiempo.
¿No es la asignación de memoria, por definición, un concepto de tiempo de ejecución?
La memoria no está en uso antes del tiempo de ejecución, pero inmediatamente antes del inicio de la ejecución, su asignación es manejada por el sistema.
Si hago una variable asignada estáticamente de 1 KB en mi código C / C ++, ¿aumentará el tamaño del ejecutable en la misma cantidad?
Simplemente declarar la estática no aumentará el tamaño de su ejecutable más de unos pocos bytes. Declararlo con un valor inicial que no sea cero lo hará (para mantener ese valor inicial). Más bien, el vinculador simplemente agrega esta cantidad de 1 KB al requisito de memoria que el cargador del sistema crea para usted inmediatamente antes de la ejecución.
La memoria asignada en tiempo de compilación significa que cuando carga el programa, una parte de la memoria se asignará inmediatamente y el tamaño y la posición (relativa) de esta asignación se determina en el tiempo de compilación.
char a[32];
char b;
char c;
Esas 3 variables se "asignan en el momento de la compilación", lo que significa que el compilador calcula su tamaño (que es fijo) en el momento de la compilación. La variable a
será un desplazamiento en la memoria, digamos que apunta a la dirección 0, b
apuntará a la dirección 33 y c
a la 34 (suponiendo que no haya optimización de alineación). Por lo tanto, la asignación de 1 Kb de datos estáticos no aumentará el tamaño de su código , ya que solo cambiará un desplazamiento dentro de él. El espacio real se asignará en el momento de la carga .
La asignación de memoria real siempre ocurre en tiempo de ejecución, porque el kernel necesita realizar un seguimiento y actualizar sus estructuras de datos internas (cuánta memoria se asigna para cada proceso, páginas, etc.). La diferencia es que el compilador ya conoce el tamaño de cada dato que vas a usar y esto se asigna tan pronto como se ejecuta tu programa.
Recuerde también que estamos hablando de direcciones relativas . La dirección real donde se ubicará la variable será diferente. En el momento de la carga, el kernel reservará algo de memoria para el proceso, digamos en la dirección x
, y todas las direcciones codificadas de forma rígida contenidas en el archivo ejecutable se incrementarán en x
bytes, de modo que la variable a
en el ejemplo estará en la dirección x
, b en la dirección x+33
y pronto.
Agregar variables en la pila que ocupan N bytes no aumenta (necesariamente) el tamaño del contenedor en N bytes. De hecho, agregará solo unos pocos bytes la mayor parte del tiempo.
Vamos a empezar con un ejemplo de cómo la adición de unos 1000 caracteres a su código será aumentar el tamaño de la papelera de forma lineal.
Si el 1k es una cadena, de mil caracteres, que se declara así
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
y luego vim your_compiled_bin
, de hecho, podría ver esa cadena en el contenedor en alguna parte. En ese caso, sí: el ejecutable será 1 k más grande, porque contiene la cadena completa.
Sin embargo, si asigna una matriz de int
s, char
so long
s en la pila y la asigna en un bucle, algo en este sentido
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
entonces, no: no aumentará el bin ... por 1000*sizeof(int)
Asignación en tiempo de compilación significa lo que ahora ha llegado a entender que significa (según sus comentarios): el bin compilado contiene información que el sistema requiere para saber cuánta memoria qué función / bloque necesitará cuando se ejecute, junto con información sobre el tamaño de pila que requiere su aplicación. Eso es lo que el sistema asignará cuando ejecute su bin y su programa se convierta en un proceso (bueno, la ejecución de su bin es el proceso que ... bueno, entiendes lo que estoy diciendo).
Por supuesto, no estoy pintando la imagen completa aquí: el contenedor contiene información sobre el tamaño de la pila que realmente necesitará. Con base en esta información (entre otras cosas), el sistema reservará una parte de la memoria, llamada pila, sobre la que el programa tendrá una especie de dominio libre. El sistema aún asigna memoria de pila cuando se inicia el proceso (el resultado de la ejecución de su contenedor). El proceso luego administra la memoria de la pila por usted. Cuando se invoca / ejecuta una función o bucle (cualquier tipo de bloque), las variables locales de ese bloque se envían a la pila y se eliminan (la memoria de la pila se "libera", por así decirlo) para ser utilizadas por otros funciones / bloques. Por lo tanto, declarar int some_array[100]
solo agregará unos pocos bytes de información adicional al contenedor, lo que le dice al sistema que la función X requerirá 100*sizeof(int)
+ algo de espacio adicional de contabilidad.
En muchas plataformas, todas las asignaciones globales o estáticas dentro de cada módulo serán consolidadas por el compilador en tres o menos asignaciones consolidadas (una para datos no inicializados (a menudo llamados "bss"), otra para datos grabables inicializados (a menudo llamados "datos") ) y uno para datos constantes ("const")), y el enlazador consolidará todas las asignaciones globales o estáticas de cada tipo dentro de un programa en una global para cada tipo. Por ejemplo, asumiendo que int
son cuatro bytes, un módulo tiene las siguientes como únicas asignaciones estáticas:
int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;
le diría al enlazador que necesita 208 bytes para bss, 16 bytes para "datos" y 28 bytes para "const". Además, cualquier referencia a una variable se reemplazaría con un selector de área y un desplazamiento, por lo que a, b, c, dye se reemplazarían por bss + 0, const + 0, bss + 4, const + 24, data +0 o bss + 204, respectivamente.
Cuando un programa está vinculado, todas las áreas bss de todos los módulos se concatenan juntas; así mismo las áreas de datos y constantes. Para cada módulo, la dirección de cualquier variable relativa a bss se incrementará por el tamaño de las áreas bss de todos los módulos anteriores (nuevamente, de la misma manera con datos y const). Por lo tanto, cuando se realiza el enlazador, cualquier programa tendrá una asignación bss, una asignación de datos y una asignación constante.
Cuando se carga un programa, generalmente sucederá una de cuatro cosas dependiendo de la plataforma:
El ejecutable indicará cuántos bytes necesita para cada tipo de datos y, para el área de datos inicializada, dónde se pueden encontrar los contenidos iniciales. También incluirá una lista de todas las instrucciones que usan una dirección relativa bss, data o const. El sistema operativo o cargador asignará la cantidad apropiada de espacio para cada área y luego agregará la dirección inicial de esa área a cada instrucción que lo necesite.
El sistema operativo asignará una parte de la memoria para almacenar los tres tipos de datos y le dará a la aplicación un puntero a esa parte de la memoria. Cualquier código que utilice datos estáticos o globales lo eliminará en relación con ese puntero (en muchos casos, el puntero se almacenará en un registro durante la vida útil de una aplicación).
El sistema operativo inicialmente no asignará memoria a la aplicación, salvo la que contenga su código binario, pero lo primero que hará la aplicación será solicitar una asignación adecuada al sistema operativo, que mantendrá para siempre en un registro.
El sistema operativo inicialmente no asignará espacio para la aplicación, pero la aplicación solicitará una asignación adecuada al inicio (como se indicó anteriormente). La aplicación incluirá una lista de instrucciones con direcciones que deben actualizarse para reflejar dónde se asignó la memoria (como con el primer estilo), pero en lugar de que el cargador del sistema operativo parchee la aplicación, la aplicación incluirá código suficiente para parchearse a sí misma. .
Los cuatro enfoques tienen ventajas y desventajas. Sin embargo, en todos los casos, el compilador consolidará una cantidad arbitraria de variables estáticas en una pequeña cantidad fija de solicitudes de memoria, y el enlazador consolidará todas esas en una pequeña cantidad de asignaciones consolidadas. Aunque una aplicación tendrá que recibir una parte de la memoria del sistema operativo o del cargador, es el compilador y el enlazador los responsables de asignar piezas individuales de esa gran parte a todas las variables individuales que lo necesitan.
El núcleo de su pregunta es este: "¿Cómo se" asigna "la memoria en un archivo compilado? ¿No se asigna siempre la memoria en la RAM con todas las cosas de administración de memoria virtual? ¿No es la asignación de memoria por definición un concepto de tiempo de ejecución?"
Creo que el problema es que hay dos conceptos diferentes involucrados en la asignación de memoria. Básicamente, la asignación de memoria es el proceso mediante el cual decimos "este elemento de datos se almacena en esta porción específica de memoria". En un sistema informático moderno, esto implica un proceso de dos pasos:
El último proceso es puramente en tiempo de ejecución, pero el primero se puede realizar en tiempo de compilación, si los datos tienen un tamaño conocido y se requiere un número fijo de ellos. Básicamente, así es como funciona:
El compilador ve un archivo fuente que contiene una línea que se parece un poco a esto:
int c;
Produce una salida para el ensamblador que le indica que reserve memoria para la variable 'c'. Esto podría verse así:
global _c
section .bss
_c: resb 4
Cuando el ensamblador se ejecuta, mantiene un contador que rastrea las compensaciones de cada elemento desde el inicio de un 'segmento' de memoria (o 'sección'). Esto es como las partes de una 'estructura' muy grande que contiene todo en el archivo completo, no tiene ninguna memoria real asignada en este momento y podría estar en cualquier lugar. Anota en una tabla que _c
tiene un desplazamiento particular (digamos 510 bytes desde el inicio del segmento) y luego incrementa su contador en 4, por lo que la siguiente variable de este tipo estará en (por ejemplo) 514 bytes. Para cualquier código que necesite la dirección de _c
, simplemente coloca 510 en el archivo de salida y agrega una nota de que la salida necesita la dirección del segmento que contiene _c
agregar más tarde.
El enlazador toma todos los archivos de salida del ensamblador y los examina. Determina una dirección para cada segmento para que no se superpongan y agrega las compensaciones necesarias para que las instrucciones aún se refieran a los elementos de datos correctos. En el caso de una memoria no inicializada como la ocupada por c
(se le dijo al ensamblador que la memoria no se inicializaría por el hecho de que el compilador la colocó en el segmento '.bss', que es un nombre reservado para la memoria no inicializada), incluye un campo de encabezado en su salida que le dice al sistema operativo cuánto necesita reservarse. Se puede reubicar (y generalmente lo es) pero generalmente está diseñado para cargarse de manera más eficiente en una dirección de memoria en particular, y el sistema operativo intentará cargarlo en esta dirección. En este punto, tenemos una idea bastante clara de cuál es la dirección virtual que utilizará c
.
La dirección física no se determinará realmente hasta que el programa se esté ejecutando. Sin embargo, desde la perspectiva del programador, la dirección física es en realidad irrelevante; ni siquiera sabremos cuál es, porque el sistema operativo generalmente no se molesta en decírselo a nadie, puede cambiar con frecuencia (incluso mientras el programa se está ejecutando), y un El propósito principal del sistema operativo es abstraer esto de todos modos.
Un ejecutable describe qué espacio asignar para variables estáticas. Esta asignación la realiza el sistema cuando ejecuta el ejecutable. Entonces, su variable estática de 1kB no aumentará el tamaño del ejecutable con 1kB:
static char[1024];
A menos que, por supuesto, especifique un inicializador:
static char[1024] = { 1, 2, 3, 4, ... };
Entonces, además del 'lenguaje de máquina' (es decir, instrucciones de la CPU), un ejecutable contiene una descripción del diseño de memoria requerido.
La memoria se puede asignar de muchas formas:
Ahora su pregunta es qué es "memoria asignada en tiempo de compilación". Definitivamente es solo un dicho incorrectamente redactado, que se supone que se refiere a la asignación de segmento binario o la asignación de pila, o en algunos casos incluso a una asignación de montón, pero en ese caso la asignación está oculta a los ojos del programador por una llamada invisible del constructor. O probablemente la persona que dijo eso solo quería decir que la memoria no está asignada en el montón, pero no sabía acerca de las asignaciones de pila o segmento (o no quería entrar en ese tipo de detalles).
Pero en la mayoría de los casos, la persona solo quiere decir que la cantidad de memoria asignada se conoce en el momento de la compilación .
El tamaño binario solo cambiará cuando la memoria esté reservada en el código o segmento de datos de su aplicación.
Tienes razón. En realidad, la memoria se asigna (pagina) en el momento de la carga, es decir, cuando el archivo ejecutable se introduce en la memoria (virtual). La memoria también se puede inicializar en ese momento. El compilador simplemente crea un mapa de memoria. [Por cierto, los espacios de pila y montón también se asignan en el momento de la carga]
Creo que debes retroceder un poco. Memoria asignada en tiempo de compilación ... ¿Qué puede significar eso? ¿Puede significar que la memoria en chips que aún no se han fabricado, para computadoras que aún no se han diseñado, se está reservando de alguna manera? No. No, viajes en el tiempo, no hay compiladores que puedan manipular el universo.
Por tanto, debe significar que el compilador genera instrucciones para asignar esa memoria de alguna manera en tiempo de ejecución. Pero si lo mira desde el ángulo correcto, el compilador genera todas las instrucciones, entonces, ¿cuál puede ser la diferencia? La diferencia es que el compilador decide, y en tiempo de ejecución, su código no puede cambiar ni modificar sus decisiones. Si decidió que necesitaba 50 bytes en tiempo de compilación, en tiempo de ejecución, no puede decidir asignar 60; esa decisión ya se ha tomado.
Si aprende a programar en ensamblador, verá que tiene que crear segmentos para los datos, la pila y el código, etc. El segmento de datos es donde viven sus cadenas y números. El segmento de código es donde vive su código. Estos segmentos están integrados en el programa ejecutable. Por supuesto, el tamaño de la pila también es importante ... ¡no querría un desbordamiento de la pila !
Entonces, si su segmento de datos es de 500 bytes, su programa tiene un área de 500 bytes. Si cambia el segmento de datos a 1500 bytes, el tamaño del programa será 1000 bytes mayor. Los datos se ensamblan en el programa real.
Esto es lo que sucede cuando compila lenguajes de nivel superior. El área de datos real se asigna cuando se compila en un programa ejecutable, aumentando el tamaño del programa. El programa también puede solicitar memoria sobre la marcha, y esta es memoria dinámica. Puede solicitar memoria de la RAM y la CPU se la dará para que la use, puede soltarla y su recolector de basura la devolverá a la CPU. Incluso se puede cambiar a un disco duro, si es necesario, mediante un buen administrador de memoria. Estas características son las que le brindan los idiomas de alto nivel.
Me gustaría explicar estos conceptos con la ayuda de algunos diagramas.
Esto es cierto que la memoria no se puede asignar en tiempo de compilación, seguro. Pero, ¿qué sucede de hecho en el momento de la compilación?
Aquí viene la explicación. Digamos, por ejemplo, que un programa tiene cuatro variables x, y, zy k. Ahora, en el momento de la compilación, simplemente hace un mapa de memoria, donde se determina la ubicación de estas variables entre sí. Este diagrama lo ilustrará mejor.
Ahora imagine que no se está ejecutando ningún programa en la memoria. Esto lo muestro con un gran rectángulo vacío.
A continuación, se ejecuta la primera instancia de este programa. Puedes visualizarlo de la siguiente manera. Este es el momento en que se asigna realmente la memoria.
Cuando se está ejecutando la segunda instancia de este programa, la memoria se vería de la siguiente manera.
Y el tercero ...
Y así sucesivamente.
Espero que esta visualización explique bien este concepto.
Hay una explicación muy agradable en la respuesta aceptada. En caso de que publique el enlace que he encontrado útil. https://www.tenouk.com/ModuleW.html
La estrella de HGTV, Christina Hall, revela que le diagnosticaron envenenamiento por mercurio y plomo, probablemente debido a su trabajo como manipuladora de casas.
Recientemente salió a la luz un informe policial que acusa a la estrella de 'Love Is Blind', Brennon, de violencia doméstica. Ahora, Brennon ha respondido a los reclamos.
Conozca cómo Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia mientras organizaba la primera celebración de Acción de Gracias desde que murió su madre, Naomi Judd.
Descubra por qué un destacado experto en lenguaje corporal cree que es fácil trazar "tales paralelismos" entre la princesa Kate Middleton y la princesa Diana.
Los inodoros arrojan columnas de aerosol invisibles con cada descarga. ¿Como sabemos? La prueba fue capturada por láseres de alta potencia.
Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?
The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!
¿Sigue siendo efectivo ese lote de repelente de insectos que te quedó del verano pasado? Si es así, ¿por cuánto tiempo?
Las mujeres del Reino Unido acuden a las urnas para las elecciones especiales de hoy en un aniversario particularmente apropiado. Han pasado 104 años desde que Emily Wilding Davison murió por la causa del sufragio femenino, pisoteada hasta la muerte en medio de una carrera de caballos de alto perfil, frente al Rey y la Reina.
Tapas elásticas de silicona de Tomorrow's Kitchen, paquete de 12 | $14 | Amazonas | Código promocional 20OFFKINJALids son básicamente los calcetines de la cocina; siempre perdiéndose, dejando contenedores huérfanos que nunca podrán volver a cerrarse. Pero, ¿y si sus tapas pudieran estirarse y adaptarse a todos los recipientes, ollas, sartenes e incluso frutas en rodajas grandes que sobran? Nunca más tendrás que preocuparte por perder esa tapa tan específica.
Hemos pirateado algunas ciudades industriales en esta columna, como Los Ángeles y Las Vegas. Ahora es el momento de una ciudad militar-industrial-compleja.
Un minorista está enlatando su sección de tallas grandes. Pero no están tomando la categoría solo en línea o descontinuándola por completo.
El equipo está a la espera de las medallas que ganó en los Juegos Olímpicos de Invierno de 2022 en Beijing, ya que se está resolviendo un caso de dopaje que involucra a la patinadora artística rusa Kamila Valieva.
Miles de compradores de Amazon recomiendan la funda de almohada de seda Mulberry, y está a la venta en este momento. La funda de almohada de seda viene en varios colores y ayuda a mantener el cabello suave y la piel clara. Compre las fundas de almohada de seda mientras tienen hasta un 46 por ciento de descuento en Amazon
El jueves se presentó una denuncia de delito menor amenazante agravado contra Joe Mixon.
El Departamento de Policía de Lafayette comenzó a investigar a un profesor de la Universidad de Purdue en diciembre después de recibir varias denuncias de un "hombre sospechoso que se acercaba a una mujer".
Al igual que el mundo que nos rodea, el lenguaje siempre está cambiando. Mientras que en eras anteriores los cambios en el idioma ocurrían durante años o incluso décadas, ahora pueden ocurrir en cuestión de días o incluso horas.
Estoy de vuelta por primera vez en seis años. No puedo decirte cuánto tiempo he estado esperando esto.
“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.
Creo, un poco tarde en la vida, en dar oportunidades a la gente. Generosamente.