Wie ordne ich ausgerichteten Speicher nur mit der Standardbibliothek zu?


421

Ich habe gerade einen Test im Rahmen eines Vorstellungsgesprächs abgeschlossen, und eine Frage hat mich verblüfft, selbst wenn ich Google als Referenz verwendet habe. Ich würde gerne sehen, was die StackOverflow-Crew damit machen kann:

Die memset_16alignedFunktion erfordert einen 16-Byte-ausgerichteten Zeiger, der an sie übergeben wird. Andernfalls stürzt sie ab.

a) Wie würden Sie 1024 Byte Speicher zuweisen und ihn an einer 16-Byte-Grenze ausrichten?
b) Geben Sie den Speicher frei, nachdem der memset_16alignedausgeführt wurde.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

89
hmmm ... für eine langfristige Lebensfähigkeit des Codes, wie wäre es mit "Feuer, wer auch immer memset_16aligned geschrieben hat und es repariert oder ersetzt, damit es keine besondere Randbedingung hat"
Steven A. Lowe

29
Sicherlich eine berechtigte Frage - "warum die eigentümliche Speicherausrichtung". Es kann jedoch gute Gründe dafür geben - in diesem Fall kann es sein, dass memset_16aligned () 128-Bit-Ganzzahlen verwenden kann. Dies ist einfacher, wenn bekannt ist, dass der Speicher ausgerichtet ist. Usw.
Jonathan Leffler

5
Wer auch immer ein Memset geschrieben hat, kann die interne 16-Byte-Ausrichtung zum Löschen der inneren Schleife und einen kleinen Datenprolog / Epilog verwenden, um die nicht ausgerichteten Enden zu bereinigen. Das wäre viel einfacher, als Codierer dazu zu bringen, zusätzliche Speicherzeiger zu handhaben.
Adisak

8
Warum sollte jemand Daten an einer 16-Byte-Grenze ausrichten wollen? Wahrscheinlich, um es in 128-Bit-SSE-Register zu laden. Ich glaube, die (neueren) nicht ausgerichteten Movs (z. B. movupd, lddqu) sind langsamer oder zielen auf Prozessoren ohne SSE2 / 3 ab

11
Das Ausrichten der Adresse führt zu einer optimierten Nutzung des Caches sowie zu einer höheren Bandbreite zwischen verschiedenen Cache- und RAM-Ebenen (für die meisten gängigen Workloads). Siehe hier stackoverflow.com/questions/381244/purpose-of-memory-alignment
Deepthought

Antworten:


585

Ursprüngliche Antwort

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

Feste Antwort

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

Erklärung wie gewünscht

Der erste Schritt besteht darin, für alle Fälle genügend freien Speicherplatz zuzuweisen. Da der Speicher 16-Byte-ausgerichtet sein muss (was bedeutet, dass die Adresse des führenden Bytes ein Vielfaches von 16 sein muss), garantiert das Hinzufügen von 16 zusätzlichen Bytes, dass wir über genügend Speicherplatz verfügen. Irgendwo in den ersten 16 Bytes befindet sich ein auf 16 Bytes ausgerichteter Zeiger. (Beachten Sie, dass malloc()angeblich einen Zeiger zurückzugeben , die gut genug für ausgerichtet ist jeder . Zweck jedoch die Bedeutung von ‚any‘ ist in erster Linie für Dinge wie Grundtypen - long, double, long double, long long., Und Zeiger auf Objekte und Zeiger auf Funktionen Wenn Sie sind Wenn sie speziellere Dinge tun, wie das Spielen mit Grafiksystemen, müssen sie möglicherweise strenger ausgerichtet werden als der Rest des Systems - daher Fragen und Antworten wie diese.)

Der nächste Schritt besteht darin, den void-Zeiger in einen char-Zeiger umzuwandeln. Ungeachtet des GCC sollten Sie keine Zeigerarithmetik für leere Zeiger durchführen (und GCC verfügt über Warnoptionen, die Sie darüber informieren, wenn Sie es missbrauchen). Fügen Sie dann 16 zum Startzeiger hinzu. Angenommen, malloc()Sie haben einen unglaublich schlecht ausgerichteten Zeiger zurückgegeben: 0x800001. Das Hinzufügen der 16 ergibt 0x800011. Jetzt möchte ich auf die 16-Byte-Grenze abrunden - also möchte ich die letzten 4 Bits auf 0 zurücksetzen. Bei 0x0F sind die letzten 4 Bits auf eins gesetzt. Daher sind ~0x0Falle Bits mit Ausnahme der letzten vier auf eins gesetzt. Und das mit 0x800011 ergibt 0x800010. Sie können die anderen Offsets durchlaufen und sehen, dass dieselbe Arithmetik funktioniert.

Der letzte Schritt, free()ist einfach: Sie immer, und nur, Rückkehr zu free()einem Wert , dass einer malloc(), calloc()oder realloc()an Sie zurückgeschickt - alles andere ist eine Katastrophe. Sie haben richtig angegeben mem, um diesen Wert zu halten - danke. Das kostenlose veröffentlicht es.

