PHP 源码 深入了解字符串写时复制

PHP字符串深入理解

无论在什么语言中字符串是常见的类型,在PHP中字符串定义更为简洁。列:$txt="Hello world!";这段代码就可以表示一个字符串,你以为就这简单?大家想个问题 如果一个包含大量字符的一个字符串,同样赋值给一个变量所占用的空间一样吗?文字描述可能不方便理解,对程序员来说还是直接上代码:

1:var_dump(memory_get_usage());  //先打印出当前内存情况
2:$arr  =  array_fill (0, 100000,  'tioncico' );
3:var_dump(memory_get_usage()); 
4:$arr_copy  =  $arr ; 
5:var_dump(memory_get_usage()); 
6:$arr_copy[1]='a';
7:var_dump(memory_get_usage());

以上代码你们觉得直接结果如何呢?大家可以复制执行实际操作下

分析下上面代码:

  1. 步骤一只是打印当前占用内存使用情况.这里假设当前内存为:100
  2. 步骤二使用 array_fill 填充一个占有10万个KEY的数组
  3. 步骤三打印当前内存使用情况。假设内存增加到150
  4. 步骤四做一个赋值操作,相信这里都可以看懂.`
  5. 重点来了步骤五在此打印发现内存并没有增加,内存使用情况还是和步骤三相同。内存还是 150.看到这里是不是感觉很奇怪了?
  6. 步骤六对新的变量改变其中一个key的值
  7. 步骤七这个是否发现内存才是真正的增加。假设内存增加到200

正主来了

其实对于上面的步骤,并不是感觉很奇怪 这是因为php采取的一个叫写实复制的方案,仔细看上面代码可以发现执行到步骤四$arr_copy$arr这两个变量的值其实是相同,对于这种相同的值不同的变量进行分配的时候是没必要在进行额外空间的占用.来看下定义:

写时复制(Copy on Write,也缩写为COW)的应用场景非常多, 比如Linux中对进程复制中内存使用的优化,在各种编程语言中,如C++STL等等中均有类似的应用。 COW是常用的优化手段,可以归类于:资源延迟分配。只有在真正需要使用资源时才占用资源, 写时复制通常能减少资源的占用

想信看到这里还是很迷,没事继续往下看,下面使用php源码进行解析:

简单了解下zend_string结构体

//string zval结构
struct _zend_string {
    zend_refcounted_h gc;  计数器
	zend_ulong        h;                /* hash value */
	size_t            len;
	char              val[1];
};

解析zstring结构

  1. zend_refcounted_h 这个字段也就是内存管理使用到的,如果有别的变量进行相同的引用就会加一,下文通过gdb分析就可以看到.
  2. zend_ulong 对 zend_string 进行哈希处理时会用到
  3. size_t 字符串长度
  4. char 存储字符串

GDB分析PHP代码

        $b='hell word';
        $a=$b;
        echo $a;   此时zendstr地址等于 0x7ffff1c6f410
        echo $b;   此时zendstr地址也等于 0x7ffff1c6f410   引用变量在和被引用变量一致时zendstr指向的是一个
        $a='hello word a';
        echo $a;    此时zendstr地址也等于 0x7ffff1c6f438   引用字符串发生改变的时候才会真正的进行复制一份

对应GDB分析代码重点:


步骤一:
(gdb) p z
$2 = (zval *) 0x7ffff1c1e090
(gdb) p *z
$3 = {value = {lval = 140737249735696, dval = 6.9533440184587449e-310, counted = 0x7ffff1c6f410, str = 0x7ffff1c6f410, arr = 0x7ffff1c6f410, obj = 0x7ffff1c6f410, res = 0x7ffff1c6f410, ref = 0x7ffff1c6f410, ast = 0x7ffff1c6f410,
zv = 0x7ffff1c6f410, ptr = 0x7ffff1c6f410, ce = 0x7ffff1c6f410, func = 0x7ffff1c6f410, ww = {w1 = 4056347664, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
(gdb) p $3.value.str
$4 = (zend_string *) 0x7ffff1c6f410
(gdb) p *$3.value.str
$5 = {gc = {refcount = 1, u = {type_info = 70}}, h = 9473262949128236454, len = 9, val = "h"}
(gdb) p *$3.value.str.val@9
$6 = "hell word"




步骤二:

(gdb) c
Continuing.
hell word
Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /www/server/php/73/src/Zend/zend_vm_execute.h:36660
36660	/www/server/php/73/src/Zend/zend_vm_execute.h: 没有那个文件或目录.
(gdb) n
36661	in /www/server/php/73/src/Zend/zend_vm_execute.h
(gdb) n
411	/www/server/php/73/src/Zend/zend_types.h: 没有那个文件或目录.
(gdb) p *z
$7 = {value = {lval = 140737249735696, dval = 6.9533440184587449e-310, counted = 0x7ffff1c6f410, str = 0x7ffff1c6f410, arr = 0x7ffff1c6f410, obj = 0x7ffff1c6f410, res = 0x7ffff1c6f410, ref = 0x7ffff1c6f410, ast = 0x7ffff1c6f410,
zv = 0x7ffff1c6f410, ptr = 0x7ffff1c6f410, ce = 0x7ffff1c6f410, func = 0x7ffff1c6f410, ww = {w1 = 4056347664, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
(gdb) p *$7.value.str
$8 = {gc = {refcount = 1, u = {type_info = 70}}, h = 9473262949128236454, len = 9, val = "h"}
(gdb) p *$7.value.str.val.@9
A syntax error in expression, near `@9'.
(gdb) p *$7.value.str.val@9
$9 = "hell word"


