资讯专栏INFORMATION COLUMN

【PHP源码学习】2019-03-11 PHP内存管理3笔记

wangjuntytl / 1372人阅读

摘要:那么问题来了,为什么要把如此简单的一个数组初始化问题复杂化这样写代码真的真的不会被别人锤吗其实源码中还有其他相关部分我们可以看到,源码中提供了三个类似的宏替换结构。

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这一列。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31288.html

相关文章

  • 【LNMPR源码学习笔记汇总

    摘要:此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考整理与心得体会,此文会不断更新视频传送门每日学习记录使用录像设备记录每天的学习源码学习源码学习内存管理笔记源码学习内存管理笔记源码学习内存管理笔记源码学习基本变量笔记 此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考、整理与心得体会,此文会不断更新 视频传送门:【每日学习记录】使用录像设备记录每天的学习 PHP7...

    gougoujiang 评论0 收藏0
  • 【LNMPR源码学习笔记汇总

    摘要:此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考整理与心得体会,此文会不断更新视频传送门每日学习记录使用录像设备记录每天的学习源码学习源码学习内存管理笔记源码学习内存管理笔记源码学习内存管理笔记源码学习基本变量笔记 此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考、整理与心得体会,此文会不断更新 视频传送门:【每日学习记录】使用录像设备记录每天的学习 PHP7...

    aristark 评论0 收藏0
  • 【LNMPR源码学习笔记汇总

    摘要:此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考整理与心得体会,此文会不断更新视频传送门每日学习记录使用录像设备记录每天的学习源码学习源码学习内存管理笔记源码学习内存管理笔记源码学习内存管理笔记源码学习基本变量笔记 此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考、整理与心得体会,此文会不断更新 视频传送门:【每日学习记录】使用录像设备记录每天的学习 PHP7...

    Barrior 评论0 收藏0
  • PHP源码学习2019-03-26 宏定义笔记

    摘要:并且在我们日常的代码学习中,我们会碰到过很多很多的宏定义。如果宏定义中带有参数,而代码中出现同样标识时没有参数,不视为宏。具体的解析见源码学习内存管理笔记。 grape 全部视频:https://segmentfault.com/a/11... 原视频地址:http://replay.xesv5.com/ll/24... 引入 我们知道宏定义的优点有方便程序的修改,提高程序运行效率等等...

    SKYZACK 评论0 收藏0
  • 【每日学习记录】使用录像设备记录每天的学习

    摘要:在这里使用学而思网校的录像设备,记录每天学习的内容闫昌李乐阶段李乐李乐李乐李乐李乐李乐马运运李乐李乐李乐源码集群闫昌源码闫昌源码主从复制李乐源码施洪宝源码施洪宝韩天 在这里使用学而思网校的录像设备,记录每天学习的内容: 2019-06-24 ~ 2019-06-28 06-27 nginx by 闫昌 06-26 nginx module by 李乐 06-25 nginx http ...

    szysky 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<