如何仅使用标准库分配对齐的内存?

时间:2008-10-22 23:23:42

标签: c memory-management

作为求职面试的一部分,我刚刚完成了一项测试,一个问题让我感到困惑 - 甚至使用谷歌作为参考。我想看看stackoverflow工作人员可以用它做什么:

“memset_16aligned”函数需要传递给它的16字节对齐指针,否则会崩溃。

a)如何分配1024字节的内存,并将其与16字节边界对齐? b)执行memset_16aligned后释放内存。

{

   void *mem;

   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here

}

18 个答案:

答案 0 :(得分:559)

答案 1 :(得分:56)

根据您对问题的看法,三个略有不同的答案:

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;

B:

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;

B:

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

注意,与(1)不同,只有15个字节被添加到mem,如果你的实现恰好保证了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-位整数类型)。可以通过将其嵌入到具有单个char后跟union的结构中来确定该并集的对齐要求:

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_memalign用于POSIX,_aligned_malloc在Windows上。

4)如果您使用C11,那么最干净 - 可移植且简洁 - 选项是使用此版本的语言规范中引入的标准库函数aligned_alloc

答案 2 :(得分:37)

您也可以尝试posix_memalign()(当然,在POSIX平台上)。

答案 3 :(得分:19)

这是'向上'部分的另一种方法。不是最精彩编码的解决方案,但它完成了工作,这种类型的语法更容易记住(加上适用于不是2的幂的对齐值)。 uintptr_t强制转换是安抚编译器的必要条件;指针算术不太喜欢分割或乘法。

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

答案 4 :(得分:18)

不幸的是,在C99中,似乎很难保证任何类型的对齐方式可以在任何符合C99的C实现中移植。为什么?因为指针不能保证是“字节地址”,人们可以想象使用平坦的内存模型。也没有保证 uintptr_t 的表示,无论如何它本身都是可选类型。

我们可能知道一些实现使用 void * (并且根据定义,也是 char * )的表示,这是一个简单的字节地址,但是通过C99它程序员对我们来说是不透明的。实现可能表示集合{偏移}的指针,其中 offset 可能具有谁知道什么是“实际”对齐。为什么,指针甚至可以是某种形式的哈希表查找值,甚至是链表查找值。它可以编码边界信息。

在最近的C标准C1X草案中,我们看到 _Alignas 关键字。这可能会有所帮助。

C99给我们的唯一保证是内存分配函数将返回一个指针,该指针适合分配给指向任何对象类型的指针。由于我们无法指定对象的对齐方式,因此我们无法以明确定义的可移植方式实现自己的分配函数,并负责对齐。

这种说法是错误的。

答案 5 :(得分:15)

在16 vs 15字节数填充前面,为了获得N的对齐而需要添加的实际数字是 max(0,NM)其中M是内存的自然对齐方式分配器(两者都是2的幂)。

由于任何分配器的最小内存对齐是1个字节,因此15 = max(0,16-1)是保守的答案。但是,如果你知道你的内存分配器将为你提供32位int对齐的地址(这是相当常见的),你可以使用12作为填充。

这对于这个例子并不重要,但它可能对于具有12K RAM的嵌入式系统很重要,其中每个int保存计数。

如果您实际上要尝试保存每个可能的字节,那么实现它的最佳方法是作为一个宏,这样您就可以将它本机内存对齐。同样,这可能仅对需要保存每个字节的嵌入式系统有用。

在下面的示例中,在大多数系统中,值1对于MEMORY_ALLOCATOR_NATIVE_ALIGNMENT来说很好,但对于具有32位对齐分配的理论嵌入式系统,以下内容可以节省一点宝贵的内存: / p>

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

答案 6 :(得分:8)

也许他们会对memalign的知识感到满意?正如Jonathan Leffler所指出的那样,有两个较新的优选函数需要了解。

哎呀,弗罗林打败了我。但是,如果您阅读我链接的手册页,您很可能会理解早期海报提供的示例。

答案 7 :(得分:5)

我很惊讶没有人投票Shaoanswer,根据我的理解,不可能做标准C99中的问题,因为正式将指针转换为整数类型是未定义的行为。 (除了允许转换uintptr_t&lt; - &gt; void*的标准之外,标准似乎不允许对uintptr_t值进行任何操作然后将其转换回来。)

答案 8 :(得分:5)

我们一直在为Accelerate.framework做这类事情,这是一个高度向量化的OS X / iOS库,我们必须始终注意对齐。有很多选择,其中一两个我上面没有提到过。

像这样的小阵列最快的方法就是将它粘在堆栈上。 GCC / clang:

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

不需要免费()。这通常是两条指令:从堆栈指针中减去1024,然后使用-alignment从堆栈指针中删除。据推测,请求者需要堆上的数据,因为它的生命周期超出了堆栈或递归正在工作或堆栈空间非常重要。

在OS X / iOS上,所有调用malloc / calloc / etc。总是16字节对齐。例如,如果你需要为AVX对齐32字节,那么你可以使用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故障。

俗气:打开防守malloc或类似物。大小为n * 16字节的缓冲区(如此字节)将对齐n * 16字节,因为VM用于捕获溢出,其边界位于页边界处。

某些Accelerate.framework函数接受用户提供的临时缓冲区作为临时空间。在这里,我们必须假设传递给我们的缓冲区严重错位,并且用户正在积极地努力使我们的生活变得艰难。 (我们的测试用例在临时缓冲区之前和之后粘贴一个保护页面以强调恶意。)这里,我们返回我们需要的最小大小,以保证其中某个位置的16字节对齐段,然后手动对齐缓冲区。这个大小是desired_size + alignment - 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进行AND运算(例如0xfff ... ff0 for alignment = 16)将其返回到对齐的地址。 < / p>

如其他帖子所述,在没有16字节对齐保证的其他操作系统上,您可以调用较大大小的malloc,稍后将指针放在free()之后,然后如上所述对齐并使用对齐的指针,就像我们的临时缓冲区情况所描述的那样。

对于aligned_memset,这是相当愚蠢的。您只需循环最多15个字节即可到达对齐的地址,然后在此之后继续使用对齐的存储,并在最后使用一些可能的清理代码。您甚至可以在向量代码中执行清理位,作为与对齐区域重叠的未对齐存储(提供长度至少是向量的长度)或使用类似movmaskdqu的内容。有人只是懒惰。然而,如果面试官想知道你是否对stdint.h,按位运算符和记忆基础知识感到满意,这可能是一个合理的面试问题,所以人为的例子可以被宽恕。

答案 9 :(得分:3)

使用memalign,Aligned-Memory-Blocks可能是解决问题的好方法。

答案 10 :(得分:3)

在阅读这个问题时,我首先想到的是定义一个对齐的结构,实例化它,然后指向它。

我缺少一个根本原因,因为没有其他人建议吗?

作为旁注,因为我使用了一个char数组(假设系统的char是8位(即1个字节)),我认为不需要属性((打包)) )必然(如果我错了,请纠正我),但无论如何我都把它放进去了。

这适用于我尝试过的两个系统,但是有可能存在一个编译器优化,我不知道在代码的功效方面给我误报。我在OSX上使用了gcc 4.9.2,在Ubuntu上使用了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;
}

答案 11 :(得分:1)

特定于MacOS X:

  1. 使用malloc分配的所有指针都是16字节对齐的。
  2. 支持C11,因此您只需调用aligned_malloc(16,size)即可。

  3. MacOS X选择在启动时针对各个处理器优化的代码,用于memset,memcpy和memmove,并且该代码使用您从未听说过的技巧来快速实现。 memset运行速度比任何手写memset16快99%,这使得整个问题毫无意义。

  4. 如果您想要100%便携式解决方案,那么在C11之前没有。因为没有可移植的方法来测试指针的对齐方式。如果它不必100%便携,您可以使用

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

    这假设在将指针转换为unsigned int时,指针的对齐存储在最低位。转换为unsigned int会丢失信息并且是实现定义的,但这并不重要,因为我们不会将结果转换回指针。

    可怕的部分当然是原始指针必须保存在某处以便用它来调用free()。总而言之,我真的怀疑这种设计的智慧。

答案 12 :(得分:0)

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

希望这是最简单的实现,让我知道您的评论。

答案 13 :(得分:0)

对于解决方案,我使用了填充的概念,它对齐内存并且不浪费     单字节的记忆。

如果存在约束,则不能浪费单个字节。 使用malloc分配的所有指针都是16字节对齐的。

支持C11,因此您只需调用aligned_malloc(16,size)即可。

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

答案 14 :(得分:0)

如果有约束,你不能浪费一个字节,那么这个解决方案有效: 注意:有一种情况可以无限执行:D

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

答案 15 :(得分:0)

您还可以添加大约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);
}

答案 16 :(得分:0)

答案 17 :(得分:-2)

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