步骤三:

(gdb) c
Continuing.
hell word
Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /www/server/php/73/src/Zend/zend_vm_execute.h:36660
36660	/www/server/php/73/src/Zend/zend_vm_execute.h: 没有那个文件或目录.
(gdb) n
36661	in /www/server/php/73/src/Zend/zend_vm_execute.h
(gdb) n
411	/www/server/php/73/src/Zend/zend_types.h: 没有那个文件或目录.
(gdb) p *z
$10 = {value = {lval = 140737249735736, dval = 6.9533440184607211e-310, counted = 0x7ffff1c6f438, str = 0x7ffff1c6f438, arr = 0x7ffff1c6f438, obj = 0x7ffff1c6f438, res = 0x7ffff1c6f438, ref = 0x7ffff1c6f438, ast = 0x7ffff1c6f438,
zv = 0x7ffff1c6f438, ptr = 0x7ffff1c6f438, ce = 0x7ffff1c6f438, func = 0x7ffff1c6f438, ww = {w1 = 4056347704, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
(gdb) p *$10.value.str
$11 = {gc = {refcount = 1, u = {type_info = 70}}, h = 15212097803322570358, len = 12, val = "h"}
(gdb) p *$10.value.str.val@12
$12 = "hello word a"

分析GDB代码,如果看不懂上面GDB打印没有关系,看下面讲解你会豁然开朗,下面也是只会讲解重点来说:

先看两个名词zval结构:

{value = {lval = 140737249735736, dval = 6.9533440184607211e-310, counted = 0x7ffff1c6f438, str = 0x7ffff1c6f438, arr = 0x7ffff1c6f438, obj = 0x7ffff1c6f438, res = 0x7ffff1c6f438, ref = 0x7ffff1c6f438, ast = 0x7ffff1c6f438,
zv = 0x7ffff1c6f438, ptr = 0x7ffff1c6f438, ce = 0x7ffff1c6f438, func = 0x7ffff1c6f438, ww = {w1 = 4056347704, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}

zstring结构:

{gc = {refcount = 1, u = {type_info = 70}}, h = 9473262949128236454, len = 9, val = "h"}
  1. 首先对上面代码分为步骤一 二 三来说对应的就是PHP代码: echo $a; echo $b; echo $a;对应的三个输出.
  2. 在第一个步骤中我们可以看到zval里面的str =0x7ffff1c6f410 这个代表的就是zstring的地址
  3. 再看步骤二,注意在步骤二之前是有赋值操作可以看下上面GDB对应的php代码,这个时候发现步骤二zstring也还是指向str=0x7ffff1c6f410 指向的是同一个地址,看到这里是不是明白了什么?对的相同的数据指向的是同一个zstring,所以并不会增加额外的空间.
  4. 步骤三 通过改变$a的值发现,zvale里面的str地址也发生了改变,这就说明只有在值真正发生改变的时候才会真正的占用额外空间来存储zstring

题外话redis中的COW
不仅是php中使用到写时复制,在简单了解redis中的写实复制技术:

Redis中使用RDB进行内存快照的时候是不是不允许当前的数据发生变化?但是不允许发生变化此时有人更新redis中数据怎么办呢?如果允许变化快照中数据肯定就会发生错乱,这个时候redis也采用写实复制解决.

redis 生成rdb 时候会fork 子进程。此时的读写操作:
读:主线程和bgsave子进程互不影响。
写:被修改的数据会被复制一份为副本,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据写入 RDB 文件。

以上就是php的写实复制,有兴趣的也可以动手试试.是不是又可以向同事露一手呢?好了,最好不要装逼,万一人家早就知道呢。反正多学点对自己还是以后面试都是有帮助的,下期看点php引用,看段代码,可以先自己猜一下:

$b='hell word';
$a=&$b;
echo $a;
echo $b;   
unset($a);
echo $a;  
echo $b

问题:删除$a $b还能打印出来吗?这也是比较常见的面试题。很多人应该都知道答案,可是为什么呢?我们要做到知其然,知其所以然

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃橘子的汤圆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值