簡體   English   中英

如何僅使用標准庫分配對齊的內存?

[英]How to allocate aligned memory only using the standard library?

作為求職面試的一部分,我剛剛完成了一項測試,一個問題讓我感到困惑,甚至使用谷歌作為參考。 我想看看 StackOverflow 工作人員可以用它做什么:

memset_16aligned函數需要一個 16 字節對齊的指針傳遞給它,否則它會崩潰。

a) 您將如何分配 1024 字節的內存,並將其與 16 字節的邊界對齊?
b) 在memset_16aligned執行后memset_16aligned內存。

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

原答案

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

固定答案

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

按要求解釋

第一步是分配足夠的備用空間,以防萬一。 由於內存必須是 16 字節對齊的(意味着前導字節地址需要是 16 的倍數),因此添加 16 個額外字節可以保證我們有足夠的空間。 在前 16 個字節的某處,有一個 16 字節對齊的指針。 (請注意, malloc()應該返回一個指針,該指針對於任何目的都足夠好。但是,“any”的含義主要用於諸如基本類型之類的東西 - longdoublelong doublelong long和指向對象和函數指針。當你做更專業的事情時,比如玩圖形系統,它們可能需要比系統的其他部分更嚴格的對齊——因此問題和答案是這樣的。)

下一步是將void指針轉換為char指針; 盡管如此,您不應該對空指針進行指針運算(並且 GCC 有警告選項會在您濫用它時告訴您)。 然后將 16 添加到開始指針。 假設malloc()您返回了一個不可能完全對齊的指針:0x800001。 添加 16 給出 0x800011。 現在我想向下舍入到 16 字節的邊界——所以我想將最后 4 位重置為 0。0x0F 將最后 4 位設置為 1; 因此, ~0x0F所有位都設置為 1,除了最后四位。 與 0x800011 一起給出 0x800010。 您可以迭代其他偏移量並查看相同的算法是否有效。

最后一步, free()很簡單:你總是,而且只是,將malloc()calloc()realloc()返回給你的值返回給free() - 其他任何事情都是一場災難。 您正確提供了mem來保存該值 - 謝謝。 免費發布它。

最后,如果您了解系統malloc包的內部結構,您可能會猜測它很可能返回 16 字節對齊的數據(或者它可能是 8 字節對齊的)。 如果它是 16 字節對齊的,那么您不需要使用這些值。 然而,這是狡猾且不可移植的——其他malloc包具有不同的最小對齊方式,因此當它做不同的事情時假設一件事會導致核心轉儲。 在廣泛的范圍內,該解決方案是可移植的。

其他人提到posix_memalign()是另一種獲取對齊內存的方法; 這並非隨處可用,但通常可以使用它作為基礎來實現。 請注意,對齊是 2 的冪很方便; 其他對齊方式更加混亂。

還有一條評論——這段代碼不檢查分配是否成功。

修正案

Windows Programmer指出你不能對指針進行位掩碼操作,事實上,GCC(3.4.6 和 4.3.1 測試)確實會抱怨這樣。 因此,基本代碼的修改版本 - 轉換為主程序,如下所示。 正如所指出的,我還冒昧地增加了 15 個而不是 16 個。 我正在使用uintptr_t因為 C99 已經存在了足夠長的時間,可以在大多數平台上訪問。 如果不是為了在printf()語句中使用PRIXPTR ,則使用#include <stdint.h>而不是使用#include <inttypes.h>就足夠了。 [此代碼包括CR指出的修復,它重申了Bill K幾年前首次提出的觀點,直到現在我才設法忽略。]

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

這是一個稍微更通用的版本,它適用於 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);
}

要將test_mask()轉換為通用分配函數,分配器的單個返回值必須對釋放地址進行編碼,正如一些人在他們的答案中指出的那樣。

面試官的問題

Uri評論說:也許我今天早上有 [a] 閱讀理解問題,但如果面試問題明確說:“你將如何分配 1024 字節的內存”,而你顯然分配了更多。 這不會是面試官的自動失敗嗎?

我的回復不適合 300 個字符的評論......

這取決於,我想。 我想大多數人(包括我)都認為這個問題的意思是“你將如何分配一個空間,其中可以存儲 1024 字節的數據,並且基地址是 16 字節的倍數”。 如果面試官的意思是你如何分配 1024 字節(僅)並使其 16 字節對齊,那么選項就更有限了。

  • 顯然,一種可能性是分配 1024 個字節,然后對該地址進行“對齊處理”; 這種方法的問題是實際可用空間沒有正確確定(可用空間在 1008 到 1024 字節之間,但沒有可用的機制來指定大小),這使得它不太有用。
  • 另一種可能性是您需要編寫一個完整的內存分配器並確保您返回的 1024 字節塊適當對齊。 如果是這種情況,您可能最終會執行與建議的解決方案非常相似的操作,但您將其隱藏在分配器中。

但是,如果面試官期望這些回答中的任何一個,我希望他們認識到這個解決方案回答了一個密切相關的問題,然后重新構建他們的問題以將對話指向正確的方向。 (此外,如果面試官真的脾氣暴躁,那么我就不會想要這份工作;如果對不夠精確的要求的答案未經修正就被撲滅了,那么面試官就不是可以安全工作的人。)

世界繼續前進

問題的標題最近發生了變化。 Solve the memory alignment in C 面試問題難倒了我 修訂后的標題(如何僅使用標准庫分配對齊的內存? )需要稍微修訂的答案 - 本附錄提供了它。

C11 (ISO/IEC 9899:2011) 添加了函數aligned_alloc()

aligned_alloc函數

概要

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

描述
aligned_alloc函數為其對齊方式由alignment指定、 sizesize指定且其值不確定的對象分配空間。 alignment的值應是實現支持的有效對齊, size的值應是alignment的整數倍。

退貨
aligned_alloc函數返回一個空指針或一個指向已分配空間的指針。

POSIX 定義了posix_memalign()

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

描述

posix_memalign()函數應分配在由alignment指定的邊界對齊的size字節,並應返回指向memptr已分配內存的memptr alignment的值應該是sizeof(void *)的兩倍的冪。

成功完成后, memptr指向的值應為alignment的倍數。

如果請求的空間大小為 0,則行為是實現定義的; memptr返回的值應為空指針或唯一指針。

free()函數應釋放先前由posix_memalign()分配的內存。

返回值

成功完成后, posix_memalign()應返回零; 否則,將返回錯誤編號以指示錯誤。

現在可以使用這兩者之一或兩者來回答問題,但最初回答問題時,只有 POSIX 函數是一個選項。

在幕后,新的對齊內存功能與問題中概述的工作大致相同,只是它們能夠更輕松地強制對齊,並在內部跟蹤對齊內存的開始,以便代碼不會必須特別處理——它只是釋放所使用的分配函數返回的內存。

三個略有不同的答案取決於您如何看待問題:

1)對於所問的確切問題來說,Jonathan Leffler 的解決方案已經足夠了,除了要四舍五入到 16 對齊,您只需要 15 個額外字節,而不是 16 個。

A:

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

乙:

free(mem);

2) 對於更通用的內存分配函數,調用者不想跟蹤兩個指針(一個使用,一個釋放)。 因此,您在對齊的緩沖區下方存儲了一個指向“真實”緩沖區的指針。

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;

乙:

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

請注意,與 (1) 不同,其中只向 mem 添加了 15 個字節,如果您的實現碰巧保證了 malloc 的 32 字節對齊,則此代碼實際上可以減少對齊(不太可能,但理論上 C 實現可能有一個 32 字節的對齊)對齊類型)。 如果您所做的只是調用 memset_16aligned,那並不重要,但是如果您將內存用於結構,那么它可能很重要。

我不確定對此有什么好的解決方法(除了警告用戶返回的緩沖區不一定適合任意結構),因為無法以編程方式確定特定於實現的對齊保證是什么。 我猜在啟動時您可以分配兩個或更多 1 字節緩沖區,並假設您看到的最差對齊是保證對齊。 如果你錯了,你就浪費了內存。 誰有更好的主意,請說出來...

[補充:'標准'技巧是創建一個'可能是最大對齊類型'的聯合來確定必要的對齊方式。 最大對齊的類型可能是(在 C99 中)' long long '、' long double '、' void * ' 或 ' void (*)(void) '; 如果您包含<stdint.h> ,您大概可以使用“ intmax_t ”代替long long (並且,在 Power 6 (AIX) 機器上, intmax_t會給您一個 128 位整數類型)。 該聯合的對齊要求可以通過將其嵌入到具有單個字符后跟聯合的結構中來確定:

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;

然后,您將使用請求的對齊(在示例中為 16)和上面計算的align值中較大的一個。

在(64 位)Solaris 10 上, malloc()結果的基本對齊方式似乎是 32 字節的倍數。
]

在實踐中,對齊的分配器通常采用一個參數來進行對齊,而不是硬連線。 因此,用戶將傳入他們關心的結構的大小(或大於或等於該大小的 2 的最小冪),一切都會好起來的。

3) 使用您的平台提供的:POSIX 的posix_memalign ,Windows 上的_aligned_malloc

4)如果你使用C11,那么最干凈的——可移植的和簡潔的——選項是使用這個版本的語言規范中引入的標准庫函數aligned_alloc

您也可以嘗試posix_memalign() (當然在 POSIX 平台上)。

這是“四舍五入”部分的另一種方法。 不是最出色的編碼解決方案,但它可以完成工作,並且這種類型的語法更容易記住(加上適用於不是 2 的冪的對齊值)。 uintptr_t對於安撫編譯器是必要的; 指針算術不太喜歡除法或乘法。

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

不幸的是,在 C99 中,以一種可移植到符合 C99 的任何 C 實現的方式來保證任何類型的對齊似乎非常困難。 為什么? 因為一個指針不能保證是人們在平面內存模型中想象的“字節地址”。 uintptr_t的表示也沒有保證,無論如何它本身是一個可選類型。

我們可能知道一些實現使用void * (並且根據定義,也是char * )的表示,這是一個簡單的字節地址,但在 C99 中它對我們程序員來說是不透明的。 一個實現可能通過 set { segment , offset } 來表示一個指針,其中offset可能“實際上”有誰知道的對齊方式。 為什么,指針甚至可以是某種形式的哈希表查找值,甚至是鏈表查找值。 它可以編碼邊界信息。

在最近針對 C 標准的 C1X 草案中,我們看到了_Alignas關鍵字。 這可能會有所幫助。

C99 給我們的唯一保證是內存分配函數將返回一個適合分配給指向任何對象類型的指針的指針。 由於我們無法指定對象的對齊方式,因此我們無法以明確定義的、可移植的方式實現我們自己的負責對齊的分配函數。

這個說法錯了會很好。

在 16 與 15 字節計數填充方面,您需要添加以獲得 N 對齊的實際數字是max(0,NM) ,其中 M 是內存分配器的自然對齊(並且兩者都是 2 的冪)。

由於任何分配器的最小內存對齊都是 1 個字節,因此 15=max(0,16-1) 是一個保守的答案。 但是,如果您知道您的內存分配器將為您提供 32 位 int 對齊地址(這很常見),您可以使用 12 作為填充。

這對於本示例並不重要,但對於具有 12K RAM 的嵌入式系統可能很重要,其中每個 int 保存都很重要。

如果您真的要嘗試保存每個可能的字節,那么實現它的最佳方法是作為宏,這樣您就可以將其提供給您的本機內存對齊。 同樣,這可能僅對需要保存每個字節的嵌入式系統有用。

在下面的示例中,在大多數系統上,值 1 對MEMORY_ALLOCATOR_NATIVE_ALIGNMENTMEMORY_ALLOCATOR_NATIVE_ALIGNMENT ,但是對於我們具有 32 位對齊分配的理論嵌入式系統,以下可以節省一點寶貴的內存:

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

也許他們會滿足於了解memalign 正如 Jonathan Leffler 指出的那樣,有兩個更新的更可取的函數需要了解。

哎呀,弗洛林打敗了我。 但是,如果您閱讀我鏈接到的手冊頁,您很可能會理解早期海報提供的示例。

我們一直在為 Accelerate.framework 做這種事情,Accelerate.framework 是一個高度矢量化的 OS X / iOS 庫,我們必須始終注意對齊。 有很多選擇,其中一兩個我沒有看到上面提到的。

對於像這樣的小數組,最快的方法就是將它粘在堆棧上。 使用 GCC / 叮當:

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

不需要 free()。 這通常是兩條指令:從堆棧指針中減去 1024,然后將堆棧指針與 -alignment 相加。 據推測,請求者需要堆上的數據,因為它的數組壽命超過了堆棧或遞歸正在工作或堆棧空間非常寶貴。

在 OS X / iOS 上,所有對 malloc/calloc/etc 的調用。 總是 16 字節對齊。 例如,如果您需要 32 字節對齊 AVX,那么您可以使用 posix_memalign:

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

有些人提到了類似工作的 C++ 接口。

不應忘記頁面對齊為 2 的大冪,因此頁面對齊緩沖區也是 16 字節對齊的。 因此, mmap() 和 valloc() 以及其他類似的接口也是選項。 mmap() 的優點是,如果需要,可以使用非零值預先初始化緩沖區來分配緩沖區。 由於這些具有頁面對齊的大小,因此您不會從中獲得最小分配,並且在您第一次接觸它時可能會遇到 VM 故障。

