PHP浮点数

<?php
    $f = 0.58;
    var_dump(intval($f * 100)); //为啥输出57

为啥输出是57啊? PHP的bug么?

我相信有很多的同学有过这样的疑问, 因为光问我类似问题的人就很多, 更不用说bugs.php.net上经常有人问…

要搞明白这个原因, 首先我们要知道浮点数的表示:

浮点数, 以64位的长度为例, 会采用1位符号位(E), 11指数位(Q), 52位尾数(M)表示(一共64位).

符号位:最高位表示数据的正负,0表示正数,1表示负数。

指数位:表示数据以2为底的幂,指数采用偏移码表示

尾数:表示数据小数点后的有效数字.

这里的关键点就在于, 小数在二进制的表示, 关于小数如何用二进制表示, 大家可以百度一下, 我这里就不再赘述, 我们关键的要了解, 0.58 对于二进制表示来说, 是无限长的值(下面的数字省掉了隐含的1)..

0.58的二进制表示基本上(52位)是: 0010100011110101110000101000111101011100001010001111
0.57的二进制表示基本上(52位)是: 0010001111010111000010100011110101110000101000111101

而两者的二进制, 如果只是通过这52位计算的话,分别是:

0.58 -> 0.57999999999999996
0.57 -> 0.56999999999999995

0.58 * 100 = 57.999999999

那你intval一下, 自然就是57了….

可见, 这个问题的关键点就是: “你看似有穷的小数, 在计算机的二进制表示里却是无穷的”

so, 不要再以为这是PHP的bug了, 这就是这样的…..

解决php内存泄露问题

网站每天大概有60万ip/300万pv的访问,网站产品很复杂,代码结构差,开发工程师来来去去,代码只能只读了。突然有一天开始频繁出现php-fpm进程耗光内存和cpu占有率飙升,前端频繁出现504错误

php-fpm进程耗光内存 这个就是传说中的内存泄露,所谓内存泄露,是指进程在运行过程中,内存占用率逐步上升而不释放,导致系统可用内存越来越少的情况

严格上说,这个也不算致命错误,“内存泄露”只对长期运行的程序有威胁,对单一任务的执行脚本不需要担心

最简单的处理方式,是定时重启进程。php-fpm的配置信息里面有个max_request,就定义一个fastcgi进程处理完多少个请求之后退出这样系统可用释放掉内存,但是如果内存占用率增长速度非常快,频繁重启进程,就会影响服务的稳定性,所以这个问题必须正面解决

内存泄露排查非常困难

  • 因为代码规模非常大,想靠做code review的方式来查基本上不可能
  • php并非运行在虚拟机上,没有什么官方的monitor(类似java hprof,JVM Monitor等)
  • 在互联网上搜索,找不到任何答案

探索解决问题

  1. 使用 valgrind 调试php-cgi进程

    Valgrind 是一个linux常用的程序的内存调试和代码剖析,对调试C/C++程序的内存泄露很有帮助,它的机制是在系统alloc/free等函数调用上加计数。用 valgrind 调试php-cgi,要求php-cgi 是debug版本,实践证明行不通:

    1. php-cgi debug 版本放在线上根本跑不起来,性能太差
    2. php程序的内存泄露,是由于一些循环引用,或者gc的逻辑错误,valgrind无法探测,它适合去检查php解释器是否有内存泄露问题
  2. php解释器(Zend core)自带有检查内存泄露的机制

    php解释器的核心代码叫做(Zend Core) 在用valgrind 调试php-cgi进程,我查看了php-cgi的代码,发现zend core 实现了内存泄露的自我检查 但是 同上原因,php-cgi debug 跑不起来,也无法得到调试信息

  3. FreeBSD 的 DTrace

    DTrace是freebsd 系统支持的核心调试器,可以在各个系统函数调用上加计数点,twitter曾经用过。这个方法最后没有使用 有如下原因:

    1. 需要找一台服务器安装freebsd,并部署到线上、或者模拟负载,非常繁琐

    2. 我仔细研究了DTrace的文档,发现这个可以认为是增强的 valgrind,也不能解决我们的问题

这3种方法都不行,陷入困境.但是换个角度思考:虽然解决php程序内存泄露没有方便的工具,但是 web 程序是按请求切分的,一个http请求,对应的php进程执行一个php文件

一个自然的想法是,记录 每次 http请求处理前后php进程的内存占用率之差,然后对结果排序,就能找出,让进程内存增加可能性最大的 文件 ,这个文件导致内存泄露的可能性最大

计算进程内存占用率有两种方式

php内置函数 memory_get_usage

  1. 这个函数是 Zend Core里面一个计数器,是zend core认为的内存使用量,但是php内存泄露有可能是zend core逻辑错误导致的,所以memory_get_usage不一定可靠

  2. linux 系统文件 /proc/{$pid}/status 会记录某个进程的运行状态,里面的 VmRSS 字段记录了该进程使用的常驻物理内存(Residence),这个就是该进程实际占用的物理内存了,用这个数据比较靠谱,在程序里面提取这个值也很容易

找到思路,就可以开始动手写程序

直接修改了php-cgi的源代码,在main.c里面处理每个fastcgi请求前后分别加计数代码,输出日志到log文件,重新编译上线

运行30分钟之后,执行

cat short.log| awk '{print $3 "\t" $7 "\t" $6 "\t" $4$5}' |sort -r -n |head -n 100

很容易找到最可能出现内存泄露的代码文件,然后进一步排查,重构代码,这就很简单了:能不加载的文件就不加载,大数组用完之后赶紧unset ….

更好的办法

后来,我才发现其实不需要去修改php的源代码,php.ini配置文件里面有两个配置项: auto_append_file,auto_prepend_file,可以在请求前后注入代码 ….

web程序做性能优化也是这个思路,但是要简单很多,无需写代码,在nginx log里面加上$request_time ,用awk/sort 处理一下就可以找出瓶颈。

PHP编码规范:日志接口

本文档用来描述日志类库的通用接口。

主要目标是让类库获得一个Psr\Log\LoggerInterface对象并且使用一个简单通用的方式来写日志。有自定义需求的框架和CMS可以根据情况扩展这个接口,但应当保持和该文档的兼容性,这将确保使用第三方库和应用能统一的写应用日志。

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

关键词实现者在这个文档被解释为:在日志相关的库和框架实现LoggerInterface接口的人。用这些实现的人都被称作用户

1. 规范

1.1 基础

  • LoggerInterface暴露八个接口用来记录八个等级(debug, info, notice, warning, error, critical, alert, emergency)的日志。

  • 第九个方法是log,接受日志等级作为第一个参数。用一个日志等级常量来调用这个方法的结果必须和调用具体等级方法的一致。如果具体的实现不知道传入的不按规范的等级来调用这个方法必须抛出一个Psr\Log\InvalidArgumentException。用户不应自定义一个当前不支持的未知等级。

1.2 消息

  • 每个方法都接受字符串,或者有__toString方法的对象作为消息。实现者可以对传入的对象有特殊的处理。如果不是,实现者必须将它转换成字符串。

  • 消息可以包含可以被上下文数组的数值替换的占位符。

    占位符名字必须和上下文数组键名对应。

    占位符名字必须使用使用一对花括号为分隔。在占位符和分隔符之间不能有任何空格。

    占位符名字应该A-Za-z0-9,下划线_和句号.。其它的字符作为以后占位符规范的保留。

    实现者可以使用占位符来实现不同的转义和翻译日志成文。用户在不知道上下文数据是什么的时候不应提前转义占位符。

下面提供一个占位符替换的例子,仅作为参考:

function interpolate($message, array $context = array())
{
    // build a replacement array with braces around the context keys
    $replace = array();
    foreach ($context as $key => $val) {
        $replace['{' . $key . '}'] = $val;
    }
    // interpolate replacement values into the message and return
    return strtr($message, $replace);
}
// a message with brace-delimited placeholder names
$message = "User {username} created";

