跟廠長學PHP7核心(八):深入理解字串的實現
在前面大致預覽了常用變數的結構之後,我們今天來仔細的剖析一下字串的具體實現。
一、字串的結構
struct _zend_string { zend_refcounted_h gc;/* 字串類別及引用計數 */ zend_ulongh;/* 字串的雜湊值 */ size_tlen;/* 字串的長度 */ charval[1];/* 柔性陣列,字串儲存位置 */ };
zend_refcounted_h對應的結構體:
typedef struct _zend_refcounted_h { uint32_trefcount;/* 引用計數 */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchartype, zend_ucharflags,/* 字串的型別 */ uint16_tgc_info/* 垃圾回收資訊 */ ) } v; uint32_t type_info; } u; } zend_refcounted_h;
下面我們來了解一下具體每個成員的作用:
- gc:就是_zend_refcounted_h結構體,主要作用是引用計數以及標記變數的類別。
- h:字串的雜湊值,在字串被用來當陣列的key時才初始化,這樣如果同一個字串被多次用來做key,就不會重複計算了。
- val:這裡的char[1]並不意味著只儲存1位,char[1]被稱為柔性陣列,下面來了解一下PHP在字串記憶體分配時做了什麼。
static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent) { zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); ...... }
巨集替換後:
static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent) { zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(XtOffsetOf(zend_string, val) + len + 1), persistent); ...... }
示例中的程式碼 XtOffsetOf(zend_string, val)
表示計算出zend_string結構體的大小,而len就是要分配字串的長度,最後的 +1
是留給結束字元 \0
的。也就是說,分配記憶體時不僅僅分配結構體大小的記憶體,還要顧及到長度不可控的val,這樣不僅柔性的分配了記憶體,還使它與其他成員儲存在同一塊連續的空間中,在分配、釋放記憶體時可以把struct統一處理。
- len:字串的長度,避免重複計算浪費時間,典型的空間換時間做法。
二、字串的二進位制安全
學習過C語言的應該知道,字串中除了最後一個字元外不允許含有 \0
,否則會被認為是字串的結束字元,這就導致了C語言的字串有很多的限制,比如不儲存圖片、檔案等二進位制資料。但是PHP就沒有這樣的限制,它的字串可以儲存二進位制資料,並不會出現任何報錯,而PHP的這種能力就叫做字串的二進位制安全。
C語言程式碼如下:
main() { char a[] = "aaa\0b";/* 含有\0的字串 */ printf("%d\n", strlen(a));/* 長度為3,\0後的b被忽略 */ }
PHP程式碼:
<?php $a = "aaa\0b"; echo strlen($a);//輸出5 ?>
但是PHP不是C語言寫的嗎?為什麼PHP不會報錯?我們再來回顧一下zend_string結構體,還記得成員變數len嗎?它是實現二進位制安全的關鍵,我們不需要像C一樣通過 \0
來判定字串是否被讀取完成,而是通過長度len來判斷,這樣就保證了字串的二進位制安全。
三、zend_string API
在瞭解了zend_string結構之後,我們來了解一下用來操作zend_string的函式集合。
函式 | 作用 |
---|---|
zend_interned_strings_init | 初始化內部字串儲存雜湊表,並把PHP的關鍵字等字串資訊寫進去 |
zend_new_interned_string | 把一個zend_string寫入CG(interned_strings)雜湊表中 |
zend_interned_strings_snapshot | 將CG(interned_strings)雜湊表中的字串標記為永久字串,這裡標記的只有PHP關鍵字、內部函式名、內部方法名等 |
zend_interned_strings_restore | 銷燬CG(interned_strings)雜湊表中型別為非永久字串的值,在php_request_shutdown階段釋放 |
zend_interned_strings_dtor | 銷燬整個CG(interned_strings)雜湊表,在php_module_shutdown階段釋放 |
zend_string_hash_val | 得到字串的雜湊值,沒有則實時計算 |
zend_string_forget_hash_val | 將字串的雜湊值置為0 |
zend_string_refcount | 讀取字串的引用計數 |
zend_string_addref | 引用計數+1 |
zend_string_delref | 引用計數-1 |
zend_string_alloc | 分配記憶體及初始化字串的值 |
zend_string_init | 初始化字串並在最後追加 \0 |
zend_string_cop | 使用引用計數方式複製字串 |
zend_string_dup | 直接複製一個字串 |
zend_string_extend | 擴容到len,保留原來的值 |
zend_string_truncate | 截斷到len,保留開頭到len的值 |
zend_string_free | 釋放字串記憶體 |
zend_string_release | GC引用遞減,直到為0時釋放記憶體 |
zend_string_equals | 普通判等 |
zend_string_equals_ci | 基於二進位制安全,兩個zend_string型別字串判等 |
zend_string_equals_literal_ci | 基於二進位制安全,zend_string型別和char*字串判等 |
zend_inline_hash_func | 計算字串的雜湊值 |
zend_intern_known_strings | 往zend_intern_known_strings全域性陣列寫入str |
下面挑幾個函式來介紹一下。
3.1、zend_string_init函式
zend_string_init函式主要負責把一個普通的字串轉化為zend_string結構體。
static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent) { zend_string *ret = zend_string_alloc(len, persistent); memcpy(ZSTR_VAL(ret), str, len); ZSTR_VAL(ret)[len] = '\0'; return ret; }
\0
3.2、zend_string_extend函式
該函式主要用於對字串的擴容,注意這裡擴容不會改變原來儲存的值,只是把長度擴大到len。
static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t len, int persistent) { zend_string *ret; ZEND_ASSERT(len >= ZSTR_LEN(s)); if (!ZSTR_IS_INTERNED(s)) { if (EXPECTED(GC_REFCOUNT(s) == 1)) { ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); ZSTR_LEN(ret) = len; zend_string_forget_hash_val(ret); return ret; } else { GC_REFCOUNT(s)--; } } ret = zend_string_alloc(len, persistent); memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), ZSTR_LEN(s) + 1); return ret; }
- 如果不是內部字串並且引用計數為1時,直接呼叫perealloc分配記憶體。
- 如果字串的引用計數大於1或者是內部字串時,就不能在原來的基礎上擴容了,先通過zend_string_alloc申請一塊新記憶體,讓後將舊內容拷貝到新記憶體中。
3.3、zend_string_equals_ci函式
主要基於二進位制安全對兩個字串進行判等,我們來看下PHP是怎麼比較兩個字串的。
#define zend_string_equals_ci(s1, s2) \ (ZSTR_LEN(s1) == ZSTR_LEN(s2) && !zend_binary_strcasecmp(ZSTR_VAL(s1), ZSTR_LEN(s1), ZSTR_VAL(s2), ZSTR_LEN(s2)))
- 先比較兩個字串的長度是否相等,注意這裡是通過zend_string中的len來比較的。
- zend_binary_strcasecmp函式在長度比較完成後,進行逐個字元進行比較。先遍歷整個字串陣列,取出每個字元,轉換為ASC碼進行判等,如果不等則返回差值。迴圈完了還沒發現差異的話就返回兩者的長度差,如果長度相等就返回0。感覺這裡做的有點多餘,引數傳進來之前就已經做了長度判等了。
ZEND_API int ZEND_FASTCALL zend_binary_strcasecmp(const char *s1, size_t len1, const char *s2, size_t len2) /* {{{ */ { size_t len; int c1, c2; if (s1 == s2) { return 0; } len = MIN(len1, len2); while (len--) { c1 = zend_tolower_ascii(*(unsigned char *)s1++); c2 = zend_tolower_ascii(*(unsigned char *)s2++); if (c1 != c2) { return c1 - c2; } } return (int)(len1 - len2); }
感興趣的同學可以到原始碼中檢視。
四、參考文獻
- 《PHP7底層設計與原始碼實現》
- 《PHP7核心剖析》