前面我們討論了內(nèi)存的工作原理,也進(jìn)行了一些性能相關(guān)的測試萄唇。那么今天開始我們來看幾個(gè)在實(shí)踐中的應(yīng)用巩检。首先我們先從PHP開始。
2015年,PHP7的發(fā)布可以說是在技術(shù)圈里引起了不小的轟動(dòng)浑槽,因?yàn)樗膱?zhí)行效率比PHP5直接翻了一倍蒋失。PHP7在內(nèi)存方面,你是否知道作者都進(jìn)行了哪些優(yōu)化桐玻?你是否能夠深層次理解到作者優(yōu)化思路的精髓篙挽?
讓我們從幾個(gè)核心的數(shù)據(jù)結(jié)構(gòu)改進(jìn)開始看起。
PHP7 zval變化
1镊靴、php5.3中的zval:
typedef unsigned int zend_object_handle;
typedef struct _zend_object_value {
zend_object_handle handle;
zend_object_handlers *handlers;
} zend_object_value;
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
我們這里只討論64位操作系統(tǒng)下的情況铣卡。該_zval_struct結(jié)構(gòu)體中的由四個(gè)成員構(gòu)成,其中zvalue_value稍微復(fù)雜一些偏竟,是一個(gè)聯(lián)合體煮落。聯(lián)合體中最長的成員是一個(gè)指針加一個(gè)int,8+4=12字節(jié)踊谋。但是默認(rèn)情況下蝉仇,會(huì)進(jìn)行內(nèi)存對齊,故_zval_struct會(huì)占用16字節(jié)殖蚕。 那么
_zval_struct總的字節(jié) = value(16)+ refcount__gc(4)+ type(1)+ is_ref__gc(1)= 占用22字節(jié)轿衔。
最后再考慮下內(nèi)存對齊,實(shí)際占用24字節(jié)睦疫。(如果算的有點(diǎn)暈話害驹,感興趣的同學(xué)可以寫段簡單的測試代碼,使用sizeof查看一下)
2笼痛、PHP7.2中的zval
typedef struct _zval_struct zval;
typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type,
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved)
} v;
int type_info;
} u1;
union { ...... } u2;
};
7.2中的_zval_struct結(jié)構(gòu)體里由3個(gè)成員構(gòu)成裙秋,其中zend_value看起來比較復(fù)雜,實(shí)際上只是一個(gè)8字節(jié)的聯(lián)合體缨伊。 u1也是一個(gè)聯(lián)合體,占用是4個(gè)字節(jié)进宝。u2也一樣刻坊。這樣_zval_struct就實(shí)際占用16個(gè)字節(jié)。
PHP7 HashTable變化
1党晋、PHP5.3里的HashTable:
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements; //注意這里:浪費(fèi)ing
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;
} HashTable;
再5.3里HashTable就是一個(gè)大struct谭胚, 有點(diǎn)小復(fù)雜,我們拆開了細(xì)說未玻,
- uint nTableSize 4字節(jié)
- uint nTableMask 4字節(jié)
- uint nNumOfElements 4字節(jié)灾而,
- ulong nNextFreeElement 8字節(jié) 注意這前面的4個(gè)字節(jié)會(huì)被浪費(fèi)掉,因?yàn)閚NextFreeElement的開始地址需要對齊
- Bucket *pInternalPointer 8字節(jié)
- Bucket *pListHead 8字節(jié)
- Bucket *pListTail 8字節(jié)
- Bucket **arBuckets 8字節(jié)
- dtor_func_t pDestructor 8字節(jié)
- zend_bool persistent 1字節(jié)
- unsigned char nApplyCoun 1字節(jié)
- zend_bool bApplyProtection 1字節(jié)
最終
總字節(jié)數(shù) = 4+4+4+4(nNextFreeElement前面這四個(gè)字節(jié)會(huì)留空)+8+8+8+8+8+8+1+1+1 = 67字節(jié)扳剿。
再加上結(jié)構(gòu)體本身要對齊到8的整數(shù)倍旁趟,所以實(shí)際占用72字節(jié)。
2庇绽、PHP7.2里的HashTable:
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar consistency)
} 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;
};s
在7.2里HashTable
- zend_refcounted_h gc 看起來唬人锡搜,實(shí)際就是個(gè)long橙困,占用8字節(jié)
- union... u 占用4字節(jié)
- uint32_t 占用4字節(jié)
- Bucket* 指針占用8字節(jié)
- uint32_t nNumUsed 占用4字節(jié)
- uint32_t nNumOfElements 占用4字節(jié)
- uint32_t nTableSize 占用4字節(jié)
- uint32_t nInternalPointer 占用4字節(jié)
- zend_long nNextFreeElement 占用8字節(jié)
- dtor_func_t pDestructor 占用8字節(jié)
總字節(jié)數(shù) = 8+4+4+8+4+4+4+4+8+8 = 56字節(jié)
占用56字節(jié),并且正好達(dá)到了內(nèi)存對齊的狀態(tài)耕餐,沒有額外的浪費(fèi)凡傅。
另外還有PHP源代碼里經(jīng)常出鏡的Buckets也從72下降到了32字節(jié),這里我就不翻源代碼了肠缔。
優(yōu)化思路精髓
我們看了兩個(gè)核心數(shù)據(jù)結(jié)構(gòu)的結(jié)構(gòu)體變化夏跷,這上面的優(yōu)化都是什么含義呢? 拿HashTable舉例明未,貌似從72字節(jié)優(yōu)化到了56字節(jié)槽华,這內(nèi)存節(jié)約的也不是特別多嘛楷力,才20%多而已谎倔!
但這中間其實(shí)隱藏了兩個(gè)較深層次優(yōu)化思路
第一、CPU在向內(nèi)存要數(shù)據(jù)的時(shí)候是以Cache Line為單位進(jìn)行的朴上,而我們說過Cache Line的大小就是64字節(jié)煮纵《遥回過頭來看HashTable,在7.2里的56字節(jié)行疏,只需要CPU向內(nèi)存進(jìn)行一次Cache Line大小的burst IO匆光,就夠了。而在5.3里的72字節(jié)酿联,雖然只比Cache Line大了那么一丟丟终息,但是對不起,必須得進(jìn)行兩次burst IO才可以贞让。 所以周崭,在計(jì)算機(jī)里,72字節(jié)相對56字節(jié)實(shí)際上是翻倍的性能提升T拧续镇!
第二、CPU的L1销部、L2摸航、L3的容量是固定的幾十K或者幾十M。假設(shè)Cache的都是HashTable舅桩,那么Cache容量不變的條件下酱虎,PHP7里能Cache住的HashTable數(shù)量將會(huì)翻倍,緩存命中率提升一大截擂涛。要知道L1命中后只需要1ns多一點(diǎn)的耗時(shí)读串,而如果穿透到內(nèi)存的話可能就需要40多納秒的延時(shí)了,整整差了幾十倍。
所以PHP內(nèi)核的作者大牛深諳CPU與內(nèi)存的工作原理爹土,表面上看起來只是幾個(gè)字節(jié)的節(jié)約甥雕,但是實(shí)際上爆發(fā)出了巨大的性能提升!胀茵!
歡迎搜索微信公眾號:開發(fā)內(nèi)功修煉