Wenn Sie die Interna des Systems kennen malloc, können Sie davon ausgehen, dass es möglicherweise 16-Byte-ausgerichtete Daten zurückgibt (oder 8-Byte-ausgerichtet ist). Wenn es 16-Byte-ausgerichtet wäre, müssten Sie nicht mit den Werten dink. Dies ist jedoch zweifelhaft und nicht portabel - andere mallocPakete haben unterschiedliche Mindestausrichtungen, und daher würde die Annahme einer Sache, wenn sie etwas anderes tun, zu Core-Dumps führen. In weiten Grenzen ist diese Lösung portabel.

Jemand anderes erwähnte posix_memalign()als einen anderen Weg, um das ausgerichtete Gedächtnis zu erhalten; das ist nicht überall verfügbar, könnte aber oft auf dieser Basis implementiert werden. Beachten Sie, dass es praktisch war, dass die Ausrichtung eine Potenz von 2 war; andere Ausrichtungen sind unordentlicher.

Noch ein Kommentar - dieser Code überprüft nicht, ob die Zuordnung erfolgreich war.

Änderung

Windows Programmer wies darauf hin, dass Sie keine Bitmaskenoperationen für Zeiger ausführen können, und tatsächlich beschwert sich GCC (3.4.6 und 4.3.1 getestet) so. Es folgt also eine geänderte Version des Basiscodes, der in ein Hauptprogramm konvertiert wurde. Ich habe mir auch erlaubt, nur 15 statt 16 hinzuzufügen, wie bereits erwähnt wurde. Ich verwende uintptr_tC99 seit langem, um auf den meisten Plattformen verfügbar zu sein. Wenn es nicht für die Verwendung PRIXPTRin den printf()Anweisungen wäre, würde es ausreichen, #include <stdint.h>anstatt zu verwenden #include <inttypes.h>. [Dieser Code enthält die Korrektur, auf die CR hingewiesen hat und die einen Punkt wiederholte, den Bill K vor einigen Jahren zum ersten Mal gemacht hatte und den ich bisher übersehen habe.]

#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);
}

Und hier ist eine geringfügig allgemeinere Version, die für Größen mit einer Potenz von 2 funktioniert:

#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);
}

Um test_mask()in eine Allzweckzuweisungsfunktion umzuwandeln , müsste der einzelne Rückgabewert des Zuweisers die Freigabeadresse codieren, wie mehrere Personen in ihren Antworten angegeben haben.

Probleme mit Interviewern

Uri kommentierte: Vielleicht habe ich heute Morgen ein Problem mit dem Leseverständnis, aber wenn die Interviewfrage speziell sagt: "Wie würden Sie 1024 Bytes Speicher zuweisen?" Und Sie weisen eindeutig mehr als das zu. Wäre das nicht ein automatischer Fehler des Interviewers?

Meine Antwort passt nicht in einen Kommentar mit 300 Zeichen ...

Es kommt darauf an, nehme ich an. Ich denke, die meisten Leute (einschließlich mir) haben die Frage so verstanden: "Wie würden Sie einen Speicherplatz zuweisen, in dem 1024 Byte Daten gespeichert werden können und in dem die Basisadresse ein Vielfaches von 16 Byte ist?". Wenn der Interviewer wirklich gemeint hat, wie Sie 1024 Bytes (nur) zuweisen und 16 Bytes ausrichten können, sind die Optionen eingeschränkter.

  • Eine Möglichkeit besteht eindeutig darin, 1024 Bytes zuzuweisen und dieser Adresse dann die "Ausrichtungsbehandlung" zu geben. Das Problem bei diesem Ansatz besteht darin, dass der tatsächlich verfügbare Speicherplatz nicht richtig bestimmt wird (der nutzbare Speicherplatz liegt zwischen 1008 und 1024 Byte, aber es war kein Mechanismus verfügbar, um die Größe anzugeben), was ihn weniger nützlich macht.
  • Eine andere Möglichkeit besteht darin, dass von Ihnen erwartet wird, dass Sie einen vollständigen Speicherzuweiser schreiben und sicherstellen, dass der von Ihnen zurückgegebene 1024-Byte-Block entsprechend ausgerichtet ist. Wenn dies der Fall ist, führen Sie wahrscheinlich einen Vorgang aus, der dem der vorgeschlagenen Lösung ziemlich ähnlich ist, aber Sie verstecken ihn im Allokator.

Wenn der Interviewer jedoch eine dieser Antworten erwartet, würde ich erwarten, dass er erkennt, dass diese Lösung eine eng verwandte Frage beantwortet, und dann seine Frage neu formuliert, um das Gespräch in die richtige Richtung zu lenken. (Wenn der Interviewer wirklich schlampig geworden wäre, würde ich den Job nicht wollen. Wenn die Antwort auf eine unzureichend genaue Anforderung ohne Korrektur in Flammen niedergeschossen wird, ist der Interviewer nicht jemand, für den es sicher ist zu arbeiten.)

Die Welt bewegt sich weiter

