¿Cómo asignar memoria alineada solo usando la biblioteca estándar?

Resuelto JimDaniel asked hace 16 años • 17 respuestas

Acabo de terminar una prueba como parte de una entrevista de trabajo y una pregunta me dejó perplejo, incluso usando Google como referencia. Me gustaría ver qué puede hacer el equipo de StackOverflow con él:

La memset_16alignedfunción requiere que se le pase un puntero alineado de 16 bytes o se bloqueará.

a) ¿Cómo asignaría 1024 bytes de memoria y los alinearía con un límite de 16 bytes?
b) Libere la memoria después de que se memset_16alignedhaya ejecutado.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}
JimDaniel avatar Oct 23 '08 06:10 JimDaniel
Aceptado

Respuesta original

{
    void *mem = malloc(1024+16);
    void *ptr = ((char *)mem+16) & ~ 0x0F;
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

Respuesta fija

{
    void *mem = malloc(1024+15);
    void *ptr = ((uintptr_t)mem+15) & ~ (uintptr_t)0x0F;
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

Explicación según lo solicitado

El primer paso es asignar suficiente espacio libre, por si acaso. Dado que la memoria debe estar alineada en 16 bytes (lo que significa que la dirección del byte inicial debe ser múltiplo de 16), agregar 16 bytes adicionales garantiza que tengamos suficiente espacio. En algún lugar de los primeros 16 bytes, hay un puntero alineado de 16 bytes. (Tenga en cuenta que malloc()se supone que devuelve un puntero que está suficientemente bien alineado para cualquier propósito. Sin embargo, el significado de 'cualquiera' es principalmente para cosas como tipos básicos: long, double, long double, long longy punteros a objetos y punteros a funciones. Cuando está Al hacer cosas más especializadas, como jugar con sistemas gráficos, pueden necesitar una alineación más estricta que el resto del sistema; de ahí preguntas y respuestas como esta).

El siguiente paso es convertir el puntero void en un puntero char; A pesar de GCC, se supone que no debes hacer aritmética de punteros en punteros vacíos (y GCC tiene opciones de advertencia para avisarte cuando abusas de ella). Luego agregue 16 al puntero de inicio. Supongamos que malloc()le devolvió un puntero increíblemente mal alineado: 0x800001. Sumar los 16 da 0x800011. Ahora quiero redondear hacia abajo hasta el límite de 16 bytes, por lo que quiero restablecer los últimos 4 bits a 0. 0x0F tiene los últimos 4 bits establecidos en uno; por lo tanto, ~0x0Ftiene todos los bits establecidos en uno excepto los últimos cuatro. Y agregar eso con 0x800011 da 0x800010. Puede iterar sobre las otras compensaciones y ver que funciona la misma aritmética.

El último paso, free(), es fácil: siempre, y sólo, regresas a free()un valor que uno de malloc(), calloc()o realloc()te devolvió; cualquier otra cosa es un desastre. Proporcionó correctamente memmantener ese valor, gracias. Lo libera gratis.

Finalmente, si conoce los aspectos internos del mallocpaquete de su sistema, puede suponer que bien podría devolver datos alineados de 16 bytes (o podría estar alineado con 8 bytes). Si estuviera alineado con 16 bytes, entonces no necesitaría preocuparse por los valores. Sin embargo, esto es dudoso y no portátil: otros mallocpaquetes tienen alineaciones mínimas diferentes y, por lo tanto, asumir una cosa cuando hace algo diferente conduciría a volcados de núcleo. Dentro de amplios límites, esta solución es portátil.

Alguien más mencionó posix_memalign()como otra forma de alinear la memoria; eso no está disponible en todas partes, pero a menudo podría implementarse usando esto como base. Nótese que era conveniente que el alineamiento fuera una potencia de 2; otras alineaciones son más complicadas.

Un comentario más: este código no verifica que la asignación se haya realizado correctamente.

Enmienda

El programador de Windows señaló que no se pueden realizar operaciones de máscara de bits en punteros y, de hecho, GCC (3.4.6 y 4.3.1 probado) se queja de eso. A continuación se presenta una versión modificada del código básico, convertido en un programa principal. También me he tomado la libertad de añadir sólo 15 en lugar de 16, como ya se ha señalado. Lo estoy usando uintptr_tdesde que C99 existe el tiempo suficiente para ser accesible en la mayoría de las plataformas. Si no fuera por el uso de PRIXPTRen las printf()declaraciones, sería suficiente #include <stdint.h>en lugar de usar #include <inttypes.h>. [Este código incluye la solución señalada por CR , que reiteraba un punto planteado por primera vez por Bill K hace varios años, que logré pasar por alto hasta ahora.]

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void memset_16aligned(void *space, char byte, size_t nbytes)
{
    assert((nbytes & 0x0F) == 0);
    assert(((uintptr_t)space & 0x0F) == 0);
    memset(space, byte, nbytes);  // Not a custom implementation of memset()
}

int main(void)
{
    void *mem = malloc(1024+15);
    void *ptr = (void *)(((uintptr_t)mem+15) & ~ (uintptr_t)0x0F);
    printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
    memset_16aligned(ptr, 0, 1024);
    free(mem);
    return(0);
}

Y aquí hay una versión ligeramente más generalizada, que funcionará para tamaños que son una potencia de 2:

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void memset_16aligned(void *space, char byte, size_t nbytes)
{
    assert((nbytes & 0x0F) == 0);
    assert(((uintptr_t)space & 0x0F) == 0);
    memset(space, byte, nbytes);  // Not a custom implementation of memset()
}

static void test_mask(size_t align)
{
    uintptr_t mask = ~(uintptr_t)(align - 1);
    void *mem = malloc(1024+align-1);
    void *ptr = (void *)(((uintptr_t)mem+align-1) & mask);
    assert((align & (align - 1)) == 0);
    printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

int main(void)
{
    test_mask(16);
    test_mask(32);
    test_mask(64);
    test_mask(128);
    return(0);
}

Para convertirla test_mask()en una función de asignación de propósito general, el valor de retorno único del asignador tendría que codificar la dirección de liberación, como lo han indicado varias personas en sus respuestas.

Problemas con los entrevistadores.

Uri comentó: Tal vez esté teniendo [un] problema de comprensión lectora esta mañana, pero si la pregunta de la entrevista dice específicamente: "¿Cómo asignarías 1024 bytes de memoria?" y claramente asignas más que eso. ¿No sería eso un fracaso automático por parte del entrevistador?

Mi respuesta no cabe en un comentario de 300 caracteres...

Depende, supongo. Creo que la mayoría de las personas (incluyéndome a mí) entendieron que la pregunta significaba "¿Cómo asignaría un espacio en el que se pueden almacenar 1024 bytes de datos y donde la dirección base es un múltiplo de 16 bytes?". Si el entrevistador realmente quiso decir cómo se pueden asignar 1024 bytes (solo) y alinearlos con 16 bytes, entonces las opciones son más limitadas.

  • Claramente, una posibilidad es asignar 1024 bytes y luego darle a esa dirección el 'tratamiento de alineación'; El problema con ese enfoque es que el espacio disponible real no está adecuadamente determinado (el espacio utilizable está entre 1008 y 1024 bytes, pero no había un mecanismo disponible para especificar qué tamaño), lo que lo hace poco útil.
  • Otra posibilidad es que se espere que escriba un asignador de memoria completo y se asegure de que el bloque de 1024 bytes que devuelve esté alineado adecuadamente. Si ese es el caso, probablemente terminará realizando una operación bastante similar a la que hizo la solución propuesta, pero la ocultará dentro del asignador.

Sin embargo, si el entrevistador esperaba cualquiera de esas respuestas, esperaría que reconociera que esta solución responde a una pregunta estrechamente relacionada y luego reformulara su pregunta para orientar la conversación en la dirección correcta. (Además, si el entrevistador se volviera muy irritable, entonces no querría el trabajo; si la respuesta a un requisito insuficientemente preciso es derribada sin corrección, entonces el entrevistador no es alguien para quien sea seguro trabajar).

El mundo sigue adelante

El título de la pregunta ha cambiado recientemente. Fue la pregunta de la entrevista Resolver la alineación de la memoria en C lo que me dejó perplejo . El título revisado (¿ Cómo asignar memoria alineada solo usando la biblioteca estándar? ) exige una respuesta ligeramente revisada: este anexo la proporciona.

Función agregada C11 (ISO/IEC 9899:2011) aligned_alloc():

7.22.3.1 La aligned_allocfunción

Sinopsis

#include <stdlib.h>
void *aligned_alloc(size_t alignment, size_t size);

Descripción
La aligned_allocfunción asigna espacio para un objeto cuya alineación está especificada por alignment, cuyo tamaño está especificado por sizey cuyo valor es indeterminado. El valor de alignmentserá una alineación válida respaldada por la implementación y el valor de sizeserá un múltiplo entero de alignment.

Devuelve
La aligned_allocfunción devuelve un puntero nulo o un puntero al espacio asignado.

Y POSIX define posix_memalign():

#include <stdlib.h>

int posix_memalign(void **memptr, size_t alignment, size_t size);

DESCRIPCIÓN

La posix_memalign()función asignará sizebytes alineados en un límite especificado por alignmenty devolverá un puntero a la memoria asignada en memptr. El valor de alignmentserá una potencia de dos múltiplo de sizeof(void *).

Una vez completado con éxito, el valor señalado por memptrserá un múltiplo de alignment.

Si el tamaño del espacio solicitado es 0, el comportamiento está definido por la implementación; el valor devuelto memptrserá un puntero nulo o un puntero único.

La free()función desasignará la memoria que ha sido previamente asignada por posix_memalign().

VALOR DEVUELTO

Una vez completado con éxito, posix_memalign()devolverá cero; de lo contrario, se devolverá un número de error para indicar el error.

Cualquiera o ambos podrían usarse para responder la pregunta ahora, pero solo la función POSIX era una opción cuando se respondió la pregunta originalmente.

Detrás de escena, la nueva función de memoria alineada hace prácticamente el mismo trabajo que se describe en la pregunta, excepto que tiene la capacidad de forzar la alineación más fácilmente y realizar un seguimiento del inicio de la memoria alineada internamente para que el código no tiene que tratar especialmente: simplemente libera la memoria devuelta por la función de asignación que se utilizó.

Jonathan Leffler avatar Oct 22 '2008 23:10 Jonathan Leffler

Tres respuestas ligeramente diferentes según cómo se mire la pregunta:

1) Lo suficientemente buena para la pregunta exacta es la solución de Jonathan Leffler, excepto que para redondear a 16 alineados, solo necesita 15 bytes adicionales, no 16.

A:

/* allocate a buffer with room to add 0-15 bytes to ensure 16-alignment */
void *mem = malloc(1024+15);
ASSERT(mem); // some kind of error-handling code
/* round up to multiple of 16: add 15 and then round down by masking */
void *ptr = ((char*)mem+15) & ~ (size_t)0x0F;

B:

free(mem);

2) Para una función de asignación de memoria más genérica, la persona que llama no quiere tener que realizar un seguimiento de dos punteros (uno para usar y otro para liberar). Entonces almacena un puntero al búfer 'real' debajo del búfer alineado.

A:

void *mem = malloc(1024+15+sizeof(void*));
if (!mem) return mem;
void *ptr = ((char*)mem+sizeof(void*)+15) & ~ (size_t)0x0F;
((void**)ptr)[-1] = mem;
return ptr;

B:

if (ptr) free(((void**)ptr)[-1]);

Tenga en cuenta que, a diferencia de (1), donde solo se agregaron 15 bytes a mem, este código podría reducir la alineación si su implementación garantiza una alineación de 32 bytes desde malloc (poco probable, pero en teoría una implementación en C podría tener una alineación de 32 bytes). tipo alineado). Eso no importa si todo lo que haces es llamar a memset_16aligned, pero si usas la memoria para una estructura, entonces podría importar.

No estoy seguro de cuál es una buena solución para esto (aparte de advertir al usuario que el búfer devuelto no es necesariamente adecuado para estructuras arbitrarias), ya que no hay forma de determinar mediante programación cuál es la garantía de alineación específica de la implementación. Supongo que al inicio podrías asignar dos o más buffers de 1 byte y asumir que la peor alineación que ves es la alineación garantizada. Si te equivocas, desperdicias memoria. Cualquiera que tenga una idea mejor, por favor dígalo...

[ Agregado : El truco 'estándar' es crear una unión de 'tipos que probablemente estén alineados al máximo' para determinar la alineación requerida. Es probable que los tipos alineados al máximo sean (en C99) ' long long', ' long double', ' void *' o ' void (*)(void)'; si incluye <stdint.h>, presumiblemente podría usar ' intmax_t' en lugar de long long(y, en máquinas Power 6 (AIX), intmax_tle daría un tipo entero de 128 bits). Los requisitos de alineación para esa unión se pueden determinar incrustándola en una estructura con un único carácter seguido de la unión:

struct alignment
{
    char     c;
    union
    {
        intmax_t      imax;
        long double   ldbl;
        void         *vptr;
        void        (*fptr)(void);
    }        u;
} align_data;
size_t align = (char *)&align_data.u.imax - &align_data.c;

Luego usaría la alineación solicitada más grande (en el ejemplo, 16) y el alignvalor calculado anteriormente.

En Solaris 10 (64 bits), parece que la alineación básica para el resultado malloc()es un múltiplo de 32 bytes.
]

