2010-09-22 21 views
142

He escuchado el término "fragmentación de la memoria" utilizado algunas veces en el contexto de la asignación de memoria dinámica C++. He encontrado algunas preguntas sobre cómo lidiar con la fragmentación de la memoria, pero no puedo encontrar una pregunta directa que trate con ella. Entonces:¿Qué es fragmentación de memoria?

  • ¿Qué es la fragmentación de la memoria?
  • ¿Cómo puedo saber si la fragmentación de la memoria es un problema para mi aplicación? ¿Qué tipo de programa es más probable que sufra?
  • ¿Cuáles son las buenas formas comunes de lidiar con la fragmentación de la memoria?

también:

  • He oído utilizando asignaciones dinámicas mucho puede aumentar la fragmentación de la memoria. ¿Es esto cierto? En el contexto de C++, entiendo que todos los contenedores estándar (std :: string, std :: vector, etc.) usan asignación de memoria dinámica. Si se utilizan en un programa (especialmente std :: string), ¿es más probable que la fragmentación de la memoria sea un problema?
  • ¿Cómo se puede tratar la fragmentación de memoria en una aplicación STL-heavy?
+0

Muchos de los grandes respuestas, gracias a todos! – AshleysBrain

+4

Ya hay muchas respuestas excelentes, pero aquí hay algunas imágenes de una aplicación real (Firefox) donde la fragmentación de la memoria era un gran problema: http://blog.pavlov.net/2007/11/10/memory-fragmentation/ –

+1

@ MariusGedminas el enlace ya no funciona por eso es importante proporcionar un breve resumen junto con el enlace o responder la pregunta con un resumen con el enlace – katta

Respuesta

218

Imagine que tiene una (32 bytes) extensión "grande" de memoria libre:

---------------------------------- 
|        | 
---------------------------------- 

Ahora, asignar parte de ella (5 asignaciones):

---------------------------------- 
|aaaabbccccccddeeee    | 
---------------------------------- 

Ahora, liberar el las primeras cuatro asignaciones pero no la quinta:

---------------------------------- 
|    eeee    | 
---------------------------------- 

Ahora intenta asignar 16 bytes. Vaya, no puedo, aunque hay casi el doble de esa cantidad gratis.

En los sistemas con memoria virtual, la fragmentación es un problema menor de lo que parece, porque las grandes asignaciones sólo tienen que ser contiguos en el espacio virtual de dirección, no en el espacio dirección física. Entonces en mi ejemplo, si tuviera memoria virtual con un tamaño de página de 2 bytes, podría hacer mi asignación de 16 bytes sin ningún problema. La memoria física se vería así:

---------------------------------- 
|ffffffffffffffeeeeff   | 
---------------------------------- 

mientras que la memoria virtual (siendo mucho más grande) podría tener este aspecto:

------------------------------------------------------... 
|    eeeeffffffffffffffff     
------------------------------------------------------... 

El síntoma clásico de la fragmentación de la memoria es que se intenta asignar un bloque grande y no puedes, aunque parece que tienes suficiente memoria libre. Otra posible consecuencia es la incapacidad del proceso para liberar memoria de vuelta al sistema operativo (porque todavía hay algún objeto en uso en todos los bloques que ha asignado desde el sistema operativo, aunque esos bloques están ahora en su mayoría sin usar).

Tácticas para evitar la fragmentación de la memoria en el trabajo C++ mediante la asignación de objetos de diferentes áreas de acuerdo con su tamaño y/o la duración esperada. Entonces, si va a crear muchos objetos y destruirlos todos juntos más tarde, asígnelos desde un grupo de memoria. Cualquier otra asignación que haga entre ellos no será del grupo, por lo tanto, no se ubicará entre ellos en la memoria, por lo que la memoria no se fragmentará como resultado.