Der Titel der Frage hat sich kürzlich geändert. Es war die Lösung der Gedächtnisausrichtung in der C-Interview-Frage, die mich verblüffte . Der überarbeitete Titel ( Wie ordne ich ausgerichteten Speicher nur mit der Standardbibliothek zu? ) Erfordert eine leicht überarbeitete Antwort - dieses Addendum enthält sie.

C11 (ISO / IEC 9899: 2011) hinzugefügte Funktion aligned_alloc():

7.22.3.1 Die aligned_allocFunktion

Zusammenfassung

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

Beschreibung
Die aligned_allocFunktion weist einem Objekt Platz zu, dessen Ausrichtung durch angegeben ist alignment, dessen Größe durch angegeben sizeist und dessen Wert unbestimmt ist. Der Wert von alignmentmuss eine gültige Ausrichtung sein, die von der Implementierung unterstützt wird, und der Wert von sizemuss ein ganzzahliges Vielfaches von sein alignment.

Gibt
Die aligned_allocFunktion gibt entweder einen Null - Zeiger oder einen Zeiger auf den zugewiesenen Platz.

Und POSIX definiert posix_memalign():

#include <stdlib.h>

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

BESCHREIBUNG

Die posix_memalign()Funktion weist sizeBytes zu, die an einer durch angegebenen Grenze ausgerichtet sind alignment, und gibt einen Zeiger auf den zugewiesenen Speicher in zurück memptr. Der Wert von alignmentist eine Potenz von zwei Vielfachen von sizeof(void *).

Nach erfolgreichem Abschluss muss der von angegebene Wert memptrein Vielfaches von sein alignment.

Wenn die Größe des angeforderten Speicherplatzes 0 ist, ist das Verhalten implementierungsdefiniert. Der zurückgegebene Wert memptrmuss entweder ein Nullzeiger oder ein eindeutiger Zeiger sein.

Die free()Funktion muss den zuvor zugewiesenen Speicher freigeben posix_memalign().

RÜCKGABEWERT

Nach erfolgreichem Abschluss posix_memalign()wird Null zurückgegeben; Andernfalls wird eine Fehlernummer zurückgegeben, um den Fehler anzuzeigen.

Eine oder beide könnten verwendet werden, um die Frage jetzt zu beantworten, aber nur die POSIX-Funktion war eine Option, als die Frage ursprünglich beantwortet wurde.

Hinter den Kulissen erledigt die neue Funktion für ausgerichteten Speicher fast die gleiche Aufgabe wie in der Frage beschrieben, außer dass sie die Ausrichtung einfacher erzwingen und den Start des ausgerichteten Speichers intern verfolgen kann, damit der Code dies nicht tut müssen sich speziell damit befassen - es gibt nur den Speicher frei, der von der verwendeten Zuordnungsfunktion zurückgegeben wird.


13
Und ich bin verrostet mit C ++, aber ich vertraue nicht wirklich darauf, dass ~ 0x0F richtig auf die Größe des Zeigers erweitert wird. Wenn dies nicht der Fall ist, bricht die Hölle los, weil Sie auch die wichtigsten Teile Ihres Zeigers maskieren. Da könnte ich mich allerdings irren.
Bill K

66
Übrigens funktioniert '+15' genauso gut wie '+16' ... in dieser Situation jedoch keine praktischen Auswirkungen.
Menkboy

15
Die '+ 15'-Kommentare von Menkboy und Greg sind korrekt, aber malloc () würde das mit ziemlicher Sicherheit auf 16 aufrunden. Die Verwendung von +16 ist geringfügig einfacher zu erklären. Die verallgemeinerte Lösung ist umständlich, aber machbar.
Jonathan Leffler

6
@Aerovistae: Dies ist eine leichte Trickfrage und hängt hauptsächlich von Ihrem Verständnis ab, wie eine beliebige Zahl (tatsächlich die vom Speicherzuweiser zurückgegebene Adresse) einer bestimmten Anforderung entspricht (Vielfaches von 16). Wenn Sie aufgefordert würden, 53 auf das nächste Vielfache von 16 aufzurunden, wie würden Sie das tun? Der Prozess ist für Adressen nicht sehr unterschiedlich; Es ist nur so, dass die Zahlen, mit denen Sie normalerweise zu tun haben, größer sind. Vergessen Sie nicht, Interviewfragen werden gestellt, um herauszufinden, wie Sie denken, und nicht um herauszufinden, ob Sie die Antwort kennen.
Jonathan Leffler

3
@akristmann: Der Originalcode ist korrekt, wenn Sie <inttypes.h>ab C99 verfügbar sind (zumindest für die Formatzeichenfolge - die Werte sollten wohl mit einer Umwandlung übergeben werden :) (uintptr_t)mem, (uintptr_t)ptr. Die Formatzeichenfolge basiert auf der Verkettung von Zeichenfolgen, und das PRIXPTR-Makro ist der richtige printf()Längen- und Typbezeichner für die Hex-Ausgabe für einen uintptr_tWert. Die Alternative ist die Verwendung, %paber die Ausgabe davon variiert je nach Plattform (einige fügen eine führende hinzu 0x, die meisten nicht) und wird normalerweise mit Hex-Ziffern in Kleinbuchstaben geschrieben, was mir nicht gefällt. Was ich geschrieben habe, ist plattformübergreifend einheitlich.
Jonathan Leffler

