月度归档:2011年01月

TIPI0103–PHP实现中的常用代码

第三节 PHP实现中的常用代码

在PHP的源码中经常会看到一些宏或一些对于刚开始看源码的童鞋比较纠结的代码。这里提取中间的一些进行说明。

1. 关于##和#


在PHP的宏定义中,最常见的要数双井号。

双井号

在C语言的宏中,”##”被称为 连接符(concatenator),用来把两个语言符号(Token)组合成单个语言符号。这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在。如下所示源码:

#define PHP_FUNCTION            ZEND_FUNCTION
#define ZEND_FUNCTION(name)             ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_FN(name) zif_##name
#define ZEND_NAMED_FUNCTION(name)       void name(INTERNAL_FUNCTION_PARAMETERS)
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, \
zval *this_ptr, int return_value_used TSRMLS_DC

PHP_FUNCTION(count);

//  预处理器处理以后, PHP_FUCNTION(count);就展开为如下代码
void zif_count(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC)

宏ZEND_FN(name)中有一个”##”,它的作用一如之前所说,是一个连接符,将zif和宏的变量name得值连接起来。

单井号

“#”的功能是将其后面的宏参数进行 字符串化操作 ,简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号,用比较官方的话说就是将语言符号(Token)转化为字符串。

2. 关于宏定义中的do-while


如下所示为PHP5.3新增加的垃圾收集机制中的一段代码:

#define ALLOC_ZVAL(z)                                   \
do {                                                \
    (z) = (zval*)emalloc(sizeof(zval_gc_info));     \
    GC_ZVAL_INIT(z);                                \
} while (0)

如上所示的代码,在宏定义中使用了 do{ }while(0) 语句格式。如果我们搜索整个PHP的源码目录,会发现这样的语句还有很多。那为什么在宏定义时需要使用do-while语句呢? 我们知道do-while循环语句是先执行再判断条件是否成立, 所以说至少会执行一次。当使用do{ }while(0)时代码肯定只执行一次, 肯定只执行一次的代码为什么要放在do-while语句里呢? 这种方式适用于宏定义中存在多语句的情况。如下所示代码:

#define TEST(a, b)  a++;b++;

if (expr)
    TEST(a, b);
else
    do_else();

代码进行预处理后,会变成:

if (expr)
    a++;b++;
else
    do_else();

这样if-else的结构就被破坏了: if后面有两个语句, 这样是无法编译通过的, 那为什么非要do-while而不是简单的用{}括起来呢.这样也能保证if后面只有一个语句。 例如上面的例子,在调用宏TEST的时候后面加了一个分号, 虽然这个分号可有可无, 但是出于习惯我们一般都会写上. 那如果是把宏里的代码用{}括起来,加上最后的那个分号. 还是不能通过编译. 所以一般的多表达式宏定义中都采用do-while(0)的方式.

3. #line 预处理


#line 838 "Zend/zend_language_scanner.c"

#line预处理用于改变当前的行号和文件名。
如上所示代码,将当前的行号改变为838,文件名Zend/zend_language_scanner.c
它的作用体现在编译器的编写中,我们知道编译器对C 源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行调试分析。

作者:TIPI Team

TIPI0101-环境搭建

第一节 环境搭建

在开始学习PHP实现之前, 我们首先需要一个实验和学习的环境. 下面介绍一下怎样在*nix环境下准备和搭建PHP环境. (*nix指的是类Unix环境,比如各种Linux发行版,FreeBSD, OpenSolaris, Mac OS X等操作系统)

1.获取PHP源码

为了学习PHP的实现,首先我们要下载源代码.下载源码首选是去PHP官方网站 http://php.net/downloads.php下载, 如果你喜欢是用类似svn/git这些版本控制软件,喜欢svn的读者可以去http://www.php.net/svn.php上 签出源代码,或者如果你喜欢用git, 则可以去http://github.com/php/php-src上clone一个. 个人比较喜欢用版本控制软件签出代码, 这样的好处是能看到php每次修改的内容及日志信息, 如果自己修改了其中的某些内容也能快速的查看到.

2.准备编译环境

在*nix环境下,首先需要gcc编译构建环境. 如果你是用的是Ubuntu或者是用apt做为包管理的系统,可以通过如下命令快速安装.

sudo apt-get install build-essential

如果用的是Mac OS的话,则需要安装Xcode。Xcode可以在Mac OS X的安装盘中找到,如果你有Apple ID的话,也可以登陆苹果开发者网站http://developer.apple.com/下载。

3. 编译

下一步就可以开始编译了,本文只简单介绍基本的编译过程,不包含apache的php支持以及mysql等模块的编译。相关资料请百度或google之。 假设源代码下载到了~/php-src的目录中,执行buildconf命令以生成所需要的Makefile文件

cd ~/php-src
./buildconf

执行完以后就可以开始configure了, configure有很多的参数, 比如指定安装目录, 是否开启相关模块等选项

./configure --help # 查看可用参数

