溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何在PHP7項目中實現(xiàn)一個變量

發(fā)布時間:2021-03-05 16:18:27 來源:億速云 閱讀:133 作者:Leah 欄目:開發(fā)技術(shù)

如何在PHP7項目中實現(xiàn)一個變量?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。

php有什么用

php是一個嵌套的縮寫名稱,是英文超級文本預(yù)處理語言,它的語法混合了C、Java、Perl以及php自創(chuàng)新的語法,主要用來做網(wǎng)站開發(fā),許多小型網(wǎng)站都用php開發(fā),因為php是開源的,從而使得php經(jīng)久不衰。

<?php
$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); // <-- 這里會進行分離

由于大量的細(xì)節(jié)描述,本文將會分成兩個部分:第一部分主要描述 zval(zend value) 的實現(xiàn)在 PHP5 和 PHP7 中有何不同以及引用的實現(xiàn)。第二部分將會分析單獨類型(strings、objects)的細(xì)節(jié)。

PHP5 中的 zval

PHP5 中 zval 結(jié)構(gòu)體定義如下:

typedef struct _zval_struct {
 zvalue_value value;
 zend_uint refcount__gc;
 zend_uchar type;
 zend_uchar is_ref__gc;
} zval;

如上,zval 包含一個 value、一個 type 以及兩個 __gc 后綴的字段。value 是個聯(lián)合體,用于存儲不同類型的值:

typedef union _zvalue_value {
 long lval;     // 用于 bool 類型、整型和資源類型
 double dval;    // 用于浮點類型
 struct {     // 用于字符串
  char *val;
  int len;
 } str;
 HashTable *ht;    // 用于數(shù)組
 zend_object_value obj;  // 用于對象
 zend_ast *ast;    // 用于常量表達式(PHP5.6 才有)
} zvalue_value;

C 語言聯(lián)合體的特征是一次只有一個成員是有效的并且分配的內(nèi)存與需要內(nèi)存最多的成員匹配(也要考慮內(nèi)存對齊)。所有成員都存儲在內(nèi)存的同一個位置,根據(jù)需要存儲不同的值。當(dāng)你需要 lval 的時候,它存儲的是有符號整形,需要 dval 時,會存儲雙精度浮點數(shù)。

需要指出的是是聯(lián)合體中當(dāng)前存儲的數(shù)據(jù)類型會記錄到 type 字段,用一個整型來標(biāo)記:

#define IS_NULL     0      /* Doesn't use value */
#define IS_LONG     1      /* Uses lval */
#define IS_DOUBLE   2      /* Uses dval */
#define IS_BOOL     3      /* Uses lval with values 0 and 1 */
#define IS_ARRAY    4      /* Uses ht */
#define IS_OBJECT   5      /* Uses obj */
#define IS_STRING   6      /* Uses str */
#define IS_RESOURCE 7      /* Uses lval, which is the resource ID */
/* Special types used for late-binding of constants */
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9

PHP5 中的引用計數(shù)

在PHP5中,zval 的內(nèi)存是單獨從堆(heap)中分配的(有少數(shù)例外情況),PHP 需要知道哪些 zval 是正在使用的,哪些是需要釋放的。所以這就需要用到引用計數(shù):zval 中 refcount__gc 的值用于保存 zval 本身被引用的次數(shù),比如 $a = $b = 42 語句中,42 被兩個變量引用,所以它的引用計數(shù)就是 2。如果引用計數(shù)變成 0,就意味著這個變量已經(jīng)沒有用了,內(nèi)存也就可以釋放了。

注意這里提及到的引用計數(shù)指的不是 PHP 代碼中的引用(使用 &),而是變量的使用次數(shù)。后面兩者需要同時出現(xiàn)時會使用『PHP 引用』和『引用』來區(qū)分兩個概念,這里先忽略掉 PHP 的部分。

一個和引用計數(shù)緊密相關(guān)的概念是『寫時復(fù)制』:對于多個引用來說,zaval 只有在沒有變化的情況下才是共享的,一旦其中一個引用改變 zval 的值,就需要復(fù)制("separated")一份 zval,然后修改復(fù)制后的 zval。

下面是一個關(guān)于『寫時復(fù)制』和 zval 的銷毀的例子:

<?php
$a = 42; // $a   -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a; // $a, $b  -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b; // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)

// 下面幾行是關(guān)于 zval 分離的
$a += 1; // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
   // $a  -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
   // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($c); // zval_1 is destroyed, because refcount=0
   // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

引用計數(shù)有個致命的問題:無法檢查并釋放循環(huán)引用(使用的內(nèi)存)。為了解決這問題,PHP 使用了循環(huán)回收的方法。當(dāng)一個 zval 的計數(shù)減一時,就有可能屬于循環(huán)的一部分,這時將 zval 寫入到『根緩沖區(qū)』中。當(dāng)緩沖區(qū)滿時,潛在的循環(huán)會被打上標(biāo)記并進行回收。

因為要支持循環(huán)回收,實際使用的 zval 的結(jié)構(gòu)實際上如下:

typedef struct _zval_gc_info {
 zval z;
 union {
  gc_root_buffer  *buffered;
  struct _zval_gc_info *next;
 } u;
} zval_gc_info;

zval_gc_info 結(jié)構(gòu)體中嵌入了一個正常的 zval 結(jié)構(gòu),同時也增加了兩個指針參數(shù),但是共屬于同一個聯(lián)合體 u,所以實際使用中只有一個指針是有用的。buffered 指針用于存儲 zval 在根緩沖區(qū)的引用地址,所以如果在循環(huán)回收執(zhí)行之前 zval 已經(jīng)被銷毀了,這個字段就可能被移除了。next 在回收銷毀值的時候使用,這里不會深入。

修改動機

下面說說關(guān)于內(nèi)存使用上的情況,這里說的都是指在 64 位的系統(tǒng)上。首先,由于 str 和 obj 占用的大小一樣, zvalue_value 這個聯(lián)合體占用 16 個字節(jié)(bytes)的內(nèi)存。整個 zval 結(jié)構(gòu)體占用的內(nèi)存是 24 個字節(jié)(考慮到內(nèi)存對齊),zval_gc_info 的大小是 32 個字節(jié)。綜上,在堆(相對于棧)分配給 zval 的內(nèi)存需要額外的 16 個字節(jié),所以每個 zval 在不同的地方一共需要用到 48 個字節(jié)(要理解上面的計算方式需要注意每個指針在 64 位的系統(tǒng)上也需要占用 8 個字節(jié))。

在這點上不管從什么方面去考慮都可以認(rèn)為 zval 的這種設(shè)計效率是很低的。比如 zval 在存儲整型的時候本身只需要 8 個字節(jié),即使考慮到需要存一些附加信息以及內(nèi)存對齊,額外 8 個字節(jié)應(yīng)該也是足夠的。

在存儲整型時本來確實需要 16 個字節(jié),但是實際上還有 16 個字節(jié)用于引用計數(shù)、16 個字節(jié)用于循環(huán)回收。所以說 zval 的內(nèi)存分配和釋放都是消耗很大的操作,我們有必要對其進行優(yōu)化。

從這個角度思考:一個整型數(shù)據(jù)真的需要存儲引用計數(shù)、循環(huán)回收的信息并且單獨在堆上分配內(nèi)存嗎?答案是當(dāng)然不,這種處理方式一點都不好。

這里總結(jié)一下 PHP5 中 zval 實現(xiàn)方式存在的主要問題:

zval 總是單獨從堆中分配內(nèi)存;

zval 總是存儲引用計數(shù)和循環(huán)回收的信息,即使是整型這種可能并不需要此類信息的數(shù)據(jù);
在使用對象或者資源時,直接引用會導(dǎo)致兩次計數(shù)(原因會在下一部分講);
某些間接訪問需要一個更好的處理方式。比如現(xiàn)在訪問存儲在變量中的對象間接使用了四個指針(指針鏈的長度為四)。這個問題也放到下一部分討論;
直接計數(shù)也就意味著數(shù)值只能在 zval 之間共享。如果想在 zval 和 hashtable key 之間共享一個字符串就不行(除非 hashtable key 也是 zval)。

PHP7 中的 zval

在 PHP7 中 zval 有了新的實現(xiàn)方式。最基礎(chǔ)的變化就是 zval 需要的內(nèi)存不再是單獨從堆上分配,不再自己存儲引用計數(shù)。復(fù)雜數(shù)據(jù)類型(比如字符串、數(shù)組和對象)的引用計數(shù)由其自身來存儲。這種實現(xiàn)方式有以下好處:

簡單數(shù)據(jù)類型不需要單獨分配內(nèi)存,也不需要計數(shù);
不會再有兩次計數(shù)的情況。在對象中,只有對象自身存儲的計數(shù)是有效的;
由于現(xiàn)在計數(shù)由數(shù)值自身存儲,所以也就可以和非 zval 結(jié)構(gòu)的數(shù)據(jù)共享,比如 zval 和 hashtable key 之間;
間接訪問需要的指針數(shù)減少了。

我們看看現(xiàn)在 zval 結(jié)構(gòu)體的定義(現(xiàn)在在 zend_types.h 文件中):

struct _zval_struct {
 zend_value  value;   /* value */
 union {
  struct {
   ZEND_ENDIAN_LOHI_4(
    zend_uchar type,   /* active type */
    zend_uchar type_flags,
    zend_uchar const_flags,
    zend_uchar reserved)  /* call info for EX(This) */
  } v;
  uint32_t type_info;
 } u1;
 union {
  uint32_t  var_flags;
  uint32_t  next;     /* hash collision chain */
  uint32_t  cache_slot;   /* literal cache slot */
  uint32_t  lineno;    /* line number (for ast nodes) */
  uint32_t  num_args;    /* arguments number for EX(This) */
  uint32_t  fe_pos;    /* foreach position */
  uint32_t  fe_iter_idx;   /* foreach iterator index */
 } u2;
};

結(jié)構(gòu)體的第一個元素沒太大變化,仍然是一個 value 聯(lián)合體。第二個成員是由一個表示類型信息的整型和一個包含四個字符變量的結(jié)構(gòu)體組成的聯(lián)合體(可以忽略 ZEND_ENDIAN_LOHI_4 宏,它只是用來解決跨平臺大小端問題的)。這個子結(jié)構(gòu)中比較重要的部分是 type(和以前類似)和 type_flags,這個接下來會解釋。

上面這個地方也有一點小問題:value 本來應(yīng)該占 8 個字節(jié),但是由于內(nèi)存對齊,哪怕只增加一個字節(jié),實際上也是占用 16 個字節(jié)(使用一個字節(jié)就意味著需要額外的 8 個字節(jié))。但是顯然我們并不需要 8 個字節(jié)來存儲一個 type 字段,所以我們在 u1 的后面增加了了一個名為 u2 的聯(lián)合體。默認(rèn)情況下是用不到的,需要使用的時候可以用來存儲 4 個字節(jié)的數(shù)據(jù)。這個聯(lián)合體可以滿足不同場景下的需求。

PHP7 中 value 的結(jié)構(gòu)定義如下:

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;

首先需要注意的是現(xiàn)在 value 聯(lián)合體需要的內(nèi)存是 8 個字節(jié)而不是 16。它只會直接存儲整型(lval)或者浮點型(dval)數(shù)據(jù),其他情況下都是指針(上面提到過,指針占用 8 個字節(jié),最下面的結(jié)構(gòu)體由兩個 4 字節(jié)的無符號整型組成)。上面所有的指針類型(除了特殊標(biāo)記的)都有一個同樣的頭(zend_refcounted)用來存儲引用計數(shù):

typedef struct _zend_refcounted_h {
 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_refcounted_h;

現(xiàn)在,這個結(jié)構(gòu)體肯定會包含一個存儲引用計數(shù)的字段。除此之外還有 type、flags 和 gc_info。type 存儲的和 zval 中的 type 相同的內(nèi)容,這樣 GC 在不存儲 zval 的情況下單獨使用引用計數(shù)。flags 在不同的數(shù)據(jù)類型中有不同的用途,這個放到下一部分講。

gc_info 和 PHP5 中的 buffered 作用相同,不過不再是位于根緩沖區(qū)的指針,而是一個索引數(shù)字。因為以前根緩沖區(qū)的大小是固定的(10000 個元素),所以使用一個 16 位(2 字節(jié))的數(shù)字代替 64 位(8 字節(jié))的指針足夠了。gc_info 中同樣包含一個『顏色』位用于回收時標(biāo)記結(jié)點。

zval 內(nèi)存管理

上文提到過 zval 需要的內(nèi)存不再單獨從堆上分配。但是顯然總要有地方來存儲它,所以會存在哪里呢?實際上大多時候它還是位于堆中(所以前文中提到的地方重點不是堆,而是單獨分配),只不過是嵌入到其他的數(shù)據(jù)結(jié)構(gòu)中的,比如 hashtable 和 bucket 現(xiàn)在就會直接有一個 zval 字段而不是指針。所以函數(shù)表編譯變量和對象屬性在存儲時會是一個 zval 數(shù)組并得到一整塊內(nèi)存而不是散落在各處的 zval 指針。之前的 zval * 現(xiàn)在都變成了 zval。

之前當(dāng) zval 在一個新的地方使用時會復(fù)制一份 zval * 并增加一次引用計數(shù)?,F(xiàn)在就直接復(fù)制 zval 的值(忽略 u2),某些情況下可能會增加其結(jié)構(gòu)指針指向的引用計數(shù)(如果在進行計數(shù))。

那么 PHP 怎么知道 zval 是否正在計數(shù)呢?不是所有的數(shù)據(jù)類型都能知道,因為有些類型(比如字符串或數(shù)組)并不是總需要進行引用計數(shù)。所以 type_info 字段就是用來記錄 zval 是否在進行計數(shù)的,這個字段的值有以下幾種情況:

#define IS_TYPE_CONSTANT   (1<<0) /* special */
#define IS_TYPE_IMMUTABLE   (1<<1) /* special */
#define IS_TYPE_REFCOUNTED   (1<<2)
#define IS_TYPE_COLLECTABLE   (1<<3)
#define IS_TYPE_COPYABLE   (1<<4)
#define IS_TYPE_SYMBOLTABLE   (1<<5) /* special */

注:在 7.0.0 的正式版本中,上面這一段宏定義的注釋這幾個宏是供 zval.u1.v.type_flags 使用的。這應(yīng)該是注釋的錯誤,因為這個上述字段是 zend_uchar 類型。

type_info 的三個主要的屬性就是『可計數(shù)』(refcounted)、『可回收』(collectable)和『可復(fù)制』(copyable)。計數(shù)的問題上面已經(jīng)提過了。『可回收』用于標(biāo)記 zval 是否參與循環(huán),不如字符串通常是可計數(shù)的,但是你卻沒辦法給字符串制造一個循環(huán)引用的情況。

是否可復(fù)制用于表示在復(fù)制時是否需要在復(fù)制時制造(原文用的 "duplication" 來表述,用中文表達出來可能不是很好理解)一份一模一樣的實體。"duplication" 屬于深度復(fù)制,比如在復(fù)制數(shù)組時,不僅僅是簡單增加數(shù)組的引用計數(shù),而是制造一份全新值一樣的數(shù)組。但是某些類型(比如對象和資源)即使 "duplication" 也只能是增加引用計數(shù),這種就屬于不可復(fù)制的類型。這也和對象和資源現(xiàn)有的語義匹配(現(xiàn)有,PHP7 也是這樣,不單是 PHP5)。

下面的表格上標(biāo)明了不同的類型會使用哪些標(biāo)記(x 標(biāo)記的都是有的特性)?!汉唵晤愋汀唬╯imple types)指的是整型或布爾類型這些不使用指針指向一個結(jié)構(gòu)體的類型。下表中也有『不可變』(immutable)的標(biāo)記,它用來標(biāo)記不可變數(shù)組的,這個在下一部分再詳述。

interned string(保留字符)在這之前沒有提過,其實就是函數(shù)名、變量名等無需計數(shù)、不可重復(fù)的字符串。

                | refcounted | collectable | copyable | immutable
----------------+------------+-------------+----------+----------
simple types    |            |             |          |
string          |      x     |             |     x    |
interned string |            |             |          |
array           |      x     |      x      |     x    |
immutable array |            |             |          |     x
object          |      x     |      x      |          |
resource        |      x     |             |          |
reference       |      x     |             |          |

要理解這一點,我們可以來看幾個例子,這樣可以更好的認(rèn)識 zval 內(nèi)存管理是怎么工作的。

