Skip to content

Commit

Permalink
GitBook: [#40] No subject
Browse files Browse the repository at this point in the history
  • Loading branch information
webxiaohua authored and gitbook-bot committed Dec 30, 2021
1 parent 6ccc29b commit c00d2d6
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 6 deletions.
Binary file added .gitbook/assets/11_17.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_18.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_19.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_20.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_21.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_22.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_23.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_24.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_25.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_26.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_27.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_28.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_29.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/11_30.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 127 additions & 6 deletions 11.-zi-ji-dong-shou-shi-xian-malloc-nei-cun-fen-pei-qi.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,139 @@

**First Fit**

最简单的就是每次从头开始找起,找到第一个满足要求的就返回,这就是所谓的First fit方法,教科书中一般称为首次适应方法,当然我们不需要记住这样拗口的名字,只需要记住这是什么意思就可以了。
最简单的就是**每次从头开始找起**,找到第一个满足要求的就返回,这就是所谓的First fit方法,教科书中一般称为首次适应方法,当然我们不需要记住这样拗口的名字,只需要记住这是什么意思就可以了。

![](.gitbook/assets/11\_16.jpg)

这种方法的优势在于简单,但该策略总是从前面的空闲块找起,因此很容易在堆区前半部分因分配出 内存留下很多小的内存块,因此下一次内存申请搜索的空闲块数量将会越来越多。
这种方法的优势在于**简单**,但该策略总是从前面的空闲块找起,因此很容易在堆区前半部分因分配出内存留下很多小的内存块,因此下一次内存申请搜索的空闲块数量将会越来越多。

**Next Fit**

该方法是大名鼎鼎的Donald Knuth首次提出来的,如果你不知道谁是Donald Knuth,那么数据结构课上折磨的你痛不欲生的字符串匹配KMP算法你一定不会错过,KMP其中的K就是指Donald Knuth,该算法全称Knuth–Morris–Pratt string-searching algorithm,如果你也没听过KMP算法那么你一定听过下面这本书:

****
![](.gitbook/assets/11\_17.jpg)

****
这就是更加大名鼎鼎的《计算机程序设计艺术》,这本书就是Donald Knuth写的,如果你没有听过这本书请面壁思过一分钟,比尔盖茨曾经说过,如果你看懂了这本书就去给微软投简历吧,这本书也是很多程序员买回来后从来不会翻一眼只是拿来当做镇宅之宝用的。 

****
不止比尔盖茨,有一次乔布斯见到Knuth老爷子后。。算了,扯远了,有机会再和大家讲这个故事,拉回来。 

Next Fit说的是什么呢?这个策略和First Fit很相似,是说我们别总是从头开始找了,而是从上一次找到合适的空闲内存块的位置找起,老爷子观察到上一次找到某个合适的内存块的地方很有可能剩下的内存块能满足接下来的内存分配请求,由于不需要从头开始搜索,因此Next Fit将远快于First Fit。

![](.gitbook/assets/11\_18.jpg)

然而也有研究表明Next Fit方法内存使用率不及First Fit,也就是同样的停车场面积,First Fit方法能停 更多的车。

**Best Fit**

First Fit和Next Fit都是找到第一个满足要求的内存块就返回,但Best Fit不是这样。 

Best Fit算法会找到所有的空闲内存块,然后将所有满足要求的并且大小为最小的那个空闲内存块返回,这样的空闲内存块才是最Best的,因此被称为Best Fit。就像下图虽然有三个空闲内存块满足要求,但是Best Fit会选择大小为8字节的空闲内存块。

![](.gitbook/assets/11\_19.jpg)

显然,从直觉上我们就能得出Best Fit会比前两种方法能更合理利用内存的结论,各项研究也证实了这一点。 

然而Best Fit最大的缺点就是分配内存时需要遍历堆上所有的空闲内存块,在速度上显然不及前面两种方法。 

以上介绍的这三种策略在各种内存分配器中非常常见,当然分配策略远不止这几种,但这些算法不是该主题下关注的重点,因此就不在这里详细阐述了,假设在这里我们选择First Fit算法。

### 没有银弹

重要的是,从上面的介绍中我们能够看到,**没有一种完美的策略**,每一种策略都有其优点和缺点,**我们能做到的只有取舍和权衡**。因此,要实现一个内存分配器,设计空间其实是非常大的,要想设计出一个通用的内存分配器,就像我们常用的malloc是很不容易的。

![](.gitbook/assets/11\_20.jpg)

其实不止内存分配器,在设计其它软件系统时我们也没有银弹。

### 分配内存

现在我们找到合适的空闲内存块了,接下来我们又将面临一个新的问题。 

如果用户需要12字节,而我们的空闲内存块也恰好是12字节,那么很好,直接返回就可以了。 

但是,如果用户申请12字节内存,而我们找到的空闲内存块大小为32字节,那么我们是要将这32字节的整个空闲内存块标记为已分配吗?就像这样:

![](.gitbook/assets/11\_21.jpg)

这样虽然速度最快,但显然会浪费内存,形成**内部碎片**,也就是说该内存块剩下的空间将无法被利用 到。

![](.gitbook/assets/11\_22.jpg)

一种显而易见的方法就是将空闲内存块进行划分,前一部分设置为已分配,返回给内存申请者使用,后一部分变为一个新的空闲内存块,只不过大小会更小而已,就像这样:

![](.gitbook/assets/11\_23.jpg)

我们需要将空闲内存块大小从32修改为16,其中消息头header占据4字节,剩下的12字节分配出去,并将标记为置为1,表示该内存块已分配。

分配出16字节后,还剩下16字节,我们需要拿出4字节作为新的header并将其标记为空闲内存块。

### 释放内存

到目前为止,我们的malloc已经能够处理内存分配请求了,还差最后的内存释放。 

内存释放和我们想象的不太一样,该过程并不比前几个环节简单。我们要考虑到的关键一点就在于,与被释放的内存块相邻的内存块可能也是空闲的。如果释放一块内存后我们仅仅简单的将其标志位置为空闲,那么可能会出现下面的场景:

![](.gitbook/assets/11\_24.jpg)

从图中我们可以看到,被释放内存的下一个内存块也是空闲的,如果我们仅仅将这16个字节的内存块标记为空闲的话,那么当下一次申请20字节时图中的这两个内存块都不能满足要求,尽管这两个空闲内存块的总数要超过20字节。 

因此一种更好的方法是当应用程序向我们的malloc释放内存时,我们查看一下相邻的内存块是否是空闲的,**如果是空闲的话我们需要合并空闲内存块**,就像这样:

![](.gitbook/assets/11\_25.jpg)

在这里我们又面临一个新的决策,那就是释放内存时我们要立即去检查能否够合并相邻空闲内存块吗?还是说我们可以推迟一段时间,推迟到下一次分配内存找不到满足要的空闲内存块时再合并相邻空闲内存块。 

释放内存时立即合并空闲内存块相对简单,但每次释放内存时将引入合并内存块的开销,如果应用程序总是释放12字节然后申请12字节,然后在释放12字节等等这样重复的模式:

```
free(ptr);
obj* ptr = malloc(12);
free(ptr);
obj* ptr = malloc(12);
...
```

![](.gitbook/assets/11\_26.jpg)

那么这种内存使用模式对立即合并空闲内存块这种策略非常不友好,我们的内存分配器会有很多的**无用功**。但这种策略最为简单,在这里我们依然选择使用这种简单的策略。 

实际上我们需要意识到,实际使用的内存分配器都会有某种推迟合并空闲内存块的策略。

### 高效合并空闲内存块

合并空闲内存块的故事到这里就完了吗?问题没有那么简单。 

让我们来看这样一个场景:

![](.gitbook/assets/11\_27.jpg)

使用的内存块其前和其后都是空闲的,在当前的设计中我们可以很容易的知道后一个内存块是空闲的,因为我们只需要从当前位置向下移动16字节就是下一个内存块,但我们怎么能知道上一个内存块是不是空闲的呢?

![](.gitbook/assets/11\_28.jpg)

我们之所以能向后跳是因为当前内存块的大小是知道的,那么我们该怎么向前跳找到上一个内存块呢? 

还是我们上文提到的Donald Knuth,老爷子提出了一个很聪明的设计,我们之所以不能往前跳是因为不知道前一个内存块的信息,那么我们该怎么快速知道前一个内存块的信息呢?

![](.gitbook/assets/11\_29.jpg)

Knuth老爷子的设计是这样的,我们不是有一个信息头header吗,那么我们就在该内存块的末尾再加一个信息尾,footer,footer一词用的很形象,**header和footer的内容是一样的** 

因为上一内存块的footer和下一个内存块的header是相邻的,**因此我们只需要在当前内存块的位置向上移动4直接就可以等到上一个内存块的信息**,这样当我们释放内存时就可以快速的进行相邻空闲内存块的合并了。

![](.gitbook/assets/11\_30.jpg)

### 收工

至此,我们的内存分配器就已经设计完毕了。 

我们的简单内存分配器采用了First Fit分配算法;找到一个满足要求的内存块后会进行切分,剩下的作为新的内存块;同时当释放内存时会立即合并相邻的空闲内存块,同时为加快合并速度,我们引入了Donald Knuth的设计方法,为每个内存块增加footer信息。 

这样,我们自己实现的内存分配就可以运行起来了,**可以真正的申请和释放内存**

### 总结

本文从0到1实现了一个简单的内存分配器,但不希望这里的阐述给大家留下内存分配器实现很简单的印象,实际上本文实现的内存分配器还有大量的优化空间,同时我们也没有考虑线程安全问题,但这些都不是本文的目的。 

本文的目的在于把内存分配器的本质告诉大家,对于想理解内存分配器实现原理的同学来说这些已经足够了,而对于要编写高性能程序的同学来说实现自己的内存池是必不可少的,内存池实现也离不开这里的讨论。

****

0 comments on commit c00d2d6

Please sign in to comment.