Saltar al contenido

¿Por qué malloc + memset es más lento que calloc?

Presta atención ya que en esta división encontrarás la respuesta que buscas.Esta sección ha sido probado por nuestros especialistas para asegurar la calidad y veracidad de nuestro contenido.

Solución:

La versión corta: utilizar siempre calloc() en lugar de malloc()+memset(). En la mayoría de los casos, serán los mismos. En algunos casos, calloc() hará menos trabajo porque puede omitir memset() enteramente. En otros casos, calloc() ¡Incluso puede hacer trampa y no asignar memoria! Sin embargo, malloc()+memset() siempre hará la cantidad completa de trabajo.

Comprender esto requiere un breve recorrido por el sistema de memoria.

Recorrido rápido por la 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 …

Asignadores de memoria como malloc() y calloc() en su mayoría están ahí para tomar pequeñas asignaciones (desde 1 byte hasta cientos de KB) y agruparlas en grupos de memoria más grandes. Por ejemplo, si asigna 16 bytes, malloc() Primero intentará obtener 16 bytes de uno de sus grupos y luego solicitará más memoria del kernel cuando el grupo se agote. Sin embargo, dado que el programa sobre el que pregunta está asignando una gran cantidad de memoria a la vez, malloc() y calloc() solo pedirá esa memoria directamente desde el kernel. El umbral de este comportamiento depende de su sistema, pero he visto que se utiliza 1 MiB como umbral.

El kernel es responsable de asignar RAM real a cada proceso y asegurarse de que los procesos no interfieran con la memoria de otros procesos. Se llama protección de la memoria, ha sido muy común desde la década de 1990, y es la razón por la que un programa puede fallar sin que todo el sistema se caiga. Entonces, cuando un programa necesita más memoria, no puede simplemente tomar la memoria, sino que solicita la memoria del kernel usando una llamada al sistema como mmap() o sbrk(). El kernel le dará RAM a cada proceso modificando la tabla de páginas.

La tabla de páginas asigna las 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 direcciones en memoria virtual. El procesador divide estas direcciones en 4 páginas KiB, y cada página puede asignarse a una pieza diferente de RAM física modificando la tabla de páginas. Solo el kernel puede modificar la tabla de páginas.

Como no funciona

Así es como se hace la asignación de 256 MiB no trabaja:

  1. Tu proceso llama calloc() y pide 256 MiB.

  2. La biblioteca estándar llama mmap() y pide 256 MiB.

  3. El kernel encuentra 256 MiB de RAM no utilizada y se los da a su proceso modificando la tabla de páginas.

  4. La biblioteca estándar pone a cero la RAM con memset() y regresa de calloc().

  5. Su proceso finalmente sale y el kernel recupera la RAM para que pueda ser utilizada 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 kernel, 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, cifrado keyso recetas secretas de salsa? Para evitar que se filtren datos confidenciales, el kernel siempre limpia la memoria antes de dársela a un proceso. También podríamos limpiar la memoria poniéndola a cero, y si la nueva memoria se pone a cero, también podríamos hacerla una garantía, por lo que mmap() garantiza que la nueva memoria que devuelve siempre se pone a cero.

  • Hay muchos programas que asignan memoria pero no la usan de inmediato. Algunas veces se asigna memoria pero nunca se usa. El núcleo lo sabe y es vago. Cuando asigna nueva memoria, el kernel no toca la tabla de páginas en absoluto 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 colocará RAM allí si su programa alguna vez la usa. Cuando su programa intenta leer o escribir desde esas direcciones, el procesador activa un error de página y los pasos del kernel para asignar RAM a esas direcciones y reanuda su programa. Si nunca usa la memoria, la falla de página nunca ocurre y su programa nunca obtiene la RAM.

  • Algunos procesos asignan memoria y luego leen sin modificarla. Esto significa que muchas páginas en la memoria a través de diferentes procesos pueden llenarse con ceros prístinos devueltos por mmap(). Dado que estas páginas son todas iguales, el kernel hace que todas estas direcciones virtuales apunten a una sola página de memoria compartida de 4 KiB 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:

  1. Tu proceso llama calloc() y pide 256 MiB.

  2. La biblioteca estándar llama mmap() y pide 256 MiB.

  3. El kernel encuentra 256 MiB de espacio de dirección, toma nota sobre para qué se usa ahora ese espacio de direcciones y devuelve.

  4. La biblioteca estándar sabe que el resultado de mmap() siempre está lleno de ceros (o estarán una vez que realmente obtiene algo de RAM), por lo que no toca la memoria, por lo que no hay fallas de página y la RAM nunca se le da a su proceso.

  5. Su proceso finalmente se cierra y el kernel no necesita recuperar la RAM porque nunca se asignó en primer lugar.

Si utiliza memset() poner a cero la página, memset() activará la falla de página, hará que la RAM se asigne y luego la pondrá a cero aunque ya esté llena de ceros. Esta es una enorme cantidad de trabajo adicional y explica por qué calloc() es más rápido que malloc() y memset(). Si terminas usando la memoria de todos modos, calloc() es aún 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 utilizar 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 gestió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 puede tener datos basura almacenados en la memoria anterior que se usó y liberó con free(), asi que calloc() podría tomar ese recuerdo y llamar memset() para aclararlo. Las implementaciones comunes rastrearán qué partes del grupo compartido son prístinas y aún están llenas de ceros, pero no todas las implementaciones hacen esto.

Disipando algunas respuestas incorrectas

Dependiendo del sistema operativo, el kernel puede o no poner a cero la memoria en su tiempo libre, en caso de que necesite obtener algo de memoria a cero más adelante. Linux no pone a cero la memoria antes de tiempo, y Dragonfly BSD recientemente también eliminó esta característica de su kernel. Sin embargo, algunos otros núcleos hacen cero memoria antes de tiempo. De todos modos, poner a cero las páginas mientras están inactivas no es suficiente para explicar las grandes diferencias de rendimiento.

los calloc() La función no está utilizando alguna versión especial alineada con la memoria de memset(), y eso no lo haría mucho más rápido de todos modos. La mayoría memset() Las implementaciones para procesadores modernos se parecen a esto:

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

Para que puedas ver memset() es muy rápido y realmente no obtendrá nada mejor para grandes bloques de memoria.

El hecho de que memset() Poner a cero la memoria que ya está a cero significa que la memoria se pone a cero dos veces, pero eso solo explica una diferencia de rendimiento de 2x. La diferencia de rendimiento aquí es mucho mayor (medí más de tres órdenes de magnitud en mi sistema entre malloc()+memset() y calloc()).

Truco de fiesta

En lugar de realizar un bucle 10 veces, escriba un programa que asigne memoria hasta malloc() o calloc() devuelve NULL.

¿Qué pasa si agregas memset()?

Porque en muchos sistemas, en el tiempo de procesamiento libre, el sistema operativo establece la memoria libre en cero por sí solo y la marca como segura para calloc(), así que cuando llames calloc(), es posible que ya tenga memoria libre puesta a cero para ofrecerle.

En algunas plataformas, en algunos modos, malloc inicializa la memoria a un valor normalmente distinto de cero antes de devolverlo, por lo que la segunda versión podría inicializar la memoria dos veces.

Reseñas y calificaciones del post

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)


Tags : / /

Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *