msgbartop
PHP语言, PHP扩展, Zend引擎相关的研究,技术,新闻分享 – 左手代码 右手诗
msgbarbottom

09 Nov 11 PHP原理之内存管理中难懂的几个点

PHP的内存管理, 分为俩大部分, 第一部分是PHP自身的内存管理, 这部分主要的内容就是引用计数, 写时复制, 等等面向应用的层面的管理. 而第二部分就是今天我要介绍的, zend_alloc中描写的关于PHP自身的内存管理, 包括它是如何管理可用内存, 如何分配内存等.

另外, 为什么要写这个呢, 因为之前并没有任何资料来介绍PHP内存管理中使用的策略, 数据结构, 或者算法. 而在我们平时开发扩展, 修复PHP的bug的时候, 却对这一部分的知识需要有一个良好的理解. PHP开发组内的很多朋友也对这块不是很清楚, 所以我觉得有必要专门写一下.

一些基本的概念, 我就不赘述了, 因为看代码很容易能看懂, 我这里就主要介绍几个看代码没那么容易看懂的点, 为什么这么说呢, 呵呵, 我在写文章之前, 查找了下已有的资料, 已避免重复功, 其中看到了TIPI项目对这部分的描述, 发现其中错误很多. 所以, 我想这部分就是看代码也没那么容易看懂的点 :)

目前, 英文版的介绍也在撰写中: Zend MM

Zend Memory Manager, 以下简称Zend MM, 是PHP中内存管理的逻辑. 其中有一个关键数据结构: zend_mm_heap:

Zend MM把内存非为小块内存和大块内存俩种, 区别对待, 对于小块内存, 这部分是最最常用的, 所以追求高性能. 而对于大块内存, 则追求的是稳妥, 尽量避免内存浪费.

所以, 对于小块内存, PHP还引入了cache机制:

Zend MM 希望通过cache尽量做到, 一次定位就能查找分配.

而一个不容易看懂的点是free_buckets的申明:

Q: 为什么free_buckets数组的长度是ZEND_MM_NUMBER_BUCKET个?

A: 这是因为, PHP在这处使用了一个技巧, 用一个定长的数组来存储ZEND_MM_NUMBER_BUCKET个zend_mm_free_block, 如上图中红色框所示. 对于一个没有被使用的free_buckets的元素, 唯一有用的数据结构就是next_free_block和prev_free_block, 所以, 为了节省内存, PHP并没有分配ZEND_MM_NUMBER_BUCKET * sizeof(zend_mm_free_block)大小的内存, 而只是用了ZEND_MM_NUMBER_BUCKET * (sizeof(*next_free_block) + sizeof(*prev_free_block))大小的内存..

我们来看ZEND_MM_SMALL_FREE_BUCKET宏的定义:

#define ZEND_MM_SMALL_FREE_BUCKET(heap, index) \
    (zend_mm_free_block*) ((char*)&heap->free_buckets[index * 2] + \
        sizeof(zend_mm_free_block*) * 2 - \
        sizeof(zend_mm_small_free_block))

之后, Zend MM 保证只会使用prev和next俩个指针, 所以不会造成内存读错..

那么, 第二个不容易看懂的点, 就是PHP对large_free_buckets的管理, 先介绍分配(TIPI项目组对此部分的描述有些含糊不清):

static zend_mm_free_block *zend_mm_search_large_block(zend_mm_heap *heap, size_t true_size)

large_free_buckets可以说是一个建树和双向列表的结合:

large_free_buckets使用一个宏来决定某个大小的内存, 落在什么index上:

#define ZEND_MM_LARGE_BUCKET_INDEX(S) zend_mm_high_bit(S)

zend_mm_high_bit获取true_size中最高位1的序号(zend_mm_high_bit), 对应的汇编指令是bsr(此处, TIPI项目错误的说明为: “这个hash函数用来计算size的位数,返回值为size二进码中1的个数-1″).

也就是说, 每一个在large_free_buckets中的元素, 都保持着指向一个大小为在对应index处为1的size的内存块的指针. 诶, 有点绕口, 举个例子:

