C 语言内存分配函数

  • ANSI C 中的内存空间分配函数
  •         ANSI C 中有 3 个分配内存的函数:malloc,calloc,realloc

    • 函数原型
           #include <stdlib.h>
    
           void *malloc(size_t size);
           void free(void *ptr);
           void *calloc(size_t nmemb, size_t size);
           void *realloc(void *ptr, size_t size);
    
    • 简要说明

            malloc  分配长度为 size 个字节的空间,返回指向该空间的空类型指针,空间中的初始内容不确定。
            calloc  分配长度为 nmemb*size 字节的空间,返回指向空间的指针,空间中的的位都被初始化为 0。
            realloc  改变 ptr 指向的内存块的大小,将其调整为 size 个字节。其中的内容,从开始到 min(old size, new size) 的位置都保持不变(可能通过拷贝实现)。如果容量被扩大,新增加的空间不会初始化。

    • 另一个函数 alloca

            除了上面 3 个最常见的函数外,还有一个 alloca 函数值得一提。它与 malloc 功能上几乎一致,不同之处在于其在栈上分配空间,而不是在堆上分配空间。正因为如此,分配的空间在函数调用结束后会自动释放。另一方面,在某些系统中栈在函数调用期间可能无法改变大小,这样的系统中不能提供对 alloca 的支持。同时我认为,如果栈帧的大小可改变的范围受到严格限制,alloca 函数在分配较大空间时很容易引起栈溢出异常。

           #include <alloca.h>
    
           void *alloca(size_t size);
    

            alloca 是它在 Linux 中的名字,在 windows 中应该叫 _alloca,而且包含在头文件 中,可以参考 MSDN 中的说明。

  • glibc 中的 memalign 函数
  •         除了 ANSI C 中规定的内存分配函数,glibc 额外提供了地址对齐的内存空间的分配函数。主要有这么几个:posix_memalign, aligned_alloc, valloc, memalign, pvalloc

    • 函数原型
    #include <stdlib.h>
    
    int posix_memalign(void **memptr, size_t alignment, size_t size);
    void *aligned_alloc(size_t alignment, size_t size);
    void *valloc(size_t size);
    
    #include <malloc.h>
    
    void *memalign(size_t alignment, size_t size);
    void *pvalloc(size_t size);
    
    • 简要说明

            posix_memalign  分配 size 字节的内存空间,将 *memptr 指向分配的空间,并且空间的地址是 alignment 的倍数。alignment 必须是 2 的乘幂,同时也要是 sizeof(void *) 的倍数。
            aligned_alloc  分配 size 字节的空间,返回指向该空间的指针。除了地址是 alignment 的倍数这一限制和 alignment 必须是 2 的乘幂的限制外,size 也必须是 alignment 的倍数。
            valloc  分配 size 字节的空间,返回指向该空间的指针,空间的地址是页大小的倍数。该函数可能已经过时,不提倡使用。

            memalign  分配 size 字节的空间,返回指向该空间的指针,空间的地址是 alignment 的倍数,alignment 必须是 2 的乘幂。该函数可能已经过时,不提倡使用。
            pvalloc  与 valloc 相似,不过将分配的空间大小扩展为页大小的倍数。该函数可能已经过时,不提倡使用。

            上述对齐分配函数都不会把分配的空间置 0 。

  • 性能测试
  •         为了观察不同内存分配函数的性能,我在 Linux 系统上测试了几个函数。测试的环境为 3.13.0-35 版本内核,编译器 gcc 4.8.2,glibc 为 libc-2.19.so。

    • malloc 和 calloc 性能测试

            对某一大小的空间重复分配-释放 10000 次,所用时间随分配空间大小的变化曲线如下图所示:

    malloc_calloc

    从图中可以看出,malloc 函数随着分配空间增大,其时间也在增长。值得注意的是,从 pow(2,24) 字节(16MB)增加为 pow(2, 25) 字节(32MB)时,malloc 执行所用时间发生了一次跳变(从不到 0.0001 秒变成了将近 0.07秒)。而 calloc 的执行时间则有一个波峰跌落的变化,跌落的地方同样是在从 pow(2,24) 到 pow(2,25) 字节(因 pow(2,24) 字节分配所用时间太长,图中并没有画出),执行时间从 181 秒多跌落到了不到 0.07 秒。

            通过查阅资料发现,在分配不同大小的内存空间时,采用的方式是不同的。分配较小内存时,用 brk 系统调用,而分配较大内存时,则用 mmap 系统调用。总的来说,前者的开销小,后者开销大。所以,malloc 执行时间的跳变就是由 brk 实现切换到 mmap 实现时产生的。而 mmap 系统调用还有一个特点:如果不发生实际读写操作,就不需要分配物理内存,当发生页缺失时,才真正分配物理页。所以当 calloc 的实现由 brk 切换到 mmap 时,因为取消了实际置 0 的操作,所以执行时间反而由 181 秒跌落到了非常短的 0.07 秒,与 malloc 执行时间相仿。

            除了上面的测试,我还具体测试了在分配字节达到多少时会由 brk 切换到 mmap 系统调用。实验的结果是,分配的空间从 0x1ffefe8 Bytes,即 31MB 1019KB 1000B,到0x1ffefe9 Bytes 时,实现由 brk 系统调用切换为 mmap 系统调用。值得注意的是,不同的系统中阈值也可能不一样,取决于 glibc 库。在比较新的 glibc 中,该值还可能是可变的。

            基本结论:分配内存空间的函数性能取决于所分配空间的大小,分配空间的不同也意味着底层实现的不同。

    • malloc 和 posix_memalign, aligned_alloc

            下图是 malloc, posix_memalign, aligned_alloc 执行 10000 次的时间随分配空间大小的变化,其中 posix_memalign 和 aligned_alloc 函数分配地址都是 64 Byte 对齐的。
    posix_size
    从图中可知,posix_memalign 和 aligned_alloc 性能接近。在分配空间较小时(小于32MB),posix_memalign 和 aligned_alloc 所用时间明显高于 malloc,但在分配空间更大时(32MB以上),两者之间的差别就不明显了。至于在分配 pow(2,25) 字节时,posix_memalign 和 aligned_alloc 为何并没有像 malloc 那样发生跳变,我并没有深究,或许是两者由 brk 切换到 mmap 的阈值不同。

            下图是在分配 16MB 和 256MB 内存空间时 posix_memalign 和 aligned_alloc 分配时间随 alignment 的变化。
    alignment
    图中数据表明,两个函数的性能没有明显差别。在分配 16MB 空间时,除 16 字节对齐时间明显更短外,其余对齐方式性能相仿。在分配 256MB (应该是通过 mmap 实现的)空间时,所用时间明显多于分配 16MB 空间,而且不同 alignment 之间无明显差别。

            基本结论:在通过 brk 分配内存时,带对齐功能的函数开销更大;总体开销与分配的大小有关,而与 alignment 基本无关。

    【本文参考】:Linux man page.
                    c中malloc realloc calloc 的区别和联系
                    calloc在4u和5u上性能差异的分析

            文中数据图由 gnuplot 绘制。

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注