// a context array of placeholder names => replacement values
$context = array('username' => 'bolivar');

// echoes "Username bolivar created"
echo interpolate($message, $context);

1.3 上下文

  • 每个方法接受一个数组作为上下文数据,用来存储不适合在字符串中填充的信息。数组可以包括任何东西。实现者必须确保他们对上下文数据足够的掌控。在上下文中一个给定值不可抛出一个异常,也不可产生任何PHP错误,警告或者提醒。

  • 如果在上下文中传入了一个异常对象,它必须以exception作为键名。记录异常轨迹是通用的模式,如果日志底层支持这样也是可以被允许的。实现者在使用它之前必须验证exception的键值是不是一个异常对象,因为它可以允许是任何东西。

1.4 助手类和接口

  • Psr\Log\AbstractLogger类让你非常简单的实现和扩展LoggerInterface接口以实现通用的log方法。其他八个方法将会把消息和上下文转发给它。

  • 类似的,使用Psr\Log\LoggerTrait只需要你实现通用的log方法。记住traits不能实现接口前,你依然需要implement LoggerInterface

  • Psr\Log\NullLogger是和接口一个提供的。它可以为使用接口的用户提供一个后备的“黑洞”。如果上下文数据非常重要,这不失为一个记录日志更好的办法。

  • Psr\Log\LoggerAwareInterface只有一个setLogger(LoggerInterface $logger)方法可以用来随意设置一个日志记录器。

  • Psr\Log\LoggerAwareTraittrait可以更简单的实现等价于接口。通过它可以访问到$this->logger

  • Psr\Log\LogLevel类拥有八个等级的常量。

2. 包

作为psr/log 的一部分,提供接口和相关异常类的一些描述以及一些测试单元用来验证你的实现。

3. Psr\Log\LoggerInterface

<?php

namespace Psr\Log;

/**
* Describes a logger instance
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data, the only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
    /**
    * System is unusable.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function emergency($message, array $context = array());

    /**
    * Action must be taken immediately.
    *
    * Example: Entire website down, database unavailable, etc. This should
    * trigger the SMS alerts and wake you up.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function alert($message, array $context = array());

    /**
    * Critical conditions.
    *
    * Example: Application component unavailable, unexpected exception.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function critical($message, array $context = array());

    /**
    * Runtime errors that do not require immediate action but should typically
    * be logged and monitored.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function error($message, array $context = array());

    /**
    * Exceptional occurrences that are not errors.
    *
    * Example: Use of deprecated APIs, poor use of an API, undesirable things
    * that are not necessarily wrong.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function warning($message, array $context = array());

    /**
    * Normal but significant events.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function notice($message, array $context = array());

    /**
    * Interesting events.
    *
    * Example: User logs in, SQL logs.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function info($message, array $context = array());

    /**
    * Detailed debug information.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function debug($message, array $context = array());

    /**
    * Logs with an arbitrary level.
    *
    * @param mixed $level
    * @param string $message
    * @param array $context
    * @return null
    */
    public function log($level, $message, array $context = array());
}

4. Psr\Log\LoggerAwareInterface

<?php

namespace Psr\Log;

/**
* Describes a logger-aware instance
*/
interface LoggerAwareInterface
{
    /**
    * Sets a logger instance on the object
    *
    * @param LoggerInterface $logger
    * @return null
    */
    public function setLogger(LoggerInterface $logger);
}

5. Psr\Log\LogLevel

<?php

namespace Psr\Log;

/**
* Describes log levels
*/
class LogLevel
{
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}

PHP编码规范:基本代码规范

本节标准包含了成为标准代码所需要的基本元素,以确保高级技术特性可以在PHP代码中共享。

