Hizalanmış belleği yalnızca standart kitaplığı kullanarak nasıl ayırabilirim?


422

Bir iş görüşmesinin bir parçası olarak bir testi yeni bitirdim ve bir soru beni Google'ı referans olarak bile kullandı. StackOverflow ekibinin onunla neler yapabileceğini görmek istiyorum:

memset_16alignedİşlev geçirilen bir 16-bayt hizalanmış işaretçi gerektirir, ya da kilitlenmesine.

a) 1024 bayt belleği nasıl ayırır ve 16 bayt sınırına hizalarsınız?
b) İşlem tamamlandıktan sonra belleği memset_16alignedboşaltın.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

89
hmmm ... uzun vadeli kod canlılığı için, "memset_16aligned yazmış olan ateş ve düzelt ya da değiştir ya da tuhaf bir sınır koşulu olmayacak şekilde değiştir"
Steven A. Lowe

29
Kesinlikle geçerli bir soru sormak - "neden tuhaf bellek hizalama". Ancak bunun için iyi nedenler olabilir - bu durumda, memset_16aligned () 128 bit tamsayıları kullanabilir ve belleğin hizalandığı biliniyorsa bu daha kolaydır. Vb
Jonathan Leffler

5
Memset yazan kişi, iç döngüyü temizlemek için 16 baytlık dahili hizalamayı ve hizalanmamış uçları temizlemek için küçük bir veri prolog / epilogunu kullanabilir. Bu, kodlayıcıların ek bellek işaretçileri kullanmasını sağlamaktan daha kolay olacaktır.
Adisak

8
Birisi neden verilerin 16 baytlık bir sınıra hizalanmasını isteyesiniz? Muhtemelen 128bit SSE kayıtlarına yüklemek. (Daha yeni) hizalanmamış movs'ların (örn. Movupd, lddqu) daha yavaş olduğuna veya belki de SSE2 / 3 olmadan işlemcileri hedeflediğine

11
Adresi hizalamak, önbellek kullanımını optimize etmenin yanı sıra farklı önbellek ve RAM seviyeleri arasında daha yüksek bant genişliğine (en yaygın iş yükleri için) yol açar. Buraya bakın stackoverflow.com/questions/381244/purpose-of-memory-alignment
Deepthought

Yanıtlar:


586

Orijinal cevap

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

Sabit cevap

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

İstendiği gibi açıklama

İlk adım, her ihtimale karşı yeterli boş alan tahsis etmektir. Bellek 16 bayt hizalı olması gerektiğinden (önde gelen bayt adresinin 16'nın katı olması gerekir), 16 ekstra bayt eklemek yeterli alanımız olduğunu garanti eder. İlk 16 baytın bir yerinde 16 baytlık hizalanmış bir işaretçi vardır. (Not malloc()yeterince iyi için hizalanmış bir işaretçi dönmek gerekiyordu herhangi . Amaç Bununla birlikte 'Herhangi bir' anlamı temel tipi gibi şeyler için öncelikle - long, double, long double, long long., Ve nesnelere işaretçiler ve işaretçileri işlevlerine sen olduğunda grafik sistemleriyle oynamak gibi daha özel şeyler yapmak, sistemin geri kalanından daha sıkı bir hizalamaya ihtiyaç duyabilirler - bu nedenle böyle sorular ve cevaplar.)

Bir sonraki adım void işaretçisini bir char işaretçisine dönüştürmektir; GCC'ye rağmen, boş işaretçilerde işaretçi aritmetiği yapmanız gerekmez (ve GCC'nin kötüye kullandığınızı size bildirmek için uyarı seçenekleri vardır). Ardından başlangıç ​​işaretçisine 16 ekleyin. Diyelim ki malloc()size inanılmaz derecede hizalanmış bir işaretçi döndürdünüz: 0x800001. 16 eklenmesi 0x800011 verir. Şimdi 16 bayt sınırına yuvarlamak istiyorum - bu yüzden son 4 biti 0'a sıfırlamak istiyorum. 0x0F son 4 biti bire ayarladı; bu nedenle, ~0x0Fson dört hariç tüm bitler bire ayarlanmıştır. Ve bunu 0x800011 ile 0x800010 verir. Diğer ofsetleri tekrarlayabilir ve aynı aritmetik çalıştığını görebilirsiniz.

Son adım, free()her zaman ve sadece, dönüş: kolaydır free()bir değere o biri malloc(), calloc()ya da realloc()size iade - başka bir şey tam bir felaket. Bu memdeğeri tutmayı doğru bir şekilde sağladınız - teşekkürler. Ücretsiz serbest bırakır.

Son olarak, sisteminizin mallocpaketinin içindekileri biliyorsanız , 16 baytlık hizalanmış veriyi geri gönderebileceğini (veya 8 baytlık hizalanmış olabilir) tahmin edebilirsiniz. 16 baytlık hizalanmışsa, değerlerle bağlantı kurmanız gerekmez. Bununla birlikte, bu tehlikeli ve taşınabilir değildir - diğer mallocpaketler farklı minimum hizalamalara sahiptir ve bu nedenle farklı bir şey yaptığında bir şeyin varsayılması çekirdek dökümlere yol açacaktır. Geniş sınırlar içinde bu çözüm taşınabilirdir.

Başka biri posix_memalign()hizalanmış belleği almanın başka bir yolu olarak bahsetti ; bu her yerde mevcut değildir, ancak genellikle bunu temel alarak uygulanabilir. Hizalamanın 2'lik bir güç olması uygun olduğunu unutmayın; diğer hizalamalar daha karışıktır.

Bir yorum daha - bu kod, ayırmanın başarılı olup olmadığını kontrol etmez.

düzeltme

Windows Programmer , işaretçiler üzerinde bit maskesi işlemleri yapamayacağınıza dikkat çekti ve gerçekten de GCC (3.4.6 ve 4.3.1 test edildi) böyle şikayet ediyor. Böylece, ana kodun değiştirilmiş bir sürümü - ana programa dönüştürülür. Ayrıca, belirtildiği gibi, 16 yerine sadece 15 ekleme özgürlüğünü aldım. Ben kullanıyorum uintptr_tC99 yeterince uzun çoğu platformlarda erişilebilir olmasını yana yaklaşık edilmiş. Bunun kullanımı için değilse PRIXPTRde printf()ifadeleri, bu yeterli olacaktır #include <stdint.h>kullanmak yerine #include <inttypes.h>. [Bu kod, birkaç yıl önce Bill K tarafından ilk kez yapılan ve şimdiye kadar göz ardı edebildiğim bir noktayı yineleyen CR'nin işaret ettiği düzeltmeyi içeriyor .]

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

Ve işte marjinal olarak daha genelleştirilmiş bir versiyon, 2 gücü olan boyutlar için çalışacak:

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

test_mask()Genel amaçlı bir tahsis fonksiyonuna dönüştürmek için , birkaç kişi cevaplarında belirttiği gibi, ayırıcıdan gelen tek dönüş değerinin, çıkış adresini kodlaması gerekecektir.

Görüşmecilerle ilgili sorunlar

Uri şöyle dedi: Belki bu sabah [a] okuduğunu anlama problemi yaşıyorum, ancak görüşme sorusu özellikle şöyle diyorsa: "1024 bayt belleği nasıl tahsis edersiniz" ve açıkça bundan daha fazlasını tahsis edersiniz. Bu, görüşmeciden gelen otomatik bir başarısızlık olmaz mı?

Yanıtım 300 karakterlik bir yoruma sığmayacak ...

Sanırım buna bağlı. Sanırım çoğu insan (ben dahil) "1024 baytlık verilerin depolanabileceği ve temel adresin 16 baytın katı olduğu bir alanı nasıl tahsis edersiniz?" Görüşmeci gerçekten 1024 baytı (yalnızca) nasıl ayırabileceğinizi ve 16 baytlık hizalayabileceğinizi kastediyorsa, seçenekler daha sınırlıdır.

  • Açıkçası, bir olasılık 1024 bayt tahsis etmek ve daha sonra bu adrese 'hizalama tedavisi' vermek; bu yaklaşımla ilgili sorun, gerçek kullanılabilir alanın düzgün bir şekilde belirlenmemesidir (kullanılabilir alan 1008 ila 1024 bayt arasındadır, ancak hangi boyutu belirtmek için kullanılabilir bir mekanizma yoktu), bu da onu kullanışlı kılar.
  • Başka bir olasılık, tam bellek ayırıcı yazmanız ve geri döndüğünüz 1024 baytlık bloğun uygun şekilde hizalandığından emin olmanızdır. Bu durumda, muhtemelen önerilen çözümün yaptıklarına oldukça benzer bir işlem yapıyorsunuz, ancak bunu ayırıcı içinde saklıyorsunuz.

Ancak, görüşmeci bu yanıtlardan herhangi birini beklerse, bu çözümün yakından ilgili bir soruyu cevapladığını fark etmelerini ve daha sonra sohbeti doğru yöne yönlendirmek için sorularını yeniden düzenlemelerini beklerdim. (Ayrıca, görüşmeci gerçekten okşadıysa, işi istemezdim; eğer yeterince kesin bir gereksinimin cevabı düzeltilmeden alevlerde vurulursa, görüşmeci çalışmanın güvenli olduğu biri değildir.)

Dünya devam ediyor

Sorunun başlığı son zamanlarda değişti. Beni röportaj C görüşme soru bellek hizalama çözmek oldu . Revize edilmiş başlık ( sadece standart kütüphaneyi kullanarak hizalanmış bellek nasıl tahsis edilir? ) Biraz revize edilmiş bir cevap gerektirir - bu ek sağlar.

C11 (ISO / IEC 9899: 2011) eklenen fonksiyon aligned_alloc():

7.22.3.1 aligned_allocİşlev

özet

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

Açıklama hizalama ile belirtilen bir nesne için işlev ayırır alan boyutu ile belirlenir, ve değeri belirsizdir. Değeri , uygulama tarafından desteklenen geçerli bir hizalama ve değeri, ayrılmaz bir katı olacaktır .
aligned_allocalignmentsizealignmentsizealignment

İade işlev döner bir boş gösterici veya ayrılmış alana bir işaretçi ya.
aligned_alloc

POSIX posix_memalign()şunları tanımlar :

#include <stdlib.h>

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

AÇIKLAMA

posix_memalign()Fonksiyonu tahsis edecektir sizetarafından belirlenen bir sınır üzerindeki hizalanmış bayt alignmentve ayrılan bellek için bir işaretçi iade eder memptr. Değeri alignmentiki katının gücüdür sizeof(void *).

Başarılı bir şekilde tamamlanmasının ardından, işaret ettiği değerin memptrkatları olacaktır alignment.

İstenen alanın boyutu 0 ise, davranış uygulama tanımlıdır; döndürülen değer memptrbir boş gösterici veya benzersiz bir işaretçi olmalıdır.

free()Fonksiyon daha önce tahsis edilmiş bellek ayırması eder posix_memalign().

GERİ DÖNÜŞ DEĞERİ

Başarılı bir şekilde tamamlanınca posix_memalign()sıfır döner; aksi halde, hatayı belirtmek için bir hata numarası döndürülecektir.

Bunların biri veya her ikisi de şimdi soruyu cevaplamak için kullanılabilir, ancak soru orijinal olarak cevaplandığında sadece POSIX işlevi bir seçimdi.

Sahnelerin arkasında, yeni hizalanmış bellek işlevi, hizalamayı daha kolay zorlama ve hizalanmış belleğin başlangıcını dahili olarak takip edebilmeleri için, soruda özetlenenle aynı işi yapar. özel olarak uğraşmak zorunda - sadece kullanılan ayırma işlevi tarafından döndürülen belleği serbest bırakır.


13
Ve ben C ++ ile paslı, ama gerçekten ~ 0x0F doğru işaretçinin boyutuna genişleyecek güvenmiyorum. Değilse, tüm cehennem gevşeyecek çünkü işaretçinizin en önemli bitlerini de maskeleyeceksiniz. Buna rağmen yanılmış olabilirim.
Bill K

66
BTW '+15' ve '+16' çalışıyor ... bu durumda pratik bir etkisi yok.
Menkboy

15
Menkboy ve Greg'in '+ 15' yorumları doğrudur, ancak malloc () neredeyse 16'ya kadar dönecektir. +16'nın açıklanması marjinal olarak daha kolaydır. Genelleştirilmiş çözüm fiddly, ancak yapılabilir.
Jonathan Leffler