Generalmente no necesita preocuparse demasiado, a menos que su programa sea de larga ejecución y le asigne y libere mucho. Es cuando tienes mezclas de objetos efímeros y de larga vida que corres mayor riesgo, pero incluso entonces, malloc hará todo lo posible para ayudar. Básicamente, ignórelo hasta que su programa tenga fallas de asignación o inesperadamente provoque que el sistema se quede sin memoria (¡tómelo en prueba, por favor!).

Las bibliotecas estándar no son peores que cualquier otra cosa que asigna memoria, y los contenedores estándar tienen un parámetro de plantilla Alloc que puede usar para ajustar su estrategia de asignación si es absolutamente necesario.

+58

+1 para el ejemplo visual. –

+1

Entonces, ¿cada personaje es un byte? Lo cual haría que tu "gran extensión" == 32 bytes (supongo que no contó) :) Buen ejemplo, pero mencionar las unidades antes de la última línea sería útil. :) – jalf

+0

@jalf: Sí. No iba a mencionar unidades en absoluto, luego me di cuenta que al final tenía que hacerlo. Estaba trabajando en eso mientras comentabas. –

67

¿Cuál es la fragmentación de memoria?

fragmentación de la memoria es cuando la mayor parte de su memoria se asigna en un gran número de bloques no contiguos, o trozos - dejando un buen porcentaje de su memoria total no asignado, pero inservible para los escenarios más típicos. Esto da como resultado excepciones de memoria insuficiente o errores de asignación (es decir, malloc devuelve nulo).

La manera más fácil de pensar sobre esto es imaginar que tiene una gran pared vacía que necesita para poner imágenes de diferentes tamaños en. Cada imagen ocupa un cierto tamaño y, obviamente, no se puede dividir en trozos más pequeños para que quepa. Necesitas un lugar vacío en la pared, el tamaño de la imagen, o bien no puedes subirlo. Ahora, si comienzas a colgar imágenes en la pared y no tienes cuidado con cómo las arreglas, pronto terminarás con una pared que está parcialmente cubierta con imágenes y, aunque tengas espacios vacíos, la mayoría de las imágenes nuevas no cabrán. porque son más grandes que los lugares disponibles. Todavía puede colgar imágenes realmente pequeñas, pero la mayoría no se ajustarán. Por lo tanto, tendrá que reorganizar (compactar) los que ya están en la pared para dejar espacio para más ...

Imagine que la pared es su (montón) de memoria y que las imágenes son objetos ... Eso es memoria fragmentation ..

¿Cómo puedo saber si la fragmentación de la memoria es un problema para mi aplicación? ¿Qué tipo de programa es más probable que sufra?

Un signo revelador de que puede estar lidiando con la fragmentación de la memoria es si obtiene muchos errores de asignación, especialmente cuando el porcentaje de memoria usada es alto, pero no ha agotado toda la memoria, por lo que técnicamente debería tener suficiente espacio para los objetos que intentas asignar.

Cuando la memoria está muy fragmentada, las asignaciones de memoria probablemente tomarán más tiempo porque el asignador de memoria tiene que trabajar más para encontrar un espacio adecuado para el nuevo objeto. Si a su vez tiene muchas asignaciones de memoria (lo cual probablemente ocurra desde que terminó con la fragmentación de la memoria), el tiempo de asignación puede incluso causar demoras notables.

¿Cuáles son las buenas formas comunes de lidiar con la fragmentación de la memoria?

Utilice un buen algoritmo para asignar memoria. En lugar de asignar memoria para muchos objetos pequeños, preasignar memoria para una matriz contigua de esos objetos más pequeños. A veces, es un poco derrochador cuando asignar memoria puede mejorar el rendimiento y puede ahorrarte el problema de tener que lidiar con la fragmentación de la memoria.

+8

+1. Acabo de eliminar mi respuesta propuesta porque su metáfora de "imágenes en la pared" es realmente buena, clara. – ctacke

+0

Me gustaría más si enfatiza el hecho de que las imágenes deben tener diferentes tamaños. De lo contrario, no habrá fragmentación. –

