編程思想雖然可以共用,不過語言間的差異還是比較明顯的,只是使用者之間沒有意識到而己,而了解其中的差異對於編寫程序以及把握性能還是有好處的。下面我們來介紹下PHP的一個很重要的機制copy on write,我們先以最簡單的變量來介紹這個機制,在說這個之前,筆者先來介紹下弱類型是怎么實現的。
大家都知道,PHP是由C實現的,可是C是強類型語言,PHP怎么做到弱類型語言。一起來看下,PHP變量在C語言底層中的代碼,
typedef struct _zval_struct zval; typedef unsigned int zend_uint; typedef unsigned char zend_uchar; struct _zval_struct { zvalue_value value; /*注意這里,這個里面存的才是變量的值*/ zend_uint refcount__gc; /*引用計數*/ zend_uchar type; /* 變量當前的數據類型 */ zend_uchar is_ref__gc; /*變量是否引用*/ }; typedef union _zvalue_value { long lval; /*PHP中整型的值*/ double dval; /*PHP的浮點數值*/ struct { char *val; int len; } str; /*PHP的字符串*/ HashTable *ht; /*數組*/ zend_object_value obj; /*對象*/ } zvalue_value;
本人加了點注釋,大家可以發現,實際上我們在PHP用的變量,低層是一個結構體zval,里面的zvalue_value結構體實際上是個聯合體,這個聯合體才是實際存放着PHP的變量值,下面我們以實際的PHP代碼例子來表示整個工作過程,注意上面的引用計數。先來看C語言的,首先是非函數部分,函數部分下一章節來講
int i = 4; //alloca方式在內存中分配空間,這個變量在內存中的棧區 int j = i; //alloca方式在內存中分配空間,並且將原先內存空間里面的數據復制到新的內存空間中,這個變量在內存的棧區 int j = 5; //不分配內存空間,對變量j所在的棧區空間的數據進行修改
來看PHP部分的
$i = 4; //內核創建一個zval指針,並且為其以堆的方式開辟空間,讓指針指向這個空間,將zval中的成員引用計數置為1,類型標記為整形,並且申請一個zvalue_value指針,同樣以堆的方式以其開辟空間,同時將該聯合體中的lval賦值為4,並且在symbal_table的hash表中記錄變量i和zval指針的映射關系 $j = $i; //沒有在申請內存空間,在zval的成員中引用計數標記為2 $j = 5; //內核重新創建zval指針,重復下上面的步驟,我就不重復說明了,重點是將舊的zval引用計數標記為1
從這個地方發現幾個重要點
1.所有的php變量開辟的內存空間都是在堆中,無論是臨時變量還是全局變量,只是php的臨時變量記錄在active_symbal_table表中,全局變量記錄中symbal_table表中
2.php干嘛比C慢。多做了這么多事,能不慢嗎?
3.當php類似$j = $i這種變量賦值時,是沒有內存開銷的,也就是你賦值個幾萬個,也只是引用計數變成幾萬而己,這個和C語言是不一樣的。而當變量的值發生變化時,才會進行重新開辟內存空間,這個機制我們稱為寫時復制機制
額外細節部分,當php內核發現,int的數值溢出時,也就是超出整型的范圍時,自動轉換為float,有興趣的讀者可以自己寫個很大的整型,但是不能超出float取值范圍,看看var_dump數據類型是什么。
最后部分:
php對象部分因為默認是引用方式的,所以就是賦值完,再改變對象的成員變量,也不會啟用寫時復制的,如以下
class Test { public $var = 999; } $test1 = new Test(); $test2 = $test1; //只是引用計數加1而己,沒有開辟新的內存空間 $test2->var = 1000; echo $test1->var; //此時的值也為1000 $test3 = clone $test1; //這個才是正在重新開辟新的內存空間