探究操作系统的内存分配(malloc)对齐策略

问题:

  我们在写程序的时候经常发现程序使用的内存往往比我们申请的多,为了优化程序的内存占用,搅尽脑汁想要优化内存占用,可是发现自己的代码也无从优化了,怎么办?现在我们把我们的焦点放到malloc上,毕竟我们向系统申请的内存都是通过它完成了,不了解他,也就不能彻底的优化内存占用。

来个小例子

//g++ -o malloc_addr_vec  mallc_addr_vec.cpp 编译
 2 #include<iostream>
 3 using namespace std;
 4 int main(int argc, char *argv[])
 5 {
 6     int malloc_size = atoi(argv[1]);
 7     char * malloc_char;
 8     for (size_t i = 0; i < 1024*1024; ++i) {
 9         malloc_char = new char[malloc_size];
10     }
11     while (1) {}//此时查看内存占用
12     return 0;
13 }

本文的测试环境为Linux 64Bit ,使用G++编译为可执行文件后,使用不同的启动参数启动,使用top命令查看程序占用的内存,这里我们主要是看RES指标

RES  --  Resident size (kb)

The non-swapped physical memory a task has used.

测试案例:

1.每次new 1 Byte   Do 1024*1024次

./malloc_addr_vec 1

启动程序后的内存占用

内存消耗 32MB

2.每次new 24 Byte  Do 1024*1024次

./malloc_addr_vec 24

启动程序后的内存占用

内存消耗32MB

3.每次new 25 Byte   Do 1024*1024次

./malloc_addr_vec 25

启动程序后的内存占用

内存消耗48MB

  为什么我们每次new 1Byte 和每次 new 24Byte系统消耗的内存一样呢?,为什么每次new 25Byte和 每次new 24Byte占用的内存完全不同呢?

  不知道大家在写程序的时候有没有关注过这个问题。我一次遇到时,吐槽一句:What the fuck malloc.

原因分析:

  在大多数情况下,编译器和C库透明地帮你处理对齐问题。POSIX 标明了通过malloc( ), calloc( ), 和 realloc( ) 返回的地址对于任何的C类型来说都是对齐的。

  对齐参数(MALLOC_ALIGNMENT) 大小的设定并需满足两个特性

1.必须是2的幂

2.必须是(void *)的整数倍

  至于为什么会要求是(void *)的整数倍,这个目前我还不太清楚,等你来发现...

  根据这个原理,在32位和64位的对齐单位分别为8字节和16字节

  但是这并解释不了上面的测试结果,这是因为系统malloc分配的最小单位(MINSIZE)并不是对齐单位

为了进一步了解细节,从GNU网站中把glibc源码下载下来,查看其 malloc.c文件

  其中request2size这个宏就是glibc的内存对齐操作,MINSIZE就是使用malloc时占用内存的最小单位。根据宏定义可推算在32位系统中MINSIZE为16字节,在64位系统中MINSIZE一般为32字节。从request2size还可以知道,如果是64位系统,申请内存为1~24字节时,系统内存消耗32字节,当申请内存为25字节时,系统内存消耗48字节。 如果是32位系统,申请内存为1~12字节时,系统内存消耗16字节,当申请内存为13字节时,系统内存消耗24字节。

一般他们的差距是一个指针大小,计算公式是

max(MINSIZE,in_use_size)

其中in_use_size=(要求大小+2*指针大小-指针大小)align to MALLOC_ALIGNMENT

(对于上面计算的由来可以参见glibc 内存池管理 ptmalloc这篇文章的第4节chuck部分以及搜一下malloc的内部实现源码 )

  为了证明这个理论的正确性,我们需要计算一次malloc到底花掉了多少内存,我们用如下代码分别在32bit Linux和 64bit Linux上做测试

2 #include<stdio.h>
 3 #include<stdlib.h>
 4 int main()
 5 {
 6         char * p1;
 7         char * p2;
 8         int i=1;
 9         printf("%d\n",sizeof(char *));
10         for(;i<100;i++)
11         {
12                 p1=NULL;
13                 p2=NULL;
14                 p1=(char *)malloc(i*sizeof(char));
15                 p2=(char *)malloc(1*sizeof(char));
16                 printf("i=%d     %d\n",i,(p2-p1));
17         }
18 
19         getchar();
20 }

其测试结果如下:

32bit

64bit

了解了malloc的内存对其原理后,对于程序的内存占用的优化又有了有的放矢。我们可以根据内存对齐的原则来请求内存,来制作我们的高效内存池,从而避免隐形的资源浪费.

例如,目前STL的内存池是以8Byte为对齐单位,内存池free_list大小为

free_list[0] --------> 8 byte

free_list[1] --------> 16 byte

free_list[2] --------> 24 byte

free_list[3] --------> 32 byte
  ... ...

free_list[15] -------> 128 byte

STL内存池在发现某个规则的内存用完了时,会进行refill,在进行chunk_alloc

例如8Byte大小的空间没有了,调用refill,refill会将其空间准备20个,也就是20*8,当然refill做不了内存分配,他把20个8Byte的需求提交给chunk_alloc