6
@Aerovistae: Bu biraz hileli bir soru ve çoğunlukla rasgele bir sayının (aslında bellek ayırıcı tarafından döndürülen adres) belirli bir gereksinimle (16'nın katları) nasıl karşılanacağına dair anlayışınıza bağlı. Eğer 53'ü 16'nın en yakın katına yuvarlamanız istendiyse, bunu nasıl yapardınız? Süreç adresler için çok farklı değildir; sadece uğraştığınız rakamların daha büyük olması. Unutmayın, röportaj soruları nasıl düşündüğünüzü öğrenmek, cevabı bilip bilmediğinizi öğrenmek için sorulur.
Jonathan Leffler

3
@akristmann: C99'dan kullanılabilirseniz orijinal kod doğrudur <inttypes.h>(en azından biçim dizesi için - tartışmalı olarak, değerler bir cast ile geçirilmelidir :) (uintptr_t)mem, (uintptr_t)ptr. Biçim dizesi, dizenin birleştirilmesine dayanır ve PRIXPTR makrosu, printf()bir uintptr_tdeğerin onaltılık çıktısı için doğru uzunluk ve tür belirleyicisidir . Alternatif kullanmaktır, %pancak bunun çıktısı platforma göre değişir (bazıları öncü ekler 0x, çoğu yok) ve genellikle sevmediğim küçük harfli onaltılık basamaklarla yazılır; yazdığım platformlar arasında tekdüze.
Jonathan Leffler

58

Soruya nasıl baktığınıza bağlı olarak üç farklı cevap:

1) Tam olarak sorulan soru için yeterince iyi olan Jonathan Leffler'ın çözümü, 16 hizaya kadar yuvarlamak için 16'ya değil, sadece 15 bayt gerekir.

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) Daha genel bir bellek ayırma işlevi için, arayan iki işaretçiyi (bir tane kullanmak için ve bir tane serbest) takip etmek istemez. Böylece, hizalanmış tamponun altındaki 'gerçek' arabelleğe bir işaretçi depolarsınız.

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

Belleğe yalnızca 15 bayt eklenmiş olan (1) 'in aksine , uygulamanızın mallocdan 32 baytlık hizalamayı garanti etmesi durumunda bu kodun hizalamayı azaltabileceğini unutmayın (olası değil, ancak teorik olarak bir C uygulaması 32 bayt olabilir hizalı tip). Yapmanız gereken tek şey memset_16aligned'i çağırmak önemli değil, ama hafızayı bir yapı için kullanırsanız önemli olabilir.

Uygulamaya özel hizalama garantisinin ne olduğunu programlı olarak belirlemenin bir yolu olmadığından, bunun için iyi bir düzeltme (kullanıcı döndürülen arabellek rasgele yapılar için uygun olmadığını uyarmak dışında) ne olduğunu emin değilim. Başlangıçta iki veya daha fazla 1 bayt tampon ayırabilir ve gördüğünüz en kötü hizalamanın garantili hizalama olduğunu varsayabilirsiniz. Eğer yanılıyorsanız, hafızayı boşa harcarsınız. Daha iyi bir fikri olan herkes, lütfen söyleyin ...

[ Eklendi : 'Standart' hile, gerekli hizalamayı belirlemek için 'maksimal olarak hizalanmış türler olması' birliği oluşturmaktır. Maksimum hizalanmış türlerin (C99'da) ' long long', ' long double', ' void *' veya ' void (*)(void)' olması muhtemeldir ; eklerseniz <stdint.h>, muhtemelen ' intmax_t' yerine kullanabilirsiniz long long(ve Power 6 (AIX) makinelerinde intmax_tsize 128 bit tamsayı türü verir). Bu birlik için uyum gereklilikleri, tek bir karakter ve ardından birlik tarafından bir yapıya gömülerek belirlenebilir:

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;

Daha sonra istenen hizalamanın (örnekte 16) daha büyük olanını ve alignyukarıda hesaplanan değeri kullanırsınız.

(64 bit) Solaris 10'da, sonuç için temel hizalamanın malloc()32 baytın katı olduğu anlaşılıyor .
]

Uygulamada, hizalanmış ayırıcılar genellikle kablo bağlantısı yerine hizalama için bir parametre alır. Böylece kullanıcı önem verdiği yapının büyüklüğünü (ya da buna eşit ya da ona eşit olan en az 2 güç) geçecek ve hepsi iyi olacak.

3) Platformunuzun posix_memalignsunduklarını kullanın: POSIX için, _aligned_mallocWindows'ta.

4) C11 kullanıyorsanız, en temiz - taşınabilir ve özlü - seçenek, aligned_allocdil belirtiminin bu sürümünde tanıtılan standart kitaplık işlevini kullanmaktır .


1
Kabul ediyorum - sorunun amacı, bellek bloğunu serbest bırakan kodun sadece 'pişmiş' 16 baytlık hizalanmış işaretçiye erişebileceğini düşünüyorum.
Michael Burr

1
Genel bir çözüm için haklısınız. Ancak, sorudaki kod şablonu her ikisini de açıkça göstermektedir.
Jonathan Leffler

1
Elbette ve iyi bir röportajda ne olacak cevabınızı vermenizdir, o zaman görüşmeci cevabımı görmek isterse soruyu değiştirirler.
Steve Jessop

1
ASSERT(mem);Tahsis sonuçlarını kontrol etmek için kullanmaya itiraz ediyorum ; assertprogramlama hatalarını yakalamak içindir ve çalışma zamanı kaynaklarının eksikliği değildir.
hlovdal

4
İkili & işaretini a char *ve a ile kullanmak size_thataya neden olur. Gibi bir şey kullanmalısın uintptr_t.
Marko


20

İşte 'topla' kısmına alternatif bir yaklaşım. En zekice kodlanmış çözüm değil, ama işi hallediyor ve bu tür sözdiziminin hatırlanması biraz daha kolay (artı 2'nin gücü olmayan hizalama değerleri için işe yarayacaktır). uintptr_tDökme derleyici yatıştırmak için gerekliydi; işaretçi aritmetiği bölünmeye veya çarpmaya çok düşkün değildir.

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

2
Genel olarak, 'imzasız uzun uzun' bulunduğunuz yerde, açıkça bir veri işaretçisini (void *) tutacak kadar büyük olacak şekilde tanımlanmış uintptr_t'ye de sahipsiniz. Ancak, bir nedenden ötürü, 2 gücü olmayan bir hizalamaya ihtiyacınız varsa, çözümünüz gerçekten de yararlıdır.
Jonathan Leffler

@Andrew: Bu tür sözdizimi için oylananları hatırlamak biraz daha kolaydır (artı 2'nin gücü olmayan hizalama değerleri için de işe yarar ) .
legends2k

19

Ne yazık ki, C99'da, C99'a uygun herhangi bir C uygulamasında taşınabilir olacak şekilde herhangi bir türün hizalanmasını garanti etmek oldukça zor görünüyor. Neden? İşaretçinin "bayt adresi" olduğu garanti edilmediğinden, düz bellek modeliyle hayal edilebilir. Uintptr_t'nin temsili de garanti edilemez, ki bu da yine de isteğe bağlı bir türdür.

Basit bir bayt adresi olan void * (ve tanım gereği char * ) için bir gösterim kullanan bazı uygulamalar hakkında bilgi sahibi olabiliriz , ancak C99 ile programcılar bizim için opaktır. Bir uygulama {kümesi tarafından bir işaretçi temsil edebilir segmenti , ofset nerede} ofset olabilir kim-bilir-ne hizalama "gerçekte". Neden, bir işaretçi karma tablo arama değerinin bir biçimi veya hatta bir bağlantılı liste arama değeri olabilir. Sınır bilgilerini kodlayabilir.

Yakın zamanda bir C Standardı için C1X taslağında, _Alignas anahtar kelimesini görüyoruz . Bu biraz yardımcı olabilir.

C99'un bize verdiği tek garanti, bellek ayırma işlevlerinin herhangi bir nesne türünü gösteren bir işaretçiye atama için uygun bir işaretçi döndürmesidir. Nesnelerin hizalanmasını belirleyemediğimiz için, kendi tahsis fonksiyonlarımızı iyi tanımlanmış, taşınabilir bir şekilde hizalama sorumluluğu ile uygulayamayız.

Bu iddia hakkında yanlış olmak iyi olur.


C11 vardır aligned_alloc(). (C ++ 11/14 / 1z hala sahip değil). _Alignas()ve C ++ alignas()dinamik ayırma için hiçbir şey yapmaz, yalnızca otomatik ve statik depolama (veya yapı düzeni) için.
Peter Cordes

15

16'ya karşı 15 bayt sayma dolgu cephesinde, N'nin bir hizalamasını elde etmek için eklemeniz gereken gerçek sayı max (0, NM) 'dir, burada M bellek ayırıcısının doğal hizalamasıdır (ve her ikisi de 2'nin gücüdür).

Herhangi bir ayırıcının minimum bellek hizalaması 1 bayt olduğundan, 15 = maks (0,16-1) muhafazakar bir cevaptır. Ancak, bellek ayırıcısının size 32-bit int uyumlu adresler vereceğini biliyorsanız (oldukça yaygındır), 12'yi bir pad olarak kullanmış olabilirsiniz.

Bu, bu örnek için önemli değildir, ancak kaydedilen her int'in önemli olduğu 12K RAM'e sahip gömülü bir sistemde önemli olabilir.

Eğer gerçekten mümkün olan her baytı kaydetmeye çalışacaksanız bunu uygulamanın en iyi yolu bir makro gibidir, böylece yerel bellek hizalamanızı besleyebilirsiniz. Yine, bu muhtemelen sadece her baytı kaydetmeniz gereken gömülü sistemler için yararlıdır.

Aşağıdaki örnekte, çoğu sistemde, 1 değeri gayet iyi MEMORY_ALLOCATOR_NATIVE_ALIGNMENT, ancak 32 bit hizalanmış ayırmalara sahip teorik gömülü sistemimiz için aşağıdakiler küçük bir miktar değerli belleği kurtarabilir:

#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

Belki de bir hatıra bilgisinden memnun kalacaklardı ? Jonathan Leffler'in belirttiği gibi, bilmeniz gereken iki yeni işlev daha var.

Hata, florin beni dövdü. Ancak, bağlandığım man sayfasını okursanız büyük olasılıkla daha önceki bir poster tarafından sağlanan örneği anlayacaksınız.


1
Not mevcut (Şubat 2016) sürümü olduğu başvurulan sayfa "diyor memalignfonksiyonu kullanılmıyor ve aligned_allocya posix_memalignonun yerine kullanılmalıdır". Ekim 2008'de ne dediğini bilmiyorum - ama muhtemelen aligned_alloc()C11'e eklendiğinden bahsetmedi .
Jonathan Leffler

5

Her zaman hizalamaya dikkat etmemiz gereken yoğun şekilde vektörize edilmiş bir OS X / iOS kütüphanesi olan Accelerate.framework için bu tür şeyleri her zaman yapıyoruz. Yukarıda bahsetmediğim birkaç seçenek var.

Bunun gibi küçük bir dizi için en hızlı yöntem sadece yığına yapıştırmaktır. GCC / clang ile:

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

Ücretsiz () gerekmez. Bu tipik olarak iki talimattır: yığın işaretçisinden 1024 çıkartın, ardından -alignment ile yığın işaretçisini çıkarın. Muhtemelen, istek sahibinin yığın üzerindeki verilere ihtiyacı vardı, çünkü dizinin ömrü yığını aştı veya özyineleme işte veya yığın alanı ciddi bir primde.

OS X / iOS'ta malloc / calloc / etc'ye yapılan tüm çağrılar. her zaman 16 bayt hizalıdır. Örneğin, AVX için hizalanmış 32 bayt gerekiyorsa, posix_memalign kullanabilirsiniz:

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

Bazı kişiler benzer şekilde çalışan C ++ arayüzünden bahsetmiştir.

Sayfaların ikisinin büyük güçlerine hizalandığı unutulmamalıdır, bu nedenle sayfa hizalı tamponlar da 16 bayt hizalıdır. Bu nedenle, mmap () ve valloc () ve diğer benzer arayüzler de seçeneklerdir. mmap (), isterseniz tamponun içinde sıfır olmayan bir şeyle önceden başlatılabilmesi avantajına sahiptir. Bunların sayfa hizalı boyutu olduğundan, bunlardan minimum ayırma elde edemezsiniz ve muhtemelen ilk kez dokunduğunuzda bir VM hatasına maruz kalacaktır.

Sevimsiz: Koruma malloc veya benzerini açın. Bunun gibi n * 16 bayt boyutundaki arabellekler n * 16 bayt hizalı olacaktır, çünkü VM taşmaları yakalamak için kullanılır ve sınırları sayfa sınırlarındadır.

Bazı Accelerate.framework işlevleri, çizik alanı olarak kullanmak için kullanıcı tarafından sağlanan geçici arabelleği alır. Burada, bize geçirilen tamponun çılgınca yanlış hizalandığını ve kullanıcının aktif olarak hayatımızı zordan çıkarmaya çalıştığını varsaymalıyız. (Test durumlarımız, döngünün altını çizmek için geçici arabellekten hemen önce ve sonra bir koruma sayfası yapıştırır.) Burada, içinde bir yerde 16 baytlık hizalanmış bir segmenti garanti etmek için ihtiyacımız olan minimum boyutu döndürüyoruz ve ardından arabelleği daha sonra manuel olarak hizalıyoruz. Bu boyut aranıyor_boyut + hizalama - 1. Yani, bu durumda 1024 + 16-1 = 1039 bayt. Ardından şu şekilde hizalayın:

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

Hizalama-1 eklenmesi işaretçiyi ilk hizalanmış adresin ötesine taşır ve ardından -alignment ile ANDing (örneğin, hizalama = 16 için 0xfff ... ff0) hizalanmış adrese geri getirir.

Diğer mesajlarda açıklandığı gibi, 16 baytlık hizalama garantisi olmayan diğer işletim sistemlerinde, daha büyük boyutta malloc'u çağırabilir, daha sonra işaretçiyi serbest bırakabilir (), sonra hemen yukarıda açıklandığı gibi hizalayabilir ve hizalanmış işaretçiyi kullanabilirsiniz. geçici tampon kılıf için tanımlanmıştır.

Aligned_memset gelince, bu oldukça saçma. Hizalanmış bir adrese ulaşmak için yalnızca 15 bayta kadar döngü yapmanız ve daha sonra, sonunda olası bir temizleme kodu ile hizalanmış mağazalarla devam etmeniz gerekir. Temizleme kodlarını, hizalanmış bölgeyle çakışan (uzunluk en azından bir vektörün uzunluğunun sağlanması koşuluyla) hizalanmamış mağazalar olarak veya movmaskdqu gibi bir şey kullanarak vektör kodunda bile yapabilirsiniz. Birisi sadece tembelleşiyor. Bununla birlikte, görüşmeci stdint.h, bitsel operatörler ve bellek temelleri ile rahat olup olmadığınızı bilmek istiyorsa, muhtemelen makul bir röportaj sorusudur, bu yüzden anlaşılan örnek affedilebilir.


5

Sürpriz kimsenin doldu olarak kulüpler Shao 'ın cevabını Anladığım kadarıyla, resmen tanımsız davranıştır ayrılmaz bir türe bir işaretçi dönüştürme beri, standart C99 sorulan neler yapmak mümkün değildir, bu. ( uintptr_t<-> dönüşümüne izin veren standardın dışında, standart değerin void*herhangi bir manipülasyonunu yapmaya uintptr_tve daha sonra geri dönüştürmeye izin vermiyor gibi görünüyor .)


Bir uintptr_t türünün mevcut olması veya bitlerinin temel gösterici içindeki bitlerle herhangi bir ilişkisi olması gerekmez. Biri depolama alanını fazla tahsis edecekse, işaretçiyi bir unsigned char* myptr; ve sonra `mptr + = (16- (uintptr_t) my_ptr) & 0x0F hesapla, davranış my_ptr'i tanımlayan tüm uygulamalarda tanımlanır, ancak sonuçtaki işaretçinin hizalanıp hizalanmayacağı uintptr_t bitleri ve adresleri arasındaki eşlemeye bağlı olur.
supercat


3

Bu soruyu okurken aklıma ilk gelen şey, hizalanmış bir yapı tanımlamak, başlatmak ve sonra ona işaret etmekti.

Başka hiç kimse bunu önermediğinden, eksik olmamın temel bir nedeni var mı?

Bir sidenote olarak, bir dizi char kullandığımdan beri (sistemin charının 8 bit (yani 1 bayt) olduğunu varsayarsak), __attribute__((packed))zorunlu olarak ihtiyacı görmüyorum (yanlışsam beni düzeltin), ama koydum herhangi bir şekilde.

Bu, denediğim iki sistemde çalışıyor, ancak kodun etkinliği karşısında yanlış pozitifler vermediğimi bilmediğim bir derleyici optimizasyonu var. gcc 4.9.2OSX ve gcc 5.2.1Ubuntu üzerinde kullandım .

#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'e özgü:

  1. Malloc ile ayrılan tüm işaretçiler 16 bayt hizalıdır.
  2. C11 desteklenmektedir, bu yüzden sadece align_malloc (16, size) numaralı telefonu arayabilirsiniz.

  3. MacOS X, memset, memcpy ve memmove için önyükleme sırasında tek tek işlemciler için optimize edilmiş kodu seçer ve bu kod, hızlı yapmak için hiç duymadığınız hileleri kullanır. Memset'in tüm soruyu anlamsız hale getiren herhangi bir elle yazılmış memset16'dan daha hızlı çalışması için% 99 şans.

% 100 taşınabilir bir çözüm istiyorsanız, C11'den önce yoktur. Çünkü bir işaretçinin hizalamasını test etmek için taşınabilir bir yol yoktur. % 100 taşınabilir olması gerekmiyorsa,

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

Bu, bir işaretçi işaretsiz int'e dönüştürülürken bir işaretçi hizalamasının en düşük bitlerde saklandığını varsayar. İmzasız int'e dönüştürüldüğünde bilgi kaybedilir ve uygulama tanımlanır, ancak bu önemli değildir, çünkü sonucu tekrar bir işaretçiye dönüştürmüyoruz.

Korkunç kısmı elbette orijinal işaretçinin onunla free () çağırmak için bir yere kaydedilmesi gerektiğidir. Sonuçta bu tasarımın bilgeliğinden gerçekten şüphe duyarım.


1
aligned_mallocOS X'te nerede buluyorsunuz ? Xcode 6.1 kullanıyorum ve iOS SDK'da hiçbir yerde tanımlanmadı veya herhangi bir yerde beyan edilmedi /usr/include/*.
Todd Lehman

El Capitan'da XCode 7.2 için Ditto (Mac OS X 10.11.3). C11 işlevi her durumda aligned_alloc(), ancak bu da bildirilmedi. GCC 5.3.0'dan ilginç mesajlar alıyorum alig.c:7:15: error: incompatible implicit declaration of built-in function ‘aligned_alloc’ [-Werror]ve alig.c:7:15: note: include ‘<stdlib.h>’ or provide a declaration of ‘aligned_alloc’. Kod gerçekten hata mesajlarını içeriyordu <stdlib.h>, ancak değiştirmedi -std=c11veya -std=gnu11değiştirmedi.
Jonathan Leffler

0

Ayrıca 16 bayt ekleyebilir ve ardından işaretçinin altında (16-mod) ekleyerek orijinal ptr'yi 16bit'e hizalayabilirsiniz:

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

Tek bir bayt harcayamayacağınız kısıtlamalar varsa, bu çözüm işe yarar: Not: Bunun sonsuza kadar yürütülebileceği bir durum vardır: D

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

Çok sayıda N bayt bloğu tahsis edip serbest bırakırsanız ve daha sonra başka bir N bayt bloğu talep ederseniz, orijinal bloğun tekrar döndürülme ihtimali çok yüksektir. Dolayısıyla, ilk tahsis hizalama gereksinimini karşılamıyorsa, sonsuz bir döngü çok olasıdır. Tabii ki, bu çok fazla CPU döngüsünü boşa harcama pahasına tek bir bayt israfını önler.
Jonathan Leffler

%Operatörün void*anlamlı bir şekilde tanımlandığından emin misiniz ?
Ajay Brahmakshatriya

0

Çözüm için hafızayı hizalayan ve tek bir baytın hafızasını boşa harcamayan bir dolgu konsepti kullandım.

Kısıtlamalar varsa, tek bir bayt harcayamazsınız. Malloc ile ayrılan tüm işaretçiler 16 bayt hizalıdır.

C11 desteklenir, böylece arayabilirsiniz aligned_alloc (16, size).

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

1
Birçok 64 bit sistemde, döndürülen işaretçi malloc()gerçekten 16 baytlık bir sınırda hizalanır, ancak herhangi bir standarttaki hiçbir şey bunu garanti etmez - herhangi bir kullanım için yeterince iyi hizalanacaktır ve 8 baytlık sınır yeterlidir ve bazıları için 4 baytlık sınır yeterlidir.
Jonathan Leffler

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

Umarım bu en basit uygulamadır, yorumlarınızı bana bildirin.


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

Ben bu konuda bir sorun olduğunu düşünüyorum çünkü ekleme malloc'd olmayan bir yere işaret edecek - Bu seninkini nasıl çalıştı emin değilim.
resultsway

@Sam olmalı add += 16 - (add % 16). (2 - (2 % 16)) == 0.
SS Anne
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.