sirve - punteros a cadenas
¿Por qué `free` en C no toma la cantidad de bytes que se liberarán? (12)
"¿Por qué el
free
in C no toma la cantidad de bytes que se liberarán?"
Porque no es necesario, y de todos modos no tendría sentido .
Cuando asigna algo, desea decirle al sistema cuántos bytes asignar (por razones obvias).
Sin embargo, cuando ya ha asignado su objeto, ahora se determina el tamaño de la región de memoria que obtiene. Está implícito. Es un bloque contiguo de memoria. No puede desasignar una parte (olvidemos realloc()
, eso no es lo que está haciendo de todos modos), solo puede desasignar todo. Tampoco puede "desasignar X bytes": libera el bloque de memoria que obtuvo de malloc()
o no lo hace.
Y ahora, si quiere liberarlo, puede decirle al sistema de administrador 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 quizás ni siquiera necesite el tamaño.
Por ejemplo, la mayoría de las implementaciones típicas de malloc()
mantienen una lista vinculada de punteros a los bloques de memoria asignados y libres. Si pasa un puntero a free()
, solo buscará ese puntero en la lista "asignado", des-enlazará el nodo correspondiente y lo adjuntará a la lista "free". Ni siquiera necesitaba el tamaño de la región. Solo necesitará esa información cuando potencialmente intente reutilizar el bloque en cuestión.
Para que quede claro: sí sé que malloc
y free
se implementan en la biblioteca C, que generalmente asigna trozos de memoria del sistema operativo y hace su propia gestión para parcelar lotes de memoria más pequeños a la aplicación y realiza un seguimiento del número de bytes asignados. Esta pregunta no es ¿Cómo sabe gratis cuánto liberar ?
Por el contrario, quiero saber por qué free
se hizo de esta manera, en primer lugar. Siendo un lenguaje de bajo nivel, creo que sería perfectamente razonable pedirle a un programador de C que no solo se mantenga al tanto de qué memoria fue asignada, sino de cuánto (de hecho, comúnmente encuentro que termino haciendo un seguimiento de la cantidad de bytes malloced de todos modos). También se me ocurre que dar explícitamente el número de bytes puede permitir algunas optimizaciones del rendimiento, por ejemplo, un asignador que tiene grupos separados para diferentes tamaños de asignación podría determinar qué grupo liberar solo con mirar los argumentos de entrada, y habría menos espacio sobrecarga general.
Entonces, en resumen, ¿por qué se creó malloc
y free
tal que se les exige realizar 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 implementación definida. No obstante, no quiero desalentar la discusión sobre otras posibilidades.
¿Por qué
free
in C no toma la cantidad de bytes que se liberarán?
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 o no haber contribuido 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 del cliente basándose en la memoria dinámica y agregaría una redundancia completamente innecesaria a su aplicación). Hacer un seguimiento de la asignación del puntero 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 función
free
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?)? Ignorar el segundo parámetro? Detener la aplicación llamando a la salida? Implementar esto agregaría puntos de falla adicionales en su aplicación, para una función que probablemente no necesite (y si la necesita, vea mi último punto, a continuación - "implementación de la solución a nivel de la aplicación").
Por el contrario, quiero saber por qué libre se hizo de esta manera, en primer lugar.
Porque esta es la forma "correcta" de hacerlo. Una API debería requerir los argumentos que necesita para realizar su operación, y no más que eso .
También se me ocurre que dar explícitamente el número de bytes puede permitir algunas optimizaciones del rendimiento, por ejemplo, un asignador que tiene grupos separados para diferentes tamaños de asignación podría determinar qué grupo liberar solo con mirar los argumentos de entrada, y habría menos espacio sobrecarga general.
Las formas adecuadas de implementar eso son:
(a nivel de 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 libre dentro de sus propias API, y usándolas en su lugar (en todas las aplicaciones que pueda necesitar).
C puede no ser tan "abstracto" como C ++, pero todavía pretende ser una abstracción sobre el ensamblado. Con ese fin, los detalles del nivel más bajo se toman de la ecuación. Esto evita que tengas que desplazarte con la alineación y el relleno, en su mayor parte, lo que haría que todos tus programas C no sean portátiles.
En resumen, este es todo el punto de escribir una abstracción .
Cinco razones vienen a la mente:
Es conveniente. Quita una carga completa de gastos generales del programador y evita una clase de errores extremadamente difíciles de rastrear.
Abre la posibilidad de liberar parte de un bloque. Pero dado que los administradores de memoria generalmente quieren tener información de seguimiento, ¿no está claro qué significaría?
Lightness Races In Orbit es perfecta para el relleno y la alineación. La naturaleza de la administración de la memoria significa que el tamaño real asignado es muy posiblemente diferente del tamaño que solicitó. Esto significa que era
free
de requerir un tamaño, así como una ubicaciónmalloc
tendría que cambiarse para devolver el tamaño real asignado también.No obstante, no está claro si hay algún beneficio real al aprobar el tamaño. Un administrador de memoria típico tiene 4-16 bytes de encabezado para cada fragmento 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, pueden colapsarse juntos. Si está haciendo que la persona que llama almacene la memoria libre, puede liberar probablemente 4 bytes por porción al no tener un campo de tamaño separado en la memoria asignada, pero ese campo de tamaño probablemente no se obtenga ya que la persona que llama necesita almacenarlo en alguna parte. Pero ahora esa información está dispersa en la memoria en lugar de estar ubicada de manera predecible en el fragmento de encabezado, que de todos modos es menos eficiente desde el punto de vista operativo.
Incluso si fuera más eficiente, es radicalmente improbable que su programa pase una gran cantidad de tiempo liberando la 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 se produjo la asignación). Esto se realiza rutinariamente en C ++.
Agregado más tarde
Otra respuesta, bastante ridícula, ha planteado std::allocator como prueba de que free
podría funcionar de esta manera, pero, de hecho, sirve como un buen ejemplo de por qué el free
no funciona de esta manera. Hay dos diferencias clave entre lo que hace malloc
/ free
y lo que hace std :: allocator. En primer lugar, malloc
y free
están orientados al usuario, están diseñados para que los programadores en general trabajen con ellos, mientras que std::allocator
está diseñado para proporcionar una asignación de memoria especializada a la biblioteca estándar. Esto proporciona un buen ejemplo de cuando el primero de mis puntos no importa o no importa. Como es una biblioteca, las dificultades para manejar las complejidades del tamaño de rastreo están ocultas para el usuario de todos modos.
En segundo lugar, std :: allocator siempre funciona con el mismo tamaño de elemento, lo que significa que es posible que use el número de elementos pasados originalmente para determinar la cantidad de free. Por qué esto difiere del free
sí mismo es ilustrativo. En std::allocator
los elementos a asignar son siempre del mismo tamaño, conocidos y siempre del mismo tipo, por lo que siempre tienen el mismo tipo de requisitos de alineación. Esto significa que el asignador podría estar especializado para asignar simplemente una matriz de estos elementos al inicio y repartirlos según sea necesario. No se puede hacer esto de forma free
porque no hay forma de garantizar que el mejor tamaño para devolver es el tamaño solicitado, en cambio es mucho más eficiente a veces devolver bloques más grandes de lo que la persona que llama pide * y, por lo tanto, el usuario o el gerente necesita rastrear el tamaño exacto 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 entender este punto, considere esto: 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 desde este. La información almacenada aquí generalmente incluye punteros al próximo 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, entonces imagina que tenemos un bloque libre de 20 bytes cuando el usuario solicita un bloque de 16 bytes, un ingenuo sistema devolvería el bloque de 16 bytes pero luego dejaría un fragmento de 4 bytes que nunca, nunca se podría usar, desperdiciando tiempo cada vez que se llama a malloc
. Si, en cambio, el administrador simplemente devuelve el bloque de 20 bytes, entonces guarda estos fragmentos desordenados y puede asignar más limpiamente la memoria disponible. Pero si el sistema debe hacer esto correctamente sin rastrear el tamaño en sí, entonces le pedimos al usuario que realice un seguimiento, para cada asignación individual, de la cantidad de memoria realmente asignada si es que se la devuelve gratis. El mismo argumento se aplica al relleno para tipos / asignaciones que no coinciden con los límites deseados. Por lo tanto, como máximo, requerir tomar un tamaño es (a) completamente inútil ya que el asignador de memoria no puede confiar en el tamaño aprobado para que coincida con el tamaño realmente asignado o (b) sin sentido requiere que el usuario haga un seguimiento del trabajo real tamaño que sería manejado fácilmente por cualquier administrador de memoria sensible.
En realidad, en el antiguo asignador de memoria del kernel de Unix, mfree()
tomó un argumento de size
. malloc()
y mfree()
mantuvieron dos matrices (una para memoria central, otra para intercambio) que contenía información sobre direcciones y tamaños de bloques libres.
No hubo asignador de espacio de usuario hasta Unix V6 (los programas simplemente usarían sbrk()
). En Unix V6, iolib incluía un asignador con alloc(size)
y una llamada free()
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 usaba en bloques libres, al recorrer la lista libre, y se reutilizaba como memoria de bloques en bloques en uso.
En Unix 32V y en Unix V7, esto fue sustituido por una nueva implementación de malloc()
y free()
, donde free()
no tomó un argumento de size
. 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). Entonces, malloc()/free()
ni siquiera hizo un seguimiento de un tamaño explícito.
No veo cómo funcionaría un asignador que no sigue el tamaño de sus asignaciones. Si no hiciera esto, ¿cómo sabría qué memoria está disponible para satisfacer una futura solicitud malloc
? 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 es equivalente a almacenar una lista de espacios asignados).
Solo estoy publicando esto como una respuesta, no porque sea la que está esperando, sino porque creo que es la única correcta y plausible:
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 lo eliminaré si se muestra incorrecto).
Habría beneficios si fuera posible: podría asignar una sola pieza grande de memoria cuyo tamaño conociera de antemano, luego liberar un poco a la vez, en lugar de asignar y liberar repetidamente trozos pequeños de memoria. Actualmente tareas como esta no son posibles.
¡Para muchos (muchos 1 !) De ustedes que piensan que pasar el tamaño es tan ridículo:
¿Puedo referirme a la decisión de diseño de C ++ para el método std::allocator<T>::deallocate
?
void deallocate(pointer p, size_type n);
Todos
T
objetosn
T
en el área apuntada porp
se deben destruir antes de esta llamada.
n
coincidirá con el valor pasado paraallocate
para obtener esta memoria.
Creo que tendrás un tiempo bastante "interesante" para analizar esta decisión de diseño.
En cuanto a la operator delete
, resulta que la propuesta N3778 2013 (" N3778 C ++") también está destinada a solucionarlo.
1 Solo mire los comentarios en la pregunta original para ver cuántas personas hicieron afirmaciones precipitadas como "el tamaño solicitado es completamente inútil para la llamada free
" para justificar la falta del parámetro de size
.
Sugeriría que sea porque es muy conveniente no tener que rastrear manualmente la información de tamaño de esta manera (en algunos casos) y también menos propenso a errores del programador.
Además, realloc necesitaría esta información de contabilidad, que espero contenga más que solo el tamaño de la asignación. es decir, permite que se defina la implementación del mecanismo por el que funciona.
Podrías escribir tu propio asignador que funcionara de algún modo en la forma en que sugieras y a menudo se hace en c ++ para asignadores de agrupaciones 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 asignar bloques de pool.
Un argumento free(void *)
(introducido en Unix V7) tiene otra gran ventaja sobre los primeros dos argumentos mfree(void *, size_t)
que no he visto mencionar aquí: un argumento free
simplifica drásticamente cualquier otra API que funcione con memoria de pila. Por ejemplo, si free
necesita el tamaño del bloque de memoria, strdup
alguna manera tendría que devolver dos valores (puntero + tamaño) en lugar de uno (puntero), y C hace que los retornos de múltiples valores sean mucho más engorrosos que los retornos de un solo valor. En lugar de char *strdup(char *)
tendríamos que escribir char *strdup(char *, size_t *)
o bien struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
struct 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 retrospectiva. Detrás de los años 70, la capacidad de C para manejar cadenas como un simple char *
realidad se consideró una ventaja definitoria frente a competidores como Pascal y Algol .) Además, no es solo el strdup
que sufre este problema (afecta a cada sistema o función definida por el usuario que asigna la memoria del montón).
Los primeros diseñadores de Unix eran personas muy inteligentes, y hay muchas razones por las que free
es mejor que mfree
así que básicamente la respuesta a la pregunta es que se dieron cuenta de esto y diseñaron su sistema en consecuencia. Dudo que encuentres un registro directo de lo que estaba pasando dentro de sus cabezas en el momento en que tomaron esa decisión. Pero podemos imaginar.
Imagine que está escribiendo aplicaciones en C para ejecutar en V6 Unix, con su mfree
dos argumentos. Hasta ahora has logrado hacerlo bien, pero mantener el seguimiento de estos tamaños de puntero se está volviendo cada vez más complicado a medida que tus programas se vuelven más ambiciosos y requieren cada vez más del uso de las variables asignadas en el montón. Pero luego tienes una idea brillante: en lugar de copiar todo este tiempo en este tamaño, puedes simplemente 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 mientras más código escriba usando estas nuevas funciones, más asombrosas parecen. No solo hacen que su código sea más fácil de escribir, también hacen que su código sea más rápido , ¡dos cosas que a menudo no van juntas! Antes de pasar estos size_t
por todo el lugar, que agregaba sobrecarga de CPU para la copia, y significaba que tenía que derramar registros más a menudo (especialmente para los argumentos de funciones adicionales) y memoria desperdiciada (ya que las llamadas a funciones anidadas a menudo da como resultado copias múltiples del size_t
se almacenan en diferentes marcos de pila). En su nuevo sistema, todavía tiene que gastar la memoria para almacenar el size_t
, pero solo una vez, y nunca se copia en ninguna parte. Estas pueden parecer pequeñas eficiencias, pero tenga en cuenta que estamos hablando de máquinas de alta gama con 256 KiB de RAM.
¡Esto te hace feliz! Así que compartes tu genial truco con los hombres barbudos que están trabajando en el próximo lanzamiento de Unix, pero no los hace felices, los pone tristes. Verás, estaban simplemente en proceso de agregar un conjunto de nuevas funciones de utilidad, como strdup
, y se dieron cuenta de que las personas que usan tu genial truco no podrán usar sus nuevas funciones, porque todas sus nuevas funciones usan el puntero engorroso + tamaño API. Y eso también te entristece, porque te das cuenta de que tendrás que volver a escribir la función strdup(char *)
en cada programa que escribes, en lugar de poder usar la versión del sistema.
¡Pero espera! ¡Esto es 1977, y la compatibilidad hacia atrás no se inventará por otros 5 años! Y, además, nadie serio realmente usa esta cosa oscura de "Unix" con su nombre descolorido. La primera edición de K & R está en camino hacia el editor ahora, pero eso no es problema - dice en la primera página que "C no proporciona operaciones para tratar directamente con objetos compuestos tales como cadenas de caracteres ... no hay ningún montón ... ". En este momento de la historia, string.h
y malloc
son extensiones de proveedor (!). Por lo tanto, sugiere Bearded Man # 1, podemos cambiarlos como nos plazca; ¿Por qué no declaramos que su asignador complicado 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 el siguiente paso a la blasfemia. Todos los demás lo miran como si estuviera loco, porque ¿qué otra cosa 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 manteniendo la nueva API en su lugar. La nueva API significa que nadie nota el cambio, pero se da cuenta de que a la mañana siguiente el compilador usa un 10% menos de RAM.
Y ahora todos están contentos: obtienes tu código más fácil de escribir y más rápido, Bearded Man # 1 puede escribir un sencillo strdup
simple que la gente realmente usará, y Bearded Man # 2 - seguro de que se ha ganado su sustento por un tiempo - vuelve a jugar con quines . ¡Envíalo!
O al menos, así es como pudo haber sucedido.
malloc y free van de la mano, con cada "malloc" emparejado por uno "libre". Por lo tanto, tiene total sentido que el emparejamiento "libre" de un "malloc" anterior simplemente libere 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 todos los programadores de todo el mundo alguna vez, necesitarían que el programador realizara un seguimiento de la cantidad asignada en malloc, y luego recuerde liberar el mismo. El escenario del que hablas debería ser utilizar múltiples mallocs / free en algún tipo de implementación de administración de memoria.
Well, the only thing you need is a pointer that you''ll use to free up the memory you previously allocated. The amount of bytes is something managed by the operating system so you don''t have to worry about it. It wouldn''t be necessary to get the number of bytes allocated returned by free(). I suggest you a manual way to count the number of bytes/positions allocated by a running program:
If you work in Linux and you want to know the amount of bytes/positions malloc has allocated, you can make a simple program that uses malloc once or n times and prints out the pointers you get. In addition, you must make the program sleep for a few seconds (enough for you to do the following). After that, run that program, look for its PID, write cd /proc/process_PID and just type "cat maps". The output will show you, in one specific line, both the beginning and the final memory addresses of the heap memory region (the one in which you are allocating memory dinamically).If you print out the pointers to these memory regions being allocated, you can guess how much memory you have allocated.
Hope it helps!
Why should it? malloc() and free() are intentionally very simple memory management primitives , and higher-level memory management in C is largely up to the developer. T
Moreover realloc() does that already - if you reduce the allocation in realloc() is it will not move the data, and the pointer returned will be the the same as the original.
It is generally true of the entire standard library that it is composed of simple primitives from which you can build more complex functions to suit your application needs. So the answer to any question of the form "why does the standard library not do X" is because it cannot do everything a programmer might think of (that''s what programmers are for), so it chooses to do very little - build your own or use third-party libraries. If you want a more extensive standard library - including more flexible memory management, then C++ may be the answer.
You tagged the question C++ as well as C, and if C++ is what you are using, then you should hardly be using malloc/free in any case - apart from new/delete, STL container classes manage memory automatically, and in a manner likely to be specifically appropriate to the nature of the various containers.