58

Drei leicht unterschiedliche Antworten, je nachdem, wie Sie die Frage betrachten:

1) Gut genug für die genaue gestellte Frage ist Jonathan Lefflers Lösung, außer dass Sie zum Aufrunden auf 16 ausgerichtet nur 15 zusätzliche Bytes benötigen, nicht 16.

EIN:

/* 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) Für eine allgemeinere Speicherzuweisungsfunktion möchte der Anrufer nicht zwei Zeiger verfolgen müssen (einen zum Verwenden und einen zum Freigeben). Sie speichern also einen Zeiger auf den 'echten' Puffer unter dem ausgerichteten Puffer.

EIN:

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]);

Beachten Sie, dass dieser Code im Gegensatz zu (1), bei dem nur 15 Bytes zu mem hinzugefügt wurden , die Ausrichtung tatsächlich reduzieren kann, wenn Ihre Implementierung eine 32-Byte-Ausrichtung von malloc garantiert (unwahrscheinlich, aber theoretisch könnte eine C-Implementierung 32 Byte haben ausgerichteter Typ). Das spielt keine Rolle, wenn Sie nur memset_16aligned aufrufen. Wenn Sie jedoch den Speicher für eine Struktur verwenden, kann dies von Bedeutung sein.

Ich bin mir nicht sicher, was eine gute Lösung dafür ist (außer den Benutzer zu warnen, dass der zurückgegebene Puffer nicht unbedingt für beliebige Strukturen geeignet ist), da es keine Möglichkeit gibt, programmgesteuert zu bestimmen, wie die implementierungsspezifische Ausrichtungsgarantie lautet. Ich denke, Sie könnten beim Start zwei oder mehr 1-Byte-Puffer zuweisen und davon ausgehen, dass die schlechteste Ausrichtung, die Sie sehen, die garantierte Ausrichtung ist. Wenn Sie sich irren, verschwenden Sie Speicher. Wer eine bessere Idee hat, sagt es bitte ...

[ Hinzugefügt : Der 'Standard'-Trick besteht darin, eine Vereinigung von' wahrscheinlich maximal ausgerichteten Typen 'zu erstellen, um die erforderliche Ausrichtung zu bestimmen. Die maximal ausgerichteten Typen sind wahrscheinlich (in C99) ' long long', ' long double', ' void *' oder ' void (*)(void)'; Wenn Sie einschließen <stdint.h>, könnten Sie vermutlich ' intmax_t' anstelle von long long(und auf Power 6 (AIX) -Maschinen intmax_teinen 128-Bit-Integer-Typ verwenden) verwenden. Die Ausrichtungsanforderungen für diese Vereinigung können bestimmt werden, indem sie in eine Struktur mit einem einzelnen Zeichen gefolgt von der Vereinigung eingebettet wird:

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;

Sie würden dann die größere der angeforderten Ausrichtung (im Beispiel 16) und den alignoben berechneten Wert verwenden.

Unter (64-Bit) Solaris 10 scheint die grundlegende Ausrichtung für das Ergebnis malloc()ein Vielfaches von 32 Byte zu sein.
]]

In der Praxis verwenden ausgerichtete Allokatoren häufig einen Parameter für die Ausrichtung, anstatt dass sie fest verdrahtet sind. Der Benutzer wird also die Größe der Struktur übergeben, die ihm wichtig ist (oder die kleinste Potenz von 2 größer oder gleich dieser), und alles wird gut.

3) Verwenden Sie das, was Ihre Plattform bietet: posix_memalignfür POSIX _aligned_mallocunter Windows.

4) Wenn Sie C11 verwenden, besteht die sauberste - tragbare und prägnante - Option darin, die Standardbibliotheksfunktion zu verwenden aligned_alloc, die in dieser Version der Sprachspezifikation eingeführt wurde.


1
Ich stimme zu - ich denke, die Absicht der Frage ist, dass der Code, der den Speicherblock freigibt, nur Zugriff auf den 'gekochten' 16-Byte-ausgerichteten Zeiger hat.
Michael Burr

1
Für eine allgemeine Lösung - Sie haben Recht. Die Codevorlage in der Frage zeigt jedoch deutlich beides.
Jonathan Leffler

1
Sicher, und in einem guten Interview geben Sie Ihre Antwort. Wenn der Interviewer meine Antwort sehen möchte, ändert er die Frage.
Steve Jessop

1
Ich lehne es ab ASSERT(mem);, die Zuordnungsergebnisse zu überprüfen. assertdient zum Abfangen von Programmierfehlern und nicht zum Mangel an Laufzeitressourcen.
Hlovdal

4
Die Verwendung von binär & mit a char *und a size_tführt zu einem Fehler. Sie müssten so etwas wie verwenden uintptr_t.
Marko


20

Hier ist eine alternative Herangehensweise an den Teil "Aufrunden". Nicht die brillanteste codierte Lösung, aber sie erledigt den Job, und diese Art von Syntax ist etwas einfacher zu merken (plus würde für Ausrichtungswerte funktionieren, die keine Zweierpotenz sind). Dasuintptr_t Besetzung war notwendig, um den Compiler zu beschwichtigen; Zeigerarithmetik ist nicht sehr angetan von Division oder Multiplikation.

void *mem = malloc(1024 + 15);
void *ptr = (void*) ((uintptr_t) mem + 15) / 16 * 16;
memset_16aligned(ptr, 0, 1024);
free(mem);

2
Wenn Sie "Long Long ohne Vorzeichen" haben, haben Sie im Allgemeinen auch uintptr_t, das explizit so definiert ist, dass es einen Datenzeiger enthält (void *). Aber Ihre Lösung hat in der Tat Vorteile, wenn Sie aus irgendeinem Grund eine Ausrichtung benötigen, die keine Potenz von 2 ist. Unwahrscheinlich, aber möglich.
Jonathan Leffler

@ Andrew: Upvoted für diese Art von Syntax ist etwas einfacher zu merken (plus würde für Ausrichtungswerte funktionieren, die keine Zweierpotenz sind) .
Legends2k

19

Leider scheint es in C99 ziemlich schwierig zu sein, eine Ausrichtung jeglicher Art auf eine Weise zu gewährleisten, die für jede C99-Implementierung, die C99 entspricht, portabel wäre. Warum? Da ein Zeiger nicht garantiert die "Byteadresse" ist, die man sich bei einem Flat-Memory-Modell vorstellen kann. Auch die Darstellung von uintptr_t ist nicht so garantiert, was selbst ohnehin ein optionaler Typ ist.

Wir kennen vielleicht einige Implementierungen, die eine Darstellung für void * (und per Definition auch char * ) verwenden, die eine einfache Byteadresse ist, aber nach C99 ist sie für uns Programmierer undurchsichtig. Eine Implementierung könnte einen Zeiger durch eine Menge { Segment , Offset } darstellen, wobei Offset "in der Realität" wer-weiß-was-Ausrichtung haben könnte. Ein Zeiger kann sogar eine Form von Hash-Tabellen-Suchwert oder sogar ein Suchwert für verknüpfte Listen sein. Es könnte Grenzinformationen codieren.

In einem aktuellen C1X-Entwurf für einen C-Standard sehen wir das Schlüsselwort _Alignas . Das könnte ein bisschen helfen.

Die einzige Garantie, die C99 uns gibt, besteht darin, dass die Speicherzuweisungsfunktionen einen Zeiger zurückgeben, der für die Zuordnung zu einem Zeiger geeignet ist, der auf einen beliebigen Objekttyp zeigt. Da wir die Ausrichtung von Objekten nicht spezifizieren können, können wir unsere eigenen Zuordnungsfunktionen, die für die Ausrichtung verantwortlich sind, nicht auf eine genau definierte, tragbare Weise implementieren.

Es wäre gut, sich in dieser Behauptung zu irren.


C11 hat aligned_alloc(). (C ++ 11/14 / 1z hat es immer noch nicht). _Alignas()und C ++ alignas()tun nichts für die dynamische Zuordnung, nur für die automatische und statische Speicherung (oder das Strukturlayout).
Peter Cordes

15

Auf der 16-gegen-15-Byte-Auffüllfront ist die tatsächliche Zahl, die Sie hinzufügen müssen, um eine Ausrichtung von N zu erhalten, max (0, NM), wobei M die natürliche Ausrichtung des Speicherzuweisers ist (und beide Potenzen von 2 sind).

Da die minimale Speicherausrichtung eines Allokators 1 Byte beträgt, ist 15 = max (0,16-1) eine konservative Antwort. Wenn Sie jedoch wissen, dass Ihr Speicherzuweiser Ihnen 32-Bit-Adressen mit int-Ausrichtung gibt (was ziemlich häufig vorkommt), hätten Sie 12 als Pad verwenden können.

Dies ist für dieses Beispiel nicht wichtig, kann jedoch auf einem eingebetteten System mit 12 KB RAM wichtig sein, bei dem jeder einzelne gespeicherte int zählt.

Der beste Weg, um es zu implementieren, wenn Sie tatsächlich versuchen, jedes mögliche Byte zu speichern, ist als Makro, damit Sie ihm Ihre native Speicherausrichtung zuführen können. Auch dies ist wahrscheinlich nur für eingebettete Systeme nützlich, bei denen Sie jedes Byte speichern müssen.

Im folgenden Beispiel ist auf den meisten Systemen der Wert 1 in Ordnung. Für MEMORY_ALLOCATOR_NATIVE_ALIGNMENTunser theoretisches eingebettetes System mit 32-Bit-ausgerichteten Zuordnungen kann jedoch Folgendes ein wenig wertvollen Speicherplatz sparen:

#define MEMORY_ALLOCATOR_NATIVE_ALIGNMENT    4
#define ALIGN_PAD2(N,M) (((N)>(M)) ? ((N)-(M)) : 0)
#define ALIGN_PAD(N) ALIGN_PAD2((N), MEMORY_ALLOCATOR_NATIVE_ALIGNMENT)

8

Vielleicht wären sie mit einem Wissen über Memalign zufrieden gewesen ? Und wie Jonathan Leffler betont, gibt es zwei neuere bevorzugte Funktionen, über die man Bescheid wissen muss.

Ups, Florin hat mich geschlagen. Wenn Sie jedoch die Manpage lesen, auf die ich verlinkt habe, werden Sie höchstwahrscheinlich das Beispiel eines früheren Posters verstehen.


1
Beachten Sie, dass die aktuelle ( im Februar 2016) Version der referenzierten Seite sagt : „Die memalignFunktion ist veraltet und aligned_allocoder posix_memalignsollte stattdessen verwendet werden“. Ich weiß nicht, was es im Oktober 2008 gesagt hat - aber es wurde wahrscheinlich nicht erwähnt, aligned_alloc()da dies zu C11 hinzugefügt wurde.
Jonathan Leffler

5

Wir machen so etwas die ganze Zeit für Accelerate.framework, eine stark vektorisierte OS X / iOS-Bibliothek, in der wir ständig auf die Ausrichtung achten müssen. Es gibt einige Optionen, von denen ich eine oder zwei oben nicht gesehen habe.

Die schnellste Methode für ein kleines Array wie dieses besteht darin, es einfach auf den Stapel zu kleben. Mit GCC / Clang:

 void my_func( void )
 {
     uint8_t array[1024] __attribute__ ((aligned(16)));
     ...
 }

Kein free () erforderlich. Dies sind normalerweise zwei Anweisungen: Subtrahieren Sie 1024 vom Stapelzeiger und dann UND den Stapelzeiger mit Ausrichtung. Vermutlich benötigte der Anforderer die Daten auf dem Heap, da seine Lebensdauer des Arrays den Stapel überschritt oder die Rekursion am Werk ist oder der Stapelspeicher einen hohen Stellenwert hat.

Unter OS X / iOS werden alle Aufrufe von malloc / calloc / etc. sind immer 16 Byte ausgerichtet. Wenn Sie beispielsweise 32 Byte für AVX benötigen, können Sie posix_memalign verwenden:

void *buf = NULL;
int err = posix_memalign( &buf, 32 /*alignment*/, 1024 /*size*/);
if( err )
   RunInCirclesWaivingArmsWildly();
