xhprof安装记录

选择一个工具分析PHP函数调用的资源耗用明细,以图表化的形式展现,方便优化代码。

安装xhprof

1
$ pecl install xhprof-beta
Read more

PHP7特性概览(一)

了解PHP7的一些特性,搭建PHP7源码编译环境,并运行官网这些新特性的代码。


在64位平台支持64位integer

在64位平台支持64位integer,长度为2^64-1字符串。

更详细查看

抽象语法树

抽象语法树是语法分析之后产物,忽略了语法细节,是解释前端和后端的中间媒介。新增抽象语法树,解耦语法分析和编译,简化开发维护,并为以后新增一些特性,如添加抽象语法树编译hook,深入到语言级别实现功能。

更详细查看

闭包this绑定

新增Closure::call,优化Closure::bindTo(JavaScript中bind,apply也是一样的应用场景)。

1
2
3
4
5
<?php
class Foo { private $x = 3; }
$foo = new Foo;
$foobar = function () { var_dump($this->x); };
$foobar->call($foo); // prints int(3)

2-3行新建Foo的对象,第4行创建了一个foobar的闭包,第5行调用闭包的call方法,将闭包体中的$this动态绑定到$foo并执行。

同时官网上进行性能测试,Closure::call的性能优于Closure::bindTo

更详细查看Closure::call

简化isset的语法糖

从使用者角度来说,比较贴心的一个语法糖,减少了不必要的重复代码,使用情景如:

1
2
3
4
5
<?php
// PHP 5.5.14
$username = isset($_GET['username']) ? $_GET['username'] : 'nobody';
// PHP 7
$username = $_GET['username'] ?? 'nobody';

在服务器端想获取$_GET中的变量时,若是PHP5语法,需要使用?:操作符,每次要重写一遍$_GET['username'],而在PHP7就可以使用这个贴心的语法糖,省略这个重复的表达式。

更详细查看isset_ternary

yield from

允许Generator方法代理Traversable的对象和数组的操作。这个语法允许把yield语句分解成更小的概念单元,正如利用分解类方法简化面向对象代码。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function g1() {
yield 2;
yield 3;
yield 4;
}

function g2() {
yield 1;
yield from g1();
yield 5;
}

$g = g2();
foreach ($g as $yielded) {
print($yielded);
}
// output:
// 12345

yield from后能跟随GeneratorArray,或Traversable的对象。

更详细查看generator delegation

匿名类

1
2
3
4
5
6
<?php
class Foo {}

$child = new class extends Foo {};

var_dump($child instanceof Foo); // true

更详细查看anonymous class

标量类型声明

1
2
3
4
5
6
7
8
9
10
11
12
<?php
declare(strict_types=1);
function add(int $a, int $b): int {
return $a + $b;
}

var_dump(add(1, 2)); // int(3)
// floats are truncated by default
var_dump(add(1.5, 2.5)); // int(3)

//strings convert if there's a number part
var_dump(add("1", "2")); // int(3)

更详细查看

返回值类型声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function get_config(): array {
return [1,2,3];
}
var_dump(get_config());

function &get_arr(array &$arr): array {
return $arr;
}
$arr = [1,2,3];
$arr1 = get_arr($arr);
$arr[] = 4;
// $arr1[] = 4;
var_dump($arr1 === $arr);

更详细查看return_types

3路比较

一个语法糖,用来简化比较操作符,常应用于需要使用比较函数的排序,消除用户自己写比较函数可能出现的错误。分为3种情况,大于(1),等于(0),小于(-1)。

%}
1
2
3
4
5
6
7
<?php
function order_func($a, $b) {
return $a <=> $b;
}
echo order_func(2, 2); // 0
echo order_func(3, 2); // 1
echo order_func(1, 2); // -1

导入包的缩写

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
<?php
import shorthand
Current use syntax:

use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion as Choice;
use Symfony\Component\Console\Question\ConfirmationQuestion;

// Proposed group use syntax:

use Symfony\Component\Console\{
Helper\Table,
Input\ArrayInput,
Input\InputInterface,
Output\NullOutput,
Output\OutputInterface,
Question\Question,
Question\ChoiceQuestion as Choice,
Question\ConfirmationQuestion,
};

更详细查看

PHP7数组实现(画图版)

主要介绍一下PHP7对于数组的实现。


预备知识

PHP的数据类型

php_types

zend_long

php中的long类型, 在64位机上是64位有符号整数, 不然是32位符号整数。