为了尽快得到可以测试的环境,我们就不加其他参数了.直接执行./configure就可以了. 以后如果需要其他功能可以重新编译. 如果configure命令出现错误,可能是缺少php所依赖的库,各个系统的环境可能不一样. 出现错误可根据出错信息上网搜索. 直到完成configure. configure完成后我们就可以开始编译了.

make

在*nix下编译过程序的读者应该都熟悉经典的configure make, make install吧. 执行make之后是否需要make install就取决于你了. 如果install的话 最好在configure的时候是用prefix参数指定安装目录, 不建议安装到系统目录, 避免和系统原有的php冲突.

在make 完以后,~/php-src目录里就已经有了php的可以执行文件. 执行一下命令:

cd ~/php-src
./sapi/cli/php -v

如果看到输出php版本信息则说明咱成功了. 如果是make install的话则执行 $prefix/bin/php 这个路径的php, 当然如果是安装在系统目录或者你的prefix 目录在$PATH环境变量里的话,直接执行php就行了.

后续的学习中可能会需要重复configure make 或者 make && make install 这几个步骤。

作者:TIPI Team

奇技淫巧一:循环加速

奇技淫巧一:循环加速
循环通常是程序性能问题的多发地段。优化某些循环能极大的提高某段程序的性能。
以下的测试环境为:win xampp1.7.3 PHP5.3.1
【数组长度】
也许,maybe,可能,你看到如下类似的代码:

1
2
3
4
5
6
7
8
9
10
<?PHP
$data = range(0, 1000000);
 
$start = xdebug_time_index();
 
for($i = 0; $i < count($data); $i++) {	//	关注点在这行
}
$end = xdebug_time_index();
 
echo $end - $start;

以上的程序运行在我的电脑上基本上是3.3秒到3.4秒的样子。
如果我们把count($data)的计算提前到循环前,以局部变量在循环中迭代。如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
<?PHP
$data = range(0, 1000000);
 
$start = xdebug_time_index();
 
$len = count($data);	//	其实局部变量是很快的。
for($i = 0; $i < $len; $i++) {
}
$end = xdebug_time_index();
 
echo $end - $start;

在同样的环境下运行,基本上是0.32到0.4秒。
对比一下,整整一个数量级的提升。只是优化了一点,性能有如此大的提升。对于这样的操作我们需要在平时注意,养成习惯。虽然PHP内部取数组的长度是从Hash Table中直接取的count值。可是这个相对于局部变量,在性能上还是有较大差别的。
这里想到一个点,如果是HTML中使用JS取DOM集合,然后计算DOM的长度,此时如果有增加或删除节点,可能最后得到的结果并不是你要的那个结果。
【计数变量】
和数组长度一样,计数变量也是一个在循环中每次迭代都会使用的变量。如果我们优化这个点,此时性能上应该也有部分提高。如下所示代码:

1
2
3
4
5
6
7
8
9
10
<?PHP
$data = range(0, 1000000);
 
$start = xdebug_time_index();
 
for($i = count($data); $i--;) {	//	我是关注点所在行
}
$end = xdebug_time_index();
 
echo $end - $start;

以上的代码将计数变量从大到小递减,当为0时自动停止。从而将判断语句和计数加1两条语句变成了一条语句。
最后的运行时间大概为0.26到0.27秒。与前面的0.3到0.4秒,又有了大约30%的提升。

只是,只是这些是针对for语句,在PHP中我们还有foreach。
【foreach】
将上面的for语句换成foreach,如下所示代码:

1
2
3
4
5
6
7
8
9
$data = range(0, 1000000);
 
$start = xdebug_time_index();
 
foreach ($data as $row) {
}
$end = xdebug_time_index();
 
echo $end - $start;

运行时间为:0.11~0.13秒,也许此时我们应该去内核里面看下为什么了,只是这不是今天的重点。嘿嘿
结果已经很明显了,如果可以,我们还是用foreach吧。
【循环展开】
在《编程珠玑》中介绍了这样一种方案。只是他是针对特定的数组查找的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$data = range(0, 1000005);
$start = xdebug_time_index();
 
$len = count($data);
 
$left = $len % 8;
 
$i = 0;
 
while($left-- > 0) {
	$i++;
}
 
for(; $i < $len; $i+=8) {
	$t = $data[$i];
	$t = $data[$i + 1];
	$t = $data[$i + 2];
	$t = $data[$i + 3];
	$t = $data[$i + 4];
	$t = $data[$i + 5];
	$t = $data[$i + 6];
	$t = $data[$i + 7];
}
$end = xdebug_time_index();
 
echo $end - $start;

以上的方法运行时间为0.3多一些,这和前面的方法类似,但是需要考虑到这里多了8个赋值语句。如果去掉这8个赋值语句,其时间为0.04秒。

以上的一些方法只是针对数量级比较大的数组,此时一些微小的优化都会产生巨大的能量。平时中,也许我们使用foreach就足够了。

【纠结的轮子】
最开始生成一百万的数组使用的是如下方式:

1
2
3
4
$data = array();
for ($i = 0; $i < 1000005; $i++) {
	$data[$i] = $i;
}

纠结的轮子!