下面是整數(shù)行為模式,在上文中 PHP5 的例子的基礎(chǔ)上進行了一些簡化 :

<?php
$a = 42; // $a = zval_1(type=IS_LONG, value=42)
$b = $a; // $a = zval_1(type=IS_LONG, value=42)
   // $b = zval_2(type=IS_LONG, value=42)
$a += 1; // $a = zval_1(type=IS_LONG, value=43)
   // $b = zval_2(type=IS_LONG, value=42)
unset($a); // $a = zval_1(type=IS_UNDEF)
   // $b = zval_2(type=IS_LONG, value=42)

這個過程其實挺簡單的。現(xiàn)在整數(shù)不再是共享的,變量直接就會分離成兩個單獨的 zval,由于現(xiàn)在 zval 是內(nèi)嵌的所以也不需要單獨分配內(nèi)存,所以這里的注釋中使用 = 來表示的而不是指針符號 ->,unset 時變量會被標(biāo)記為 IS_UNDEF。下面看一下更復(fù)雜的情況:

<?php
$a = []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
$b = $a; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
   // $b = zval_2(type=IS_ARRAY) ---^
// zval 分離在這里進行
$a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
   // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
unset($a); // $a = zval_1(type=IS_UNDEF), zend_array_2 被銷毀
   // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

這種情況下每個變量變量有一個單獨的 zval,但是是指向同一個(有引用計數(shù)) zend_array 的結(jié)構(gòu)體。修改其中一個數(shù)組的值時才會進行復(fù)制。這點和 PHP5 的情況類似。

類型(Types)

我們大概看一下 PHP7 支持哪些類型(zval 使用的類型標(biāo)記):

/* regular data types */
#define IS_UNDEF     0
#define IS_NULL      1
#define IS_FALSE     2
#define IS_TRUE      3
#define IS_LONG      4
#define IS_DOUBLE     5
#define IS_STRING     6
#define IS_ARRAY     7
#define IS_OBJECT     8
#define IS_RESOURCE     9
#define IS_REFERENCE    10
/* constant expressions */
#define IS_CONSTANT     11
#define IS_CONSTANT_AST    12
/* internal types */
#define IS_INDIRECT     15
#define IS_PTR      17

這個列表和 PHP5 使用的類似,不過增加了幾項:

IS_UNDEF 用來標(biāo)記之前為 NULL 的 zval 指針(和 IS_NULL 并不沖突)。比如在上面的例子中使用 unset 注銷變量;
IS_BOOL 現(xiàn)在分割成了 IS_FALSE 和 IS_TRUE 兩項?,F(xiàn)在布爾類型的標(biāo)記是直接記錄到 type 中,這么做可以優(yōu)化類型檢查。不過這個變化對用戶是透明的,還是只有一個『布爾』類型的數(shù)據(jù)(PHP 腳本中)。

PHP 引用不再使用 is_ref 來標(biāo)記,而是使用 IS_REFERENCE 類型。這個也要放到下一部分講;
IS_INDIRECT  和  IS_PTR 是特殊的內(nèi)部標(biāo)記。

實際上上面的列表中應(yīng)該還存在兩個 fake types,這里忽略了。

IS_LONG 類型表示的是一個 zend_long 的值,而不是原生的 C 語言的 long 類型。原因是 Windows 的 64 位系統(tǒng)(LLP64)上的 long 類型只有 32 位的位深度。所以 PHP5 在 Windows 上只能使用 32 位的數(shù)字。PHP7 允許你在 64 位的操作系統(tǒng)上使用 64 位的數(shù)字,即使是在 Windows 上面也可以。

zend_refcounted 的內(nèi)容會在下一部分講。下面看看 PHP 引用的實現(xiàn)。

引用

PHP7 使用了和 PHP5 中完全不同的方法來處理 PHP & 符號引用的問題(這個改動也是 PHP7 開發(fā)過程中大量 bug 的根源)。我們先從 PHP5 中 PHP 引用的實現(xiàn)方式說起。

通常情況下, 寫時復(fù)制原則意味著當(dāng)你修改一個 zval 之前需要對其進行分離來保證始終修改的只是某一個 PHP 變量的值。這就是傳值調(diào)用的含義。

