## 3.5 運行時緩存
在本節開始之前我們先分析一個例子:
```php
class my_class {
public $id = 123;
public function test() {
echo $this->id;
}
}
$obj = new my_class;
$obj->test();
$obj->test();
...
```
這個例子定義了一個類,然后多次調用同一個成員方法,這個成員方法功能很簡單:輸出一個成員屬性,根據前面對成員屬性的介紹可以知道其查找過程為:"首先根據對象找到所屬zend_class_entry,然后再根據屬性名查找`zend_class_entry.properties_info`哈希表,得到`zend_property_info`,最后根據屬性結構的offset定位到屬性值的存儲位置",概括一下這個過程就是:zend_object->zend_class_entry->properties_info->屬性值,那么問題來了:每次執行`my_class::test()`時難道上面的過程都要完整走一遍嗎?
我們再仔細看下這個過程,字面量"id"在"$this->id"此條語句中就是用來索引屬性的,不管執行多少次它的任務始終是這個,那么有沒有一種辦法將"id"與查找到的zend_class_entry、zend_property_info.offset建立一種關聯關系保存下來,這樣再次執行時直接根據"id"拿到前面關聯的這兩個數據,從而避免多次重復相同的工作呢?這就是本節將要介紹的內容:運行時緩存。
在執行期間,PHP經常需要根據名稱去不同的哈希表中查找常量、函數、類、成員方法、成員屬性等,因此PHP提供了一種緩存機制用于緩存根據名稱查找到的結果,以便再次執行同一opcode時直接復用上次緩存的值,無需重復查找,從而提高執行效率。
開始提到的那個例子中會緩存兩個東西:zend_class_entry、zend_property_info.offset,此緩存可以認為是opcode操作的緩存,它只屬于"$this->id"此語句的opcode:這樣再次執行這條opcode時就直接取出上次緩存的兩個值。
所以運行時緩存機制是在同一opcode執行多次的情況下才會生效,特別注意這里的同一opcode指的并不是opcode值相同,而是指內存里的同一份數據,比如:`echo $a; echo $a;`這種就不算,因為這是兩條opcode。
那么緩存是如何保存和索引的呢?執行opcode時如何知道緩存的位置?
實際上運行時緩存是基于所屬opcode中CONST操作數存儲的,也就是說只有包含IS_CONST類型的操作數才有可能用到此機制,其它類型都不會用到,這是因為只有CONST操作數是固定不變的,其它CV、VAR等類型值都不是固定的,既然其值是不固定的那么緩存的值也就不是固定的,所以不會針對CONST以外類型的opcode操作進行緩存,還是以開始那個例子為例,比如:`echo $this->$var;`這種,操作數類型是CV,其正常查找時的zend_property_info是隨$var值而變的,所以給他們建立一種不可變的關聯關系,而:`echo $this->id;`中"id"是固定寫死的,它索引到zend_property_info始終是不變的。
緩存的存儲格式是一個數組,用于保存緩存的數據指針,而指針在數組中的起始存儲位置則保存在CONST操作數對應的`zval.u2.cache_slot`中(前面講過,CONST操作數對應值的zval保存在zend_op_array->literals數組中)。上面那個例子對應的緩存結構:

* __(1)__ 第一次執行`echo $this->id;`時首先根據$this取出zend_class_entry,然后根據“id”查找zend_class_entry.properties_info找到屬性zend_property_info,取出此結構的offset,第一次執行后將zend_class_entry及offset保存到了test()函數的zend_op_array->run_time_cache中,占用16字節,起始位置為0,這個值記錄在“id”的zval.u2.cache_slot中;
* __(2)__ 之后再次執行`echo $this->id;`時直接根據opline從zend_op_literals中取出“id”的zval,得到緩存數據保存位置:0,然后去zend_op_array->run_time_cache取出緩存的zend_class_entry、offset。
這個例子緩存數據占用了16字節(2個sizeof(void*))大小的空間,而有的只需要8字節,取決于操作類型:
* 8字節:常量、函數、類
* 16字節:成員屬性、成員方法、類常量
另外一個問題是這些操作數的緩存位置(zval.u2.cache_slot)是在什么階段確定的呢?實際上這個值是在編譯階段確定的,通過zend_op_array.cache_size記錄緩存可用起始位置,編譯過程中如果發現當前操作適用緩存機制,則根據緩存數據的大小從cache_size開始分配8或16字節給那個操作數,cache_size向后移動對應大小,然后將起始位置保存于CONST操作數的zval.u2.cache_slot中,執行時直接根據這個值確定緩存位置。
具體緩存的讀寫通過以下幾個宏完成:
```c
//設置緩存
CACHE_PTR(Z_CACHE_SLOT_P(EX_CONSTANT(opline->op1/2)), ptr); //ptr: 緩存的數據指針
//讀取緩存
CACHED_PTR(Z_CACHE_SLOT_P(EX_CONSTANT(opline->op1/2)));
//EX_CONSTANT(opline->op1/2)是取當前IS_CONST操作數對應數據的zval
```
展開后:
```c
((void**)((char*)execute_data->run_time_cache + (num)))[0]
```
`execute_data->run_time_cache`緩存的`zend_op_array->run_time_cache`。
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述