前面了解了虚拟内存的相关知识,这节课我们来看看动态内存分配的基本概念,相信这之后就知道诸如 malloc
和 new
这类方法是怎么做的了。
程序员通过动态内存分配(例如 malloc
)来让程序在运行时得到虚拟内存。动态内存分配器会管理一个虚拟内存区域,称为堆(heap),如下图所示:
分配器以 block 为单位来维护 heap,可以进行 allocate 或 free。有两种类型的分配器:
malloc
和 free
) 我们来看看 malloc
函数:
一个简单的例子:
#include <stdio.h>
#include <stdlib.h>
void foo(int n) {
int i, *p;
/* Allocate a block of n ints */
p = (int *) malloc(n * sizeof(int));
if (p == NULL) {
perror("malloc");
exit(0);
}
/* Initialize allocated block */
for (i=0; i<n; i++)
p[i] = i;
/* Return allocated block to the heap */
free(p);
}
这节课中,为了讲述方便,我们做如下假设:
具体的例子:
程序可以用任意的顺序发送 malloc
和 free
请求, free
请求必须作用与已被分配的 block。
分配器有如下的限制:
malloc
请求(不能缓存或者给请求重新排序) 现在我们可以来看看如何去评测具体的分配算法了。假设给定一个 malloc
和 free
的请求的序列:
$$R_0, R_1, ..., R_k, ..., R_{n-1}$$
目标是尽可能提高吞吐量以及内存利用率(注意,这两个目标常常是冲突的)
吞吐量是在单位时间内完成的请求数量。假设在 10 秒中之内进行了 5000 次 malloc
和 5000 次 free
调用,那么吞吐量是 1000 operations/second
另外一个目标是 Peak Memory Utilization,就是最大的内存利用率,具体如下:
影响内存利用率的主要因素就是『内存碎片』,有两种类型:
对于给定的 block,internal fragmentation 主要是因为 payload 小于 block size 造成的,如下图所示:
主要是由以下原因导致;
只依赖于上一个请求的具体模式,所以比较容易测量。
指的是内存中没有足够的连续空间,如下图所示:
依赖于未来的请求模式,所以比较难测量。
在具体实现之前,需要考虑以下问题:
一个标准的方式是在指针的前一个 word 中保存 block 的大小,通常称之为 header field 或 header,这种方式需要额外的一个 word,具体如下:
这里我们先给出常用的四种方式,这节课主要介绍第一种,下节课会介绍后面的方法:
对于每个 block 来说,我们需要知道大小和具体的状态(已分配/未分配),可以用两个 word 来存储,但是这样太浪费了。
如果一个 block 已经对其,低位地址一定是 0,所以我们可以用来当做 allocated/free 标志,当读入 word 大小的时候,需要标记出这个值。
寻找未分配的空间的方式如下,主要有三种:
确定空间之后,具体进行分配如下:
void addblock(ptr p, int len) {
int newsize = ((len + 1) >> 1) << 1; // round up to even
int oldsize = *p & -2; // mask out low bit
*p = newsize | 1; // set new length
if (newsize < oldsize)
*(p+newsize) = oldsize - newsize; // set length in remaining
// part of block
}
在释放空间的时候如果 block 后面也是未分配的空间,只做基本的处理的话,会出现有足够的位置,但是因为多余的分隔而找不到对应的位置,如下所示:
解决的办法是 Coalescing:
另一种方法是双向 coalescing:
具体 coalescing 的时候有四种情况:
下面是四种情况的:
Case 1:
Case 2:
Case 3:
Case 4:
Boundary Tags 的坏处就是会导致 internal fragmentation。
总结一下:
最后是 implicit list 的总结:
总体来说这一部分还是比较简单的,虽然简单,但是一定要理解清楚,因为下节课会介绍更加复杂的机制。