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 <stdio.h>

#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 <stdio.h>
    
    #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这一列。
  • 虽然这种利用了有一定技巧性的东西,在某种程度上,这样做可能会带来一点点的性能提升。但在实际开发过程中,个人觉得要尽量避免写让别人难以理解的、低可读性的代码,这样做换来的是可维护性、可扩展性的下降,这是得不偿失的。但是如果性能提升是指数这种数量级的,还是可以考虑采用的。其实这就是架构师常常需要做的一个不断权衡的过程,这就需要具体场景具体代入分析了。

NoSay
449 声望544 粉丝