Cheesy:打開保護 malloc 或類似的。 像這樣大小為 n*16 字節的緩沖區將是 n*16 字節對齊,因為 VM 用於捕獲溢出並且其邊界位於頁邊界。

一些 Accelerate.framework 函數采用用戶提供的臨時緩沖區作為臨時空間。 在這里,我們必須假設傳遞給我們的緩沖區嚴重未對齊,並且用戶正積極地試圖讓我們的生活變得艱難。 (我們的測試用例在臨時緩沖區之前和之后粘貼一個保護頁以強調惡意。)在這里,我們返回我們需要的最小大小,以保證其中某處的 16 字節對齊段,然后手動對齊緩沖區。 這個大小是期望大小 + 對齊 - 1。所以,在這種情況下,是 1024 + 16 - 1 = 1039 字節。 然后這樣對齊:

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

添加alignment-1 會將指針移過第一個對齊的地址,然后使用-alignment 進行ANDing(例如0xfff...ff0 表示alignment=16)將其帶回對齊的地址。

正如其他帖子所述,在沒有 16 字節對齊保證的其他操作系統上,您可以調用更大尺寸的 malloc,稍后將指針留出以供 free() 使用,然后按照上面的描述進行對齊並使用對齊的指針,就像描述了我們的臨時緩沖區情況。

至於aligned_memset,這是相當愚蠢的。 您只需循環最多 15 個字節即可到達對齊的地址,然后在最后使用一些可能的清理代碼繼續對齊存儲。 您甚至可以在向量代碼中進行清理位,或者作為與對齊區域重疊的未對齊存儲(假設長度至少是向量的長度)或使用類似 movmaskdqu 的東西。 有人只是懶惰。 但是,如果面試官想知道您是否熟悉 stdint.h、按位運算符和內存基礎知識,這可能是一個合理的面試問題,因此可以原諒人為的例子。

我很驚訝沒有人投票支持Shao回答,據我所知,不可能執行標准 C99 中要求的操作,因為將指針正式轉換為整數類型是未定義的行為。 (除了標准允許轉換uintptr_t <-> void* ,但標准似乎不允許對uintptr_t值進行任何操作然后將其轉換回來。)

閱讀這個問題時,我腦海中浮現的第一件事是定義一個對齊的結構,實例化它,然后指向它。

由於沒有其他人建議這樣做,是否有我失蹤的根本原因?

作為旁注,由於我使用了一個字符數組(假設系統的字符是 8 位(即 1 個字節)),我認為不需要__attribute__((packed)) (如果我錯了,請糾正我),但我還是把它放進去了。

這適用於我嘗試過的兩個系統,但可能存在編譯器優化,我不知道它會給我帶來與代碼功效相比的誤報。 我在 OSX 上使用了gcc 4.9.2 ,在 Ubuntu 上使用了gcc 4.9.2 gcc 5.2.1

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

使用 memalign、 Aligned-Memory-Blocks可能是解決該問題的好方法。

MacOS X 特定:

  1. 所有用 malloc 分配的指針都是 16 字節對齊的。
  2. 支持 C11,因此您可以調用aligned_malloc (16, size)。

  3. MacOS X 在啟動時為 memset、memcpy 和 memmove 挑選針對單個處理器優化的代碼,這些代碼使用了您從未聽說過的技巧來加快速度。 99% 的可能性 memset 運行得比任何手寫的 memset16 都快,這使得整個問題毫無意義。

如果你想要一個 100% 便攜的解決方案,在 C11 之前沒有。 因為沒有可移植的方法來測試指針的對齊方式。 如果它不必是 100% 便攜的,你可以使用

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

這假設在將指針轉換為 unsigned int 時,指針的對齊方式存儲在最低位。 轉換為 unsigned int 會丟失信息並且是實現定義的,但這並不重要,因為我們不會將結果轉換回指針。

可怕的部分當然是原始指針必須保存在某個地方才能用它調用 free() 。 所以總而言之,我真的懷疑這種設計的智慧。

您還可以添加一些 16 個字節,然后通過在指針下方添加 (16-mod) 將原始 ptr 推送到 16 位對齊:

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

如果有限制,你不能浪費一個字節,那么這個解決方案是有效的:注意:有一種情況可能會無限執行:D

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

對於解決方案,我使用了填充的概念,它可以對齊內存並且不會浪費單個字節的內存。

如果有限制,你不能浪費一個字節。 所有用 malloc 分配的指針都是 16 字節對齊的。

支持 C11,因此您可以調用aligned_alloc (16, size)

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

希望這是最簡單的實現,讓我知道您的意見。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM