Risposta originale
{
void *mem = malloc(1024+16);
void *ptr = ((char *)mem+16) & ~ 0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);
}
Risposta fissa
{
void *mem = malloc(1024+15);
void *ptr = ((uintptr_t)mem+15) & ~ (uintptr_t)0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);
}
Spiegazione come richiesto
Il primo passo è quello di allocare abbastanza spazio libero, per ogni evenienza. Poiché la memoria deve essere allineata a 16 byte (il che significa che l'indirizzo di byte iniziale deve essere un multiplo di 16), l'aggiunta di 16 byte aggiuntivi garantisce che abbiamo spazio sufficiente. Da qualche parte nei primi 16 byte, c'è un puntatore allineato a 16 byte. (Si noti che malloc()
dovrebbe restituire un puntatore che è sufficientemente ben allineata per qualsiasi . Scopo Tuttavia, il significato di 'qualsiasi' è in primo luogo per le cose come base i tipi - long
, double
, long double
, long long
., E puntatori a oggetti e puntatori a funzioni Quando si è facendo cose più specializzate, come giocare con i sistemi grafici, possono aver bisogno di un allineamento più rigoroso rispetto al resto del sistema, quindi domande e risposte come questa.)
Il prossimo passo è convertire il puntatore vuoto in un puntatore carattere; Nonostante GCC, non dovresti eseguire l'aritmetica dei puntatori su puntatori vuoti (e GCC ha opzioni di avviso che ti informano quando lo abusi). Quindi aggiungi 16 al puntatore iniziale. Supponiamo che malloc()
ti abbia restituito un puntatore incredibilmente mal allineato: 0x800001. Aggiungendo il 16 si ottiene 0x800011. Ora voglio arrotondare per difetto al limite di 16 byte - quindi voglio reimpostare gli ultimi 4 bit su 0. 0x0F ha gli ultimi 4 bit impostati su uno; pertanto, ~0x0F
tutti i bit sono impostati su uno tranne gli ultimi quattro. Anding che con 0x800011 dà 0x800010. Puoi iterare sugli altri offset e vedere che la stessa aritmetica funziona.
L'ultimo passo free()
è semplice: tu ritorni sempre e solo a free()
un valore di cui sei tornato malloc()
, calloc()
o realloc()
qualsiasi altra cosa è un disastro. Hai fornito correttamente mem
per mantenere quel valore - grazie. Il libero lo rilascia.
Infine, se conosci gli interni del malloc
pacchetto del tuo sistema , puoi immaginare che potrebbe restituire dati allineati a 16 byte (o potrebbe essere allineato a 8 byte). Se fosse allineato a 16 byte, non sarà necessario annusare i valori. Tuttavia, questo è ingannevole e non portatile - altri malloc
pacchetti hanno diversi allineamenti minimi e quindi assumere una cosa quando fa qualcosa di diverso porterebbe a core dump. Entro ampi limiti, questa soluzione è portatile.
Qualcun altro ha menzionato posix_memalign()
come un altro modo per ottenere la memoria allineata; che non è disponibile ovunque, ma spesso può essere implementato usando questo come base. Si noti che era conveniente che l'allineamento fosse una potenza di 2; altri allineamenti sono più disordinati.
Ancora un commento: questo codice non verifica che l'allocazione abbia avuto esito positivo.
Emendamento
Il programmatore di Windows ha sottolineato che non è possibile eseguire operazioni di maschera di bit sui puntatori e, in effetti, GCC (testato 3.4.6 e 4.3.1) si lamenta in questo modo. Quindi, segue una versione modificata del codice di base, convertita in un programma principale. Ho anche preso la libertà di aggiungere solo 15 invece di 16, come è stato sottolineato. Sto usando da uintptr_t
quando C99 è stato abbastanza a lungo per essere accessibile sulla maggior parte delle piattaforme. Se non fosse per l'uso di PRIXPTR
nelle printf()
dichiarazioni, sarebbe sufficiente #include <stdint.h>
invece di usare #include <inttypes.h>
. [Questo codice include la correzione evidenziata da CR , che stava ribadendo un punto sollevato per la prima volta da Bill K alcuni anni fa, che fino ad ora sono riuscito a trascurare.]
#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);
}
Ed ecco una versione leggermente più generalizzata, che funzionerà per dimensioni che hanno una potenza di 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);
}
Per convertire test_mask()
in una funzione di allocazione per scopi generici, il singolo valore di ritorno dall'allocatore dovrebbe codificare l'indirizzo di rilascio, come indicato da diverse persone nelle loro risposte.
Problemi con gli intervistatori
Uri ha commentato: Forse stamattina sto riscontrando un problema di comprensione della lettura, ma se la domanda dell'intervista dice nello specifico: "Come assegneresti 1024 byte di memoria" e tu assegneresti chiaramente altro. Non sarebbe un fallimento automatico dell'intervistatore?
La mia risposta non si adatta a un commento di 300 caratteri ...
Dipende, suppongo. Penso che la maggior parte delle persone (incluso me) abbia posto la domanda nel senso "Come assegneresti uno spazio in cui è possibile memorizzare 1024 byte di dati e dove l'indirizzo di base è un multiplo di 16 byte". Se l'intervistatore intendeva davvero come si possono allocare 1024 byte (solo) e averlo allineato a 16 byte, le opzioni sono più limitate.
- Chiaramente, una possibilità è di allocare 1024 byte e quindi assegnare a tale indirizzo il "trattamento di allineamento"; il problema con questo approccio è che lo spazio disponibile effettivo non è determinato correttamente (lo spazio utilizzabile è compreso tra 1008 e 1024 byte, ma non era disponibile un meccanismo per specificare quale dimensione), il che lo rende poco utile.
- Un'altra possibilità è che si prevede di scrivere un allocatore di memoria completo e assicurarsi che il blocco da 1024 byte restituito sia adeguatamente allineato. In tal caso, probabilmente finisci per fare un'operazione abbastanza simile a quella della soluzione proposta, ma la nascondi all'interno dell'allocatore.
Tuttavia, se l'intervistatore si aspettasse una di quelle risposte, mi aspetterei che riconoscessero che questa soluzione risponde a una domanda strettamente correlata, e quindi riformulere la loro domanda per indirizzare la conversazione nella direzione corretta. (Inoltre, se l'intervistatore si fosse davvero mosso, non avrei voluto il lavoro; se la risposta a un requisito insufficientemente preciso venisse abbattuta in fiamme senza correzione, l'intervistatore non sarebbe qualcuno per il quale è sicuro lavorare.)
Il mondo va avanti
Il titolo della domanda è cambiato di recente. È stato Risolvere l'allineamento della memoria nella domanda dell'intervista in C che mi ha lasciato senza parole . Il titolo rivisto ( Come allocare la memoria allineata usando solo la libreria standard? ) Richiede una risposta leggermente rivista - questo addendum lo fornisce.
C11 (ISO / IEC 9899: 2011) funzione aggiunta aligned_alloc()
:
7.22.3.1 La aligned_alloc
funzione
Sinossi
#include <stdlib.h>
void *aligned_alloc(size_t alignment, size_t size);
Descrizione
La aligned_alloc
funzione alloca spazio per un oggetto il cui allineamento è specificato da alignment
, la cui dimensione è specificata da size
e il cui valore è indeterminato. Il valore di alignment
deve essere un allineamento valido supportato dall'attuazione e il valore di size
deve essere un multiplo integrale di alignment
.
Restituisce
La aligned_alloc
funzione restituisce un puntatore nullo o un puntatore allo spazio allocato.
E POSIX definisce posix_memalign()
:
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
DESCRIZIONE
La posix_memalign()
funzione deve allocare size
byte allineati su un limite specificato da alignment
, e deve restituire un puntatore alla memoria allocata in memptr
. Il valore di alignment
deve essere una potenza di due multipli di sizeof(void *)
.
In caso di completamento con esito positivo, il valore indicato da memptr
deve essere un multiplo di alignment
.
Se la dimensione dello spazio richiesto è 0, il comportamento è definito dall'implementazione; il valore restituito memptr
deve essere un puntatore nullo o un puntatore univoco.
La free()
funzione deve deallocare la memoria precedentemente allocata da posix_memalign()
.
VALORE DI RITORNO
In caso di completamento con esito positivo, posix_memalign()
deve restituire zero; in caso contrario, verrà restituito un numero di errore per indicare l'errore.
Uno o entrambi questi potrebbero essere utilizzati per rispondere alla domanda ora, ma solo la funzione POSIX era un'opzione quando la domanda era originariamente risposta.
Dietro le quinte, la nuova funzione di memoria allineata fa lo stesso lavoro delineato nella domanda, tranne per il fatto che hanno la capacità di forzare l'allineamento più facilmente e di tenere traccia dell'inizio della memoria allineata internamente in modo che il codice non devono occuparsi in modo particolare: libera solo la memoria restituita dalla funzione di allocazione utilizzata.