分类目录归档:PHP

PHP源码,PHP扩展,PHP程序

PHP的压缩函数实现:gzencode、gzdeflate和gzcompress

  • gzencode 默认使用ZLIB_ENCODING_GZIP编码,使用gzip压缩格式,实际上是使用defalte 算法压缩数据,然后加上文件头和adler32校验
  • gzdeflate 默认使用ZLIB_ENCODING_RAW编码方式,使用deflate数据压缩算法,实际上是先用 LZ77 压缩,然后用霍夫曼编码压缩
  • gzcompress ;默认使用ZLIB_ENCODING_DEFLATE编码,使用zlib压缩格式,实际上是用 deflate 压缩数据,然后加上 zlib 头和 CRC 校验
  • 这三个函数的比较实质上是三种压缩方法:deflate, zlib, gzip的比较。
    从性能的维度看:deflate 好于 gzip 好于 zlib
    从文本文件默认压缩率压缩后体积的维度看:deflate 好于 zlib 好于 gzip

    这三种算法中gzip 、zlib的作者都是Jean-Loup Gailly和 Mark Adler。
    这两种算法以及图形格式png,使用的压缩算法却都是deflate算法。
    deflate算法是同时使用了LZ77算法与哈夫曼编码(Huffman Coding)的一个无损数据压缩算法。
    它最初是由Phil Katz为他的PKZIP归档工具第二版所定义的,后来定义在 RFC 1951规范中。

    deflate算法的压缩与解压的实现过程可以在压缩库zlib上找到。
    PHP的压缩实现依赖于zlib,zlib是一个提供了 deflate, zlib, gzip 压缩方法的函数库。
    我们所使用的上面三个函数,将参数中的encoding转为相同,压缩率设置相同,则其最终调用的是同一个函数,效果和性能一样。

    PHP的zlib实现是以扩展的方式存在于ext/zlib目录中。通过deflateInit2() + deflate() + deflateEnd()三个函数配合完成压缩功能,inflateInit2() + inflate() + inflateEnd()三个函数配合完成解压功能。压缩最终都是通过php_zlib_encode函数实现调用,除了输入的字符串,压缩率,结果的输出外,不同的入口函数调用参数不同的是其encoding。deflateInit2的第四个参数指定encoding,PHP定义了三个常量:

     #define PHP_ZLIB_ENCODING_RAW          -0xf      //deflate -15
    #define PHP_ZLIB_ENCODING_GZIP          0x1f      //gzip 15 + 16
    #define PHP_ZLIB_ENCODING_DEFLATE     0x0f      // zlib 15

    三个函数在调用过程可以直接指定encoding使用其它的算法:

    zlib:   ZLIB_ENCODING_DEFLATE 
    gzip: ZLIB_ENCODING_GZIP
    deflate: ZLIB_ENCODING_RAW

    此三个函数是三种算法的简单调用方式,以更好的命名展现。三个函数间可以通过指定相同的encoding达到相同的效果,并且PHP也提供zlib_encode函数作为通用的压缩函数。

    参考资料:

    http://www.gzip.org/zlib/rfc-deflate.html

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语法结构的不要用函数,能使用内置函数的不要自己写,能用函数的不要用对象

PHP的$this变量

手册上的一个有意思的小示例。

http://www.php.net/manual/zh/language.variables.basics.php

	$this = 'text'; // error
	$name = 'this';
	$$name = 'text'; // sets $this to 'text'
	echo $$name;

在PHP的词法分析时,$this变量是符合其规则的,在语法解析生成中间代码时,PHP内核会根据变量类型在生成赋值的中间代码时判断是否为$this变量,如果是则报错。这里为什么要报错呢?因为this作为一个特殊的变量,在对象的成员方法等调用初始化时会将this变量添加到活动符号表。

在类的成员方法里面,可以用 ->(对象运算符):$this->property(其中 property 是该属性名)这种方式来访问非静态属性。

当一个方法在类定义内部被调用时,有一个可用的伪变量 $this。$this 是一个到主叫对象的引用(通常是该方法所从属的对象,但如果是从第二个对象静态调用时也可能是另一个对象)。

在词法分析、语法分析并生成中间代码时,$this作为一个特殊的变量存在,特别是在生成中间代码时,代码中充斥着对于this的特殊处理。这些都是为后面的运行做准备,如识别标记出某处使用this变量,在存储opcode的zend_op_array结构体中专门有一个变量this_var标识是否有this变量。一个函数或一个类方法都会生成一个新的zend_op_array,在生成中间代码时,判断当前变量是否为this变量。

this变量在执行过程中会有两种存在状态,一种是全局传递的状态,存储在EG(This),一种是当前作用域状态,以this变量存储在EG(active_symbol_table)(当前执行环境的活动符号表)。
在我们执行一个 op_array 时,比如一个对象的方法,PHP内核会给这个 op_array 生成一个 zendexecutedata ,在生成初始化时,EG(This) 会添加到EG(active_symbol_table) 。
在方法调用过程中,如果有用到this变量,则会直接取EG(active_symbol_table)的值。

那么一个对象中的EG(This)在哪里初始化呢?
就EG(This)变量本身来说,在我们初始化PHP的执行环境时,它和其它全局变量(如EG(scope)等)一样都会被初始化为NULL。
对于一个对象来说,当我们创建了一个对象,调用时,PHP内核会将当前获得的对象直接赋值给EG(This),而这个当前获得的对象是在通过new操作生成对象时创建的对象本身。

如下这个简单示例:

	class Foo {
	     public $var = 10;
 
	     function t() {
	          echo $this->var;     
	     }
 
	     function t2() {
	     	echo 33;
		}
	}
 
	$foo = new Foo();
	$foo->t();

其主程序流程生成的中间代码如下:

        function name:  (null)
	number of ops:  8
	compiled vars:  !0 = $foo
	line     # *  op                           fetch          ext  return  operands
	---------------------------------------------------------------------------------
	   2     0  >   NOP                                                      
	  15     1      ZEND_FETCH_CLASS                              4  :1      'Foo'
	         2      NEW                                              $2      :1
	         3      DO_FCALL_BY_NAME                              0          
	         4      ASSIGN                                                   !0, $2
	  16     5      ZEND_INIT_METHOD_CALL                                    !0, 't'
	         6      DO_FCALL_BY_NAME                              0          
	         7    > RETURN                                                   1

this变量原始的对象值出生在 opcode NEW,经过了赋值(ASSIGN)后,在方法初始化时,将变量本身传递给执行环境的调用者,调用者又在执行调用(DO_FCALL_BY_NAME)时将变量传递给EG(This),当执行这个方法的op_array时,初始化当前作用域的环境(zend_execute_data)时,会将EG(This)作为$this变量添加到活动符号表,后续方法中的$this变量的使用就会直接取符号表的变量。