En la práctica, los asignadores alineados a menudo toman un parámetro para la alineación en lugar de estar cableado. Entonces, el usuario pasará el tamaño de la estructura que le interesa (o la potencia mínima de 2 mayor o igual a esa) y todo estará bien.

3) Utilice lo que proporciona su plataforma: posix_memalignpara POSIX, _aligned_mallocen Windows.

4) Si usa C11, entonces la opción más limpia, portátil y concisa, es usar la función de biblioteca estándar aligned_allocque se introdujo en esta versión de la especificación del lenguaje.

Steve Jessop avatar Oct 23 '2008 00:10 Steve Jessop

También puedes intentarlo posix_memalign()(en plataformas POSIX, por supuesto).

florin avatar Oct 22 '2008 23:10 florin

Aquí hay un enfoque alternativo para la parte de "redondeo". No es la solución codificada más brillantemente, pero hace el trabajo, y este tipo de sintaxis es un poco más fácil de recordar (además, funcionaría para valores de alineación que no son una potencia de 2). El uintptr_treparto era necesario para apaciguar al compilador; A la aritmética de punteros no le gusta mucho la división o la multiplicación.

void *mem = malloc(1024 + 15);
void *ptr = (void*) ((uintptr_t) mem + 15) / 16 * 16;
memset_16aligned(ptr, 0, 1024);
free(mem);
An̲̳̳drew avatar Oct 23 '2008 00:10 An̲̳̳drew

Desafortunadamente, en C99 parece bastante difícil garantizar cualquier tipo de alineación que sea portátil en cualquier implementación de C conforme a C99. ¿Por qué? Porque no se garantiza que un puntero sea la "dirección de byte" que uno podría imaginar con un modelo de memoria plana. Tampoco está tan garantizada la representación de uintptr_t , que de todos modos es un tipo opcional.

We might know of some implementations which use a representation for void * (and by definition, also char *) which is a simple byte address, but by C99 it is opaque to us, the programmers. An implementation might represent a pointer by a set {segment, offset} where offset could have who-knows-what alignment "in reality." Why, a pointer could even be some form of hash table lookup value, or even a linked-list lookup value. It could encode bounds information.

In a recent C1X draft for a C Standard, we see the _Alignas keyword. That might help a bit.

The only guarantee C99 gives us is that the memory allocation functions will return a pointer suitable for assignment to a pointer pointing at any object type. Since we cannot specify the alignment of objects, we cannot implement our own allocation functions with responsibility for alignment in a well-defined, portable manner.

It would be good to be wrong about this claim.

Shao avatar Aug 07 '2010 10:08 Shao