RFC 2119中的特性关键词”必须”(MUST),“不可”(MUST NOT),“必要”(REQUIRED),“将会”(SHALL),“不会”(SHALL NOT),“应当”(SHOULD),“不应”(SHOULD NOT),“推荐”(RECOMMENDED),“可以”(MAY)和“可选”(OPTIONAL)在这文档中将被用来描述。

1. 大纲

  • 文件必须使用 <?php<?= 标签。

  • 文件必须使用不带BOM的UTF-8代码文件。

  • 文件应当声明符号(类,函数,常量等…)或者引起副作用(例如:生成输出,修改.ini配置等),但不能同时存在。

  • 命名空间和类名必须遵守 PSR-0

  • 类名必须使用骆驼式StudlyCaps写法 (译者注:驼峰式的一种变种,后文将直接用StudlyCaps表示)。

  • 类名常量必须使用全大写和下划线分隔符。

  • 方法名必须使用驼峰式cameCase写法(译者注:后文将直接用camelCase表示)。

2. 文件

2.1. PHP标签

PHP代码必须使用长标签<?php ?>或者短输出式<?= ?>标签;它不可使用其他的标签变种。

2.2. 字符编码

PHP代码必须只使用不带BOM的UTF-8。

2.3. 副作用

一个文件应当声明新符号 (类名,函数名,常量等)并且不产生副作用,或者应当执行有边缘影响的逻辑,但不能同时使用。

短语”副作用”意思是不直接执行逻辑的类,函数,常量等 仅包括文件

“副作用”包含但不局限于:生成输出,明确使用requireinclude,连接外部服务,修改ini配置,触发错误和异常,修改全局或者静态变量,读取或修改文件等等

下面是一个例子文件同时包含声明和副作用 即避免的例子:

<?php   
// side effect: change ini settings
ini_set('error_reporting', E_ALL);

// side effect: loads a file
include "file.php";

// side effect: generates output
echo "<html>\n";

// declaration
function foo()
{
    // function body
}

下面这个例子仅仅包含声明并且没有副作用; 即需要提倡的例子:

<?php
// declaration
function foo()
{
    // function body
}

// conditional declaration is *not* a side effect
if (! function_exists('bar')) {
    function bar()
    {
        // function body
    }
}

3. 命名空间和类名

命名空间和类名必须遵守 PSR-0: http://www.zzphp.net/?p=278.

这意味着每个类只能是一个文件本身,并且至少有一个层级的命名空间:顶级的组织名称。

类名必须使用骆驼式StudlyCaps写法

代码必须使用PHP5.3及以后编写正式的命名空间 例子:

<?php
// PHP 5.3 and later:
namespace Vendor\Model;

class Foo
{
}

代码使用5.2.x及之前编写应当使用Vendor_作为前缀的伪命名空间作为类

<?php
// PHP 5.2.x and earlier:
class Vendor_Model_Foo
{
}

4. 类常量,属性和方法

术语“类”指所有的类,接口和特性(traits)

4.1. 常量

类常量必须使用全大写,分隔符使用下划线作为声明。 例子:

<?php
namespace Vendor\Model;

class Foo
{
    const VERSION = '1.0';
    const DATE_APPROVED = '2012-06-01';
}

4.2. 属性

本手册有意避免推荐使用$StulyCaps$cameCase或者unser_score作为属性名字

不管名称约定是不是在一个应当接受的合理范围。这个范围可能是组织,包,类,方法。

4.3. 方法

方法名必须用cameCase()写法

PHP编码规范:自动加载器特性强制性要求

下面描述了关于自动加载器特性强制性要求:

强制性

  • 一个完全标准的命名空间必须要有一下的格式结构\<Vendor Name>\(<Namespace>\)*<Class Name>
  • 命名空间必须有一个顶级的组织名称 (“Vendor Name”).
  • 命名空间中可以根据情况决定使用多少个子空间
  • 命名空间中的分隔符当从文件系统加载的时候将被映射为 DIRECTORY_SEPARATOR
  • 命名空间中的类名中的_没有特殊含义,也将被作为DIRECTORY_SEPARATOR对待.
  • 命名空间中的类名在从文件系统加载时文件名都需要以.php结尾
  • 组织名,空间名,类名都可以随意选择使用大小写英文字符

示例

  • \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
  • \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php
  • \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php
  • \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php

命名空间和类名中的下划线

  • \namespace\package\Class_Name => /path/to/project/lib/vendor/namespace/package/Class/Name.php
  • \namespace\package_name\Class_Name => /path/to/project/lib/vendor/namespace/package_name/Class/Name.php

以上是我们为实现无痛的自动加载特性设定的最低标准。你可以按照此标准实现一个SplClassLoader在PHP 5.3中去加载类。

实例

下面是一个函数实例简单展示如何使用上面建议的标准进行自动加载

<?php

function autoload($className)
{
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strrpos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    require $fileName;
}

SplClassLoader实现

下面的gist是一个SplClassLoader实例可以按照上面建议的自动加载特性来加载类。这也是我们当前推荐在PHP5.3中按照上述标准加载类的方式

PHP中的二进制位运算和权限存储

在很多系统的权限/选项设置中 很多都用到了位运算的方法来存储多种标志位。这样可以节省字段。一个字段只需要一个数字 就可以标识很多种设置和信息。

举例 dicuz的帖子表的status字段,官方预留了16个标志位(0×0000 – 0xFFFF) 即216

目前规划使用了只有8个标志位,如下

0000 0000 0000 0001 是否缓存帖子位置信息
0000 0000 0000 0010 是否回帖只对管理人员和发帖者可见
0000 0000 0000 0100 是否抢楼贴
0000 0000 0000 1000 是否倒序查看回帖
0000 0000 0001 0000 是否存在主题图章标志位
0000 0000 0010 0000 回复是否通知作者
0000 0000 0100 0000 是否推送到QQ空间
0000 0000 1000 0000 是否推送到腾讯微博

这8种状态可以使用一个数字来同时表示,节省了字段

那么这种东西的原理是什么呢

这个我们可以复习一下的位运算单算法

例子 名称 结果
$a & $b And(按位与) 将把 $a 和 $b 中都为 1 的位设为 1
$a | $b Or(按位或) 将把 $a 或者 $b 中为 1 的位设为 1
$a ^ $b Xor(按位异或) 将把 $a 和 $b 中不同的位设为 1
~ $a Not(按位非) 将 $a 中为 0 的位设为 1,反之亦然
$a << $b Shift left(左移) 将 $a 中的位向左移动 $b 次(每一次移动都表示“乘以 2”)
$a >> $b Shift right(右移) 将 $a 中的位向右移动 $b 次(每一次移动都表示“除以 2”)

比如

与运算

14 = 0b1110

11 = 0b1011

那么 14 & 11 = 0b1110 & 0b1011 = 0b1010 = 10

或运算

还是上面那个例子

14 | 11 = 0b1110 | 0b1011 = 0b1111 = 15

异或运算

14 ^ 11 = 0b1110 ^ 0b1011 = 0b0101 = 5

非运算

非运算比较特殊 涉及到符号 这里要说一下补码 反码 原码的概念

1.二进制最高是符号位 0是正数 1表示负数

2.正数的 原码 反码 补码 都一样(我上面没有单独算补码的原因 ,正数补码和反码一样)

3.用二进制表示一个数 这个码 就是原码 比如 14的原码就是 1110

4.负数的反码 等于 他符号位不变 其他取反,而负数的补码等于他的反码+1

5.计算机运算的时候 全都是以补码的形式来运算的 不管正负数

那么

1 是正数,那么他的原码 0001 = 反码 = 补码 = 0001 =>取反 后补码1110 <=反码 1101<=原码1010

那么这个符号位是1就是负数 也就是010代表的负数就是-2 也就是 ~1 = -2

左右位移运算

1<< 2 1的补码 00000001 移动2位 00000100 正数的反码 补码 原码 都一样 所以 是个4