但是使用 PHP 引用時這條規(guī)則就不適用了。如果一個 PHP 變量是 PHP 引用,就意味著你想要在將多個 PHP 變量指向同一個值。PHP5 中的 is_ref 標(biāo)記就是用來注明一個 PHP 變量是不是 PHP 引用,在修改時需不需要進行分離的。比如:

<?php
$a = []; // $a  -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b =& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[])

$b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1])
   // 因為 is_ref 的值是 1, 所以 PHP 不會對 zval 進行分離

但是這個設(shè)計的一個很大的問題在于它無法在一個 PHP 引用變量和 PHP 非引用變量之間共享同一個值。比如下面這種情況:

<?php
$a = []; // $a   -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b = $a; // $a, $b  -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$c = $b // $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[])
$d =& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
   // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
   // $d 是 $c 的引用, 但卻不是 $a 的 $b, 所以這里 zval 還是需要進行復(fù)制
   // 這樣我們就有了兩個 zval, 一個 is_ref 的值是 0, 一個 is_ref 的值是 1.
$d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
   // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
   // 因為有兩個分離了的 zval, $d[] = 1 的語句就不會修改 $a 和 $b 的值.

這種行為方式也導(dǎo)致在 PHP 中使用引用比普通的值要慢。比如下面這個例子:

<?php
$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); // <-- 這里會進行分離

因為 count() 只接受傳值調(diào)用,但是 $array 是一個 PHP 引用,所以 count() 在執(zhí)行之前實際上會有一個對數(shù)組進行完整的復(fù)制的過程。如果 $array 不是引用,這種情況就不會發(fā)生了。

現(xiàn)在我們來看看 PHP7 中 PHP 引用的實現(xiàn)。因為 zval 不再單獨分配內(nèi)存,也就沒辦法再使用和 PHP5 中相同的實現(xiàn)了。所以增加了一個 IS_REFERENCE 類型,并且專門使用 zend_reference 來存儲引用值:

struct _zend_reference {
 zend_refcounted gc;
 zval    val;
};

本質(zhì)上 zend_reference 只是增加了引用計數(shù)的 zval。所有引用變量都會存儲一個 zval 指針并且被標(biāo)記為 IS_REFERENCE。val 和其他的 zval 的行為一樣,尤其是它也可以在共享其所存儲的復(fù)雜變量的指針,比如數(shù)組可以在引用變量和值變量之間共享。

我們還是看例子,這次是 PHP7 中的語義。為了簡潔明了這里不再單獨寫出 zval,只展示它們指向的結(jié)構(gòu)體:

<?php
$a = []; // $a          -> zend_array_1(refcount=1, value=[])
$b =& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])
$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1])

上面的例子中進行引用傳遞時會創(chuàng)建一個 zend_reference,注意它的引用計數(shù)是 2(因為有兩個變量在使用這個 PHP 引用)。但是值本身的引用計數(shù)是 1(因為 zend_reference 只是有一個指針指向它)。下面看看引用和非引用混合的情況:

<?php
$a = []; // $a   -> zend_array_1(refcount=1, value=[])
$b = $a; // $a, $b, -> zend_array_1(refcount=2, value=[])
$c = $b // $a, $b, $c -> zend_array_1(refcount=3, value=[])
$d =& $c; // $a, $b         -> zend_array_1(refcount=3, value=[])
   // $c, $d -> zend_reference_1(refcount=2) ---^
   // 注意所有變量共享同一個 zend_array, 即使有的是 PHP 引用有的不是
$d[] = 1; // $a, $b         -> zend_array_1(refcount=2, value=[])
   // $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1])
   // 只有在這時進行賦值的時候才會對 zend_array 進行賦值

這里和 PHP5 最大的不同就是所有的變量都可以共享同一個數(shù)組,即使有的是 PHP 引用有的不是。只有當(dāng)其中某一部分被修改的時候才會對數(shù)組進行分離。這也意味著使用 count() 時即使給其傳遞一個很大的引用數(shù)組也是安全的,不會再進行復(fù)制。不過引用仍然會比普通的數(shù)值慢,因為存在需要為 zend_reference 結(jié)構(gòu)體分配內(nèi)存(間接)并且引擎本身處理這一塊兒也不快的的原因。

看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI