内存管理一直是一个程序设计绕不开的一个话题,尤其是当存在性能瓶颈或内存泄漏时,一个合理的内存分配和释放策略尤为重要。由于之前使用HTK作为快速验证语音相关算法的软件原型时,出现过内存泄漏的问题,借此机会研究了一下HTK的内存设计方法。本文以HTK内存管理设计为例介绍内存管理三个层次的最上层设计应用案例。
1 内存管理的一些基本问题
1.1 内存管理三个层次
内存管理实现一般有三个层次,分别为内核态、用户态的底层软件应用和上层软件应用。
- 内核态
内核态是操作系统负责内存管理。操作系统直接面对各种物理存储器,利用各种策略进行合理的分配,同时抽象后提供给用户态进行系统调用。
- 用户态底层
用户态底层一般是各种高度优化的并面对用户的内存管理器,通过系统调用获得大块内存,然后面对用户的不同内存请求使用不用的策略,如glibc中的ptmalloc2,通过brk和mmap这两个系统调用获取内存空间,又或者google的tcmalloc,tcmalloc在单线程或者高并发时相对ptmalloc2都有很不错的提升。
- 用户态上层
用户态上层是用户面向应用直接实现的各种功能库,由于各类应用有不一样的内存分配侧重点,所以在该层面实现一个内存管理器,可以减少和下一层次内存管理器的交互代价,且了解用户的使用逻辑,达到更加轻便和高效的效果。
1.2 一个好的内存管理器应该具备哪些优点
- 分配迅速:对于某些高并发请求类场景,内存分配速度可能会成为一个瓶颈。
- 释放方便:对于大型应用,可能存在复杂的内存分配逻辑,如果不加以统一管理,很有可能存在内存泄漏的问题。
- 减少内存碎片:合理的分配和回收内存空间。
1.3 用户态内存分配思想
- 大内存和小内存分开管理,大内存一般利用系统调用,而小内存则更加考验内存管理器的实现能力。
- 不同层次的精细化管理,如多线程分配问题,不同大小的精细申请内存。
- 何时选择从内核态申请内存和返还内存。
- 尽量减少同低层次内存管理器交互也是一个关键因素,越上层的实现可能越高效,当然也可能增加了软件实现成本和迁移成本。
2 HTK内存管理
HTK内存管理是由HMEM模块实现,所有程序通过调用HMEM实现内存分配和释放。
HTK中很多tools需要为不同的数据结构动态的申请大量的内存。为了能够精准和高效的分配内存,并且能够统一管理内存,HTK实现了自己的内存管理器HMEM。注意HTK的数学计算库等基础模块的内存分配也是利用HMEM,可以说HMEM是HTK很底层的存在。
2.1 颗粒度设计
大颗粒度划分为三个形态:
MHEAP:每次调用该HEAP只能分配固定大小的内存,即无视用户临时意图,但是new/free是可以随机访问的。同时提供global reset,即统一管理。好处就是由于分配的大小固定,所以分配迅速,且支持随机访问,则可以随时进行分配和释放。使用场景有限,一般用于大量重复的结构体等数据结构的内存管理。
随即访问含义:能够从block list中任意block和该block内部的任意位置(任意一个element)分配和回收用户内存。
思想:分配删除都比较方便,但是只对特定大小的结构体好用。
应用场合:大量重复且分配和释放逻辑不确定的结构体,如TOKEN。
MSTACK:每次申请可以获得任意大小的内存,但是new/free只支持LIFO,即入栈出栈模式。同时提供global reset,即统一管理。好处是用户可以申请任意大小的内存,所以应用比较广泛。释放时只能释放位于栈顶的内存块。
LIFO含义:只能从block list中第一个block和该block内部最上层空间分配和回收用户内存。
思想:分配删除受限,但是可以分配任意大小的内存,注意删除时,会把比要删除内存更早分配的所有内存释放,此举意味着最好内存使用逻辑符合后分配先释放原则,用于符合层次调用的内存顺序分配尤其好用,能够保证内存按逻辑释放(防止内存泄漏),如后调用的深层次内存在程序结束时首先被释放。
应用场合:用户申请大小不一致,但是逻辑符合内存后分配先释放原则的层次调用比较好用。
CHEAP:顾名思义直接调用C库,进行任意大小的内存分配,同时可以随机malloc/free,但是无法global reset。
对应代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
typedef enum{MHEAP,MSTAK,CHEAP} HEAPTYPE; typedef struct{ char* name; HEAPTYPE type; float growf; size_t elemSize; size_t minElem; size_t maxElem; size_t curElem; size_t totUsed; size_t totAlloc; BlockP heap; Boolean protectStk; }MemHeap;
中颗粒度:
block:除了CHEAP,直接遵循glibc实现标准,其他两个以block为颗粒度从下层申请内存。block的大小可以自适应的小幅度增长,受到
growf
参数支配。对应代码如下:
1 2 3 4 5 6 7 8
typedef struct _Block{ size_t numFree; size_t firstFree; size_t numElem; ByteP used; Ptr data; BlockP next; }Block;
小颗粒度:
- element:一个block内有多个element,MHEAP每个block的elementSize可以是任意大小,而MSTACK每个block的elementSize大小永远是
1byte
2.2 统一管理
使用全局的heapList记录所有类型的内存分配操作:
1
2
3
4
5
typedef struct _MemHeapRec{
MemHeap *heap;
struct _MemHeapRec *next;
}MemHeapRec;
static MemHeapRec *heapList = NULL;
MemHeapRec
主要通过RecordHeap
和UnRecordHeap
两个函数来完成内存堆的记录和擦除操作。维护一个全局的列表,有利于进行统一的操作如:global reset。同时提供了一个很实用的函数PrintHeapStats()
,用于查看当前所有已经分配的Heap的状态。
2.3 接口设计
上层接口 | 功能 |
---|---|
void CreateHeap(MemHeap *x, char *name, HeapType type, size_t elemSize, float growf, size_t numElem, size_t maxElem) | 创建任意一个类型的Heap,并将其记录在heapList中。对于三种Heap创建虽然输入参数一致,但是内部逻辑基本不一致,有些参数可能会失效。 |
void ResetHeap(MemHeap *x) | 重置Heap,对于CHEAP该操作不起作用,MHEAP释放所有的block,而MSTACK会保留第一个block,因此要注意可能会导致内存泄漏。 |
void DeleteHeap(MemHeap *x) | 彻底删除一个HEAP,该操作将某个类型的HEAP直接抹除,释放其所有block,对于MSTACK会额外进行一次block释放,从全局heapList删除其信息,最终删除该HEAP结构体内存。 |
void *New(MemHeap *x, size_t size) | 用户主要接口:从指定类型的堆中分配指定大小的内存 |
void Dispose(MemHeap *x, void *p) | 从指定类型的堆中删除指定的内存指针 |
核心重要函数 | 功能 |
static void *GetElem(BlockP p, size_t elemSize, HeapType type) | 从指定的块中分配内存,被New函数调用,用于现有block满足内存申请大小时。 |
BlockP AllocBlock(size_t size , size_t num , HeapType type) | 从下一层如glibc层内存管理器申请内存,被New调用,用于现有block无法满足内存申请大小时。 |
2.4 代码简析
章节2.3所有接口中最能体现内存管理设计思想的函数为以下三个:
New():
从new函数可以看出用户内存被分配时永远使用的是位于block链表的第一个block。
MHEAP是因为及时调整用于用户申请内存分配的block的位置,MSTACK是因为其只能从栈顶分配内存也即第一个block。
MHEAP由于分配的是固定大小的elem,所以有一个table状态表维护分配和释放信息,分配时flag = 1。
MSTACK由于可以是随机大小,同时在block list维度和block内部都遵循栈顶申请和释放准则。
Dispose():
- MHEAP可以删除block list维度和block内部任意位置的内存,MSTACK只能删除block list维度和block内部维度上的栈顶内存,因此会把之前所有比指针p先分配但是不属于指针p指向的内存的所有内存全部释放掉,包括其他block。
GetElem():
- MHEAP从任意block分配一个合适的固定elementSize大小的内存。
- MSTACK栈顶block及block内部顶部分配合适的任意大小的内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
void *New(MemHeap *x, size_t size) //返回从内存堆x分配大小为size的新元素指针
{
void *q;
BlockP newp;
size_t num,bytes,*ip,chdr;
Boolean noSpace;
Ptr *pp;
/*检查当前堆是否创建,只有没有创建的堆才会elemSize = 0*/
if (x->elemSize <= 0)
HError(5174, "New: heap %s not initialised",
(x->name==NULL) ? "Unnamed" : x->name);
switch(x->type)
{
case MHEAP:
// ATTENTION:用户每次只能申请elemSize大小的内存,但是每次申请的新block大小是num* elemSize大小的内存
// 不论新申请的block还是已存在的满足要求的block都会被放到表头。这样每次都是从表头分配内存。
if (size != 0 && size != x->elemSize)
HError(5173,"New: MHEAP req for %u size elem from heap %s size %u",
size , x->name , x->elemSize);
noSpace = x->totUsed == x->totAlloc;
if (noSpace || (q = GetElem(x->heap , x->elemSize , x->type)) == NULL)
{
// 有空间的情况下,搜索存在至少一个空闲element的block,每次也只能分配一个element。
// BlockReorder遍历所有block 如果存在满足要求的block,移动到最前面。
if (!noSpace)
BlockReorder(&(x->heap), 1);
// MHEAP没有空间,或者分配失败
if (noSpace || (q = GetElem(x->heap, x->elemSize, x->type)) == NULL)
{
// curElem记录当前最大block分配element个数,num为根据增长因子growf自适应增长后的大小,但同时被 //maxElem约束
num = (size_t) ((double)x->curElem * (x->growf + 1.0) + 0.5);
if (num > x->maxElem)
num = x->maxElem;
// 分配新block
newp = AllocBlock(x->elemSize, num, x->type);
// x->totAlloc和 num都是element的个数;
x->totAlloc += num;
x->curElem = num;
// 头插法放入表头,即新分配的block永远为第一个block
newp->next = x->heap;
x->heap = newp;
if ((q=GetElem(x->heap, x->elemSize, x->type)) == NULL)
HError(5191,"New: null elem but just made block in heap %s", x->name);
}
}
// 虽然可能分配的block很大,但是分配给用户的elem个数 = 1
x->totUsed++;
if (trace&T_MHP)
printf("HMem: %s[M] %u bytes at %p allocated\n", x->name, size, q);
return q;
case CHEAP:
// 在每次申请内存时多分配一个sizeof(size_t)大小的空间:chdr,用于记录分配内存的大小。
chdr = MRound(sizeof(size_t));
q = malloc(size+chdr); //直接使用malloc分配
if (q==NULL)
HError(5105,"New: memory exhausted");
x->totUsed += size;
x->totAlloc += size+chdr;
ip = (size_t *)q;
*ip = size;
if (trace&T_CHP)
printf("HMem: %s[C] %u+%u bytes at %p allocated\n", x->name, chdr,size, q);
return (Ptr)((ByteP)q+chdr);
case MSTAK:
// 用户可以申请任意大小的内存,但elementSize被限制为1bytes
if (x->protectStk)
size += sizeof(Ptr);
size = MRound(size);
// 由于MSTACK的性质,导致每次只能从表头分配内存或者释放内存。不再有查找空余内存的动作(BlockReorder)
if ((q = GetElem(x->heap, size, x->type)) == NULL)
{
// 每次分配bytes大小的新block
bytes = (size_t)((double)x->curElem * (x->growf + 1.0) + 0.5);
if (bytes > x->maxElem)
bytes = x->maxElem;
x->curElem = bytes;
// 由于bytes大小有突破x->maxElem的可能性,所以可以重新分配
if (bytes < size)
bytes = size;
bytes = MRound(bytes);
newp = AllocBlock(1, bytes, x->type);
x->totAlloc += bytes;
// 新block位置和MHEAP操作一致
newp->next = x->heap;
x->heap = newp;
if ((q=GetElem(x->heap, size, x->type)) == NULL)
HError(5191,"New: null elem but just made block in heap %s",x->name);
}
x->totUsed += size;
if (trace&T_STK)
printf("HMem: %s[S] %u bytes at %p allocated\n", x->name, size, q);
if (x->protectStk)
{
pp = (Ptr *)((long)q + size - sizeof(Ptr));
*pp = q;
}
return q;
}
return NULL;
}
static void *GetElem(BlockP p, size_t elemSize, HeapType type)
{
int i,index;
if (p == NULL)
return NULL;
switch (type)
{
case MHEAP:
// MHEAP由于每次只能分配一个element,所以会维护一个table:p->used,用于查找下一个空闲elem索引。
if (p->numFree == 0)
return NULL;
index = p->firstFree; //第一个空闲elem index,直接分配给用户
p->used[p->firstFree/8] |= 1<<(p->firstFree&7); //table对应索引位置flag置1
p->numFree--;
// 查找下一个空闲元素索引,为下一次分配elem做准备。当没有空闲元素时p->firstFree指向最后一个elem的下一个index。
if (p->numFree > 0)
{
for (i=p->firstFree+1; i<p->numElem;i++)
{
if ((p->used[i/8] & (1 <<(i&7))) == 0)
{
p->firstFree = i;
break;
}
}
}
else
p->firstFree = p->numElem;
return (void *)((ByteP)p->data+index*elemSize); //返回分配的数据区指针
case MSTAK:
// 栈顶的block不满足要求直接返回。比较简单,因为
if (p->numFree < elemSize)
return NULL;
index = p->firstFree;
p->firstFree += elemSize;
p->numFree = p->numFree - elemSize;
return (void *)((ByteP)p->data + index); //返回分配的数据区指针
default:
HError(5190,"GetElem: bad type %d", type);
}
return NULL;
}
void Dispose(MemHeap *x, void *p) //从内存堆x中释放p
{
BlockP head , cur , prev;
Boolean found = FALSE;
ByteP bp;
size_t size,chdr;
size_t num,index, *ip;
Ptr *pp;
if (x->totUsed == 0)
HError(5105 , "Dispose: heap %s is empty" , x->name);
switch(x->type)
{
case MHEAP:
// 支持链表中任意block的删除操作
head = x->heap;
cur=head;
prev=NULL;
size = x->elemSize;
while (cur != NULL && !found)
{
num = cur->numElem;
// 判断指针在该位置
found = cur->data <= p &&(((void*)((ByteP)cur->data+(num-1)*size)) >= p);
if (!found)
{
prev=cur;
cur=cur->next;
}
}
if (cur == NULL)
HError(5175,"Dispose: Item to free in MHEAP %s not found",x->name);
index = ((size_t)p-(size_t)cur->data)/size;
cur->used[index/8] &= ~(1 <<(index&7));
if (index < cur->firstFree)
cur->firstFree = index;
cur->numFree++;
x->totUsed--;
// 如果该block没有elem被使用,则释放
if (cur->numFree == cur->numElem)
{
if (cur != head)
prev->next = cur->next;
else
head = cur->next;
x->heap = head;
x->totAlloc -= cur->numElem;
free(cur->data);
free(cur->used);
free(cur);
}
if (trace&T_MHP)
printf("HMem: %s[M] %u bytes at %p de-allocated\n", x->name, size, p);
return;
case MSTAK:
// 由于MSTACK遵从LIFO,因此要想释放掉p所在的block,需要将之前的block全部释放才能释放处于栈顶的p所在的block
cur = x->heap;
if (x->protectStk)
{
if (cur->firstFree > 0 )
pp = (Ptr *)((size_t)cur->data+cur->firstFree-sizeof(Ptr));
else
{
if (cur->next == NULL)
HError(5175,"Dispose: empty stack");
pp = (Ptr *)((size_t)cur->next->data+cur->next->firstFree-sizeof(Ptr));
}
if (*pp != p)
HError(-5175,"Dispose: violation of stack discipline in %s [%p != %p]",
x->name, *pp, p);
}
while (cur != NULL && !found)
{
num = cur->numElem;
found = cur->data <= p &&
(((void*)((ByteP)cur->data+num)) > p);
if (!found)
{
x->heap = cur->next;
x->totAlloc -= cur->numElem;
x->totUsed -= cur->firstFree;//fristFree体现了使用量
free(cur->data);
free(cur);
cur = x->heap;
if (trace&T_STK)
printf("HMem: deleleting block in %s[S]\n", x->name);
}
}
if (!found)
HError(5175,"Dispose: Item to free in MSTAK %s not found", x->name);
// CAUTION:block内同样遵循stack准则,会将p之后不属于p指针指向的内存空间一块释放掉。
size = ((ByteP)cur->data + cur->firstFree) - (ByteP)p; //分配数据区的实际大小
if (size < 0)
HError( 5175 , "Dispose: item to free in MSTAK %s is above stack top",
x->name);
cur->firstFree -= size;
cur->numFree += size;
x->totUsed -= size;
if (trace&T_STK)
printf("HMem: %s[S] %u bytes at %p de-allocated\n", x->name, size, p);
return;
case CHEAP:
// 只需要注意释放内存时释放储存分配空间大小的chdr空间即可。
chdr = MRound(sizeof(size_t));
bp = (ByteP)p-chdr;
ip = (size_t *)bp;
x->totAlloc -= (*ip + chdr);
x->totUsed -= *ip;
free(bp);
if (trace&T_CHP)
printf("HMem: %s[C] %u+%u bytes at %p de-allocated\n",
x->name, chdr, *ip, bp);
return;
}
}