chunk_alloc 能真正分配内存,但是它分配的时候会将内存空间*2,所以最终malloc的内存为8*20*2=320 ,32bit系统给malloc的内存为328,64bit系统给malloc的内存为336

在32位和64位操作系统分别浪费掉8Byte和16Byte,其实我们可以在chunk_alloc内部简单的计算一下系统的内存对齐,达到 chunk_alloc 级零浪费...

至于 allocate级别的浪费,我想是避免不了了,譬如,我需要一个6Byte的空间,STL内存池给我的确实8Byte

时间: 2024-10-09 19:48:08

探究操作系统的内存分配(malloc)对齐策略的相关文章

内存分配有哪些策略

1.内存分配有哪些策略 我们从编译原理讲起,不同的开发环境.开发语言都会有不同的策略.一般来说,程序运行时有三种内存分配策略:静态的.栈式的.堆式的 静态存储是指在编译时就能够确定每个数据目标在运行时的存储空间需求,因而在编译时就可以给它们分配固定的内存空间. 这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间. 栈式存储栈式存储分配是动态存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反. 

OpenCV源码之内存分配-指针对齐

首先,为什么要指针对齐(Pointer Alignment)? 指针对齐有时候非常重要,因为许多硬件相关的东西在对齐上存在限制.在有些系统中,某种数据类型只能存储在偶数边界的地址处. 例如,在经典的 SPARC架构(以及经典的ARM)上,你不能从奇数地址读取一个超过1字节的整型数据.尝试这么做将会立即终止程序,并伴随着总线错误.而在X86架构上,CPU硬件处理了这个问题,只是这么做将会花费更多时间:通常RISC架构是不会为你做这些.举例如下: char c; char *Pc = &c; int

【007】【JVM——内存分配与收回策略】

 内存分配与收回策略 JVM的自动内存管理要自动化地解决两个问题:对象分配内存以及回收分配给对象的内存.回收内存前几篇已经讲了,现在说内存分配.对象的内存分配一般分配在堆内存中,也可能经过JIT 编译后被拆散为标量类型间接地在栈上分配.对象主要分配在新生代的Eden 区上,如果启动了本地线程分配缓存,将按线程优先在TLAB (本地线程分配缓存)上分配.少数情况下也可能会直接分配在老年代中,分配的规则不是固定的,与使用哪一种垃圾收集器组合,还与虚拟机中内存相关参数设置有关. 对象优先在Eden

Java千百问_07JVM架构(003)_内存分配有哪些策略

点击进入_更多_Java千百问 1.内存分配有哪些策略 我们从编译原理讲起,不同的开发环境.开发语言都会有不同的策略.一般来说,程序运行时有三种内存分配策略:静态的.栈式的.堆式的 静态存储 是指在编译时就能够确定每个数据目标在运行时的存储空间需求,因而在编译时就可以给它们分配固定的内存空间. 这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间. 栈式存储 栈式存储分配是动态存储分配,是由一个类似于堆栈的运行栈来

垃圾收集器与内存分配策略(六)之内存分配与回收策略

垃圾收集器与内存分配策略(六)--内存分配与回收策略 对象的内存分配,一般来说就是在堆上的分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象分配的细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置. 区分Minor GC与 Full GC: 新生代GC(Minor GC):指发生在新生代的的垃圾收集动作,因为Java对象大多具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快. 老年代GC(Full GC / Major GC):老

【007】【JVM——内存分配和恢复策略】

 内存分配与收回策略 JVM的自己主动内存管理要自己主动化地解决两个问题:对象分配内存以及回收分配给对象的内存.回收内存前几篇已经讲了.如今说内存分配.对象的内存分配一般分配在堆内存中,也可能经过JIT 编译后被拆散为标量类型间接地在栈上分配.对象主要分配在新生代的Eden 区上,假设启动了本地线程分配缓存,将按线程优先在TLAB (本地线程分配缓存)上分配. 少数情况下也可能会直接分配在老年代中,分配的规则不是固定的,与使用哪一种垃圾收集器组合,还与虚拟机中内存相关參数设置有关. 对象优先

二 内存分配与回收策略

内存分配与回收策略 对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将线程优先在TLAB上分配,少数情况下也可能直接分配在老年代中. 对象优先在Eden分配  大多数情况下,对象在新生代Eden区中分配.当Eden区没有足够空间进行分配时,虚拟机讲发起一次MinorGC. 大对象直接进入老年代 所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字

java虚拟机(3)--内存分配与回收策略

三.内存分配与回收策略 1.1 Minor GC 和 Full GC Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快. Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多. 1.2 内存分配策略 1.2.1            对象优先在 Eden 分配 大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor

[转]内存分配malloc, new , heapalloc

这里比较的VC++编译的C++代码中的性能 我用的是VC6.0测试的 就不介绍这几个的用法了 我写了一段简单的测试代码 测试结果是: malloc:390new:391VirtualAlloc:454HeapAlloc:47 很明显的是HeapAlloc分配速度最快,malloc次之,new和malloc差不多,VirtualAlloc最慢了(以前小强跟我说这个最快) 我有跟踪了一下 new调用了这段代码 void * __cdecl _nh_malloc ( size_t nSize, int