+1

He agregado eso ... gracias Space_C0wb0y –

6

La fragmentación de la memoria es más probable que ocurra cuando asigna y desasignar muchos objetos de diferentes tamaños. Suponga que tiene la siguiente distribución en la memoria:

obj1 (10kb) | obj2(20kb) | obj3(5kb) | unused space (100kb) 

Ahora, cuando se libera obj2, que tienen 120kb de memoria no utilizada, pero no se puede asignar un bloque completo de 120kb, porque la memoria está fragmentada.

Las técnicas comunes para evitar ese efecto incluyen ring buffers y object pools. En el contexto del STL, métodos como std::vector::reserve() pueden ayudar.

3

¿Qué es la fragmentación de la memoria?

Cuando su aplicación utiliza memoria dinámica, asigna y libera trozos de memoria. Al principio, todo el espacio de la memoria de su aplicación es un bloque contiguo de memoria libre. Sin embargo, cuando asigna y libera bloques de diferente tamaño, la memoria comienza a tener fragmentado, es decir, en lugar de un gran bloque libre contiguo y un número de bloques asignados contiguos, habrá un bloque asignado y libre mezclado. Como los bloques libres tienen un tamaño limitado, es difícil reutilizarlos. P.ej. puede tener 1000 bytes de memoria libre, pero no puede asignar memoria para un bloque de 100 bytes, porque todos los bloques libres tienen como máximo 50 bytes de longitud.

Otro, inevitable, pero fuente menos problemática de la fragmentación es que en la mayoría de arquitecturas, direcciones de memoria deben ser alineado a 2, 4, 8 etc. límites de bytes (es decir, las direcciones deben ser múltiplos de 2, 4, 8 etc.) Esto significa que incluso si tiene, por ejemplo, una estructura que contiene 3 char campos, su estructura puede tener un tamaño de 12 en lugar de 3, debido al hecho de que cada campo está alineado con un límite de 4 bytes.

¿Cómo puedo saber si la fragmentación de la memoria es un problema para mi aplicación? ¿Qué tipo de programa es más probable que sufra?

La respuesta obvia es que obtiene una excepción de falta de memoria.

Aparentemente, no existe una buena forma portátil de detectar la fragmentación de memoria en las aplicaciones C++. Ver this answer para más detalles.

¿Cuáles son las buenas formas comunes de lidiar con la fragmentación de la memoria?

Es difícil en C++, ya que utiliza direcciones de memoria directa en los punteros, y no tiene control sobre quién hace referencia a una dirección de memoria específica. Así que reorganizar los bloques de memoria asignados (la forma en que lo hace el recolector de basura de Java) no es una opción.

Un asignador personalizado puede ayudar administrando la asignación de objetos pequeños en un trozo más grande de memoria y reutilizando los espacios libres dentro de ese trozo.

2

Cuando quiere agregar un elemento en el montón, lo que sucede es que la computadora tiene que buscar espacio para que se ajuste a ese elemento. Es por eso que las asignaciones dinámicas cuando no se realizan en un grupo de memoria o con un asignador agrupado pueden "ralentizar" las cosas. Para una aplicación STL pesada, si está haciendo multi-threading existe la versión Hoard allocator o TBB Intel.

Ahora, cuando la memoria está fragmentada dos cosas pueden ocurrir:

  1. Habrá que ser más búsquedas para encontrar un buen espacio para pegar objetos "grandes". Es decir, con muchos objetos pequeños diseminados para encontrar un buen pedazo de memoria contundente, bajo ciertas condiciones podría ser difícil (estos son extremos).
  2. La memoria no es una entidad fácil de leer. Los procesadores están limitados a cuánto pueden contener y dónde. Lo hacen intercambiando páginas si un elemento que necesitan es un lugar, pero las direcciones actuales son otro. Si constantemente tiene que intercambiar páginas, el procesamiento puede ralentizarse (una vez más, escenarios extremos donde esto afecta el rendimiento). Consulte esta publicación en virtual memory.
13
  • ¿Cuál es la fragmentación de memoria?

fragmentación de la memoria es el problema de la memoria volviendo inservible a pesar de que es teóricamente disponible. Hay dos tipos de fragmentación: fragmentación interna es la memoria que se asigna pero no se puede usar (por ejemplo, cuando la memoria se asigna en trozos de 8 bytes, pero el programa realiza repeticiones únicas cuando solo necesita 4 bytes). fragmentación externa es el problema de que la memoria libre se divida en muchos fragmentos pequeños, por lo que las solicitudes de asignación grandes no se pueden cumplir aunque haya suficiente memoria libre total.

  • ¿Cómo puedo saber si la fragmentación de memoria es un problema para mi aplicación? ¿Qué tipo de programa es más probable que sufra?

fragmentación de la memoria es un problema si su programa utiliza mucho más la memoria del sistema que sus datos reales paylod requerirían (y que ha descartado pérdidas de memoria).

  • ¿Cuáles son buenas maneras comunes para hacer frente a la fragmentación de memoria?

Utilice un buen asignador de memoria. IIRC, aquellos que usan una estrategia de "mejor ajuste" generalmente son mucho mejores para evitar la fragmentación, aunque un poco más lento. Sin embargo, también se ha demostrado que para cualquier estrategia de asignación, existen los peores casos patológicos. Afortunadamente, los patrones de asignación típicos de la mayoría de las aplicaciones son en realidad relativamente benignos para los asignadores. Hay un montón de documentos disponibles si está interesado en los detalles:

  • Paul R. Wilson, Mark S. Johnstone, Michael Neely y David Boles. Asignación dinámica de almacenamiento: una encuesta y revisión crítica. En las actas del 1995 Taller internacional sobre la gestión de la memoria, Springer Verlag LNCS, 1995
  • Mark S.Johnstone, Paul R. Wilson. El problema de la fragmentación de la memoria: ¿Resuelto? En avisos SIG-PLAN de ACM, volumen 34 Nº 3, páginas 26-36, 1999
  • M.R. Garey, R.L. Graham y J.D. Ullman. Análisis del peor caso de algoritmos de asignación de memoria.En el cuarto Simposio anual de ACM sobre la teoría de la computación, 1972
1

La fragmentación de la memoria se produce porque se solicitan bloques de memoria de diferentes tamaños. Considere un buffer de 100 bytes. Solicitas dos caracteres, luego un número entero. Ahora liberas los dos caracteres, luego solicitas un nuevo entero, pero ese número entero no cabe en el espacio de los dos caracteres. Esa memoria no se puede reutilizar porque no se encuentra en un bloque contiguo suficientemente grande para reasignarla. Además de eso, has invocado una gran cantidad de sobrecarga del asignador para tus caracteres.

Esencialmente, la memoria solo viene en bloques de un cierto tamaño en la mayoría de los sistemas. Una vez que se dividen estos bloques, no se pueden volver a unir hasta que se libere todo el bloque. Esto puede conducir a bloques enteros en uso cuando en realidad solo se usa una pequeña parte del bloque.

La forma principal de reducir la fragmentación del montón es hacer asignaciones más grandes y menos frecuentes. En el extremo, puede usar un montón administrado que sea capaz de mover objetos, al menos, dentro de su propio código. Esto elimina por completo el problema, desde el punto de vista de la memoria, de todos modos. Obviamente mover objetos tiene un costo. En realidad, realmente solo tiene un problema si asigna cantidades muy pequeñas fuera del montón a menudo. El uso de contenedores contiguos (vector, cadena, etc.) y la asignación en la pila tanto como sea humanamente posible (siempre una buena idea para el rendimiento) es la mejor manera de reducirlo. Esto también aumenta la coherencia del caché, lo que hace que su aplicación se ejecute más rápido.