1
2
3
4
5
6
7
8
9
10
11
// zend_long.h
/* This is the heart of the whole int64 enablement in zval. */
#if defined(__x86_64__) || defined(__LP64__) || defined(_LP64) || defined(_WIN64)
# define ZEND_ENABLE_ZVAL_LONG64 1
#endif

#ifdef ZEND_ENABLE_ZVAL_LONG64
typedef int64_t zend_long;
#else
typedef int32_t zend_long;
#endif

double

双精度浮点数

zend_refcounted

引用计数类型, 记录引用次数, 以及u用于存储元素类型信息type字段(如is_string, is_array, is_object等), 标明对象是否调用过free, destructor函数的flags, 记录GC root number (or 0) and color的gc_info字段(详情见php的zend_gc.h及zend_gc.c).

1
2
3
4
5
6
7
8
9
10
11
12
13
// zend_types.h
struct _zend_refcounted {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
};

zend_string

字符串类型, 带有引用计数, 和string的hash值h, 避免每次需要计算, 字段长度len, 以及val用来索引字符串, 这里tricky地避免了2次malloc内存.

1
2
3
4
5
6
7
8
// zend_types.h
typedef struct _zend_string zend_string;
struct _zend_string {
zend_refcounted gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};

zend_object

1
2
3
4
5
6
7
8
9
typedef struct _zend_object     zend_object;
struct _zend_object {
zend_refcounted gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};

zend_resource

1
2
3
4
5
6
struct _zend_resource {
zend_refcounted gc;
int handle; // TODO: may be removed ???
int type;
void *ptr;
};

zend_reference

1
2
3
4
struct _zend_reference {
zend_refcounted gc;
zval val;
};

zend_array

详细见下文具体实现.

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
// zend_types.h
typedef struct _zend_array zend_array;

typedef struct _zend_array HashTable;

struct _zend_array {
zend_refcounted gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar reserve)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};

位运算

给出一个上限Max, 利用位运算求出指定N的表达式-(Max-(N % Max))的值.

1
2
3
4
uint32_t max = 2;
uint32_t mask = (uint32_t)-max; // 二进制表示: 11111111 11111111 11111111 11111110
uint32_t n = 12345678;
int32_t answser = (int32_t)(n | mask); // -2

散列表

散列表原理维基百科

局部性原理

空间局部性, 时间局部性

具体实现

初始化

1
2
<?php
$arr = array();

php_init

arData指针指向的内存, 是真实数据的起始地址, 在邻近的低址内存是存储hash值到真实数据地址的映射表, 这个映射表是uint32_t类型的数组, 大小相同于nTableSize, 这样最好情况下, 每个hash值都能映射一个真实数据.

插入

插入key为$key1, $key1.hash为0, 值为10的元素

1
2
<?php
$arr[$key1] = 10;

php_insert

上文提到的位运算就是应用在插入元素场景, 由于arData指向的是真实数据的起始地址, 而索引信息(即存储hash值到真实数据的映射)处于arData更低地址, 那么要更新索引信息, 就需要计算出-(nTableSize-(nHash % nTableSize)), nHash就是键的hash值, 例如向大小为2的数组, 插入hash值为0的元素, 那么索引到hash值为0的区域就是((uint32_t*)arData)-(2-(0%2)), 如图, 将hash值为0的数据偏移0*sizeof(Bucket)存储到了((uint32_t*)arData)-2.

哈希冲突

插入key为$key2($key2 != $key1), $key2.hash为0, 值为20的元素, 造成哈希冲突

1
2
<?php
$arr[$key2] = 20;

php_collide

数组拓容

插入key为$key3, $key3.hash为1, 值为30的元素, 造成数组的load factor过高, 触发拓容

1
2
<?php
$arr[$key3] = 30;

php_extend

删除

删除key为$key2的元素

1
2
<?php
delete $arr[$key2];

php_del

遍历

1
2
3
4
5
6
7
8
9
<?php
foreach ($arr as $v) {
print $v . "\n";
}

// output
// 10
// 30

直接遍历arData, 最大边界为arData+nNumUsed, 跳过被UNDEF的元素.

与历史版本比较

改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// zend_hash.h
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer; /* Used for element traversal */
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
  1. 连续的内存, 更高的效率
    通过简单地观察数据结构, 可以发现5.3.23版本是使用Bucket **arBuckets分配一块二维Bucket数组存放键值, 与7.0的预分配连续内存的做法不同, 每次需要插入元素都要申请一块sizeof(Bucket)的内存, 更容易造成内存碎片, 以及低效率;
  2. 更小的数据结构, 更优美的实现
    此外, 废弃了Bucket *pListHead以及Bucket *pListTail这两个头尾指针, 本来是为了实现数组特性, 实现正反序遍历等功能, 而7.0既然已经是连续的一块内存, 那么直接从Bucket *arData下标0处, 到达边界arData+nNumUsed就可以实现这个功能.
  3. 良好的局部性
    遍历数组有更好的局部性, 相较于5.3.23的链表遍历, 使得遍历时cache能更准确地加载数据, 拥有更好的时间空间局部性.

可以说这个7.0版本对PHP数组的优化是非常成功的, 除此之外, 对于其他的数据结构7.0也是有”瘦身”优化, 对于整个效率和内存占用有比较明显的改善.

PHP数组的内存布局

PHP数组的内存布局

内存布局

它的内存结构,目前的实现方案是分配一块连续的内存区间,用来存储hash元数据和具体数据。
连续的内存,上面部分是存储hash值到值地址的映射,而下面部分就是值的存储区域,如下图(摘自php-src/Zend/zend_types.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* HashTable Data Layout
* =====================
*
* +=============================+
* | HT_HASH(ht, ht->nTableMask) |
* | ... |
* | HT_HASH(ht, -1) |
* +-----------------------------+
* ht->arData ---> | Bucket[0] |
* | ... |
* | Bucket[ht->nTableSize-1] |
* +=============================+
*/

新建数组

初始化一块内存,大小为HT_SIZE(ht) = HT_HASH_SIZE(ht) + HT_DATA_SIZE(ht),如果你查看zend_types.h,能看到这三个宏定义:

1
2
3
4
5
6
7
8
#define HT_SIZE(ht) \
(HT_HASH_SIZE(ht) + HT_DATA_SIZE(ht))

#define HT_DATA_SIZE(ht) \
((size_t)(ht)->nTableSize * sizeof(Bucket))

#define HT_HASH_SIZE(ht) \
(((size_t)(uint32_t)-(int32_t)(ht)->nTableMask) * sizeof(uint32_t))

这里要知道nTableMask是什么,从字面意思是掩码,它的类型是uint32_t,值是(uint32_t)(-nTableSize),nTableSize最小为2,那么此时nTableMask为4294967294,表示成二进制,11111111 11111111 11111111 11111110,这个值在之后计算对应hash所在的下标值会用到。
下面就是这块内存最开始的状态,假设数组大小为2,nNumUsed即已有元素为0,则下面是初始状态:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* HashTable Data Layout
* =====================
* ht->nNumUsed = 0
* +=============================+
* 0 | |
* 1 | |
* +-----------------------------+
* ht->arData ---> | |
* | |
* +=============================+
*/

插入key的hash值为12345678的元素E

事实上对于这块内存,我们仅仅是根据arData这个指针去使用,而它是Bucket* 类型,上半部分的类型是uint32_t* ,怎么在不超出上半部分边界的情况下进行索引,这里用到了刚才提到的nTableMask,下标为(int32_t)(HASH | nTableMask),计算出为-2,那么((uint32_t*)arData)[-2]所指向的就是下标为0处的内存地址,通过nTableMask保证不会越界。
E元素首先根据nNumUsed找到存储自身的位置,即ht->arData[ht->nNumUsed],并且修改hash元数据部分,0位置的值指向存储区域,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* HashTable Data Layout
* =====================
* ht->nNumUsed = 1
* +=============================+
* 0 | 0 |
* 1 | |
* +-----------------------------+
* ht->arData ---> | E | next->HT_INVALID_IDX
* | |
* +=============================+
*/

插入key的hash值为12345678的元素F

使用链表法解决冲突。

1
2
3
4
5
6
7
8
9
10
11
12
/*
* HashTable Data Layout
* =====================
* ht->nNumUsed = 2
* +=============================+
* 0 | 1*sizeof(Bucket) |
* 1 | |
* +-----------------------------+
* ht->arData ---> | E | next->HT_INVALID_IDX
* | F | next->0
* +=============================+
*/

哈希拓容,rehash

扩大数组大小,将数据转存到新数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* HashTable Data Layout
* =====================
* ht->nNumUsed = 2
* +=============================+
* 0 | 1*sizeof(Bucket) |
* 1 | |
* 2 | |
* 3 | |
* +-----------------------------+
* ht->arData ---> | E | next->HT_INVALID_IDX
* | F | next->0
* | |
* | |
* +=============================+
*/
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×