變數在 PHP7 內部的實現(一)

Scholer's Blog發表於2015-12-24

本文第一部分和第二均翻譯自Nikita Popov(nikic,PHP 官方開發組成員,柏林科技大學的學生) 的部落格。為了更符合漢語的閱讀習慣,文中並不會逐字逐句的翻譯。

要理解本文,你應該對 PHP5 中變數的實現有了一些瞭解,本文重點在於解釋 PHP7 中 zval 的變化。

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

PHP5 中的 zval

PHP5 中 zval 結構體定義如下:

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 是個聯合體,用於儲存不同型別的值:

typedef union _zvalue_value {
    long lval;                 // 用於 bool 型別、整型和資源型別
    double dval;               // 用於浮點型別
    struct {                   // 用於字串
        char *val;
        int len;
    } str;
    HashTable *ht;             // 用於陣列
    zend_object_value obj;     // 用於物件
    zend_ast *ast;             // 用於常量表示式(PHP5.6 才有)
} zvalue_value;

C 語言聯合體的特徵是一次只有一個成員是有效的並且分配的記憶體與需要記憶體最多的成員匹配(也要考慮記憶體對齊)。所有成員都儲存在記憶體的同一個位置,根據需要儲存不同的值。當你需要 lval 的時候,它儲存的是有符號整形,需要 dval 時,會儲存雙精度浮點數

需要指出的是是聯合體中當前儲存的資料型別會記錄到 type 欄位,用一個整型來標記:

#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 中的引用計數

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

注意這裡提及到的引用計數指的不是 PHP 程式碼中的引用(使用 &),而是變數的使用次數。後面兩者需要同時出現時會使用『PHP 引用』和『引用』來區分兩個概念,這裡先忽略掉 PHP 的部分。

一個和引用計數緊密相關的概念是『寫時複製』:對於多個引用來說,zaval 只有在沒有變化的情況下才是共享的,一旦其中一個引用改變 zval 的值,就需要複製(”separated”)一份 zval,然後修改複製後的 zval。

下面是一個關於『寫時複製』和 zval 的銷燬的例子:

$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)

// 下面幾行是關於 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)

引用計數有個致命的問題:無法檢查並釋放迴圈引用(使用的記憶體)。為了解決這問題,PHP 使用了迴圈回收的方法。當一個 zval 的計數減一時,就有可能屬於迴圈的一部分,這時將 zval 寫入到『根緩衝區』中。當緩衝區滿時,潛在的迴圈會被打上標記並進行回收。

因為要支援迴圈回收,實際使用的 zval 的結構實際上如下:

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

zval_gc_info 結構體中嵌入了一個正常的 zval 結構,同時也增加了兩個指標引數,但是共屬於同一個聯合體 u,所以實際使用中只有一個指標是有用的。buffered 指標用於儲存 zval 在根緩衝區的引用地址,所以如果在迴圈回收執行之前 zval 已經被銷燬了,這個欄位就可能被移除了。next 在回收銷燬值的時候使用,這裡不會深入。

修改動機

下面說說關於記憶體使用上的情況,這裡說的都是指在 64 位的系統上。首先,由於 strobj 佔用的大小一樣, zvalue_value 這個聯合體佔用 16 個位元組(bytes)的記憶體。整個 zval 結構體佔用的記憶體是 24 個位元組(考慮到記憶體對齊),zval_gc_info 的大小是 32 個位元組。綜上,在堆(相對於棧)分配給 zval 的記憶體需要額外的 16 個位元組,所以每個 zval 在不同的地方一共需要用到 48 個位元組(要理解上面的計算方式需要注意每個指標在 64 位的系統上也需要佔用 8 個位元組)。

在這點上不管從什麼方面去考慮都可以認為 zval 的這種設計效率是很低的。比如 zval 在儲存整型的時候本身只需要 8 個位元組,即使考慮到需要存一些附加資訊以及記憶體對齊,額外 8 個位元組應該也是足夠的。

在儲存整型時本來確實需要 16 個位元組,但是實際上還有 16 個位元組用於引用計數、16 個位元組用於迴圈回收。所以說 zval 的記憶體分配和釋放都是消耗很大的操作,我們有必要對其進行優化。

從這個角度思考:一個整型資料真的需要儲存引用計數、迴圈回收的資訊並且單獨在堆上分配記憶體嗎?答案是當然不,這種處理方式一點都不好。

這裡總結一下 PHP5 中 zval 實現方式存在的主要問題:

  • zval 總是單獨從堆中分配記憶體;
  • zval 總是儲存引用計數和迴圈回收的資訊,即使是整型這種可能並不需要此類資訊的資料;
  • 在使用物件或者資源時,直接引用會導致兩次計數(原因會在下一部分講);
  • 某些間接訪問需要一個更好的處理方式。比如現在訪問儲存在變數中的物件間接使用了四個指標(指標鏈的長度為四)。這個問題也放到下一部分討論;
  • 直接計數也就意味著數值只能在 zval 之間共享。如果想在 zval 和 hashtable key 之間共享一個字串就不行(除非 hashtable key 也是 zval)。

PHP7 中的 zval

在 PHP7 中 zval 有了新的實現方式。最基礎的變化就是 zval 需要的記憶體不再是單獨從堆上分配,不再自己儲存引用計數。複雜資料型別(比如字串、陣列和物件)的引用計數由其自身來儲存。這種實現方式有以下好處:

  • 簡單資料型別不需要單獨分配記憶體,也不需要計數;
  • 不會再有兩次計數的情況。在物件中,只有物件自身儲存的計數是有效的;
  • 由於現在計數由數值自身儲存,所以也就可以和非 zval 結構的資料共享,比如 zval 和 hashtable key 之間;
  • 間接訪問需要的指標數減少了。

我們看看現在 zval 結構體的定義(現在在 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;
};

結構體的第一個元素沒太大變化,仍然是一個 value 聯合體。第二個成員是由一個表示型別資訊的整型和一個包含四個字元變數的結構體組成的聯合體(可以忽略 ZEND_ENDIAN_LOHI_4 巨集,它只是用來解決跨平臺大小端問題的)。這個子結構中比較重要的部分是 type(和以前類似)和 type_flags,這個接下來會解釋。

上面這個地方也有一點小問題:value 本來應該佔 8 個位元組,但是由於記憶體對齊,哪怕只增加一個位元組,實際上也是佔用 16 個位元組(使用一個位元組就意味著需要額外的 8 個位元組)。但是顯然我們並不需要 8 個位元組來儲存一個 type 欄位,所以我們在 u1 的後面增加了了一個名為 u2 的聯合體。預設情況下是用不到的,需要使用的時候可以用來儲存 4 個位元組的資料。這個聯合體可以滿足不同場景下的需求。

PHP7 中 value 的結構定義如下:

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;

首先需要注意的是現在 value 聯合體需要的記憶體是 8 個位元組而不是 16。它只會直接儲存整型(lval)或者浮點型(dval)資料,其他情況下都是指標(上面提到過,指標佔用 8 個位元組,最下面的結構體由兩個 4 位元組的無符號整型組成)。上面所有的指標型別(除了特殊標記的)都有一個同樣的頭(zend_refcounted)用來儲存引用計數:

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;

現在,這個結構體肯定會包含一個儲存引用計數的欄位。除此之外還有 typeflagsgc_infotype 儲存的和 zval 中的 type 相同的內容,這樣 GC 在不儲存 zval 的情況下單獨使用引用計數。flags 在不同的資料型別中有不同的用途,這個放到下一部分講。

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

zval 記憶體管理

上文提到過 zval 需要的記憶體不再單獨從堆上分配。但是顯然總要有地方來儲存它,所以會存在哪裡呢?實際上大多時候它還是位於堆中(所以前文中提到的地方重點不是,而是單獨分配),只不過是嵌入到其他的資料結構中的,比如 hashtable 和 bucket 現在就會直接有一個 zval 欄位而不是指標。所以函式表編譯變數和物件屬性在儲存時會是一個 zval 陣列並得到一整塊記憶體而不是散落在各處的 zval 指標。之前的 zval * 現在都變成了 zval

之前當 zval 在一個新的地方使用時會複製一份 zval * 並增加一次引用計數。現在就直接複製 zval 的值(忽略 u2),某些情況下可能會增加其結構指標指向的引用計數(如果在進行計數)。

那麼 PHP 怎麼知道 zval 是否正在計數呢?不是所有的資料型別都能知道,因為有些型別(比如字串或陣列)並不是總需要進行引用計數。所以 type_info 欄位就是用來記錄 zval 是否在進行計數的,這個欄位的值有以下幾種情況:

#define IS_TYPE_CONSTANT            (1/* special */
#define IS_TYPE_IMMUTABLE           (1/* special */
#define IS_TYPE_REFCOUNTED          (1
#define IS_TYPE_COLLECTABLE         (1
#define IS_TYPE_COPYABLE            (1
#define IS_TYPE_SYMBOLTABLE         (1/* special */

注:在 7.0.0 的正式版本中,上面這一段巨集定義的註釋這幾個巨集是供 zval.u1.v.type_flags 使用的。這應該是註釋的錯誤,因為這個上述欄位是 zend_uchar 型別。

type_info 的三個主要的屬性就是『可計數』(refcounted)、『可回收』(collectable)和『可複製』(copyable)。計數的問題上面已經提過了。『可回收』用於標記 zval 是否參與迴圈,不如字串通常是可計數的,但是你卻沒辦法給字串製造一個迴圈引用的情況。

