Para que quede claro: lo sé malloc
y free
se implementan en la biblioteca C, que generalmente asigna fragmentos de memoria del sistema operativo y hace su propia administración para distribuir lotes más pequeños de memoria en la aplicación y realiza un seguimiento del número de bytes asignados . Esta pregunta no es ¿Cómo saber gratis cuánto liberar ?
Más bien, quiero saber por qué free
se hizo de esta manera en primer lugar. Al ser un lenguaje de bajo nivel, creo que sería perfectamente razonable pedirle a un programador de C que realice un seguimiento no solo de la memoria asignada, sino de la cantidad (de hecho, normalmente encuentro que termino realizando un seguimiento de la cantidad de bytes malloced de todos modos). También se me ocurre que dar explícitamente la cantidad de bytes free
podría permitir algunas optimizaciones de rendimiento, por ejemplo, un asignador que tiene grupos separados para diferentes tamaños de asignación podría determinar de qué grupo liberarse con solo mirar los argumentos de entrada, y en general, habría menos espacio sobre la cabeza.
Entonces, en resumen, ¿por qué se crearon malloc
y se free
crearon de tal manera que se requiere que realicen un seguimiento interno de la cantidad de bytes asignados? ¿Es solo un accidente histórico?
Una pequeña edición: algunas personas han proporcionado puntos como "¿qué pasa si liberas una cantidad diferente a la que asignaste?". Mi API imaginada podría simplemente requerir una para liberar exactamente la cantidad de bytes asignados; liberar más o menos podría ser simplemente UB o una implementación definida. Sin embargo, no quiero desalentar la discusión sobre otras posibilidades.
fuente
malloc
no conoce el tamaño del bloque devuelto .malloc
a menudo devuelve un bloque que es más grande de lo solicitado. En el mejor de los casos, el programador podría pasar el tamaño solicitado en lamalloc()
llamada, lo que no ayudaríafree()
en absoluto al implementador de .Respuestas:
Un argumento
free(void *)
(introducido en Unix V7) tiene otra ventaja importante sobre los dos argumentos anterioresmfree(void *, size_t)
que no he visto mencionados aquí: un argumentofree
simplifica drásticamente todas las demás API que funcionan con memoria dinámica. Por ejemplo, si sefree
necesita el tamaño del bloque de memoria, entonces destrdup
alguna manera tendría que devolver dos valores (puntero + tamaño) en lugar de uno (puntero), y C hace que los retornos de valores múltiples sean mucho más engorrosos que los retornos de un solo valor. En lugar dechar *strdup(char *)
tener que escribirchar *strdup(char *, size_t *)
o de lo contrariostruct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
. (Hoy en día, esa segunda opción parece bastante tentadora, porque sabemos que las cadenas terminadas en NUL son el "error de diseño más catastrófico en la historia de la informática", pero eso es en retrospectiva. En los años 70, la capacidad de C para manejar cadenas como algo simple enchar *
realidad se consideraba una ventaja definitoria sobre competidores como Pascal y Algol ). Además, no es solostrdup
que sufre este problema, afecta a todos los sistemas o definidos por el usuario. función que asigna memoria de pila.Los primeros diseñadores de Unix eran personas muy inteligentes, y hay muchas razones por las que
free
es mejor quemfree
eso, básicamente, creo que la respuesta a la pregunta es que se dieron cuenta de esto y diseñaron su sistema en consecuencia. Dudo que encuentres algún registro directo de lo que estaba pasando dentro de sus cabezas en el momento en que tomaron esa decisión. Pero podemos imaginar.Imagina que estás escribiendo aplicaciones en C para que se ejecuten en V6 Unix, con sus dos argumentos
mfree
. Lo ha hecho bien hasta ahora, pero realizar un seguimiento de estos tamaños de puntero se está volviendo cada vez más complicado a medida que sus programas se vuelven más ambiciosos y requieren cada vez más el uso de variables asignadas al montón. Pero entonces tienes una idea brillante: en lugar de copiar estossize_t
correos electrónicos todo el tiempo, puedes escribir algunas funciones de utilidad, que guardan el tamaño directamente dentro de la memoria asignada:void *my_alloc(size_t size) { void *block = malloc(sizeof(size) + size); *(size_t *)block = size; return (void *) ((size_t *)block + 1); } void my_free(void *block) { block = (size_t *)block - 1; mfree(block, *(size_t *)block); }
Y cuanto más código escriba con estas nuevas funciones, más impresionantes parecerán. No solo hacen que su código sea más fácil de escribir, sino que también hacen que su código sea más rápido , ¡dos cosas que no suelen ir juntas! Antes, pasaba estos
size_t
mensajes por todas partes, lo que agregaba sobrecarga de CPU para la copia, y significaba que tenía que derramar registros con más frecuencia (especialmente para los argumentos de función adicionales) y memoria desperdiciada (ya que las llamadas de funciones anidadas a menudo resultarán en múltiples copias delsize_t
almacenado en diferentes marcos de pila). En su nuevo sistema, todavía tiene que gastar la memoria para almacenar elsize_t
, pero solo una vez, y nunca se copia en ningún lado. Estas pueden parecer pequeñas eficiencias, pero tenga en cuenta que estamos hablando de máquinas de gama alta con 256 KiB de RAM.¡Esto te hace feliz! Así que comparte su genial truco con los hombres barbudos que están trabajando en el próximo lanzamiento de Unix, pero no los hace felices, los entristece. Verá, estaban en el proceso de agregar un montón de nuevas funciones de utilidad como
strdup
, y se dan cuenta de que las personas que usan su truco genial no podrán usar sus nuevas funciones, porque todas sus nuevas funciones usan el engorroso puntero + tamaño API. Y eso también te entristece, porque te das cuenta de que tendrás que reescribirstrdup(char *)
tú mismo la buena función en cada programa que escribas, en lugar de poder usar la versión del sistema.¡Pero espera! ¡Estamos en 1977 y la compatibilidad con versiones anteriores no se inventará hasta dentro de 5 años! Y además, nadie en serio usa esta cosa oscura de "Unix" con su nombre subido de tono. La primera edición de K&R está en camino al editor ahora, pero eso no es un problema - dice en la primera página que "C no proporciona operaciones para tratar directamente con objetos compuestos como cadenas de caracteres ... no hay montón ... ". En este punto de la historia,
string.h
yamalloc
están las extensiones de proveedor (!). Entonces, sugiere Bearded Man # 1, podemos cambiarlos como queramos; ¿Por qué no simplemente declaramos que su difícil asignador es el asignador oficial ?Unos días más tarde, Bearded Man # 2 ve la nueva API y dice oye, espera, esto es mejor que antes, pero todavía está gastando una palabra entera por asignación almacenando el tamaño. Él ve esto como lo próximo a la blasfemia. Todos los demás lo miran como si estuviera loco, porque ¿qué más puedes hacer? Esa noche se queda hasta tarde e inventa un nuevo asignador que no almacena el tamaño en absoluto, sino que lo infiere sobre la marcha al realizar cambios de bits de magia negra en el valor del puntero, y lo intercambia mientras mantiene la nueva API en su lugar. La nueva API significa que nadie se da cuenta del cambio, pero sí notan que a la mañana siguiente el compilador usa un 10% menos de RAM.
Y ahora todos están felices: obtienes tu código más fácil de escribir y más rápido, Bearded Man # 1 puede escribir un simple y agradable
strdup
que la gente realmente usará, y Bearded Man # 2, confiado en que se ha ganado su sustento por un tiempo. - vuelve a jugar con quines . ¡Envíalo!O al menos, así pudo haber sucedido.
fuente
Porque no es necesario, y de todos modos no tendría mucho sentido .
Cuando asigna algo, quiere decirle al sistema cuántos bytes asignar (por razones obvias).
Sin embargo, cuando ya haya asignado su objeto, ahora se determina el tamaño de la región de memoria que recupera. Está implícito. Es un bloque de memoria contiguo. No puede desasignar parte de él (olvidemos
realloc()
, eso no es lo que está haciendo de todos modos), solo puede desasignar todo. Tampoco puede "desasignar X bytes": o libera el bloque de memoria del que obtuvomalloc()
o no lo hace.Y ahora, si desea liberarlo, puede decirle al sistema de administración de memoria: "Aquí está este puntero,
free()
el bloque al que apunta". - y el administrador de memoria sabrá cómo hacerlo, ya sea porque conoce implícitamente el tamaño o porque es posible que ni siquiera lo necesite.Por ejemplo, las implementaciones más típicas de
malloc()
mantener una lista vinculada de punteros a bloques de memoria libres y asignados. Si pasa un puntero afree()
, simplemente buscará ese puntero en la lista "asignada", desvinculará el nodo correspondiente y lo adjuntará a la lista "libre". Ni siquiera necesitaba el tamaño de la región. Solo necesitará esa información cuando potencialmente intente reutilizar el bloque en cuestión.fuente
Es posible que C no sea tan "abstracto" como C ++, pero aún así se pretende que sea una abstracción sobre el ensamblaje. Con ese fin, los detalles del nivel más bajo se eliminan de la ecuación. Esto evita que tenga que perder el tiempo con la alineación y el relleno, en su mayor parte, lo que haría que todos sus programas en C no fueran portátiles.
En resumen, este es el objetivo de escribir una abstracción .
fuente
malloc
N bytes y, en cambio, devuelve un puntero al principio de una página completa (debido a la alineación, el relleno u otras restricciones , no habría forma de que el usuario lo rastreara, obligándolos a hacerlo sería contraproducente.malloc
simplemente siempre puede devolver un puntero alineado sin almacenar el tamaño de la asignación.free
luego podría redondearse a la alineación adecuada para garantizar que todo se libere correctamente. No veo dónde está el problema.size
parámetro afree
abre otra fuente de errores.En realidad, en el antiguo asignador de memoria del núcleo de Unix,
mfree()
tomó unsize
argumento.malloc()
ymfree()
mantuvo dos matrices (una para la memoria central y otra para el intercambio) que contenían información sobre direcciones y tamaños de bloques libres.No había un asignador de espacio de usuario hasta Unix V6 (los programas solo lo usarían
sbrk()
). En Unix V6, iolib incluía un asignador conalloc(size)
y unafree()
llamada que no tomaba un argumento de tamaño. Cada bloque de memoria estaba precedido por su tamaño y un puntero al siguiente bloque. El puntero solo se usó en bloques libres, al recorrer la lista libre, y se reutilizó como memoria de bloque en bloques en uso.En Unix 32V y en Unix V7, este fue sustituido por un nuevo
malloc()
efree()
implementación, dondefree()
no hacía falta ser unsize
argumento. La implementación era una lista circular, cada fragmento estaba precedido por una palabra que contenía un puntero al siguiente fragmento y un bit "ocupado" (asignado). Por lo tanto,malloc()/free()
ni siquiera realizó un seguimiento de un tamaño explícito.fuente
Porque no es necesario. La información ya está disponible en la gestión interna realizada por malloc / free.
Aquí hay dos consideraciones (que pueden haber contribuido o no a esta decisión):
¿Por qué esperaría que una función reciba un parámetro que no necesita?
(esto complicaría prácticamente todo el código de cliente que depende de la memoria dinámica y agregaría una redundancia completamente innecesaria a su aplicación). Hacer un seguimiento de la asignación de punteros ya es un problema difícil. Hacer un seguimiento de las asignaciones de memoria junto con los tamaños asociados aumentaría innecesariamente la complejidad del código del cliente.
¿Qué haría la
free
función alterada en estos casos?void * p = malloc(20); free(p, 25); // (1) wrong size provided by client code free(NULL, 10); // (2) generic argument mismatch
¿ No sería gratis (causaría una pérdida de memoria?)? ¿Ignora el segundo parámetro? ¿Detener la aplicación llamando a exit? Implementar esto agregaría puntos de falla adicionales en su aplicación, para una característica que probablemente no necesite (y si la necesita, vea mi último punto, a continuación: "implementación de la solución a nivel de aplicación").
Porque esta es la forma "correcta" de hacerlo. Una API debe requerir los argumentos que necesita para realizar su operación, y nada más .
Las formas adecuadas de implementar eso son:
(a nivel del sistema) dentro de la implementación de malloc: no hay nada que impida que el implementador de la biblioteca escriba malloc para usar varias estrategias internamente, según el tamaño recibido.
(a nivel de aplicación) envolviendo malloc y free dentro de sus propias API, y usándolas en su lugar (en todas partes de su aplicación que pueda necesitar).
fuente
Me vienen a la mente cinco razones:
Es conveniente. Elimina una carga completa de sobrecarga del programador y evita una clase de errores extremadamente difíciles de rastrear.
Abre la posibilidad de soltar parte de un bloque. Pero dado que los administradores de memoria generalmente quieren tener información de seguimiento, no está claro qué significaría esto.
Lightness Races In Orbit es acertado sobre el relleno y la alineación. La naturaleza de la gestión de la memoria significa que el tamaño real asignado es muy posiblemente diferente del tamaño que solicitó. Esto significa que si se
free
requiere un tamaño y una ubicación,malloc
se tendría que cambiar para devolver también el tamaño real asignado.De todos modos, no está claro que haya algún beneficio real de pasar el tamaño. Un administrador de memoria típico tiene de 4 a 16 bytes de encabezado para cada porción de memoria, que incluye el tamaño. Este encabezado de fragmento puede ser común para la memoria asignada y no asignada y, cuando los fragmentos adyacentes se liberan, se pueden contraer juntos. Si está haciendo que la persona que llama almacene la memoria libre, puede liberar probablemente 4 bytes por fragmento al no tener un campo de tamaño separado en la memoria asignada, pero ese campo de tamaño probablemente no se gane de todos modos, ya que la persona que llama necesita almacenarlo en algún lugar. Pero ahora esa información está dispersa en la memoria en lugar de estar ubicada de manera predecible en el fragmento de encabezado, que probablemente sea menos eficiente desde el punto de vista operativo de todos modos.
Incluso si fuera más eficiente, es radicalmente poco probable que su programa esté gastando una gran cantidad de tiempo liberando memoria de todos modos, por lo que el beneficio sería mínimo.
Por cierto, su idea sobre asignadores separados para artículos de diferentes tamaños se implementa fácilmente sin esta información (puede usar la dirección para determinar dónde ocurrió la asignación). Esto se hace habitualmente en C ++.
Agregado más tarde
Otra respuesta, bastante ridícula, ha presentado std :: allocator como prueba de que
free
podría funcionar de esta manera pero, de hecho, sirve como un buen ejemplo de por quéfree
no funciona de esta manera. Hay dos diferencias clave entre quémalloc
/free
hacer y qué hace std :: allocator. En primer lugar,malloc
yfree
se enfrenta el usuario - que están diseñados para los programadores generales para trabajar con - mientras questd::allocator
está diseñado para proporcionar la asignación de memoria especialista para la biblioteca estándar. Esto proporciona un buen ejemplo de cuándo el primero de mis puntos no importa o no importa. Dado que es una biblioteca, las dificultades de manejar las complejidades del tamaño de seguimiento están ocultas al usuario de todos modos.En segundo lugar, std :: allocator siempre funciona con el mismo tamaño de elemento, lo que significa que es posible utilizar el número de elementos pasados originalmente para determinar cuánto de libre. Por qué esto difiere de
free
sí mismo es ilustrativo. Enstd::allocator
los artículos a asignar son siempre del mismo, conocido, tamaño y siempre del mismo tipo de artículo por lo que siempre tienen el mismo tipo de requisitos de alineación. Esto significa que el asignador podría estar especializado para simplemente asignar una matriz de estos elementos al principio y distribuirlos según sea necesario. No podría hacer esto confree
porque no hay forma de garantizar que el mejor tamaño para devolver sea el tamaño solicitado, en cambio, es mucho más eficiente devolver bloques más grandes de lo que pide la persona que llama * y, por lo tanto , tampocoel usuario o el administrador debe realizar un seguimiento del tamaño exacto realmente otorgado. Pasar este tipo de detalles de implementación al usuario es un dolor de cabeza innecesario que no beneficia a la persona que llama.- * Si alguien todavía tiene dificultades para comprender este punto, considere lo siguiente: un asignador de memoria típico agrega una pequeña cantidad de información de seguimiento al inicio de un bloque de memoria y luego devuelve un desplazamiento de puntero a partir de esto. La información almacenada aquí generalmente incluye punteros al siguiente bloque libre, por ejemplo. Supongamos que el encabezado tiene solo 4 bytes de longitud (que en realidad es más pequeño que la mayoría de las bibliotecas reales) y no incluye el tamaño, luego imaginemos que tenemos un bloque libre de 20 bytes cuando el usuario solicita un bloque de 16 bytes, un ingenuo El sistema devolvería el bloque de 16 bytes, pero luego dejaría un fragmento de 4 bytes que nunca podría usarse perdiendo el tiempo cada vez.
malloc
se llama. Si, en cambio, el administrador simplemente devuelve el bloque de 20 bytes, entonces evita que estos fragmentos desordenados se acumulen y puede asignar de manera más limpia la memoria disponible. Pero si el sistema debe hacer esto correctamente sin rastrear el tamaño en sí, entonces requerimos que el usuario rastree, para cada asignación única, la cantidad de memoria realmente asignada para devolverla gratis. El mismo argumento se aplica al relleno para tipos / asignaciones que no coinciden con los límites deseados. De este modo, a lo sumo, lo que requierefree
tomar un tamaño puede ser (a) poco útil ya que el asignador de memoria no puede basarse en el tamaño pasado para que coincida con el tamaño realmente asignado o (b) requiere que el usuario sin sentido para hacer el trabajo de seguimiento de la verdadera tamaño que sería fácilmente manejado por cualquier administrador de memoria sensato.fuente
free
llamada va a pasar correctamente el tamaño para ser liberada, debe saber esto.Solo publico esto como una respuesta, no porque sea la que estás esperando, sino porque creo que es la única plausiblemente correcta:
Probablemente se consideró conveniente originalmente y no se pudo mejorar a partir de entonces.
Probablemente no haya una razón convincente para ello. (Pero felizmente eliminaré esto si se muestra que es incorrecto).
No sería haber beneficios si era posible: se podría asignar un solo pedazo grande de memoria cuyo tamaño se sabía de antemano, a continuación, liberar un poco a la vez - en lugar de asignar y liberar pequeños trozos de la memoria en varias ocasiones. Actualmente, tareas como esta no son posibles.
Para los muchos (¡muchos 1 !) De ustedes que piensan que pasar el tamaño es tan ridículo:
¿Puedo remitirlo a la decisión de diseño de C ++ para el
std::allocator<T>::deallocate
método?void deallocate(pointer p, size_type n);
Creo que pasará un tiempo bastante "interesante" analizando esta decisión de diseño.
En cuanto a
operator delete
, resulta que la propuesta N3778 de 2013 (" Desasignación de tamaño de C ++") también tiene la intención de arreglar eso.1 Solo mire los comentarios debajo de la pregunta original para ver cuántas personas hicieron afirmaciones apresuradas como "el tamaño solicitado es completamente inútil para la
free
llamada" para justificar la falta delsize
parámetro.fuente
malloc()
implementación, también eliminaría la necesidad de recordar cualquier cosa sobre una región asignada, reduciendo la sobrecarga de asignación a la sobrecarga de alineación.malloc()
podría hacer toda la contabilidad dentro de los propios trozos liberados. Hay casos de uso en los que esto sería una gran mejora. Sin embargo, debería desalentarse la desasignación de una gran parte de la memoria en varios fragmentos pequeños, lo que aumentaría drásticamente la fragmentación.std::allocator
asigna solo elementos de un tamaño específico y conocido. No es un asignador de propósito general, la comparación es manzanas con naranjas.malloc y free van de la mano, con cada "malloc" emparejado con uno "free". Por lo tanto, tiene mucho sentido que la coincidencia "gratuita" con un "malloc" anterior debería simplemente liberar la cantidad de memoria asignada por ese malloc; este es el caso de uso mayoritario que tendría sentido en el 99% de los casos. Imagine todos los errores de memoria si todos los usos de malloc / free por parte de todos los programadores del mundo alguna vez, necesitaran que el programador hiciera un seguimiento de la cantidad asignada en malloc y luego recuerde liberar el mismo. El escenario del que habla realmente debería usar múltiples mallocs / free en algún tipo de implementación de administración de memoria.
fuente
gets
,printf
bucles manuales (uno por uno), comportamientos indefinidos, cadenas de formato, conversiones implícitas, trucos de bits, etc. cetera.Sugeriría que se debe a que es muy conveniente no tener que rastrear manualmente la información de tamaño de esta manera (en algunos casos) y también es menos propenso a errores del programador.
Además, realloc necesitaría esta información contable, que espero que contenga más que solo el tamaño de la asignación. es decir, permite definir la implementación del mecanismo por el que trabaja.
Sin embargo, podría escribir su propio asignador que funcionó de alguna manera de la manera que sugiere y, a menudo, se hace en c ++ para asignadores de grupo de una manera similar para casos específicos (con ganancias de rendimiento potencialmente masivas), aunque esto generalmente se implementa en términos de operador nuevo para la asignación de bloques de piscina.
fuente
No veo cómo funcionaría un asignador que no rastrea el tamaño de sus asignaciones. Si no hiciera esto, ¿cómo sabría qué memoria está disponible para satisfacer una
malloc
solicitud futura ? Tiene que almacenar al menos algún tipo de estructura de datos que contenga direcciones y longitudes, para indicar dónde están los bloques de memoria disponibles. (Y, por supuesto, almacenar una lista de espacios libres equivale a almacenar una lista de espacios asignados).fuente
Bueno, lo único que necesita es un puntero que usará para liberar la memoria que asignó anteriormente. La cantidad de bytes es algo que administra el sistema operativo, por lo que no tiene que preocuparse por ello. No sería necesario obtener la cantidad de bytes asignados devueltos por free (). Le sugiero una forma manual de contar el número de bytes / posiciones asignados por un programa en ejecución:
Si trabaja en Linux y desea saber la cantidad de bytes / posiciones que malloc ha asignado, puede crear un programa simple que use malloc una o n veces e imprima los punteros que obtiene. Además, debes hacer que el programa duerma durante unos segundos (lo suficiente para que puedas hacer lo siguiente). Después de eso, ejecute ese programa, busque su PID, escriba cd / proc / process_PID y simplemente escriba "cat maps". La salida le mostrará, en una línea específica, las direcciones de memoria inicial y final de la región de memoria del montón (aquella en la que está asignando memoria dinámicamente) .Si imprime los punteros a estas regiones de memoria que se están asignando, puede adivinar cuánta memoria ha asignado.
¡Espero eso ayude!
fuente
¿Por qué debería hacerlo? malloc () y free () son primitivas de administración de memoria intencionalmente muy simples , y la administración de memoria de alto nivel en C depende en gran medida del desarrollador. T
Además, realloc () ya lo hace, si reduce la asignación en realloc () no moverá los datos y el puntero devuelto será el mismo que el original.
En general, es cierto que toda la biblioteca estándar está compuesta de primitivas simples a partir de las cuales puede construir funciones más complejas para satisfacer las necesidades de su aplicación. Entonces, la respuesta a cualquier pregunta del tipo "por qué la biblioteca estándar no hace X" es porque no puede hacer todo lo que un programador podría pensar (para eso están los programadores), por lo que elige hacer muy poco: construir la suya propia o utilizar bibliotecas de terceros. Si desea una biblioteca estándar más extensa, incluida una administración de memoria más flexible, C ++ puede ser la respuesta.
Etiquetó la pregunta C ++ y C, y si C ++ es lo que está usando, entonces difícilmente debería usar malloc / free en cualquier caso; aparte de new / delete, las clases de contenedor STL administran la memoria automáticamente y de una manera probable para ser específicamente apropiado a la naturaleza de los diversos contenedores.
fuente
realloc
está absolutamente permitido mover los datos. Según el estándar C, es totalmente legal implementarlorealloc
comomalloc
+memcpy
+free
. Y hay buenas razones por las que una implementación podría querer mover una asignación que se ha reducido en tamaño, por ejemplo, para evitar fragmentar la memoria.