Jak przydzielić wyrównaną pamięć tylko przy użyciu standardowej biblioteki?


421

Właśnie skończyłem test w ramach rozmowy kwalifikacyjnej i jedno pytanie mnie zaskoczyło, nawet używając Google w celach informacyjnych. Chciałbym zobaczyć, co załoga StackOverflow może z tym zrobić:

Ta memset_16alignedfunkcja wymaga przekazania 16-bajtowego wyrównanego wskaźnika, w przeciwnym razie nastąpi awaria.

a) W jaki sposób przydzielisz 1024 bajty pamięci i dopasujesz ją do granicy 16 bajtów?
b) Zwolnij pamięć po memset_16alignedwykonaniu.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

89
hmmm ... jeśli chodzi o długoterminową żywotność kodu, co powiesz na „Zwolnij tego, kto napisał memset_16, wyrównaj go i napraw lub wymień, aby nie miał szczególnego warunku brzegowego”
Steven A. Lowe

29
Z pewnością ważne pytanie, które należy zadać - „dlaczego szczególne wyrównanie pamięci”. Ale mogą być ku temu dobre powody - w tym przypadku może być tak, że memset_16aligned () może używać 128-bitowych liczb całkowitych i jest to łatwiejsze, jeśli wiadomo, że pamięć jest wyrównana. Itd.
Jonathan Leffler

5
Ktokolwiek napisał zestaw, mógłby użyć wewnętrznego 16-bajtowego wyrównania do wyczyszczenia wewnętrznej pętli i małego prologu danych / epilogu do wyczyszczenia niepasujących końców. Byłoby to znacznie łatwiejsze niż spowodowanie, że kodery obsługiwałyby dodatkowe wskaźniki pamięci.
Adisak

8
Dlaczego ktoś miałby chcieć wyrównywać dane do granicy 16 bajtów? Prawdopodobnie załaduje go do 128-bitowych rejestrów SSE. Wierzę, że (nowsze) niezaangażowane movs (np. Movupd, lddqu) są wolniejsze, a może

11
Wyrównanie adresu prowadzi do optymalnego wykorzystania pamięci podręcznej, a także większej przepustowości między różnymi poziomami pamięci podręcznej i pamięci RAM (w przypadku większości typowych obciążeń). Zobacz tutaj stackoverflow.com/questions/381244/purpose-of-memory-alignment
Głębokie przemyślenie,

Odpowiedzi:


585

Oryginalna odpowiedź

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

Naprawiono odpowiedź

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

Wyjaśnienie zgodnie z prośbą

Pierwszym krokiem jest przydzielenie wystarczającej ilości wolnego miejsca, na wszelki wypadek. Ponieważ pamięć musi być wyrównana do 16 bajtów (co oznacza, że ​​początkowy adres bajtu musi być wielokrotnością 16), dodanie 16 dodatkowych bajtów gwarantuje, że mamy wystarczająco dużo miejsca. Gdzieś w pierwszych 16 bajtach znajduje się 16-bajtowy wyrównany wskaźnik. (Należy pamiętać, że malloc()ma powrócić wskaźnik, który jest dostatecznie dobrze wyrównany dla dowolnego . Celów, jednak sens „każdy” jest przede wszystkim na takie rzeczy jak podstawowe typy - long, double, long double, long long., A wskaźniki do obiektów i wskaźniki do funkcji Kiedy jesteś robiąc bardziej wyspecjalizowane rzeczy, np. grając z systemami graficznymi, mogą wymagać bardziej rygorystycznego dostosowania niż reszta systemu - stąd takie pytania i odpowiedzi).

Następnym krokiem jest konwersja wskaźnika pustki na wskaźnik char; Niezależnie od GCC, nie powinieneś wykonywać arytmetyki wskaźnika na pustych wskaźnikach (a GCC ma opcje ostrzegania, aby poinformować cię, gdy nadużyjesz). Następnie dodaj 16 do wskaźnika początkowego. Załóżmy, że malloc()zwrócił ci niemożliwie źle ustawiony wskaźnik: 0x800001. Dodanie 16 daje 0x800011. Teraz chcę zaokrąglić w dół do granicy 16 bajtów - więc chcę zresetować ostatnie 4 bity na 0. 0x0F ma ostatnie 4 bity ustawione na jeden; dlatego ~0x0Fma wszystkie bity ustawione na jeden oprócz ostatnich czterech. Po dodaniu 0x800011 otrzymujemy 0x800010. Możesz iterować po innych odsunięciach i zobaczyć, że działa ta sama arytmetyka.

Ostatnim krokiem free()jest proste: zawsze i tylko, powrót do free()wartości, która z jednej malloc(), calloc()lub realloc()zwrócone do siebie - nic innego nie jest katastrofą. Podałeś poprawnie, memaby zachować tę wartość - dziękuję. Bezpłatny wydaje to.

Wreszcie, jeśli wiesz o wewnętrznych mallocelementach pakietu systemu , możesz zgadywać, że może on zwrócić 16-bajtowe dane (lub może być 8-bajtowe). Jeśli byłby wyrównany do 16 bajtów, nie musiałbyś mrugać z wartościami. Jest to jednak podejrzane i nieprzenośne - inne mallocpakiety mają różne minimalne wyrównania, a zatem zakładanie jednej rzeczy, gdy robi coś innego, prowadziłoby do zrzutów rdzenia. W szerokich granicach to rozwiązanie jest przenośne.

Ktoś inny wymieniony posix_memalign()jako inny sposób na uzyskanie wyrównanej pamięci; nie jest to dostępne wszędzie, ale często można je wdrożyć, wykorzystując to jako podstawę. Zauważ, że wygodnie było, aby wyrównanie miało moc 2; inne dopasowania są bardziej chaotyczne.

Jeszcze jeden komentarz - ten kod nie sprawdza, czy alokacja się powiodła.

Poprawka

Programista Windows zauważył, że nie można wykonywać operacji maskowania bitów na wskaźnikach, i rzeczywiście GCC (testowane 3.4.6 i 4.3.1) tak narzeka. Tak więc następuje poprawiona wersja kodu podstawowego - przekonwertowana na program główny. Jak już wspomniano, mogłem również dodać tylko 15 zamiast 16. Używam, uintptr_todkąd C99 jest wystarczająco długi, aby był dostępny na większości platform. Gdyby nie do użycia PRIXPTRw printf()instrukcjach, wystarczyłoby użyć #include <stdint.h>zamiast #include <inttypes.h>. [Ten kod zawiera poprawkę wskazaną przez CR , która przypominała punkt, który Bill K po raz pierwszy przedstawił kilka lat temu, a który udało mi się przeoczyć do tej pory.]

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

A oto wersja nieco bardziej uogólniona, która będzie działać dla rozmiarów o sile 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);
}

Aby przekształcić test_mask()w funkcję alokacji ogólnego przeznaczenia, pojedyncza wartość zwrotna z alokatora musiałaby zakodować adres wydania, jak wskazało kilka osób w swoich odpowiedziach.

Problemy z ankieterami

Uri skomentował: Może mam dziś rano problem ze zrozumieniem czytania, ale jeśli pytanie z wywiadu wyraźnie mówi: „Jak byś przydzielił 1024 bajty pamięci” i wyraźnie przydzielisz więcej. Czy nie byłaby to automatyczna porażka ankietera?

Moja odpowiedź nie pasuje do komentarza złożonego z 300 znaków ...

To chyba zależy. Myślę, że większość ludzi (w tym ja) przyjęła pytanie w znaczeniu „Jak byś przydzielił przestrzeń, w której można przechowywać 1024 bajty danych, a adres podstawowy to wielokrotność 16 bajtów”. Jeśli ankieter naprawdę miał na myśli, jak można przydzielić 1024 bajty (tylko) i ustawić 16 bajtów na wyrównanie, wówczas opcje są bardziej ograniczone.

  • Oczywiście jedną z możliwości jest przydzielenie 1024 bajtów, a następnie nadanie temu adresowi „traktowania wyrównania”; problem z tym podejściem polega na tym, że rzeczywista dostępna przestrzeń nie jest właściwie określona (przestrzeń użyteczna wynosi od 1008 do 1024 bajtów, ale nie był dostępny żaden mechanizm określania, który rozmiar), co czyni ją mniej niż użyteczną.
  • Inną możliwością jest zapisanie pełnego alokatora pamięci i upewnienie się, że zwracany blok 1024-bajtowy jest odpowiednio wyrównany. W takim przypadku prawdopodobnie wykonasz operację dość podobną do tego, co zrobiło proponowane rozwiązanie, ale ukryjesz ją w alokatorze.

Jeśli jednak ankieter oczekiwałby jednej z tych odpowiedzi, spodziewałbym się, że rozpozna, że ​​to rozwiązanie odpowiada na ściśle powiązane pytanie, a następnie ponownie sformułuje swoje pytanie, aby skierować rozmowę we właściwym kierunku. (Ponadto, jeśli ankieter stałby się naprawdę niespokojny, to nie chciałbym pracy; jeśli odpowiedź na niewystarczająco precyzyjne wymaganie zostanie zestrzelona w płomieniach bez korekty, to ankieter nie jest kimś, dla kogo można bezpiecznie pracować).

Świat się rozwija

Tytuł pytania zmienił się ostatnio. Było Rozwiązać wyrównanie pamięci w C pytanie wywiad, które stumped mnie . Zmieniony tytuł ( Jak przydzielić wyrównaną pamięć tylko przy użyciu biblioteki standardowej? ) Wymaga nieco zmienionej odpowiedzi - ten dodatek ją zawiera.

Dodano funkcję C11 (ISO / IEC 9899: 2011) aligned_alloc():

7.22.3.1 aligned_allocFunkcja

Streszczenie

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

Opis przestrzeń przydziela funkcyjne dla obiektu, który jest określony przez dopasowanie , której wielkość jest określona , a którego wartość jest nieokreślona. Wartość powinna być poprawnym dopasowaniem popartym wdrożeniem, a wartość będzie całkowitą wielokrotnością .
aligned_allocalignmentsizealignmentsizealignment

Zwraca
The aligned_alloczwraca albo wskaźnik NULL lub wskaźnik do przydzielonej przestrzeni.

POSIX definiuje posix_memalign():

#include <stdlib.h>

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

OPIS

posix_memalign()Funkcja powinna przydzielić sizebajtów wyrównane do granicy określonej przez alignment, i zwraca wskaźnik do przydzielonej pamięci w memptr. Wartość alignmentbędzie potęgą dwóch wielokrotności sizeof(void *).

Po pomyślnym zakończeniu wartość wskazywana przez memptrbędzie wielokrotnością alignment.

Jeśli wielkość żądanego miejsca wynosi 0, zachowanie jest zdefiniowane w implementacji; zwracana wartość memptrto wskaźnik zerowy lub wskaźnik niepowtarzalny.

free()Funkcja powinna cofnąć przydział pamięci, który został wcześniej przydzielony przez posix_memalign().

WARTOŚĆ ZWRACANA

Po pomyślnym zakończeniu posix_memalign()zwraca zero; w przeciwnym razie zwracany jest numer błędu w celu wskazania błędu.

Można użyć jednego lub obu z nich, aby odpowiedzieć na pytanie teraz, ale tylko funkcja POSIX była opcją, gdy pierwotnie udzielono odpowiedzi na pytanie.

Za kulisami nowa funkcja pamięci wyrównanej wykonuje dokładnie to samo zadanie, co przedstawione w pytaniu, z tym wyjątkiem, że ma możliwość łatwiejszego wymuszenia wyrównania i śledzenia wewnętrznego początku wyrównanej pamięci, aby kod nie mam do czynienia szczególnie - zwalnia pamięć zwróconą przez użytą funkcję alokacji.


13
I jestem zardzewiały z C ++, ale tak naprawdę nie ufam, że ~ 0x0F prawidłowo rozszerzy się do rozmiaru wskaźnika. Jeśli tak się nie stanie, rozpęta się piekło, ponieważ również zamaskujesz najważniejsze fragmenty wskaźnika. Mogę się jednak mylić.
Bill K

66
BTW „+15” działa tak samo jak „+16” ... nie ma jednak praktycznego wpływu w tej sytuacji.
Menkboy,

15
Komentarze „+ 15” od Menkboya i Grega są poprawne, ale i tak malloc () prawie na pewno zaokrągliby to do 16. Użycie +16 jest nieco łatwiejsze do wyjaśnienia. Uogólnione rozwiązanie jest trudne, ale wykonalne.
Jonathan Leffler

6
@Aerovistae: To nieco podchwytliwe pytanie i zależy głównie od twojego zrozumienia, w jaki sposób ustawić dowolną liczbę (w rzeczywistości adres zwracany przez alokator pamięci) spełniający określone wymagania (wielokrotność 16). Gdybyś kazał zaokrąglić w górę 53 do najbliższej wielokrotności 16, jak byś to zrobił? Proces ten nie różni się bardzo w przypadku adresów; po prostu liczby, z którymi zwykle masz do czynienia, są większe. Nie zapomnij, zadawane są pytania podczas wywiadu, aby dowiedzieć się, jak myślisz, a nie dowiedzieć się, czy znasz odpowiedź.
Jonathan Leffler

3
@akristmann: Oryginalny kod jest poprawny, jeśli masz <inttypes.h>dostępny z C99 (przynajmniej dla ciągu formatu - prawdopodobnie wartości należy przekazać za pomocą rzutowania :) (uintptr_t)mem, (uintptr_t)ptr. Łańcuch formatu zależy od konkatenacji łańcucha, a makro PRIXPTR jest poprawnym specyfikatorem printf()długości i typu dla danych szesnastkowych dla uintptr_twartości. Alternatywą jest użycie, %pale wynik jest różny w zależności od platformy (niektóre dodają wiodące 0x, większość nie) i zwykle jest zapisywany małymi cyframi szesnastkowymi, co mi się nie podoba; to, co napisałem, jest jednolite na różnych platformach.
Jonathan Leffler

58

Trzy nieco różne odpowiedzi w zależności od tego, jak spojrzysz na pytanie:

1) Odpowiednim rozwiązaniem dla dokładnie zadanego pytania jest rozwiązanie Jonathana Lefflera, z tym wyjątkiem, że aby zaokrąglić w górę do wyrównania do 16, potrzebujesz tylko 15 dodatkowych bajtów, a nie 16.

ZA:

/* 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) Aby uzyskać bardziej ogólną funkcję alokacji pamięci, osoba dzwoniąca nie musi śledzić dwóch wskaźników (jednego do użycia, a drugiego do zwolnienia). Więc przechowujesz wskaźnik do „prawdziwego” bufora poniżej bufora wyrównanego.

ZA:

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

Zauważ, że w przeciwieństwie do (1), w którym do mema dodano tylko 15 bajtów, ten kod może faktycznie zmniejszyć wyrównanie, jeśli twoja implementacja zagwarantuje wyrównanie 32 bajtów z malloc (mało prawdopodobne, ale teoretycznie implementacja C może mieć 32 bajty wyrównany typ). To nie ma znaczenia, jeśli wszystko, co robisz, to zadzwoń do memset_16aligned, ale jeśli użyjesz pamięci dla struktury, może to mieć znaczenie.

Nie jestem pewien, co jest dobrym rozwiązaniem w tym zakresie (poza ostrzeżeniem użytkownika, że ​​zwrócony bufor niekoniecznie jest odpowiedni dla dowolnych struktur), ponieważ nie ma możliwości programowego określenia, co to jest gwarancja wyrównania specyficzna dla implementacji. Wydaje mi się, że przy starcie możesz przydzielić dwa lub więcej 1-bajtowych buforów i zakładam, że najgorszym wyrównaniem, jakie widzisz, jest wyrównanie gwarantowane. Jeśli się mylisz, marnujesz pamięć. Każdy, kto ma lepszy pomysł, powiedz tak ...

[ Dodano : „Standardowa” sztuczka polega na utworzeniu związku „prawdopodobnie będą to maksymalnie wyrównane typy” w celu ustalenia wymaganego wyrównania. Maksymalnie wyrównane typy to (w C99) „ long long”, „ long double”, „ void *” lub „ void (*)(void)”; jeśli to <stdint.h>zrobisz, prawdopodobnie możesz użyć „ intmax_t” zamiast long long(a na maszynach Power 6 (AIX) intmax_tdałbyś 128-bitową liczbę całkowitą). Wymagania dotyczące wyrównania dla tego związku można określić, osadzając go w strukturze za pomocą pojedynczego znaku, po którym następuje związek:

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;

Następnie użyłbyś większego z żądanego wyrównania (w przykładzie 16) i alignwartości obliczonej powyżej.

W (64-bitowym) systemie Solaris 10 wydaje się, że podstawowym wyrównaniem wyniku malloc()jest wielokrotność 32 bajtów.
]

W praktyce wyrównani alokatorzy często przyjmują parametr wyrównania, a nie jest on podłączony na stałe. Więc użytkownik przekaże rozmiar struktury, na której mu zależy (lub najmniejszą moc 2 większą lub równą temu) i wszystko będzie dobrze.

3) Użyj tego, co zapewnia platforma: posix_memaligndla POSIX, _aligned_mallocw systemie Windows.

4) Jeśli używasz C11, najczystszą - przenośną i zwięzłą - opcją jest użycie standardowej funkcji biblioteki, aligned_allocktóra została wprowadzona w tej wersji specyfikacji języka.


1
Zgadzam się - myślę, że celem tego pytania jest to, że kod, który zwalnia blok pamięci, miałby dostęp tylko do „gotowanego” 16-bajtowego wyrównanego wskaźnika.
Michael Burr

1
Ogólne rozwiązanie - masz rację. Jednak szablon kodu w pytaniu wyraźnie pokazuje oba.
Jonathan Leffler,

1
Jasne, a w dobrym wywiadzie zdarza się, że udzielasz odpowiedzi, a jeśli ankieter chce zobaczyć moją odpowiedź, zmieniają pytanie.
Steve Jessop

1
Sprzeciwiam się użyciu ASSERT(mem);do sprawdzania wyników alokacji; assertsłuży do wychwytywania błędów programowania i nie braku zasobów w czasie wykonywania.
hlovdal

4
Korzystanie z plików binarnych oraz za pomocą a char *i a size_tspowoduje błąd. Musisz użyć czegoś takiego uintptr_t.
Marko


20

Oto alternatywne podejście do części „zaokrąglania w górę”. Nie jest to najlepiej kodowane rozwiązanie, ale wykonuje zadanie, a ten typ składni jest nieco łatwiejszy do zapamiętania (plus działałby dla wartości wyrównania, które nie są potęgą 2). uintptr_tObsada była konieczna, aby uspokoić kompilator; arytmetyka wskaźników nie przepada za dzieleniem ani mnożeniem.

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

2
Ogólnie rzecz biorąc, jeśli masz „niepodpisany długi długi”, masz także uintptr_t, który jest wyraźnie zdefiniowany jako wystarczająco duży, aby pomieścić wskaźnik danych (void *). Ale twoje rozwiązanie rzeczywiście ma zalety, jeśli z jakiegoś powodu potrzebowałeś dostosowania, które nie było potęgą 2. Nie jest prawdopodobne, ale możliwe.
Jonathan Leffler

@Andrew: Pozytywne dla tego typu składni jest nieco łatwiejsze do zapamiętania (plus działałoby dla wartości wyrównania, które nie są potęgą 2) .
legends2k

19

Niestety w C99 wydaje się dość trudne zagwarantowanie dowolnego wyrównania w sposób, który byłby przenośny w dowolnej implementacji C zgodnej z C99. Dlaczego? Ponieważ nie można zagwarantować, że wskaźnik będzie „adresem bajtu”, można sobie wyobrazić, używając płaskiego modelu pamięci. Nie jest też zagwarantowana reprezentacja uintptr_t , który sam jest opcjonalnym typem.

Być może znamy niektóre implementacje, które używają reprezentacji dla void * (i z definicji także char * ), który jest prostym adresem bajtowym, ale do C99 jest nieprzejrzysty dla nas, programistów. Implementacja może reprezentować wskaźnik przez zestaw { segment , offset }, gdzie offset może mieć wyrównanie „kto wie, co” „w rzeczywistości”. Wskaźnik może być nawet jakąś formą wartości wyszukiwania w tablicy skrótów, a nawet wartością odnośnika z listą połączoną. Może kodować informacje o granicach.

W ostatnim szkicu C1X dla standardu C widzimy słowo kluczowe _Alignas . To może trochę pomóc.

Jedyną gwarancją, jaką daje nam C99, jest to, że funkcje alokacji pamięci zwrócą wskaźnik odpowiedni do przypisania do wskaźnika wskazującego na dowolny typ obiektu. Ponieważ nie możemy określić wyrównania obiektów, nie możemy wdrożyć własnych funkcji alokacji odpowiedzialnych za wyrównanie w dobrze zdefiniowany, przenośny sposób.

Dobrze byłoby pomylić się z tym twierdzeniem.


C11 ma aligned_alloc(). (C ++ 11/14 / 1z wciąż go nie ma). _Alignas()i C ++ alignas()nie robią nic dla dynamicznej alokacji, tylko dla automatycznego i statycznego przechowywania (lub struktury).
Peter Cordes,

15

Na froncie wypełniającym 16 vs 15 bajtów rzeczywista liczba, którą musisz dodać, aby uzyskać wyrównanie N, wynosi max (0, NM), gdzie M jest naturalnym wyrównaniem alokatora pamięci (i oba są potęgami 2).

Ponieważ minimalne wyrównanie pamięci dowolnego alokatora wynosi 1 bajt, 15 = maks. (0,16-1) jest konserwatywną odpowiedzią. Jeśli jednak wiesz, że twój alokator pamięci da ci 32-bitowe wyrównane adresy int (co jest dość powszechne), mógłbyś użyć 12 jako podkładki.

Nie jest to ważne w tym przykładzie, ale może być ważne w systemie wbudowanym z 12 KB pamięci RAM, gdzie liczy się każdy zapisany int.

Najlepszym sposobem na jego zaimplementowanie, jeśli naprawdę chcesz zapisać każdy możliwy bajt, jest makra, dzięki czemu możesz zasilić go rodzimym wyrównaniem pamięci. Ponownie jest to prawdopodobnie przydatne tylko w systemach wbudowanych, w których trzeba zapisać każdy bajt.

W poniższym przykładzie w większości systemów wartość 1 jest odpowiednia dla MEMORY_ALLOCATOR_NATIVE_ALIGNMENT , jednak w przypadku naszego teoretycznego systemu osadzonego z 32-bitowymi wyrównanymi alokacjami, poniższe mogą zaoszczędzić trochę cennej pamięci:

#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

Być może byliby zadowoleni ze znajomości memalign ? Jonathan Leffler podkreśla, że ​​istnieją dwie nowsze preferowane funkcje, o których warto wiedzieć.

Ups, Florin mnie pobił. Jeśli jednak przeczytasz stronę podręcznika, do której linkowałem, najprawdopodobniej zrozumiesz przykład dostarczony przez wcześniejszy plakat.


1
Należy zauważyć, że obecny (luty 2016) wersja strony odwołuje się mówi, że „ memalignfunkcja jest przestarzała i aligned_allocczy posix_memalignpowinien być stosowany zamiast”. Nie wiem, co powiedział w październiku 2008 r. - ale prawdopodobnie nie wspomniał aligned_alloc()o tym, ponieważ dodano go do C11.
Jonathan Leffler,

5

Robimy to cały czas dla Accelerate.framework, mocno wektorowej biblioteki OS X / iOS, w której musimy cały czas zwracać uwagę na wyrównanie. Jest całkiem sporo opcji, z których jednej lub dwóch nie widziałem wspomnianych powyżej.

Najszybszą metodą dla takiej małej tablicy jest po prostu przyklejenie jej na stos. Z GCC / clang:

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

Nie wymaga darmowego (). Zazwyczaj są to dwie instrukcje: odejmij 1024 od wskaźnika stosu, a następnie ORAZ wskaźnik stosu za pomocą opcji -alignment. Prawdopodobnie requester potrzebował danych na stercie, ponieważ jego żywotność tablicy przekroczyła stos lub rekurencja jest w pracy lub przestrzeń stosu ma poważną wagę.

W OS X / iOS wszystkie połączenia do malloc / calloc / etc. są zawsze wyrównane 16 bajtów. Jeśli potrzebujesz na przykład wyrównania 32 bajtów dla AVX, możesz użyć posix_memalign:

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

Niektórzy wspominali o interfejsie C ++, który działa podobnie.

Nie należy zapominać, że strony są wyrównane do dużych potęg dwóch, więc bufory wyrównane do strony są również wyrównane do 16 bajtów. Zatem mmap () i valloc () oraz inne podobne interfejsy są również opcjami. Zaletą mmap () jest to, że bufor można przydzielić wstępnie zainicjowany z czymś niezerowym, jeśli chcesz. Ponieważ mają one wyrównany rozmiar strony, nie uzyskasz minimalnego przydziału z nich, i prawdopodobnie będzie podlegać usterce maszyny wirtualnej przy pierwszym dotknięciu.

Tandetny: Włącz malloc strażnika lub podobny. Bufory o rozmiarze n * 16 bajtów, takie jak ten, będą wyrównane n * 16 bajtów, ponieważ maszyna wirtualna jest używana do przechwytywania przekroczeń, a jej granice znajdują się na granicach strony.

Niektóre funkcje Accelerate.framework przyjmują dostarczony przez użytkownika bufor tymczasowy do wykorzystania jako przestrzeń do rysowania. Tutaj musimy założyć, że przekazany nam bufor jest bardzo źle ustawiony, a użytkownik aktywnie próbuje utrudnić nam życie. (Nasze przypadki testowe przyklejają stronę ochronną tuż przed i za buforem temp, aby podkreślić złośliwość.) Tutaj zwracamy minimalny rozmiar, którego potrzebujemy, aby zagwarantować gdzieś w nim 16-bajtowy segment, a następnie ręcznie wyrównać bufor. Ten rozmiar jest pożądany_wymiar + wyrównanie - 1. Tak więc w tym przypadku jest to 1024 + 16-1 = 1039 bajtów. Następnie wyrównaj tak:

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

Dodanie wyrównania-1 spowoduje przesunięcie wskaźnika poza pierwszy wyrównany adres, a następnie ORAZ za pomocą -alignment (np. 0xfff ... ff0 dla wyrównania = 16) sprowadzi go z powrotem do wyrównanego adresu.

Jak opisano w innych postach, w innych systemach operacyjnych bez 16-bajtowych gwarancji wyrównania, możesz wywołać malloc o większym rozmiarze, odłożyć wskaźnik za darmo () później, a następnie wyrównać, jak opisano bezpośrednio powyżej i użyć wyrównanego wskaźnika, podobnie jak opisane dla naszego przypadku bufora temp.

Jeśli chodzi o wyrównany_memset, jest to raczej głupie. Musisz tylko zapętlić do 15 bajtów, aby osiągnąć wyrównany adres, a następnie przejść do wyrównanych sklepów, a następnie na końcu jakiś możliwy kod czyszczenia. Możesz nawet wykonać bity czyszczące w kodzie wektorowym, albo jako niepasowane sklepy, które pokrywają się z wyrównanym regionem (pod warunkiem, że długość wynosi co najmniej długość wektora), lub używając czegoś takiego jak movmaskdqu. Ktoś jest po prostu leniwy. Prawdopodobnie rozsądnym pytaniem jest, czy osoba przeprowadzająca wywiad chce wiedzieć, czy czujesz się komfortowo ze standardem stdint.h, operatorami bitowymi i podstawami pamięci, więc wymyślony przykład można wybaczyć.


5

Dziwię noone przegłosowanych się Shao „s odpowiedź że, jak rozumiem, jest to niemożliwe do zrobienia, co poprosił w standardzie C99, ponieważ konwersja wskaźnik do integralnego formalnie typu zachowanie jest niezdefiniowane. (Oprócz standardu pozwalającego na konwersję uintptr_t<-> void*, ale standard nie wydaje się pozwalać na jakiekolwiek manipulowanie uintptr_twartością, a następnie konwertowanie jej z powrotem.)


Nie ma wymogu istnienia typu uintptr_t ani tego, że jego bity mają jakikolwiek związek z bitami w podstawowym wskaźniku. Jeśli ktoś nadmiernie przydzieli pamięć, zapisz wskaźnik jako unsigned char* myptr; a następnie obliczymy `mptr + = (16- (uintptr_t) my_ptr) i 0x0F, zachowanie będzie zdefiniowane na wszystkich implementacjach, które definiują my_ptr, ale to, czy wynikowy wskaźnik zostanie wyrównany, będzie zależeć od odwzorowania między bitami i adresami uintptr_t.
supercat,


3

Pierwszą rzeczą, która przyszła mi do głowy podczas czytania tego pytania, było zdefiniowanie wyrównanej struktury, utworzenie jej, a następnie wskazanie.

Czy brakuje mi podstawowego powodu, skoro nikt inny tego nie sugerował?

Jako sidenote, ponieważ użyłem tablicy char (zakładając, że char systemu to 8 bitów (tj. 1 bajt)), nie widzę potrzeby użycia __attribute__((packed)) koniecznej (popraw mnie, jeśli się mylę), ale to ułożyłem w każdym razie.

Działa to na dwóch systemach, na których wypróbowałem, ale możliwe jest, że istnieje optymalizacja kompilatora, której nie jestem świadomy, dając mi fałszywie pozytywne wyniki w zakresie skuteczności kodu. Użyłem gcc 4.9.2na OSX i gcc 5.2.1Ubuntu.

#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

Specyficzne dla MacOS X:

  1. Wszystkie wskaźniki przypisane do malloc są wyrównane 16 bajtów.
  2. Obsługiwane jest C11, więc możesz po prostu wywołać wyrównany_malloc (16, rozmiar).

  3. MacOS X wybiera kod, który jest zoptymalizowany dla poszczególnych procesorów w czasie uruchamiania, dla memset, memcpy i memmove, a ten kod wykorzystuje sztuczki, o których nigdy nie słyszałeś, aby przyspieszyć. 99% szans, że zestaw działa szybciej niż jakikolwiek zestaw odręczny16, co sprawia, że ​​całe pytanie jest bezcelowe.

Jeśli chcesz w 100% przenośne rozwiązanie, przed C11 nie ma takiego rozwiązania. Ponieważ nie ma przenośnego sposobu testowania wyrównania wskaźnika. Jeśli nie musi być w 100% przenośny, możesz użyć

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

Zakłada się, że wyrównanie wskaźnika jest przechowywane w najniższych bitach podczas konwertowania wskaźnika na int bez znaku. Konwersja na unsigned int traci informacje i jest zdefiniowana implementacja, ale to nie ma znaczenia, ponieważ nie przekształcamy wyniku z powrotem na wskaźnik.

Straszne jest oczywiście to, że oryginalny wskaźnik należy zapisać gdzieś, aby wywołać z nim free (). Podsumowując, naprawdę wątpiłbym w mądrość tego projektu.


1
Gdzie znajdujesz się aligned_mallocw OS X? Używam Xcode 6.1 i nie jest on nigdzie zdefiniowany w iOS SDK, ani nie jest zadeklarowany w żadnym miejscu /usr/include/*.
Todd Lehman,

To samo dotyczy XCode 7.2 na El Capitan (Mac OS X 10.11.3). W każdym razie funkcja C11 aligned_alloc(), ale też nie została zadeklarowana. Z GCC 5.3.0 otrzymuję ciekawe wiadomości alig.c:7:15: error: incompatible implicit declaration of built-in function ‘aligned_alloc’ [-Werror]i alig.c:7:15: note: include ‘<stdlib.h>’ or provide a declaration of ‘aligned_alloc’. Kod rzeczywiście zawierał <stdlib.h>, ale ani -std=c11nie -std=gnu11zmieniał komunikatów o błędach.
Jonathan Leffler,

0

Możesz także dodać 16 bajtów, a następnie przesunąć oryginalny plik ptr do 16-bitowego wyrównany, dodając (16-mod) jak poniżej wskaźnika:

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

Jeśli istnieją ograniczenia, których nie można zmarnować ani jednym bajtem, to rozwiązanie działa: Uwaga: Istnieje przypadek, w którym można to wykonać w nieskończoność: D

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

Istnieje bardzo duża szansa, że ​​jeśli przydzielisz, a następnie zwolnisz blok N bajtów, a następnie zażądasz kolejnego bloku N bajtów, oryginalny blok zostanie ponownie zwrócony. Tak więc nieskończona pętla jest bardzo prawdopodobna, jeśli pierwszy przydział nie spełnia wymagań wyrównania. Oczywiście pozwala to uniknąć marnowania jednego bajtu kosztem marnowania wielu cykli procesora.
Jonathan Leffler,

Czy na pewno %operator jest zdefiniowany void*w sensowny sposób?
Ajay Brahmakshatriya

0

Do rozwiązania wykorzystałem koncepcję wypełniania, która wyrównuje pamięć i nie marnuje pamięci pojedynczego bajtu.

Jeśli istnieją takie ograniczenia, nie można zmarnować ani jednego bajtu. Wszystkie wskaźniki przypisane do malloc są wyrównane 16 bajtów.

Obsługiwane jest C11, więc możesz po prostu zadzwonić aligned_alloc (16, size).

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

1
W wielu systemach 64-bitowych zwracany wskaźnik malloc()jest rzeczywiście wyrównany do 16-bajtowej granicy, ale nic w żadnym standardowym standardzie nie gwarantuje - będzie po prostu wystarczająco dobrze wyrównany do dowolnego zastosowania, a w wielu systemach 32-bitowych wyrównanie na 8-bajtowa granica jest wystarczająca, a dla niektórych wystarczająca jest 4-bajtowa granica.
Jonathan Leffler,

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

Mam nadzieję, że to najprostsza implementacja, daj mi znać swoje komentarze.


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

Myślę, że jest z tym problem, ponieważ Twój dodatek będzie wskazywał lokalizację, która nie jest Malloc'd - Nie jestem pewien, jak to działało na Twoim.
2013

@Sam Powinno być add += 16 - (add % 16). (2 - (2 % 16)) == 0.
SS Anne
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.