标签归档:PHP源码

PHP成员变量获取对比

有如下4个代码示例,你认为他们创建对象,并获得成员变量的速度排序是怎样的?

1:将成员变量设置为public,通过赋值操作给成员变量赋值,直接获取变量

	class Foo {
		public $id;
	}
 
	$data = new Foo;
	$data->id = 10;
	echo $data->id;

2:将成员变量设置为public,通过构造函数设置成员变量的值,直接获取变量

        class Foo2 {
		public $id;
		public function __construct($id) {
			$this->id = $id;
		}
	}
 
	$data = new Foo2(10);
	echo $data->id;

3:将成员变量设置为protected,通过构造函数设置成员变量的值,通过成员方法获取变量

 
     class Foo3 {
		protected $id;
		public function __construct($id) {
			$this->id = $id;
		}
 
		public function getId() {
			return $this->id;
		}
	}
	$data = new Foo3(10);
	echo $data->getId();

4:将成员变量设置为protected,通过构造函数设置成员变量的值,通过魔术方法获取变量

 
     class Foo4 {
		protected $id;
		public function __construct($id) {
			$this->id = $id;
		}
 
		public function __get($key) {
			return $this->id;
		}
	}
	$data = new Foo4(10);
	echo $data->id;

按执行速度快慢排序: 1243
咱们先看其opcode:

1:

   	1  ZEND_FETCH_CLASS	4 	:4 	'Foo'
	2  NEW      			$5	:4
	3  DO_FCALL_BY_NAME			0          
	4  ASSIGN     				!0, $5
	5  ZEND_ASSIGN_OBJ			!0, 'id'
	6  ZEND_OP_DATA				10
	7  FETCH_OBJ_R			$9	!0, 'id'
	8  ECHO        				$9

2:

	1  ZEND_FETCH_CLASS	4 	:10	'Foo2'
	2  NEW             		$11	:10
	3  SEND_VAL        			10
	4  DO_FCALL_BY_NAME		1 
	5  ASSIGN    				!1, $11
	6  FETCH_OBJ_R			$14	!1, 'id'
	7  ECHO        				$14

3:

	1  ZEND_FETCH_CLASS	4 	:15	'Foo3'
	2  NEW         			$16	:15
	3  SEND_VAL     			10
	4  DO_FCALL_BY_NAME			1          
	5  ASSIGN  	   			!2, $16
	6  ZEND_INIT_METHOD_CALL	!2, 'getId'
	7  DO_FCALL_BY_NAME		0 	$20     
	8  ECHO       				$20

4:

	1  ZEND_FETCH_CLASS	4  :21	'Foo4'
	2  NEW          		$22	:21
	3  END_VAL      			10
	4  DO_FCALL_BY_NAME		1          
	5  ASSIGN        			!3, $22
	6  FETCH_OBJ_R  		$25 !3, 'id'
	7   ECHO  				$25

根据上面的opcode,参照其在zend_vm_execute.h文件对应的opcode实现,我们可以发现什么?

一、PHP内核创建对象的过程分为三步:

  1. ZEND_FETCH_CLASS 根据类名获取存储类的变量,其实现为一个hashtalbe EG(class_table) 的查找操作
  2. NEW 初始化对象,将EX(call)->fbc指向构造函数指针。
  3. 调用构造函数,其调用和其它的函数调用是一样,都是调用zend_do_fcall_common_helper_SPEC

二、魔术方法的调用是通过条件触发的,并不是直接调用,如我们示例中的成员变量id的获取(zend_std_read_property),其步骤为:

  1. 获取对象的属性,如果存在,转第二步;如果没有相关属性,转第三步
  2. 从对象的properties查找是否存在与名称对应的属性存在,如果存在返回结果,如果不存在,转第三步
  3. 如果存在__get魔术方法,则调用此方法获取变量,如果不存在,报错

回到排序的问题:

一、第一个和第二个的区别是什么?

第二个的opcode比第一个要少,反而比第一个要慢一些,因为构造函数多了参数,多了一个参数处理的opcode。参数处理是一个比较费时的操作,当我们在做代码优化时,一些不必要的参数能去掉就去掉;当一个函数有多个参数时,可以考虑通过一个数组将其封装后传递进来。

二、为啥第三个最慢?

因为其获取参数其本质上是一次对象成员方法的调用,方法的调用成本高于变量的获取

三、为啥第四个比第三个要快?

因为第四个的操作实质上获取变量,只不过其内部实现了魔术方法的调用,相对于用户定义的方法,内部函数的调用的效率会高。因此,当我们有一些PHP内核实现的方法可以调用时就不要重复发明轮子了。

四、为啥第四个比第二个要慢?

因为在PHP的对象获取变量的过程中,当成员变量在类的定义不在在时,会去调用PHP特有的魔术方法__get,多了一次魔术方法的调用。

总结一下:

  1. 使用PHP内置函数
  2. 并不是事必面向对象(OOP),面向对象往往开销很大,每个方法和对象调用都会消耗很多内存。
  3. 尽量少用魔术方法 — 除非有必要,不要用框架,因为框架都有大量的魔术方法使用。
  4. 在性能优先的应用场景中,将成员变量设置为public,不失为一种比较好的方法,当你需要用到OOP时。
  5. 能使用PHP语法结构的不要用函数,能使用内置函数的不要自己写,能用函数的不要用对象

foreach的指针问题

在PHP中,foreach 语法结构提供了遍历数组的简单方式。 foreach 仅能够应用于数组和对象,如果尝试应用于其他数据类型的变量,或者未初始化的变量,将导致错误。 foreach每次循环时,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。

但是手册中提醒我们:

Note:
当 foreach 开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在 foreach 循环之前调用 reset()。
在循环中修改 foreach 依赖其内部数组指针将可能导致意外的行为。

这里我们所要说的是foreach可能导致的意外情况。如代码1示例:

<?php
$arr = array(1,2,3,4,5);
 
foreach($arr as $key => &$row) {
echo key($arr), '=>', current($arr), "\r\n";
}

会输出什么?

如代码2示例呢?

<?php
$arr = array(1,2,3,4,5);
 
foreach($arr as $key => $row) {
echo key($arr), '=>', current($arr), "\r\n";
}

会输出什么?

代码1会依次输出变量,但是第一个元素并没有在输出结果中出现。

代码2只会输出数组的第二个元素。

为什么呢?

将代码2在VLD扩展中查看,

number of ops:  22
compiled vars:  !0 = $arr, !1 = $key, !2 = $row
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   INIT_ARRAY                                       ~0      1
         1      ADD_ARRAY_ELEMENT                                ~0      2
         2      ADD_ARRAY_ELEMENT                                ~0      3
         3      ADD_ARRAY_ELEMENT                                ~0      4
         4      ADD_ARRAY_ELEMENT                                ~0      5
         5      ASSIGN                                                   !0, ~0
   4     6    > FE_RESET                                         $2      !0, ->20
         7  > > FE_FETCH                                         $3      $2, ->20
         8  >   ZEND_OP_DATA                                     ~5      
         9      ASSIGN                                                   !2, $3
        10      ASSIGN                                                   !1, ~5
   5    11      SEND_REF                                                 !0
        12      DO_FCALL                                      1  $7      'key'
        13      ECHO                                                     $7
        14      ECHO                                                     '%3D%3E'
        15      SEND_REF                                                 !0
        16      DO_FCALL                                      1  $8      'current'
        17      ECHO                                                     $8
        18      ECHO                                                     '%0D%0A'
   6    19    > JMP                                                      ->7
        20  >   SWITCH_FREE                                              $2
   8    21    > RETURN                                                   1

从上面VLD扩展输出结果结合PHP的源代码可以知道,在foreach遍历之前, PHP内核首先会有个FE_RESET操作来重置数组的内部指针,也就是pInternalPointer, 然后通过每次FE_FETCH将pInternalPointer指向数组的下一个元素,从而实现顺序遍历。
并且每次FE_FETCH的结果都会被一个全局的中间变量存储,以给下一次的获取元素使用。

从这两个例子可以引申出三个问题:

1、为什么foreach循环体中执行key或current会显示第二个元素(非引用情况)?
以key函数为例,我们执行函数调用时,会执行中间代码SEND_REF,此中间代码会将没有设置引用的变量复制一份并设置为引用。当进入循环体时,PHP内核已经经过了一次fetch操作,相当于执行了一次next操作,当前元素指向第二个元素。因此我们在foreach的循环体中执行key函数时,key中调用的数组变量为PHP执行了一次fetch操作的数组拷贝,此时foreach的内部指针指向第二个元素。

2、为什么在foreach中执行end等操作,其循环过程不变?
在遍历的代码中通过end,next等操作数组的指针,数组的指针不会变化,这是因为在PHP内核进行FETCH操作时,会通过中间变量存储当前操作数组的内部指针,每遍历一个元素,会先获取之前存储的指针位置,获取下一个元素后,再恢复指针位置。

3、为什么$row的引用和非引用情况下输出结果不同?
如果是引用,PHP内核在reset数组时,会直接分裂数组,生成一个数组的拷贝,并将其设置为引用。
如果是非引用,PHP内核在reset数组时,当数组的引用计数大于1,并且不存在引用时,会拷贝数组供foreach使用,其它情况使用原数组,将其引用计数加1。

因为引用的不同,在循环体中给函数传递参数时其结果不同,导致看到的foreach数组内部指针变化的不同。对于非引用且引用计数大于1的情况,其本身就是两个不同的数组,在RESET时就不同了。

PHP的ticks机制

PHP的ticks机制

要过年了,在年前完成这篇文章,如果有缘可以看到,祝福看到的朋友新年快乐,在新的一年里,万事顺意!

按今年的计划每个月至少有两篇文章,而一月份因为各种理由而只有一篇2012的总结,无论什么原因,总归是不对的。这篇算是补上的,也作为今年的开始。

回正题,今天要研究的是PHP的ticks机制。

PHP提供declare关键字和ticks关键字来声明ticks机制。如:declare(ticks = N); 这表示:在当前scope内,每执行N句internal statements(opcodes),就会中断当前的业务语句,去执行通过register_tick_function注册的函数(如果存在的话),然后再继续之前的代码。需要注意的是这里的N是指的PHP的一些OPCODE,而OPCODE与我们见到的PHP语句却不是一一对应的。

最开始我以为PHP内核是在编译时记录是否有ticks机制,在真正执行中间代码时插入判断代码,实现此机制。但是事实上却不是这样滴。

看PHP代码示例1:

    $name = "phppan";
    echo $name;
    class Tipi {
        public function test() {
            echo "test";
        }
    }
    function f_tipi() {
    }

如上代码包括了我们常见的几种语句,赋值,输出,定义类,定义函数。通常我们用VLD查看PHP生成的中间代码,上面的代码通过 php -dvld.active=1 t.php 我们会看到 ECHO、ASSIGN、NOP等中间代码。

现在我们在示例1的代码上添加上ticks机制。如PHP代码示例2:

    declare(ticks=1);
    $name = "phppan";
    echo $name;
    class Tipi {
        public function test() {
            echo "test";
        }
    }
    function f_tipi() {
    }

示例2与示例1相比也就是多了第一条语句: declare(ticks=1); 如果我们此时再次通过VLD查看中间代码,会发现在每个中间代码的后面都多了一句中间代码:TICKS

是否因为ticks=1的原因而在每个中间代码的后面添加了TICKS?将declare(ticks=1);换成declare(ticks=100);,再次VLD,结果没有变化。从以上的结果可以看出,PHP内核在语法分析过程中实现了ticks机制。

从实现过程来说定义ticks机制分为两个过程:一个是定义是否需要执行ticks或者说声明ticks机制,另一个实现在声明了ticks机制的情况下控制语句的执行。

声明ticks机制过程

声明的过程就是调用declare(ticks = N); 在语法分析时根据declare关键字和参数中的ticks关键字来声明ticks机制。通过zend_compile.c文件中的zend_do_declare_begin、declare_statement、zend_do_declare_end三个函数来编译声明ticks机制。在declare_statement函数中我们可以看到:declare除了可以声明ticks之外,还可以声明encoding,这在代码里面就是一个if else的判断。

ticks机制的声明仅在编译过程有用,它为后面的声明控制语句服务。其编译过程中的全局变量为:CG(declarables)。这是一个结构体,它仅有一个成员:ticks。当然后面应该还会有更多的成员出现。

声明控制语句

示例1和示例2已经充分说明在每条语句的语法分析时,会根据是否声明了ticks机制来添加TICKS中间代码,其实现在于每条语句在语法解析时都会添加一条函数调用:zend_do_ticks。从zend_language_parser.y文件中可以看出:zend_do_ticks函数添加在类定义语句,函数定义语句和常规语句的后面。 zend_compile.c文件中的zend_do_ticks函数会根据前面提到的 CG(declarables).ticks 来判断是否生成 ZEND_TICKS 中间代码(在VLD中看到的中间代码都是没有ZEND开头)。

除了声明ticks机制,还有执行。执行过程中关键的变量是在声明时的ticks=N。其实这里的N可以换个角度去理解:ticks指定的数字是指执行了多少次TICKS语句。在TICKS中间代码的执行函数ZEND_TICKS_SPEC_CONST_HANDLER中,会统计执行当前函数的次数,存储变量为EG(ticks_count)。当达到当初声明的界限,就会调用一次所有通过register_tick_function注册的函数,并计数清零。

与当初自己设想的实现相比,PHP内核对ticks机制的实现满足了功能单一原则和松耦合原则。将ticks机制作为一个中间代码添加到整个中间代码的执行体系中,包括状态的转移,函数的切换这些都是直接使用原有的机制。

ticks机制的应用场景

手册上说:Ticks 很适合用来做调试,以及实现简单的多任务,后台 I/O 和很多其它任务。

在调试过程中,对于定位一段特定代码中速度慢的语句比较有用,我们可以每执行两条低级语句就记录一次时间。虽然这个过程也可以用其它方法完成,但用 tick 更方便也更容易实现。

PCNTL也使用ticks机制来作为信号处理机制(signal handle callback mechanism),可以最小程度地降低处理异步事件时的负载。这里的关键在于PCNTL扩展的模块初始化函数(PHP_MINIT_FUNCTION(pcntl))。在此模块做模块初始化时,它会调用: php_add_tick_function(pcntl_signal_dispatch);将pcntl的分发执行函数添加到ticks机制的调用函数中去,从而当ticks触发时就会调用PCNTL扩展函数中指定的所有方法。