一。前言 HashTable是PHP的靈魂,因為在Zend引擎中 大量的使用了HashTable,如變數表,常量表,函數表等,這些都是 適應HashTable儲存的,另外,PHP的數組也是通過使用HashTble實現的,所以, 瞭解PHP的HashTable才能真正瞭解PHP 。
為了方便閱讀,這裡列舉一下HashTable實現中出現的基本概念。 雜湊表是一種通過雜湊函數,將特定的鍵映射到特定值的一種資料結構,它維護鍵和值之間一一對應關係。 鍵(key):用於操作資料的標示,例如PHP數組中的索引,或者字串鍵等等。 槽(slot/bucket):雜湊表中用於儲存資料的一個單元,也就是資料真正存放的容器。 雜湊函數(hash function):將key映射(map)到資料應該存放的slot所在位置的函數。 雜湊衝突(hash collision):雜湊函數將兩個不同的key映射到同一個索引的情況。 PHP中的雜湊表實現在Zend/zend_hash.h中,先看看PHP實現中的資料結構, PHP使用如下兩個資料結構來實現雜湊表, HashTable結構體用於儲存整個雜湊表需要的基本資料, 而Bucket結構體用於儲存具體的資料內容,(具體源碼見最後)
二。舉例 那麼,以建立一個變數為例,在底層到底發生了什麼呢。
建立變數的步驟: $str = "hello"; 1:建立zval結構,並設定其類型 IS_STRING 2:設定其值為 hello 3:將其加入符號表
{ zval *fooval; MAKE_STD_ZVAL(fooval); ZVAL_STRING(fooval, "hello", 1); ZEND_SET_SYMBOL( EG(active_symbol_table) , "foo" , fooval);} 前兩步在上一篇的變數結構中有提到過, 詳見 PHP核心的儲存機制(分離/改變)
符號表是什麼? 答:符號表是一張雜湊表,裡面儲存了變數名->變數的zval結構體的地址 // zend/zend_globals.h 161行 符號表
struct _zend_executor_globals { ... ...HashTable *active_symbol_table; /*活動符號表*/HashTable symbol_table;/* 全域符號表 */HashTable included_files;/* files already included */ ...} 當執行到函數時,會產生函數的"執行環境結構體",包含函數名,參數,執行步驟,所在的類(如果是方法),以及為這個函數產生一個符號表.符號表統一放在棧上.並把active_symbol_table指向剛產生的符號表
Zend/zend_compiles.h 384行,執行環境結構體:
struct _zend_execute_data {struct _zend_op *opline;zend_function_state function_state;zend_op_array *op_array;//函數編譯後的執行邏輯,編譯後的opcode二進位代碼,稱為op_arrayzval *object;HashTable *symbol_table;//此函數的符號表地址struct _zend_execute_data *prev_execute_data;zval *old_error_reporting;zend_bool nested;zval **original_return_value;zend_class_entry *current_scope;zend_class_entry *current_called_scope;zval *current_this; struct _zend_op *fast_ret; /* used by FAST_CALL/FAST_RET (finally keyword) */call_slot *call_slots;call_slot *call;}; 上面這個,是當前函數執行時的符號表。
通過下邊例子,來描述下函數在執行中,PHP對各個儲存空間的分配,以及解釋了為什麼PHP的靜態變數可以共用。 當執行到函數時,會產生函數的"執行環境結構體",包含函數名,參數,執行步驟,所在的類(如果是方法),以及為這個函數產生一個符號表.符號表統一放在棧上.並把active_symbol_table指向剛產生的符號表
解釋: 1.執行t1時,形成t1的環境結構體,t1調入到執行棧,t1也有自己的符號表,符號表裡邊儲存的變數對應這個t1環境(局部變數嘛) 2.執行t1到第三行,執行了t2,形成t2的環境結構體,t2入棧,t2也有自己的變數自己的符號表,與t1互不影響。 3.假使t1函數內部出現了遞迴調用t1,此時會產生第二個t1環境結構體,和【1】中是兩個結構體,互不影響
函數執行時的棧變化 當函數調用時,為此函數產生了一個”執行環境變數”的結構體,裡面儲存了當前函數的名稱,參數,對應的類....等等資訊.稱為_zend_execute_data {}結構體
struct _zend_execute_data {struct _zend_op *opline;zend_function_state function_state;zend_op_array *op_array;//函數編譯後的執行邏輯,編譯後的opcode二進位代碼,稱為op_arrayzval *object;HashTable *symbol_table;//此函數的符號表地址struct _zend_execute_data *prev_execute_data;zval *old_error_reporting;zend_bool nested;zval **original_return_value;zend_class_entry *current_scope;zend_class_entry *current_called_scope;zval *current_this; struct _zend_op *fast_ret; /* used by FAST_CALL/FAST_RET (finally keyword) */call_slot *call_slots;call_slot *call;}; 這個結構體中,有2個重要的資訊需要注意。: { *op_array ------>是函數的執行步驟,公用(靜態變數欄位儲存於此。所以改一次依賴於此邏輯的函數全修改。) *hash_table---->symbol_table 這個函數對應的符號表 } 思考一下: 1個函數,遞迴調用自己3次, 如t1 問:在棧上,肯定要有3個 execute_data產生.但是,這3個execute_data--->對應幾個*op_array; 答:函數編譯完了,產生一份*op_array,因為函數的執行邏輯是固定的. 問:產生了幾個 symbol_table? 答:產生3個符號表.
結論: 1. 每一個函數調用是都會產生自己的環境棧和符號表棧,不同的環境棧對應了自己的符號表棧,所以每個函數中的變數常量等,他們是有對應函數內的範圍限制 2. 雖然每次會產生不同的環境棧與範圍,但是如果調用的是同一個函數,其 *op_array;是公用1份的,換句話說,t1遞迴調用自己,每次都會開闢一個環境棧區分獨立,但是他們是同一個函數邏輯,所以op_array是一樣的,而
三。其他 通過一個雜湊演算法,它總有碰撞的時候吧。PHP中的雜湊表是使用拉鏈法來解決衝突 (具體點講就是使用鏈表來儲存雜湊到同一個槽位的資料,Zend為了儲存資料之間的關係使用了雙向鏈表來連結元素)。 對於HashTable的初始化_zend_hash_init, 插入_zend_hash_add_or_update, 元素訪問_zend_hash_add_or_find等操作,源碼中有就不再這裡敘述。
這樣回頭一想,變數表,常量表,函數表等,他們在PHP中都是靠HashTable來實現的,如[二]中敘述,hashtable是不是很強大呢。
Zend引擎雜湊表結構和關係:
Zend/zend_hash.h 55行
typedef struct bucket {ulong h;/* Used for numeric indexing */uint nKeyLength;void *pData;void *pDataPtr;struct bucket *pListNext;struct bucket *pListLast;struct bucket *pNext;struct bucket *pLast;const char *arKey;} Bucket;typedef struct _hashtable { uint nTableSize; // hash Bucket的大小,最小為8,以2x增長。 uint nTableMask; // nTableSize-1 , 索引取值的最佳化 uint nNumOfElements; // hash Bucket中當前存在的元素個數,count()函數會直接返回此值 ulong nNextFreeElement; // 下一個數字索引的位置 Bucket *pInternalPointer; // 當前遍曆的指標(foreach比for快的原因之一) Bucket *pListHead; // 儲存數組頭元素指標 Bucket *pListTail; // 儲存數組尾元素指標 Bucket **arBuckets; // 儲存hash數組 dtor_func_t pDestructor; // 在刪除元素時執行的回呼函數,用於資源的釋放 zend_bool persistent; //指出了Bucket記憶體配置的方式。如果persisient為TRUE,則使用作業系統本身的記憶體配置函數為Bucket分配記憶體,否則使用PHP的記憶體配置函數。 unsigned char nApplyCount; // 標記當前hash Bucket被遞迴訪問的次數(防止多次遞迴) zend_bool bApplyProtection;// 標記當前hash桶允許不允許多次訪問,不允許時,最多隻能遞迴3次#if ZEND_DEBUG int inconsistent;#endif} HashTable;
Zend/zend_compiles.h 261行,op_array結構代碼
struct _zend_op_array {/* Common elements */zend_uchar type;const char *function_name;zend_class_entry *scope;zend_uint fn_flags;union _zend_function *prototype;zend_uint num_args;zend_uint required_num_args;zend_arg_info *arg_info;/* END of common elements */zend_uint *refcount;zend_op *opcodes;zend_uint last;zend_compiled_variable *vars;int last_var;zend_uint T;zend_uint nested_calls;zend_uint used_stack;zend_brk_cont_element *brk_cont_array;int last_brk_cont;zend_try_catch_element *try_catch_array;int last_try_catch;zend_bool has_finally_block;/* static variables support */HashTable *static_variables;zend_uint this_var;const char *filename;zend_uint line_start;zend_uint line_end;const char *doc_comment;zend_uint doc_comment_len;zend_uint early_binding; /* the linked list of delayed declarations */zend_literal *literals;int last_literal;void **run_time_cache;int last_cache_slot;void *reserved[ZEND_MAX_RESERVED_RESOURCES];};