La versión corta: siempre use en calloc()
lugar de malloc()+memset()
. En la mayoría de los casos, serán lo mismo. En algunos casos, calloc()
hará menos trabajo porque puede saltarse por memset()
completo. En otros casos, ¡ calloc()
incluso puede hacer trampa y no asignar memoria! Sin embargo, malloc()+memset()
siempre hará todo el trabajo.
Comprender esto requiere un breve recorrido por el sistema de memoria.
Recorrido rápido de memoria
Aquí hay cuatro partes principales: su programa, la biblioteca estándar, el kernel y las tablas de páginas. Ya conoces tu programa, así que ...
A los asignadores de memoria les gusta malloc()
y calloc()
están principalmente allí para tomar pequeñas asignaciones (desde 1 byte hasta 100s de KB) y agruparlas en grandes grupos de memoria. Por ejemplo, si asigna 16 bytes, malloc()
primero intentará obtener 16 bytes de uno de sus grupos, y luego solicitará más memoria del núcleo cuando el grupo se seque. Sin embargo, dado que el programa que está preguntando está asignando una gran cantidad de memoria a la vez, malloc()
y calloc()
solo pedirá esa memoria directamente desde el núcleo. El umbral para este comportamiento depende de su sistema, pero he visto 1 MiB utilizado como umbral.
El núcleo es responsable de asignar RAM real a cada proceso y asegurarse de que los procesos no interfieran con la memoria de otros procesos. Esto se llama protección de memoria, ha sido muy común desde la década de 1990, y es la razón por la cual un programa puede bloquearse sin derribar todo el sistema. Entonces, cuando un programa necesita más memoria, no solo puede tomar la memoria, sino que pide la memoria del núcleo usando una llamada al sistema como mmap()
o sbrk()
. El núcleo le dará RAM a cada proceso modificando la tabla de páginas.
La tabla de páginas asigna direcciones de memoria a la RAM física real. Las direcciones de su proceso, 0x00000000 a 0xFFFFFFFF en un sistema de 32 bits, no son memoria real, sino que son direcciones en memoria virtual. El procesador divide estas direcciones en 4 páginas KiB, y cada página se puede asignar a una parte diferente de RAM física modificando la tabla de páginas. Solo el núcleo puede modificar la tabla de páginas.
Como no funciona
He aquí cómo la asignación de 256 MiB qué no funciona:
Su proceso llama calloc()
y solicita 256 MiB.
La biblioteca estándar llama mmap()
y pide 256 MiB.
El kernel encuentra 256 MiB de RAM no utilizada y la entrega a su proceso modificando la tabla de páginas.
La biblioteca estándar pone a cero la RAM con memset()
y regresa de calloc()
.
Su proceso finalmente se cierra y el núcleo recupera la RAM para que pueda ser utilizado por otro proceso.
Cómo funciona realmente
El proceso anterior funcionaría, pero simplemente no sucede de esta manera. Hay tres diferencias principales.
Cuando su proceso obtiene nueva memoria del núcleo, esa memoria probablemente fue utilizada por algún otro proceso anteriormente. Este es un riesgo de seguridad. ¿Qué pasa si esa memoria tiene contraseñas, claves de cifrado o recetas secretas de salsa? Para evitar que se filtren datos confidenciales, el núcleo siempre elimina la memoria antes de entregarla a un proceso. También podríamos restregar la memoria poniéndola a cero, y si la memoria nueva se pone a cero, también podríamos hacerla una garantía, por lo mmap()
que garantiza que la nueva memoria que devuelve siempre se pone a cero.
Existen muchos programas que asignan memoria pero no la utilizan de inmediato. Algunas veces la memoria se asigna pero nunca se usa. El núcleo lo sabe y es flojo. Cuando asigna nueva memoria, el kernel no toca la tabla de páginas y no le da RAM a su proceso. En cambio, encuentra algo de espacio de direcciones en su proceso, toma nota de lo que se supone que debe ir allí y promete que pondrá RAM allí si su programa realmente lo usa. Cuando su programa intenta leer o escribir desde esas direcciones, el procesador desencadena un error de página y los pasos del núcleo asignan RAM a esas direcciones y reanuda su programa. Si nunca usa la memoria, la falla de la página nunca ocurre y su programa nunca obtiene la RAM.
Algunos procesos asignan memoria y luego leen de ella sin modificarla. Esto significa que muchas páginas en la memoria a través de diferentes procesos pueden llenarse con ceros prístinos devueltos mmap()
. Dado que estas páginas son todas iguales, el núcleo hace que todas estas direcciones virtuales apunten a una sola página compartida de 4 KiB de memoria llena de ceros. Si intenta escribir en esa memoria, el procesador activa otra falla de página y el kernel interviene para darle una nueva página de ceros que no se comparte con ningún otro programa.
El proceso final se parece más a esto:
Su proceso llama calloc()
y solicita 256 MiB.
La biblioteca estándar llama mmap()
y pide 256 MiB.
El kernel encuentra 256 MiB de espacio de direcciones no utilizado , toma nota de para qué se usa ese espacio de direcciones y regresa.
La biblioteca estándar sabe que el resultado de mmap()
siempre está lleno de ceros (o lo será una vez que realmente obtenga algo de RAM), por lo que no toca la memoria, por lo que no hay falla de página y la RAM nunca se entrega a su proceso .
Su proceso finalmente se cierra, y el núcleo no necesita reclamar la RAM porque nunca se asignó en primer lugar.
Si usa memset()
la página para ponerla a cero, memset()
activará la falla de la página, hará que se asigne la RAM y luego la pondrá a cero aunque ya esté llena de ceros. Esta es una enorme cantidad de trabajo extra y explica por qué calloc()
es más rápido que malloc()
y memset()
. Si termina usando la memoria de todos modos, calloc()
sigue siendo más rápido que malloc()
y, memset()
pero la diferencia no es tan ridícula.
Esto no siempre funciona
No todos los sistemas tienen memoria virtual paginada, por lo que no todos los sistemas pueden usar estas optimizaciones. Esto se aplica a procesadores muy antiguos como el 80286, así como a procesadores integrados que son demasiado pequeños para una unidad de administración de memoria sofisticada.
Esto tampoco siempre funcionará con asignaciones más pequeñas. Con asignaciones más pequeñas, calloc()
obtiene memoria de un grupo compartido en lugar de ir directamente al kernel. En general, el grupo compartido podría tener datos basura almacenados en él desde la memoria anterior que se utilizó y se liberó free()
, por lo que calloc()
podría tomar esa memoria y llamar memset()
para borrarla. Las implementaciones comunes rastrearán qué partes del grupo compartido son impecables y aún están llenas de ceros, pero no todas las implementaciones hacen esto.
Disipando algunas respuestas incorrectas
Dependiendo del sistema operativo, el núcleo puede o no tener memoria cero en su tiempo libre, en caso de que necesite obtener algo de memoria puesta a cero más tarde. Linux no pone a cero la memoria antes de tiempo, y Dragonfly BSD recientemente también eliminó esta característica de su núcleo . Sin embargo, algunos otros núcleos hacen cero memoria antes de tiempo. Poner a cero las páginas durante la inactividad no es suficiente para explicar las grandes diferencias de rendimiento de todos modos.
La calloc()
función no está utilizando alguna versión especial de memoria alineada memset()
, y eso no lo haría mucho más rápido de todos modos. La mayoría de las memset()
implementaciones para procesadores modernos se ven así:
function memset(dest, c, len)
// one byte at a time, until the dest is aligned...
while (len > 0 && ((unsigned int)dest & 15))
*dest++ = c
len -= 1
// now write big chunks at a time (processor-specific)...
// block size might not be 16, it's just pseudocode
while (len >= 16)
// some optimized vector code goes here
// glibc uses SSE2 when available
dest += 16
len -= 16
// the end is not aligned, so one byte at a time
while (len > 0)
*dest++ = c
len -= 1
Como puede ver, memset()
es muy rápido y realmente no obtendrá nada mejor para grandes bloques de memoria.
El hecho de que memset()
esté poniendo a cero la memoria que ya está puesta a cero significa que la memoria se pone a cero dos veces, pero eso solo explica una diferencia de rendimiento 2x. La diferencia de rendimiento aquí es mucho mayor (midí más de tres órdenes de magnitud en mi sistema entre malloc()+memset()
y calloc()
).
Truco de fiesta
En lugar de recorrer 10 veces, escriba un programa que asigne memoria hasta malloc()
o calloc()
devuelva NULL.
¿Qué pasa si agregas memset()
?