负数的计算过程相同 不再赘述 左移也类似 4>>2 就是1

其实可以理解为右移在十进制的表现上就是乘以2 左移 在十进制的表现上就是除以2


那么回到本文正题 如何用一个数字来标识这些权限位呢?

以刚才discuz的帖子表达status字段为例,检查帖子回复是否通知作者 就看二进制上第六位是否是置位为1 那么怎么检查呢?就是用上面我们提到的与运算。

与运算是将把 $a 和 $b 中都为 1 的位设为 1。那么假设

$a=36=0b 0010 0100

$b=0b 0010 0000

$a&$b = 0b 0010 0100 & 0b 0010 0000 = 0b 0010 0000 = 32 = 26-1 = 25

因此 检查,某个数代表的第N个权限标志位有没有置位(是1) 只要选择该数与仅该标志位置位的操作数2N-1进行与运算即可,相反要计算某个标志位被置位的数字 只要选择合适的操作数进行或运算即可。我们可以看discuz对此的实现:

function getstatus($status, $position) {
    $t = $status & pow(2, $position - 1) ? 1 : 0;
    return $t;
}

function setstatus($position, $value, $baseon = null) {
    $t = pow(2, $position - 1);
    if($value) {
        $t = $baseon | $t;
    } elseif ($baseon !== null) {
        $t = $baseon & ~$t;
    } else {
        $t = ~$t;
    }
    return $t & 0xFFFF;
}

注意 写这段代码的人显然受到了C的影响 其实 $a & ~$b 和 $a ^ $b 是等效的 只不过 ^是PHP的写法 另外 pow(2, $position – 1)换成 1 << ($position -1) 其实更好理解。

转载自:http://blog.ihipop.info/2013/01/3309.html

PHP类属性3种不同访问方式的性能测试

PHP给了我们3种访问类属性的方法,我们来测试以下他们的性能差异。

1.直接访问

class dog { 
    public $name = "";
} 
$rover = new dog(); 
for ($x=0; $x<10; $x++) { 
    $t = microtime(true); 
    for ($i=0; $i<1000000; $i++) { 
        $rover->name = "rover"; 
        $n = $rover->name;
    } 
    echo microtime(true) - $t;echo "\n";
}

2.使用类函数访问

class dog {
    public $name = "";

    public function setName($name) {
        $this->name = $name; 
    }

    public function getName() {
        return $this->name; 
    } 
}

$rover = new dog();
for ($x=0; $x<10; $x++) { 
    $t = microtime(true);
    for ($i=0; $i<1000000; $i++) { 
        $rover->setName("rover");
        $n = $rover->getName();
    }
    echo microtime(true) - $t;echo "\n";
}

3.使用PHP提供的魔术方法__set和__get访问

class dog { 
    private $data = array();
    function __set($name, $value) {
        $this->data[$name] = $value;
    }
    function __get($name) {
        if(array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        return null;
    }
} 
$rover = new dog(); 
for ($x=0; $x<10; $x++) { 
    $t = microtime(true); 
    for ($i=0; $i<1000000; $i++) { 
        $rover->name = "rover"; 
        $n = $rover->name;
    } 
    echo microtime(true) - $t;echo "\n";
}

执行结果分别为:

1.直接访问

0.59869480133057
0.59985685348511
0.61411023139954
0.61089015007019
0.59671401977539
0.60318994522095
0.60059809684753
0.59980893135071
0.59794902801514
0.59721207618713

2.使用类函数访问

4.107736825943
4.0344598293304
4.009449005127
4.0501089096069
4.070415019989
4.0576939582825
4.0664989948273
4.0609030723572
4.0520219802856
4.0485868453979

3.使用PHP提供的魔术方法__set和__get访问

7.6956799030304
7.810909986496
7.7758450508118
7.7235188484192
7.7205560207367
7.7772619724274
7.9401290416718
7.8542349338531
7.8687150478363
7.9177348613739

差距还是挺大的。

为了让大家方便自己测试,下面把3种直接可以在命令行执行的代码整理出来:

php -d implicit_flush=off -r  'class dog { public $name = "";} $rover = new dog(); for ($x=0; $x<10; $x++) { $t = microtime(true); for ($i=0; $i<1000000; $i++) { $rover->name = "rover"; $n = $rover->name;} echo microtime(true) - $t;echo "\n";}'

php -d implicit_flush=off -r  'class dog {public $name = "";public function setName($name) {$this->name = $name; }public function getName() {return $this->name; } }$rover = new dog();for ($x=0; $x<10; $x++) { $t = microtime(true);for ($i=0; $i<1000000; $i++) { $rover->setName("rover");$n = $rover->getName();}echo microtime(true) - $t;echo "\n";}'

php -d implicit_flush=off -r 'class dog { private $data = array();function __set($name, $value) {$this->data[$name] = $value;}function __get($name) {if(array_key_exists($name, $this->data)) {return $this->data[$name];}return null;}} $rover = new dog(); for ($x=0; $x<10; $x++) { $t = microtime(true); for ($i=0; $i<1000000; $i++) { $rover->name = "rover"; $n = $rover->name;} echo microtime(true) - $t;echo "\n";}'

你们结果如何?

(End)

PHP匿名函数,闭包函数,use关键字

从PHP 5.3开始新增了匿名函数(Anonymous functions),也叫闭包函数(closures),关键字 use 同时也在匿名函数中。

先看一下匿名函数的示例,作为回调函数的参数:

<?php
echo preg_replace_callback('~-([a-z])~', function ($match) {
    return strtoupper($match[1]);
}, 'hello-world');
// 输出 helloWorld
?>

匿名函数也可以作为变量的值来使用。把一个匿名函数赋值给一个变量的方式和普通变量赋值的语法是一样的,最后也需要加上分号。

<?php
$greet = function($name)
{
    printf("Hello %s\r\n", $name);
};

$greet('World');
$greet('PHP');
?>

匿名函数不会自动从父作用域中继承变量,注意从父作用域继承变量和使用全局变量是不同的。如果父作用域本身就是全局的 情况下就不存在从父作用域继承变量了,如果不是全局的话,想要使用父作用域中的变量,必须在声明匿名函数时候使用use换键字 来定义继承父作用域的变量。

<?php
// 一个基本的购物车,包括一些已经添加的商品和每种商品的数量。
// 其中有一个方法用来计算购物车中所有商品的总价格。该方法使用了一个closure作为回调函数。
class Cart
{
    const PRICE_BUTTER  = 1.00;
    const PRICE_MILK    = 3.00;
    const PRICE_EGGS    = 6.95;

    protected   $products = array();

    public function add($product, $quantity)
    {
        $this->products[$product] = $quantity;
    }

    public function getQuantity($product)
    {
        return isset($this->products[$product]) ? $this->products[$product] :
               FALSE;
    }

    public function getTotal($tax)
    {
        $total = 0.00;

        $callback =
            function ($quantity, $product) use ($tax, &$total)
            {
                $pricePerItem = constant(__CLASS__ . "::PRICE_" .
                    strtoupper($product));
                $total += ($pricePerItem * $quantity) * ($tax + 1.0);
            };

        array_walk($this->products, $callback);
        return round($total, 2);;
    }
}

$my_cart = new Cart;

// 往购物车里添加条目
$my_cart->add('butter', 1);
$my_cart->add('milk', 3);
$my_cart->add('eggs', 6);

// 打出出总价格,其中有 5% 的销售税.
print $my_cart->getTotal(0.05) . "\n";
// The result is 54.29
?>

匿名函数(闭包函数)是一个独立的命名空间,你不能访问这个命名空间之外的变量,使用use关键字可以把外部的变量 带到这个命名空间中。可以通过使用 & 符号来声明指针变量。

(End)

PHP与递归(Recursion):递归尾调用(Tail Call)

在程序设计中,递归(Recursion)是一个很常见的概念,合理使用递归,可以提升代码的可读性,但同时也可能会带来一些问题。

下面以阶乘(Factorial)为例来说明一下递归的用法,实现语言是PHP:

<?php
function factorial($n) {
    if ($n == 0) {
        return 1;
    }
    return factorial($n - 1) * $n;
}
var_dump(factorial(100));

如果安装了XDebug的话,可能会遇到如下错误:

Fatal error: Maximum function nesting level of ’100′ reached, aborting!

注:这是XDebug的一个保护机制,可以通过max_nesting_level选项来设置。

即便代码能正常运行,只要我们不断增大参数,程序迟早会报错:

Fatal error:  Allowed memory size of … bytes exhausted

为什么呢?简单点说就是递归造成了栈溢出。有几个方法可以用来规避这个问题,比如说利用尾调用(Tail Call)来消除递归对栈的影响。

让我们先来看看尾调用的定义:如果一个函数在执行了一次函数调用后,不再做别的事就称为尾调用。形象点说就是直接返回一个函数调用。尾调用不会返回原来的函数,所以不需要额外的栈保留调用函数的数据。上面代码改成尾调用后类似下面代码的样子:

我们用PHP来实现一个尾调用版本的阶乘:

<?php
function factorial($n, $accumulator = 1) {
    if ($n == 0) {
        return $accumulator;
    }
    return factorial($n - 1, $accumulator * $n);
}

var_dump(factorial(100));

可惜测试后才发现PHP根本不支持尾调用!好在天无绝人之路,仔细阅读维基百科中关于尾调用的介绍,你会发现里面提到了Trampoline的概念。简单点说就是利用高阶函数消除递归,依照这样的理论基础,我们可以把上面的尾调用代码改写成如下方式:

<?php

function factorial($n, $accumulator = 1) {
    if ($n == 0) {
        return $accumulator;
    }

    return function() use($n, $accumulator) {
        return factorial($n - 1, $accumulator * $n);
    };
}

function trampoline($callback, $params) {
    $result = call_user_func_array($callback, $params);

    while (is_callable($result)) {
        $result = $result();
    }

    return $result;
}

var_dump(trampoline('factorial', array(100)));

看上去不错,不过我不得不向大家道个歉,本文用递归实现阶乘其实是个玩笑,实际上只要用一个循环就行了,《代码大全》里专门提到了这一点:

<?php

function factorial($n) {
    $result = 1;

    for ($i = 1; $i <= $n; $i++) {
        $result *= $i;
    }

    return $result;
}

var_dump(factorial(100));

还有很多别的方法可以用来规避递归引起的栈溢出问题,比如说Python中可以通过装饰器和异常来消灭尾调用,让人有一种别有洞天的感觉

除非能提升代码可读性,否则没有必要使用递归;迫不得已之时,最好考虑使用Tail Call或Trampoline等技术来规避潜在的栈溢出问题。

(End)

extension,zend_extension和zend_extension_ts

extension是php扩展

zend_extension为zend engine扩展

zend engine是php脚本解释引擎,zend engine 2(ZE2)与php 5同时发布,php 5开始使用ZE2作为脚本解释引擎;php 6目前还处于开发阶段,但已经引入zend engine 3(ZE3)。

所以,php有2种扩展,一个基于zend engine引擎的,一个是php自身的。才会出现有些扩展使用extension,有些使用zend_extension来引用。

与zend_extension相关的,还有以下指令:

  • zend_extension
    (non ZTS, non debug build)
  • zend_extension_ts
    ( ZTS, non debug build)
  • zend_extension_debug
    (non ZTS, debug build)
  • zend_extension_debug_ts
    ( ZTS, debug build)

ZTS全称为:ZEND Thread Safety(线程安全)

查看你的php是否为线程安全,可以检查phpinfo()中的Thread Safety的值,enable为线程安全,disable为非线程安全。