...
free(buf);

Einige Leute haben die C ++ - Schnittstelle erwähnt, die ähnlich funktioniert.

Es sollte nicht vergessen werden, dass Seiten auf große Zweierpotenzen ausgerichtet sind, sodass seitenausgerichtete Puffer ebenfalls 16 Byte ausgerichtet sind. Daher sind auch mmap () und valloc () sowie andere ähnliche Schnittstellen Optionen. mmap () hat den Vorteil, dass der Puffer auf Wunsch vorinitialisiert mit etwas ungleich Null zugewiesen werden kann. Da diese eine seitenausgerichtete Größe haben, erhalten Sie nicht die Mindestzuordnung von diesen und es wird wahrscheinlich ein VM-Fehler auftreten, wenn Sie sie zum ersten Mal berühren.

Cheesy: Schalten Sie Guard Malloc oder ähnliches ein. Puffer mit einer Größe von n * 16 Byte wie dieser werden n * 16 Byte ausgerichtet, da VM zum Abfangen von Überläufen verwendet wird und ihre Grenzen an Seitengrenzen liegen.

Einige Accelerate.framework-Funktionen verwenden einen vom Benutzer bereitgestellten temporären Puffer, um ihn als Arbeitsbereich zu verwenden. Hier müssen wir davon ausgehen, dass der an uns übergebene Puffer völlig falsch ausgerichtet ist und der Benutzer aktiv versucht, unser Leben trotz allem schwer zu machen. (Unsere Testfälle kleben eine Schutzseite direkt vor und nach dem temporären Puffer, um den Trotz zu unterstreichen.) Hier geben wir die Mindestgröße zurück, die erforderlich ist, um ein 16-Byte-ausgerichtetes Segment irgendwo darin zu gewährleisten, und richten den Puffer anschließend manuell aus. Diese Größe ist erwünscht_Größe + Ausrichtung - 1. In diesem Fall sind das also 1024 + 16 - 1 = 1039 Bytes. Dann so ausrichten:

