2011-01-05 15 views
19

Tengo curiosidad por saber si la asignación de memoria utilizando un nuevo operador predeterminado es una operación sin bloqueo.¿La asignación de memoria en Linux no es bloqueante?

p. Ej.

struct Node { 
    int a,b; 
}; 

...

Node foo = new Node(); 

Si varios subprocesos trataron de crear un nuevo nodo y si uno de ellos fue suspendida por el sistema operativo en el medio de la asignación, sería bloquear otros hilos de avanzar ?

La razón por la que pregunto es porque tenía una estructura de datos concurrente que creaba nuevos nodos. Luego modifiqué el algoritmo para reciclar los nodos. El rendimiento de rendimiento de los dos algoritmos era prácticamente idéntico en una máquina de 24 núcleos. Sin embargo, luego creé un programa de interferencia que se ejecutaba en todos los núcleos del sistema con el fin de crear la mayor apropiación posible del sistema operativo. El rendimiento del algoritmo que creó nuevos nodos disminuyó en un factor de 5 en relación con el algoritmo que recicló los nodos.

Tengo curiosidad por saber por qué ocurrirá esto.

Gracias.

* Editar: señalarme el código del asignador de memoria C++ para linux también sería útil. Intenté mirar antes de publicar esta pregunta, pero tuve problemas para encontrarla.

+2

Interesante pregunta. Sin embargo, "no bloqueo" no es la palabra correcta, creo. El hilo que pide memoria está, por supuesto, bloqueado hasta que recibe la memoria. Lo que está preguntando es si otros hilos también serían bloqueados en sus asignaciones de memoria (mi suposición es sí, ya que la memoria del montón es un recurso compartido). No tiene un buen término para eso, tal vez "simultaneidad de asignación de memoria". – Thilo

+1

"sin bloqueo" es la terminología correcta. Los algoritmos simultáneos se incluyen en las clases de bloqueo, bloqueo, no bloqueo o espera libre. Los algoritmos de bloqueo son obvios; sin embargo, hay sutiles distinciones entre las últimas tres clases. – Mark

+0

Todo depende. Algunos sistemas tienen diferentes versiones de la biblioteca estándar que están vinculadas con el ejecutable si el enhebrado está habilitado. –

Respuesta

6

me parece si su aplicación de interferencia utilizaba new/delete (malloc/free), entonces la aplicación de interferencia interferiría más con la prueba de no reciclaje. Pero no sé cómo se implementa su prueba de interferencia.

Dependiendo de cómo reciclas (es decir, si usas pthread mutexes, dios no lo quiera) tu código de reciclaje podría ser lento (gcc atomic ops sería 40 veces más rápido implementando reciclaje).

Malloc, en alguna variación desde hace mucho tiempo en al menos algunas plataformas, ha tenido conocimiento de los hilos. Use el compilador enciende gcc para asegurarse de obtenerlo. Los algoritmos más recientes mantienen agrupaciones de fragmentos de memoria pequeños para cada uno de los hilos, por lo que no hay ningún bloqueo o poco si su subproceso tiene el pequeño elemento disponible. Lo simplifiqué en exceso y depende de qué malloc esté usando su sistema. Además, si va y asigna millones de elementos para hacer una prueba ... bueno, entonces no verá ese efecto, porque los grupos de elementos pequeños tienen un tamaño limitado. O tal vez lo harás. No lo sé. Si liberaste el artículo justo después de la asignación, es más probable que lo veas. Los artículos pequeños liberados vuelven a las listas de artículos pequeños en lugar del montón compartido. Aunque "lo que sucede cuando el hilo B libera un elemento asignado por el hilo A" es un problema que puede o no tratarse en su versión de malloc y no se puede tratar de una manera no bloqueante. Por supuesto, si no lo liberó inmediatamente durante una prueba grande, entonces el hilo tendría que volver a llenar su lista de elementos pequeños muchas veces. Eso puede bloquear si lo intenta más de un hilo.Finalmente, en algún punto, el montón de su proceso solicitará al sistema la memoria dinámica, que obviamente puede bloquear.

¿Está utilizando elementos de memoria pequeña? Para su Malloc, no sé lo pequeño que sería, pero si usted es < 1k, eso es ciertamente pequeño. ¿Estás asignando y liberando uno después del otro, o asignando miles de nodos y luego liberando miles de nodos? ¿Estaba asignando su aplicación de interferencia? Todas estas cosas afectarán los resultados.

cómo reciclar con operaciones atómicas (CAS = comparar y swap):

primero Añadir un pNextFreeNode a su objeto de nodo. Utilicé void *, puedes usar tu tipo. Este código es para punteros de 32 bits, pero también funciona para 64 bits. Luego haz una pila de reciclaje global.

void *_pRecycleHead; // global head of recycle list. 

Agregar a reciclan la pila:

void *Old; 
while (1) { // concurrency loop 
    Old = _pRecycleHead; // copy the state of the world. We operate on the copy 
    pFreedNode->pNextFreeNode = Old; // chain the new node to the current head of recycled items 
    if (CAS(&_pRecycleHead, Old, pFreedNode)) // switch head of recycled items to new node 
    break; // success 
} 

quitar de la pila:

void *Old; 
while (Old = _pRecycleHead) { // concurrency loop, only look for recycled items if the head aint null 
    if (CAS(&_pRecycleHead, Old, Old->pNextFreeNode)) // switch head to head->next. 
    break; // success 
} 
pNodeYoucanUseNow = Old; 

Usando CAS significa que la operación tendrá éxito solamente si el artículo que está cambiando es el valor antiguo que pase in. Si hay una carrera y otro subía primero, entonces el valor anterior será diferente. En la vida real, esta carrera ocurre muy, muy raramente. CAS es solo un poco más lento que el hecho de establecer un valor en comparación con los mutexes ... se mece.

La eliminación de la pila, arriba, tiene una condición de carrera si agrega y quita el mismo elemento rápidamente. Lo resolvemos agregando una versión # a los datos de CAS'able. Si haces la versión # al mismo tiempo que el puntero a la cabeza de la pila de reciclaje, ganas. Usa una unión. No cuesta nada extra para CAS 64 bits.

union TRecycle { 
    struct { 
    int iVersion; 
    void *pRecycleHead; 
    } ; // we can set these. Note, i didn't name this struct. You may have to if you want ANSI 
    unsigned long long n64; // we cas this 
} 

Nota, Tendrás que ir a la estructura de 128 bit para 64 bit OS. por lo que la pila reciclaje mundial se parece a esto ahora:

TRecycle _RecycleHead; 

Agregar a reciclan la pila:

while (1) { // concurrency loop 
    TRecycle New,Old; 
    Old.n64 = _RecycleHead.n64; // copy state 
    New.n64 = Old.n64; // new state starts as a copy 
    pFreedNode->pNextFreeNode = Old.pRecycleHead; // link item to be recycled into recycle pile 
    New.pRecycleHead = pFreedNode; // make the new state 
    New.iVersion++; // adding item to list increments the version. 
    if (CAS(&_RecycleHead.n64, Old.n64, New.n64)) // now if version changed...we fail 
    break; // success 
} 

eliminar de pila:

while (1) { // concurrency loop 
    TRecycle New,Old; 
    Old.n64 = _RecycleHead.n64; // copy state 
    New.n64 = Old.n64; // new state starts as a copy 
    New.pRecycleHead = New.pRecycledHead.pNextFreeNode; // new will skip over first item in recycle list so we can have that item. 
    New.iVersion++; // taking an item off the list increments the version. 
    if (CAS(&_RecycleHead.n64, Old.n64, New.n64)) // we fail if version is different. 
    break; // success 
} 
pNodeYouCanUseNow = Old.pRecycledHead; 

Apuesto a que si usted recicla esta manera verá un aumento de perf.

2

Esto es más o menos lo mismo que this question.

Básicamente, malloc no está definido para ser seguro para subprocesos, pero los implementadores son libres de agregar la implementación para que sea seguro para subprocesos. Según su descripción, parece que su versión particular es.

Para estar seguro, en las palabras de Obi-Wan, "Use the Source, Luke". La fuente malloc estará disponible y, en general, es bastante fácil de leer.

@ Marcos, se puede obtener la fuente de libc estándar de GNU por

$ git clone git://sourceware.org/git/glibc.git 
$ cd glibc 
$ git checkout --track -b glibc-2_11-branch origin/release/2.11/master 

Véase también here. Recuerde que malloc se encuentra en la sección 3 del manual: es una función de biblioteca, por lo que no estará en las fuentes de su kernel. Sin embargo, es posible que deba leer en brk, sbrk, getrlimit y setrlimit y similares para averiguar qué hace el kernel.

Un enlace más: el GCC project.

De acuerdo, una más (Puedo parar en cualquier momento): here's a page desde la que puede descargar las fuentes. Descomprima el archivo y debe encontrarlo en ./malloc/malloc.c.

+1

-D_REENTRANT, p. – bmargulies

+1

Lo sentimos, pero la pregunta no era si malloc era seguro para subprocesos. Para que haya una programación concurrente, tiene que haber algún tipo de algoritmo de asignación de memoria segura para subprocesos. Lo que quiero saber es si el asignador de memoria en Linux es un algoritmo de no bloqueo (esto es diferente a ser seguro para subprocesos, o estar libre de bloqueos) – Mark

+0

No seas tonto. Mark, "thread safe" y "concurrent" requieren la misma propiedad: la atomicidad se conserva en toda la sección crítica de la operación, ya sea que el hilo de control esté siendo manejado por un método ligero (lo que generalmente llamamos "hilo") , un cambio de contexto de peso pesado en un modelo de multiprogramación, o cálculos reales paralelos en multiprocesamiento de memoria compartida. Cuando pregunta si es "no bloqueante", solo está preguntando si la sección crítica se maneja de manera tal que permita que varios subprocesos continúen. –

0

Respuesta corta: No.

Un hilo puede estar en el medio de new node(), y otro hilo también se puede ir a hacer new node(). El primer hilo se puede suspender, y el segundo puede terminar primero. Está bien. (suponiendo que nada en su constructor use un mutex)

Respuesta más larga: Multithreading es una jungla.El código no seguro podría funcionar bien durante una década, y luego falla todos los días durante una semana. Las condiciones de carrera pueden no provocar ningún problema en su máquina, pero explotar en la máquina de un cliente. Las aplicaciones de subprocesos múltiples introducen un nivel de incertidumbre, lo que requiere un esfuerzo adicional para escribir y comprender.

Entonces, ¿por qué estos dos programas funcionarían casi idénticos un día, y masivamente diferentes con la contención de la CPU? No lo sé. new no bloquea otros hilos de new, por lo que no es eso. Sospecho que con la sobrecarga adicional de nuevo/eliminar, el sistema operativo tiene más oportunidad de adelantarse a su programa (y tal vez incluso más probabilidades de hacerlo). Por lo tanto, cuando no hay interferencia, los dos programas obtienen la CPU tanto como quieren y funcionan bien, pero cuando la CPU es un recurso escaso, el programa nuevo/eliminar se ve afectado con más frecuencia que el de reciclaje. ¿Ver? Vale la pena reciclar ;-)

+1

Quizás esto se deba a que 'malloc' y' free' requieren un cambio de contexto en el kernel, y ese es un momento perfecto para que el kernel se adelante. De lo contrario, debe adelantarse al proceso en el territorio del usuario, y no creo que le guste tanto. – cdhowie

+1

@cdhowie: No me sorprendería si 'free' did * not * siempre requiere un cambio de contexto. 'malloc' tiene que ser sincrónico, mientras que' libre' simplemente se puede posponer para reducir el número de cambios de contexto (no sé si es el caso en Linux). Esto podría explicar la diferencia en los rendimientos –

+1

¿Malloc requiere un cambio de contexto al kernel? ¿En qué planeta es eso? – bmargulies

1

Esta pregunta tiene una serie de buenas respuestas: In multithreaded C/C++, does malloc/new lock the heap when allocating memory.

El consenso no es que no es de bloqueo. Por lo tanto, una asignación grande o una que requiera algún intercambio podría bloquear una asignación menor en otro hilo, incluso si el más pequeño pudiera terminar si no fuera por la asignación más grande en progreso.

gcc's new es seguro para subprocesos, si compila con soporte pthreads, pero eso no es realmente lo que está pidiendo.

Sé en Windows que puede crear su propio montón, que podría usarse para configurar la memoria al comienzo de su programa. No conozco ninguna llamada de linux/unix para hacer cosas similares.

+1

Fuera de interés [Leap Heap] (http://www.leapheap.com/) es un montón personalizado sin bloqueo para Windows. Ellos tienen una gran información en el sitio web en sus partes internas, lectura interesante. –

+0

@Chibacity, Gracias por compartir ese enlace. –

3

En los sistemas multiproceso, malloc() y free() (y new/delete) hacer suelen utilizar primitivas de sincronización para hacerlos seguros para llamar desde varios subprocesos.

Esta sincronización también afecta el rendimiento de algunas aplicaciones, en particular las aplicaciones que realizan una gran cantidad de asignación y desasignación en entornos altamente paralelos. Los asignadores de memoria multiproceso más eficientes son un campo activo de investigación; ver jemalloc y tcmalloc para dos conocidos.

+0

gracias por jemalloc, no lo sabía :) Dado que tanto jemalloc como tcmalloc usan el almacenamiento en caché local de subprocesos, supongo que no bloquean. –

+0

@Matthieu M .: En el camino rápido, sí. Todavía habría rutas lentas que se activan a veces y que usan bloqueo. Realmente no puede alejarse de eso, porque el asignador debe ser capaz de manejar casos de esquina, como un gran volumen de asignaciones en el subproceso A que se liberan por el subproceso B. – caf

+0

Consulte también Hoard (http: // www. .org), que fue el primer asignador escalable. – EmeryBerger

Cuestiones relacionadas