是否可複製用於表示在複製時是否需要在複製時製造(原文用的 “duplication” 來表述,用中文表達出來可能不是很好理解)一份一模一樣的實體。”duplication” 屬於深度複製,比如在複製陣列時,不僅僅是簡單增加陣列的引用計數,而是製造一份全新值一樣的陣列。但是某些型別(比如物件和資源)即使 “duplication” 也只能是增加引用計數,這種就屬於不可複製的型別。這也和物件和資源現有的語義匹配(現有,PHP7 也是這樣,不單是 PHP5)。

下面的表格上標明瞭不同的型別會使用哪些標記(x 標記的都是有的特性)。『簡單型別』(simple types)指的是整型或布林型別這些不使用指標指向一個結構體的型別。下表中也有『不可變』(immutable)的標記,它用來標記不可變陣列的,這個在下一部分再詳述。

interned string(保留字元)在這之前沒有提過,其實就是函式名、變數名等無需計數、不可重複的字串。

                | 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     |             |          |

要理解這一點,我們可以來看幾個例子,這樣可以更好的認識 zval 記憶體管理是怎麼工作的。

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

$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)

這個過程其實挺簡單的。現在整數不再是共享的,變數直接就會分離成兩個單獨的 zval,由於現在 zval 是內嵌的所以也不需要單獨分配記憶體,所以這裡的註釋中使用 = 來表示的而不是指標符號 ->,unset 時變數會被標記為 IS_UNDEF。下面看一下更復雜的情況:

$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,但是是指向同一個(有引用計數) zend_array 的結構體。修改其中一個陣列的值時才會進行復制。這點和 PHP5 的情況類似。

型別(Types)

我們大概看一下 PHP7 支援哪些型別(zval 使用的型別標記):

/* 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 用來標記之前為 NULL 的 zval 指標(和 IS_NULL 並不衝突)。比如在上面的例子中使用 unset 登出變數;
  • IS_BOOL 現在分割成了 IS_FALSEIS_TRUE 兩項。現在布林型別的標記是直接記錄到 type 中,這麼做可以優化型別檢查。不過這個變化對使用者是透明的,還是隻有一個『布林』型別的資料(PHP 指令碼中)。
  • PHP 引用不再使用 is_ref 來標記,而是使用 IS_REFERENCE 型別。這個也要放到下一部分講;
  • IS_INDIRECTIS_PTR 是特殊的內部標記。

實際上上面的列表中應該還存在兩個 fake types,這裡忽略了。

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

zend_refcounted 的內容會在下一部分講。下面看看 PHP 引用的實現。

引用

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

通常情況下, 寫時複製原則意味著當你修改一個 zval 之前需要對其進行分離來保證始終修改的只是某一個 PHP 變數的值。這就是傳值呼叫的含義。

但是使用 PHP 引用時這條規則就不適用了。如果一個 PHP 變數是 PHP 引用,就意味著你想要在將多個 PHP 變數指向同一個值。PHP5 中的 is_ref 標記就是用來註明一個 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 進行分離

但是這個設計的一個很大的問題在於它無法在一個 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 還是需要進行復制
          // 這樣我們就有了兩個 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 的值.

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

$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); //

因為 count() 只接受傳值呼叫,但是 $array 是一個 PHP 引用,所以 count() 在執行之前實際上會有一個對陣列進行完整的複製的過程。如果 $array 不是引用,這種情況就不會發生了。

現在我們來看看 PHP7 中 PHP 引用的實現。因為 zval 不再單獨分配記憶體,也就沒辦法再使用和 PHP5 中相同的實現了。所以增加了一個 IS_REFERENCE 型別,並且專門使用 zend_reference 來儲存引用值:

struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};

本質上 zend_reference 只是增加了引用計數的 zval。所有引用變數都會儲存一個 zval 指標並且被標記為 IS_REFERENCEval 和其他的 zval 的行為一樣,尤其是它也可以在共享其所儲存的複雜變數的指標,比如陣列可以在引用變數和值變數之間共享。

我們還是看例子,這次是 PHP7 中的語義。為了簡潔明瞭這裡不再單獨寫出 zval,只展示它們指向的結構體:

$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])

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

$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 最大的不同就是所有的變數都可以共享同一個陣列,即使有的是 PHP 引用有的不是。只有當其中某一部分被修改的時候才會對陣列進行分離。這也意味著使用 count() 時即使給其傳遞一個很大的引用陣列也是安全的,不會再進行復制。不過引用仍然會比普通的數值慢,因為存在需要為 zend_reference 結構體分配記憶體(間接)並且引擎本身處理這一塊兒也不快的的原因。

結語

總結一下 PHP7 中最重要的改變就是 zval 不再單獨從堆上分配記憶體並且不自己儲存引用計數。需要使用 zval 指標的複雜型別(比如字串、陣列和物件)會自己儲存引用計數。這樣就可以有更少的記憶體分配操作、更少的間接指標使用以及更少的記憶體分配。

文章的第二部分我們會討論複雜型別的問題。

相關文章