baiyan
全部视频:https://segmentfault.com/a/11...
源视频地址:http://replay.xesv5.com/ll/24...
复习
PHP内存分配流程
利用gdb回顾PHP内存分配流程
- 首先在_emalloc()函数处打一个断点
- alloc_globals是一个全局变量,可以利用AG宏访问内部字段
-
接下来观察mm_heap中的字段:
- size = 0,peak = 0,表示当前没有使用任何内存
- real_size = 2097152 = 2M,表示当前已经向操作系统申请了一个chunk的内存
-
跟进main_chunk字段:
- heap字段的地址与上述mm_heap字段的地址相同,这样可以方便快速定位到该chunk的mm_heap。注意到其地址为0x7ffff3800040,注意倒数第二位有一个4(十进制为64),即其起始地址并不是正好2MB对齐的,而是2MB+64B。这是为什么呢?看下图,该结构体前几个字段正好占用了64B,所以heap_slot字段就只能从64B的偏移量位置开始。
- next、prev字段代表chunk与chunk之间以双向链表链接,当只有一个chunk结点时,next和prev指针均指向自己
- free_pages字段为511,说明512个page中,有1个page被使用了,剩余511个空闲page
- free_map字段为8个uint64_t类型,标记每个page是否被使用,共512bit。每个chunk中的512个page被分成了8组,每组64个page,正好对应free_map中的每一项,0是未使用,1是已使用
- map字段为512个uint32_t类型,共2KB。来标记是small内存还是large内存,并可以利用低位存储bit_num或已用的page数量等信息。这里它是一个large内存,有1个page已经被使用(存mm_heap)
- 我们跟着gdb走一遍,在_emalloc()之后,调用了zend_mm_alloc_heap()函数,然后判断size的大小,这里小于了3KB(ZEND_MM_MAX_SMALL_SIZE),所以这里调用了zend_mm_alloc_small()函数,也有可能调用zend_mm_alloc_small_slow()函数,视情况而定。然后计算bin_num,这里size为11,小于64。所以直接return (size - !!size) >> 3,打印结果为1,根据其bin_num去bin_data_size数组中找到应该分配的size大小是16B,1个page可以分成256个16B,需要1个page。这样,PHP会拿出1个16B返回给用户,剩余255个16B会挂在free_slot链表上,且数组下标为1(下标为0保存8B内存,下标为1保存16B内存...),再次打印free_slot字段,观察第1个下标已经有了元素,由此可以验证我们的结论。
复杂的宏替换
- 针对视频中bin_data_size数组为什么会最终经过替换后,为什么最终bin_data_size数组只存了size一列数据的问题,为了方便讲解,在此对源码示例做了简化:
#include
#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)
int main() {
int bin_data_size[] = { ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y) };
for (int i = 0; i < sizeof(bin_data_size) / sizeof(bin_data_size[0]); i++) {
printf("%d, ", bin_data_size[i]);
}
//打印结果为:
//{8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2560, 3072}
}
-
这样看确实很难看出来,在这个基础上,我做了一个简化版本:
#include
#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size, #define ZEND_MM_BINS_INFO(_, x, y) _( 0, 8, 512, 1, x, y) int main() { int data[] = { ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y) }; for (int i = 0; i < sizeof(data) / sizeof(data[0]); i++) { printf("%d, ", data[i]); } //打印结果为: //{8, } } - 我们利用分而治之的思想,从代码层面简化问题。首先观察的顺序是由外到内,先看外面这个ZEND_MM_BINS_INFO(_, x, y)宏,宏就是替换,那么我们将下划线_的位置用_BIN_DATA_SIZE这个东西替换,那么外层宏的替换结果就直接变成了_BIN_DATA_SIZE( 0, 8, 512, 1, x, y)了。而_BIN_DATA_SIZE又是一个宏,根据定义,直接替换为 size, ,注意这里第一个英文逗号(并非第二个中文逗号)也是宏替换结果的一部分,这样做是为了凑成一个数组初始化的语法。C语言数组初始化的语法为int a[2] = {1,2},由于我们简化了问题,只用了一个数组元素,在这里的替换结果就是8。那么应用到上面那个复杂版本也同理,将_BIN_DATA_SIZE这个东西替换到所有30行下划线的位置,这样最终就构成了一个由size组成的数组。
- 那么问题来了,为什么要把如此简单的一个数组初始化问题复杂化?这样写代码真的真的不会被别人锤吗?
- 其实源码中还有其他相关部分:
#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
static const uint32_t bin_data_size[] = {
ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y)
};
#define _BIN_DATA_ELEMENTS(num, size, elements, pages, x, y) elements,
static const uint32_t bin_elements[] = {
ZEND_MM_BINS_INFO(_BIN_DATA_ELEMENTS, x, y)
};
#define _BIN_DATA_PAGES(num, size, elements, pages, x, y) pages,
static const uint32_t bin_pages[] = {
ZEND_MM_BINS_INFO(_BIN_DATA_PAGES, x, y)
};
- 我们可以看到,PHP源码中提供了三个类似的宏替换结构。这里的下划线_本质上就是一个占位符,也可以理解为一个函数参数,根据这个参数的不同,来返回不同的值。
- 这个占位符的作用,就是可以根据你传入的占位符(或参数),从这个ZEND_MM_BINS_INFO这个宏中,提取出不同列的元素,并巧妙地构成了数组初始化语法。比如下划线位置替换成_BIN_DATA_SIZE,那么就是取出size这一列;如果是_BIN_DATA_ELEMENTS,就是取出elements这一列;如果是_BIN_DATA_PAGES,就是取出pages这一列。
- 虽然这种利用了有一定技巧性的东西,在某种程度上,这样做可能会带来一点点的性能提升。但在实际开发过程中,个人觉得要尽量避免写让别人难以理解的、低可读性的代码,这样做换来的是可维护性、可扩展性的下降,这是得不偿失的。但是如果性能提升是指数这种数量级的,还是可以考虑采用的。其实这就是架构师常常需要做的一个不断权衡的过程,这就需要具体场景具体代入分析了。