Lo que debe recordar es que en un sistema de escritorio de 32 bits x 86, tiene una memoria completa de 2 GB dividida en 4KB "páginas" (bastante seguro de que el tamaño de página es el mismo en todos los sistemas x86). Tendrá que invocar alguna fragmentación omgwtfbbq para tener un problema. La fragmentación realmente es un problema del pasado, ya que los montones modernos son excesivamente grandes para la gran mayoría de las aplicaciones, y hay una prevalencia de sistemas que son capaces de resistirlo, como montones administrados.

19

La fragmentación de la memoria es el mismo concepto que la fragmentación del disco: se refiere a un desperdicio de espacio porque las áreas en uso no se empaquetan lo suficientemente juntas.

Supongamos por un simple ejemplo de juguete que tiene diez bytes de memoria:

| | | | | | | | | | | 
    0 1 2 3 4 5 6 7 8 9 

Ahora vamos a asignar tres bloques de tres bytes, el nombre de A, B, y C:

| A | A | A | B | B | B | C | C | C | | 
    0 1 2 3 4 5 6 7 8 9 

Ahora deallocate bloque B:

| A | A | A | | | | C | C | C | | 
    0 1 2 3 4 5 6 7 8 9 

Ahora lo que sucede si tratamos de asignar un bloque de cuatro bytes D? Bueno, tenemos cuatro bytes de memoria libre, pero no tenemos cuatro contiguos bytes de memoria libre, por lo que no podemos asignar D! Este es un uso ineficiente de la memoria, porque deberíamos haber podido almacenar D, pero no pudimos. Y no podemos mover C para dejar espacio, porque muy probablemente algunas variables en nuestro programa apuntan a C, y no podemos encontrar y cambiar automáticamente todos estos valores.

¿Cómo sabes que es un problema? Bueno, la señal más importante es que el tamaño de la memoria virtual de su programa es considerablemente mayor que la cantidad de memoria que está usando realmente. En un ejemplo del mundo real, tendría muchos más de diez bytes de memoria, por lo que D se asignaría comenzando con un byte 9 y los bytes 3-5 permanecerían sin usar a menos que luego asignara algo de tres bytes de longitud o menos.

En este ejemplo, 3 bytes no son demasiado para desperdiciar, pero consideren un caso más patológico donde dos asignaciones de un par de bytes son, por ejemplo, diez megabytes separados en memoria, y necesita asignar un bloque de tamaño 10 megabytes + 1 byte. Tienes que pedirle al sistema operativo más de diez megabytes más de memoria virtual para hacer eso, aunque estés a un paso de tener suficiente espacio.

¿Cómo lo previenen? Los peores casos tienden a surgir cuando crea y destruye objetos pequeños con frecuencia, ya que esto tiende a producir un efecto de "queso suizo" con muchos objetos pequeños separados por muchos agujeros pequeños, por lo que es imposible asignar objetos más grandes en esos agujeros. Cuando sepa que va a hacer esto, una estrategia efectiva es preasignar un gran bloque de memoria como grupo para sus objetos pequeños, y luego administrar manualmente la creación de los objetos pequeños dentro de ese bloque, en lugar de dejarlo el asignador predeterminado lo maneja.

En general, cuantas menos asignaciones hagas, menos memoria habrá de fragmentarse. Sin embargo, STL trata esto de manera bastante efectiva. Si tiene una cadena que está utilizando la totalidad de su asignación actual y le agrega un carácter, no se vuelve a asignar a su longitud actual más uno, dobla su longitud. Esta es una variación de la estrategia del "grupo para pequeñas asignaciones frecuentes". La cuerda está agarrando un gran trozo de memoria para que pueda manejar eficientemente pequeños aumentos de tamaño repetidos sin realizar reasignaciones repetidas y pequeñas. De hecho, todos los contenedores STL hacen este tipo de cosas, por lo que, en general, no tendrá que preocuparse demasiado por la fragmentación causada por la reasignación automática de contenedores STL.

Aunque, por supuesto, contenedores STL no reunir la memoria entre sí, por lo que si usted va a crear muchos pequeños contenedores (en lugar de unos contenedores que consiguen cambiar de tamaño con frecuencia) puede que tenga que preocuparse por la prevención fragmentación de la misma manera que lo haría con cualquier objeto pequeño creado con frecuencia, STL o no.

8

Actualización:
Google TCMalloc: Thread-Caching Malloc
Se ha encontrado que es bastante bueno en el manejo de fragmentación en un proceso de larga duración.


he estado desarrollando una aplicación de servidor que tenía problemas con la fragmentación de memoria en HP-UX 11.23/11.31 IA64.

Parecía así. Hubo un proceso que hizo asignaciones de memoria y desasignaciones y se ejecutó durante días. Y a pesar de que no hubo pérdidas de memoria, el consumo de memoria del proceso siguió aumentando.

Sobre mi experiencia. En HP-UX, es muy fácil encontrar la fragmentación de memoria utilizando HP-UX gdb. Establece un punto de interrupción y, cuando lo alcanza, ejecuta este comando: info heap y ve todas las asignaciones de memoria para el proceso y el tamaño total de Heap. Luego, continúa con tu programa y luego, un tiempo después, vuelves a alcanzar el punto de ruptura. Lo haces de nuevo info heap. Si el tamaño total del montón es mayor, pero el número y el tamaño de las asignaciones por separado son los mismos, es probable que tenga problemas de asignación de memoria. Si es necesario, haga esto pocas veces.

Mi forma de mejorar la situación fue esta. Después de hacer un análisis con HP-UX gdb, vi que los problemas de memoria se debían al hecho de que usé std::vector para almacenar algunos tipos de información de una base de datos. std::vector requiere que sus datos se mantengan en un bloque. Tenía algunos contenedores basados ​​en std::vector.Estos contenedores fueron recreados regularmente. A menudo hubo situaciones en las que se agregaron nuevos registros a la base de datos y luego se recrearon los contenedores. Y dado que los contenedores recreados eran más grandes, no encajaban en los bloques disponibles de memoria libre y el tiempo de ejecución solicitó un nuevo bloque más grande del sistema operativo. Como resultado, aunque no hubo pérdidas de memoria, el consumo de memoria del proceso creció. Mejoré la situación cuando cambié los contenedores. En lugar de std::vector comencé a usar std::deque que tiene una forma diferente de asignar memoria para los datos.

Sé que una de las maneras de evitar la fragmentación de la memoria en HP-UX es utilizar Small Block Alocator o utilizar MallocNextGen. En RedHat Linux, el asignador predeterminado parece manejar bastante bien la asignación de una gran cantidad de bloques pequeños. En Windows existe Low-fragmentation Heap y se aborda el problema de una gran cantidad de pequeñas asignaciones.

Según tengo entendido, en una aplicación STL-heavy primero tiene que identificar los problemas. Los asignadores de memoria (como en libc) realmente resuelven el problema de muchas asignaciones pequeñas, lo que es típico de std::string (por ejemplo, en mi aplicación de servidor hay muchas cadenas STL pero como veo al ejecutar info heap no están causando ningún problema) . Mi impresión es que debe evitar las grandes asignaciones frecuentes. Lamentablemente, hay situaciones en las que no puede evitarlos y tiene que cambiar su código. Como digo en mi caso, mejoré la situación cuando cambié al std::deque. Si identifica la fragmentación de su memoria, podría ser posible hablar de ella con mayor precisión.

3

Esta es una versión súper simplificada para los principiantes.

A medida que los objetos se crean en la memoria, se agregan al final de la porción utilizada en la memoria.

Si se elimina un objeto que no está al final de la porción de memoria utilizada, lo que significa que este objeto estaba entre otros 2 objetos, creará un "agujero".

Esto es lo que se llama fragmentación.

Cuestiones relacionadas