#include <stdint.h>
void My_func( uint8_t *tempBuf, ... )
{
    uint8_t *alignedBuf = (uint8_t*) 
                          (((uintptr_t) tempBuf + ((uintptr_t)alignment-1)) 
                                        & -((uintptr_t) alignment));
    ...
}

Durch Hinzufügen von Ausrichtung-1 wird der Zeiger an der ersten ausgerichteten Adresse vorbei bewegt, und durch UND-Verknüpfung mit -ausrichtung (z. B. 0xfff ... ff0 für Ausrichtung = 16) wird er zur ausgerichteten Adresse zurückgebracht.

Wie in anderen Beiträgen beschrieben, können Sie auf anderen Betriebssystemen ohne 16-Byte-Ausrichtungsgarantie malloc mit der größeren Größe aufrufen, den Zeiger später kostenlos () beiseite legen, dann wie unmittelbar oben beschrieben ausrichten und den ausgerichteten Zeiger verwenden beschrieben für unseren temporären Pufferfall.

Aligned_memset ist ziemlich dumm. Sie müssen nur bis zu 15 Bytes durchlaufen, um eine ausgerichtete Adresse zu erreichen, und anschließend mit ausgerichteten Speichern fortfahren, wobei am Ende möglicherweise ein Bereinigungscode angezeigt wird. Sie können die Bereinigungsbits sogar im Vektorcode ausführen, entweder als nicht ausgerichtete Speicher, die den ausgerichteten Bereich überlappen (vorausgesetzt, die Länge entspricht mindestens der Länge eines Vektors), oder Sie verwenden etwas wie movmaskdqu. Jemand ist nur faul. Es ist jedoch wahrscheinlich eine vernünftige Interviewfrage, wenn der Interviewer wissen möchte, ob Sie mit stdint.h, bitweisen Operatoren und Speichergrundlagen vertraut sind, damit das erfundene Beispiel vergeben werden kann.


5

Ich bin überrascht, dass niemand Shao gewählt hat ‚s Antwort , dass, wie ich es verstehe, ist es unmöglich, zu tun , was in Standard C99 gefragt ist, da formal nicht definiertes Verhalten einen Zeiger auf einen integralen Typ Umwandlung ist. (Abgesehen von dem Standard, der die Konvertierung von uintptr_t<-> void*zulässt, scheint der Standard jedoch keine Manipulationen des uintptr_tWerts und anschließende Konvertierung zuzulassen .)


Es ist nicht erforderlich, dass ein uintptr_t-Typ vorhanden ist oder dass seine Bits eine Beziehung zu den Bits im zugrunde liegenden Zeiger haben. Wenn Sie Speicher zu viel zuweisen, speichern Sie den Zeiger als unsigned char* myptr; und dann `mptr + = (16- (uintptr_t) my_ptr) & 0x0F berechnen, würde das Verhalten bei allen Implementierungen definiert, die my_ptr definieren, aber ob der resultierende Zeiger ausgerichtet wäre, würde von der Zuordnung zwischen uintptr_t Bits und Adressen abhängen.
Supercat

3

Die Verwendung von memalign, Aligned-Memory-Blöcken könnte eine gute Lösung für das Problem sein.


Beachten Sie, dass in der aktuellen Version (Februar 2016) der Seite , auf die verwiesen wird , "Die memalignFunktion ist veraltet undaligned_alloc oder posix_memalignsollte stattdessen verwendet werden“. Ich weiß nicht, was es im Oktober 2010 gesagt hat.
Jonathan Leffler

3

Das erste, was mir beim Lesen dieser Frage in den Sinn kam, war, eine ausgerichtete Struktur zu definieren, sie zu instanziieren und dann darauf zu zeigen.

Gibt es einen fundamentalen Grund, warum ich vermisse, da dies sonst niemand vorgeschlagen hat?

Als Randnotiz sehe ich keine Notwendigkeit für das, da ich ein Array von Zeichen verwendet habe (vorausgesetzt, das Zeichen des Systems ist 8 Bit (dh 1 Byte)) __attribute__((packed)) Notwendige (korrigieren Sie mich, wenn ich falsch liege), aber ich sage es in sowieso.

Dies funktioniert auf zwei Systemen, auf denen ich es ausprobiert habe, aber es ist möglich, dass es eine Compiler-Optimierung gibt, von der ich nicht weiß, dass sie mir hinsichtlich der Wirksamkeit des Codes falsch positive Ergebnisse liefert. ich benutztegcc 4.9.2 unter OSX und gcc 5.2.1Ubuntu verwendet.

#include <stdio.h>
#include <stdlib.h>

int main ()
{

   void *mem;

   void *ptr;

   // answer a) here
   struct __attribute__((packed)) s_CozyMem {
       char acSpace[16];
   };

   mem = malloc(sizeof(struct s_CozyMem));
   ptr = mem;

   // memset_16aligned(ptr, 0, 1024);

   // Check if it's aligned
   if(((unsigned long)ptr & 15) == 0) printf("Aligned to 16 bytes.\n");
   else printf("Rubbish.\n");

   // answer b) here
   free(mem);

   return 1;
}

1

MacOS X-spezifisch:

  1. Alle mit malloc zugewiesenen Zeiger sind 16 Byte ausgerichtet.
  2. C11 wird unterstützt, Sie können also einfach align_malloc (16, Größe) aufrufen.

  3. MacOS X wählt Code aus, der beim Booten für einzelne Prozessoren für memset, memcpy und memmove optimiert ist, und dieser Code verwendet Tricks, von denen Sie noch nie gehört haben, um ihn schnell zu machen. 99% ige Wahrscheinlichkeit, dass Memset schneller ausgeführt wird als jedes handgeschriebene Memset16, wodurch die gesamte Frage sinnlos wird.

Wenn Sie eine 100% tragbare Lösung wünschen, gibt es vor C11 keine. Weil es keine tragbare Möglichkeit gibt, die Ausrichtung eines Zeigers zu testen. Wenn es nicht 100% portabel sein muss, können Sie verwenden

char* p = malloc (size + 15);
p += (- (unsigned int) p) % 16;

Dies setzt voraus, dass die Ausrichtung eines Zeigers in den niedrigsten Bits gespeichert wird, wenn ein Zeiger in vorzeichenloses int konvertiert wird. Die Konvertierung in unsigned int verliert Informationen und ist in der Implementierung definiert. Dies spielt jedoch keine Rolle, da das Ergebnis nicht zurück in einen Zeiger konvertiert wird.

Das Schreckliche ist natürlich, dass der ursprüngliche Zeiger irgendwo gespeichert werden muss, um damit free () aufzurufen. Alles in allem würde ich die Weisheit dieses Entwurfs wirklich bezweifeln.


1
Wo finden Sie aligned_mallocin OS X? Ich verwende Xcode 6.1 und es ist nirgendwo im iOS SDK definiert oder irgendwo in deklariert /usr/include/*.
Todd Lehman

Das Gleiche gilt für XCode 7.2 unter El Capitan (Mac OS X 10.11.3). Die C11-Funktion ist auf jeden Fall aligned_alloc(), aber das wird auch nicht deklariert. Von GCC 5.3.0 bekomme ich die interessanten Nachrichten alig.c:7:15: error: incompatible implicit declaration of built-in function ‘aligned_alloc’ [-Werror]und alig.c:7:15: note: include ‘<stdlib.h>’ or provide a declaration of ‘aligned_alloc’. Der Code enthielt zwar die Fehlermeldungen, änderte sie <stdlib.h>jedoch weder -std=c11noch -std=gnu11.
Jonathan Leffler

0

Sie können auch 16 Bytes hinzufügen und dann den ursprünglichen ptr auf 16 Bit ausrichten, indem Sie (16-mod) wie unter dem Zeiger hinzufügen:

main(){
void *mem1 = malloc(1024+16);
void *mem = ((char*)mem1)+1; // force misalign ( my computer always aligns)
printf ( " ptr = %p \n ", mem );
void *ptr = ((long)mem+16) & ~ 0x0F;
printf ( " aligned ptr = %p \n ", ptr );

printf (" ptr after adding diff mod %p (same as above ) ", (long)mem1 + (16 -((long)mem1%16)) );


free(mem1);
}

0

Wenn es Einschränkungen gibt, bei denen Sie kein einzelnes Byte verschwenden können, funktioniert diese Lösung: Hinweis: Es gibt einen Fall, in dem dies unendlich ausgeführt werden kann: D.

   void *mem;  
   void *ptr;
try:
   mem =  malloc(1024);  
   if (mem % 16 != 0) {  
       free(mem);  
       goto try;
   }  
   ptr = mem;  
   memset_16aligned(ptr, 0, 1024);

Es besteht eine sehr gute Chance, dass der ursprüngliche Block erneut zurückgegeben wird, wenn Sie einen Block mit N Bytes zuweisen und dann freigeben und dann einen weiteren Block mit N Bytes anfordern. Eine Endlosschleife ist also sehr wahrscheinlich, wenn die erste Zuordnung die Ausrichtungsanforderungen nicht erfüllt. Dadurch wird natürlich vermieden, dass ein einzelnes Byte auf Kosten vieler CPU-Zyklen verschwendet wird.
Jonathan Leffler

Sind Sie sicher, dass der %Operator void*auf sinnvolle Weise definiert ist?
Ajay Brahmakshatriya

0

Für die Lösung habe ich ein Konzept des Auffüllens verwendet, das den Speicher ausrichtet und nicht den Speicher eines einzelnen Bytes verschwendet.

Wenn es Einschränkungen gibt, können Sie kein einzelnes Byte verschwenden. Alle mit malloc zugewiesenen Zeiger sind 16 Byte ausgerichtet.

C11 wird unterstützt, Sie können also einfach anrufen aligned_alloc (16, size).

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

1
Auf vielen 64-Bit-Systemen ist der von zurückgegebene Zeiger malloc()zwar an einer 16-Byte-Grenze ausgerichtet, aber nichts in einem Standard garantiert dies - er ist einfach für jede Verwendung ausreichend gut ausgerichtet, und auf vielen 32-Bit-Systemen, die auf einer ausgerichtet sind Eine 8-Byte-Grenze ist ausreichend, und für einige ist eine 4-Byte-Grenze ausreichend.
Jonathan Leffler

0
size =1024;
alignment = 16;
aligned_size = size +(alignment -(size %  alignment));
mem = malloc(aligned_size);
memset_16aligned(mem, 0, 1024);
free(mem);

Hoffe, dies ist die einfachste Implementierung, lassen Sie mich Ihre Kommentare wissen.


-3
long add;   
mem = (void*)malloc(1024 +15);
add = (long)mem;
add = add - (add % 16);//align to 16 byte boundary
ptr = (whatever*)(add);

Ich denke, es gibt ein Problem damit, weil Ihr Add auf einen Ort verweist, der nicht malloc'd ist - Ich bin mir nicht sicher, wie dies bei Ihnen funktioniert hat.
Ergebnisseway

@ Sam Es sollte sein add += 16 - (add % 16). (2 - (2 % 16)) == 0.
SS Anne
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.