比如对于large_free_buckets[2], 就只会保存, 大小在0b1000到0b1111大小的内存. 再比如: large_free_buckets[6], 就保存着大小为0b10000000到0b11111111大小的内存的指针.

这样, 再分配内存的时候, Zend MM就可以快速定位到最可能适合的区域来查找. 提高性能.

而, 每一个元素又同时是一个双向列表, 保持着同样size的内存块, 而左右孩子(child[0]和child[1])分别代表着键值0和1, 这个键值是指什么呢?

我们来举个例子, 比如我向PHP申请一个true_size为0b11010大小的内存, 经过一番步骤以后, 没有找到合适的内存, PHP进入了zend_mm_search_large_block的逻辑来在large_free_buckets中寻找合适的内存:

1. 首先, 计算true_size对应的index, 计算方法如之前描述的ZEND_MM_LARGE_BUCKET_INDEX

2. 然后在一个位图结构中, 判断是否存在一个大于true_size的可用内存已经存在于large_free_buckets, 如果不存在就返回:

size_t bitmap = heap->large_free_bitmap >> index;
if (bitmap == 0) {
   return NULL;
}

3. 判断, free_buckets[index]是否存在可用的内存:

if (UNEXPECTED((bitmap & 1) != 0))

4. 如果存在, 则从free_buckets[index]开始, 寻找最合适的内存, 步骤如下:

4.1. 从free_buckets[index]开始, 如果free_buckets[index]当前的内存大小和true_size相等, 则寻找结束, 成功返回.

4.2. 查看true_size对应index后(true_size << (ZEND_MM_NUM_BUCKETS - index))的当前最高位, 如果为1. 则在free_buckets[index]->child[1]下面继续寻找, 如果free_buckets[index]->child[1]不存在, 则跳出. 如果true_size的当前最高位为0, 则在free_buckets[index]->child[0]下面继续寻找, 如果free_buckets[index]->child[0]不存在, 则在free_buckets[index]->child[1]下面寻找最小内存(因为此时可以保证, 在free_buckets[index]->child[1]下面的内存都是大于true_size的)

4.3. 出发点变更为2中所述的child, 左移一位ture_size.

5. 如果上述逻辑并没有找到合适的内存, 则寻找最小的”大块内存”:

   /* Search for smallest "large" free block */
    best_fit = p = heap->large_free_buckets[index + zend_mm_low_bit(bitmap)];
    while ((p = p->child[p->child[0] != NULL])) {
        if (ZEND_MM_FREE_BLOCK_SIZE(p) < ZEND_MM_FREE_BLOCK_SIZE(best_fit)) {
            best_fit = p;
        }
    }

注意上面的逻辑, (p = p->child[p->child[0] != NULL]), PHP在尽量寻找最小的内存.

为什么说, large_free_buckets是个键树呢, 从上面的逻辑我们可以看出, PHP把一个size, 按照二进制的0,1做键, 把内存大小信息反应到了键树上, 方便了快速查找.

另外, 还有一个rest_buckets, 这个结构是个双向列表, 用来保存一些PHP分配后剩下的内存, 避免无意义的把剩余内存插入free_buckets带来的性能问题(此处, TIPI项目错误的描述为: “这是一个只有两个元素的数组。 而我们常用的插入和查找操作是针对第一个元素,即heap->rest_buckets[0]“).


分享到:



Related Posts:

Tags: , ,

23 Responses to “PHP原理之内存管理中难懂的几个点”

  1. ww |

    这是用php写的吗?

  2. more |

    mark

  3. Anonymous |

    mark

  4. PHP与MySQL通讯那点事 - 博客 - 伯乐在线 |

    [...] (中文地址:php-zend的内存管理中文版)这块,对于末学来说,太复杂了,只是稍微看懂直接 [...]

  5. 小灰马 |

    之前的问题更正一下,ZEND_MM_MAX_SMALL_SIZE并非256,而是((ZEND_MM_NUM_BUCKETS<<ZEND_MM_ALIGNMENT_LOG2)+ZEND_MM_ALIGNED_MIN_HEADER_SIZE),比256稍大一点,但是也是存在之前提的那个问题,large_buckets前面几个bucket都是没用的。

  6. 小灰马 |

    鸟哥你好,对于large_free_buckets我看代码后与你的理解有些出入,希望探讨一下:对于zend_mm_high_bit,我的理解是它判断size二进制最高位的1是第几位,例如0100 0000 则对应的index应该是7,而large_free_buckets存储的内存块大小都是>ZEND_MM_MAX_SMALL_SIZE,在32位机器上,也就是256字节,这样的话,large_fee_buckets存储的最小的内存块应该大于等于256字节,也就是0b1 0000 0000,其对应的index应该是8,那么large_free_buckets前面的0~7个buckets应该是空的,没用用到才对,这样的话,你上面的的图中对于large_free_buckets的序号0~7下面就不应有节点才对,希望回复,谢谢~

  7. 小灰马 |

    其实,我看php内存管理纯粹是抄袭dlmalloc的,基本完全一样。鸟哥说是不是

  8. Anonymous |

    帮忙分析下
    #define ZEND_MM_SMALL_FREE_BUCKET(heap, index) \
    (zend_mm_free_block*) ((char*)&heap->free_buckets[index * 2] + \
    sizeof(zend_mm_free_block*) * 2 – \
    sizeof(zend_mm_small_free_block))
    真的不理解为什么要减去sizeof(zend_mm_small_free_block)
    这个结构是什么意思,抓狂了,鸟哥,帮帮忙

  9. php与mysql通讯那点事 | 莿鸟栖草堂 |

    [...] (中文地址:php-zend的内存管理中文版)这块,对于末学来说,太复杂了,只是稍微看懂直接 [...]

  10. PHP原理之内存管理中难懂的几个点树林/咖啡 成都专业php网站制作 | 树林/咖啡 成都专业php网站制作 |

    [...] 风雪之隅 » PHP源码分析 Posted in: php / Tagged: PHP原理之内存管理中难懂的几个点 [...]

  11. gerald |

    你好,想请教一下,在MVC中的数据层类中,方法和属性都用static 是否是更好的选择?因为这样速度快,而且可以省一个new,不知道这样做有没有潜在隐患,比如内存问题

  12. PHP原理之内存管理中难懂的几个点 | 我心飞翔 |

    [...] 作者: Laruence( )   原文地址: http://www.laruence.com/2011/11/09/2277.html [...]

  13. bitsucker |

    这段不是很理解,请大神指教:
    “(p = p->child[p->child[0] != NULL]), PHP在尽量寻找最小的内存.”
    我理解的是沿着child[0]走才能找到比父节点更小的内存;但这个逻辑却是只要发现child[0]方向有路就不走了……咋回事呢?

  14. PHP原理之内存管理中难懂的几个点 | 赛尔IT知识库 |

    [...] 本文地址: http://www.laruence.com/2011/11/09/2277.html [...]

  15. wedgwood |

    写的真不错。很清晰的阐述了内存管理这块内容,看完之后很有收获。
    一个问题,感觉ZEND_MM_SMALL_FREE_BUCKET那块的看了好久才想明白,可能是我经验不足。不过,如果加上类似“这个entry是双向链表的头节点,只包含prev和next两个元素,为了用统一用(zend_mm_free_block *)指针遍历双向链表的节点,所以会将这个头结点转换成了zend_mm_free_block指针。”的说明,可能会对新手好理解很多。:)

  16. eric |

    很有收获。但对位图(bitmap)在这里的作用还是比较模糊,继续读代码去。

  17. Rhythm |

    膜拜!这个东东,看来得多读几遍才能理解。

  18. henosteven |

    估计的看几遍 , 才会懂

  19. liexusong |

    不太好懂~

  20. Qiang |

    大神,你的那个示意图画得不怎么直观啊:)

  21. |

    这个必须得顶啊

  22. taylor |

    最近在看php的源代码,觉得好多东西都得再看几遍才能理解

  23. walu |

    先mark,慢慢看

Leave a Reply

*