PHP數組和值到底有多大
這篇文章是關於PHP 5的記憶體使用情況。對於本文所述的情況,PHP 7的記憶體使用量大約低3倍。
在這篇文章中,我想以下面的腳本為例來研究PHP數組(以及一般的值)的記憶體使用情況,該腳本創建了100000個惟一的整數數組元素,並測量了結果的內存使用情況:
$startMemory = memory_get_usage(); $array = range(1, 100000); echo memory_get_usage() - $startMemory, ' bytes';
你希望它是多少?簡單來說,一個整數是8字節(在64位unix機器上使用long類型),您得到100,000個整數,因此顯然需要800000位元組。
現在嘗試運行上面的程式碼。這就得到了14649024位元組。是的,你沒聽錯,是13.97 MB,比我們估計的多18倍。
那麼,18的額外因數是怎麼來的呢?
總結
#對於那些不想知道整個故事的人,這裡有一個涉及到的不同組件的記憶體使用的快速總結:
| 64 bit | 32 bit --------------------------------------------------- zval | 24 bytes | 16 bytes + cyclic GC info | 8 bytes | 4 bytes + allocation header | 16 bytes | 8 bytes =================================================== zval (value) total | 48 bytes | 28 bytes =================================================== bucket | 72 bytes | 36 bytes + allocation header | 16 bytes | 8 bytes + pointer | 8 bytes | 4 bytes =================================================== bucket (array element) total | 96 bytes | 48 bytes =================================================== total total | 144 bytes | 76 bytes
上述數字將根據您的作業系統、編譯器和編譯選項的不同而有所不同。例如,如果您使用偵錯或線程安全性來編譯PHP,您將得到不同的數字。但是我認為上面給出的大小是您將在Linux上的PHP 5.3的64位元生產版本中看到的大小。
如果你用這144位元組乘以100000個元素,你會得到14400000字節,也就是13.73 MB,這與實際數字非常接近——剩下的大部分都是未初始化bucket的指針,但是我將在後面討論這個問題。
現在,如果您想對上面提到的值進行更詳細的分析,請繼續閱讀:)
zvalue_value聯盟
首先看看PHP是如何儲存值的。正如您所知道的,PHP是一種弱類型語言,因此它需要某種方式在各種類型之間快速切換。 PHP為此使用union,它在zend中定義如下。
typedef union _zvalue_value { long lval; // For integers and booleans double dval; // For floats (doubles) struct { // For strings char *val; // consisting of the string itself int len; // and its length } str; HashTable *ht; // For arrays (hash tables) zend_object_value obj; // For objects } zvalue_value;
如果您不知道C,這不是一個問題,因為程式碼非常簡單:union是一種使某些值可以作為各種類型存取的方法。例如,如果您執行zvalue_value->lval,您將得到一個被解釋為整數的值。另一方面,如果您使用zvalue_value->ht,則該值將被解釋為指向哈希表(即數組)的指標。
但我們不要在這裡講太多。對我們來說,唯一重要的是一個union的大小等於它的最大組件的大小。這裡最大的元件是字串結構體(zend_object_value結構體的大小與str結構體相同,但為了簡單起見,我將省略它)。 string struct儲存一個指標(8位元組)和一個整數(4位元組),總共是12位元組。由於記憶體對齊(12位元組的結構並不酷,因為它們不是64位元/ 8位元組的倍數),結構的總大小將是16位元組,這也是union作為一個整體的大小。
現在我們知道,由於PHP的動態類型,每個值不需要8位元組,而是16位元組。乘以100000個值得到1600000字節,也就是1.53 MB,但是實際的值是13.97 MB,所以我們還不能得到它。
zval的結構
這非常符合邏輯-union只儲存值本身,但PHP顯然還需要儲存型別和一些垃圾收集資訊。保存此資訊的結構稱為zval,您可能已經聽說過它。關於PHP為什麼需要它的更多信息,我建議閱讀Sara Golemon的一篇文章。無論如何,這個結構的定義如下:
struct _zval_struct { zvalue_value value; // The value zend_uint refcount__gc; // The number of references to this value (for GC) zend_uchar type; // The type zend_uchar is_ref__gc; // Whether this value is a reference (&) };
結構的大小由其組件的大小之和決定:zvalue_value為16字節(如上所計算),zend_uint為4字節,zend_uchars為1字節。總共是22位元組。由於記憶體對齊,實際大小將是24位元組。
因此,如果我們儲存100,000個元素a 24字節,那麼總共就是2400000,也就是2.29 MB,差距正在縮小,但是實際值仍然是原來的6倍多。
循環收集器(從PHP 5.3開始)
PHP 5.3引進了一個新的循環引用垃圾收集器。為此,PHP必須儲存一些額外的資料。我不想在這裡解釋這個演算法是如何運作的,你可以在手冊的連結頁面上讀到。對於我們的大小計算來說,重要的是PHP將把每個zval包裝成zval_gc_info:
typedef struct _zval_gc_info { zval z; union { gc_root_buffer *buffered; struct _zval_gc_info *next; } u; } zval_gc_info;
正如您所看到的,Zend只在它上面添加了一個union,它由兩個指針組成。希望您還記得,union的大小就是它最大的元件的大小:兩個union元件都是指針,因此它們的大小都是8位元組。所以union的大小也是8位元組。
如果我們把它加到24位元組上面我們已經有32位元組了。再乘以100000個元素,我們得到的記憶體使用量是3。05 MB。
Zend MM分配器
C與PHP不同,它不會為您管理記憶體。你需要自己記錄你的分配。為此,PHP使用了專門針對其需要最佳化的自訂記憶體管理器:Zend記憶體管理器。 Zend MM基於Doug Lea的malloc,並添加了一些PHP特有的優化和特性(如內存限制、每次請求後清理等)。
這裡對我們來說重要的是,MM為透過它完成的每個分配添加一個分配頭。定義如下:
typedef struct _zend_mm_block { zend_mm_block_info info; #if ZEND_DEBUG unsigned int magic; # ifdef ZTS THREAD_T thread_id; # endif zend_mm_debug_info debug; #elif ZEND_MM_HEAP_PROTECTION zend_mm_debug_info debug; #endif } zend_mm_block; typedef struct _zend_mm_block_info { #if ZEND_MM_COOKIES size_t _cookie; #endif size_t _size; // size of the allocation size_t _prev; // previous block (not sure what exactly this is) } zend_mm_block_info;
如您所见,这些定义充斥着大量的编译选项检查。如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么分配头文件会更大。
对于本例,我们假设所有这些选项都是禁用的。在这种情况下,只剩下两个size_ts _size和_prev。size_t有8个字节(在64位上),所以分配头的总大小是16个字节——并且在每个分配上都添加了这个头。
现在我们需要再次调整zval大小。实际上,它不是32字节,而是48字节,这是由分配头决定的。乘以100000个元素是4。58 MB,实际值是13。97 MB,所以我们已经得到了大约三分之一的面积。
Buckets
到目前为止,我们只考虑单个值。但是PHP中的数组结构也会占用大量空间:“数组”在这里实际上是一个不合适的术语。PHP数组实际上是散列表/字典。那么哈希表是如何工作的呢?基本上,对于每个键,都会生成一个散列,该散列用作“real”C数组的偏移量。由于哈希值可能会冲突,具有相同哈希值的所有元素都存储在链表中。当访问一个元素时,PHP首先计算散列,查找正确的bucket并遍历链接列表,逐个元素比较确切的键。bucket的定义如下:
typedef struct bucket { ulong h; // The hash (or for int keys the key) uint nKeyLength; // The length of the key (for string keys) void *pData; // The actual data void *pDataPtr; // ??? What's this ??? struct bucket *pListNext; // PHP arrays are ordered. This gives the next element in that order struct bucket *pListLast; // and this gives the previous element struct bucket *pNext; // The next element in this (doubly) linked list struct bucket *pLast; // The previous element in this (doubly) linked list const char *arKey; // The key (for string keys) } Bucket;
正如您所看到的,需要存储大量数据才能获得PHP使用的抽象数组数据结构(PHP数组同时是数组、字典和链表,这当然需要大量信息)。单个组件的大小为无符号long为8字节,无符号int为4字节,指针为7乘以8字节。总共是68。添加对齐,得到72字节。
像zvals这样的bucket需要在头部分配,因此我们需要再次为分配头添加16个字节,从而得到88个字节。我们还需要在“real”C数组中存储指向这些Bucket的指针(Bucket ** arbucket;)我上面提到过,每个元素增加8个字节。所以总的来说,每个bucket需要96字节的存储空间。
如果每个值都需要一个bucket,那么bucket是96字节,zval是48字节,总共144字节。对于100000个元素,也就是14400000字节,即13.73 MB。
神秘的解决。
等等,还有0.24 MB !
最后的0.24 MB是由于未初始化的存储bucket造成的:理想情况下,存储bucket的实际C数组的大小应该与存储的数组元素的数量大致相同。通过这种方式,冲突最少(除非希望浪费大量内存)。但是PHP显然不能在每次添加元素时重新分配整个数组——这将非常缓慢。相反,如果内部bucket数组达到限制,PHP总是将其大小加倍。所以数组的大小总是2的幂。
在我们的例子中是2 ^ 17 = 131072。但是我们只需要100000个bucket,所以我们留下31072个bucket没有使用。这些bucket不会被分配(因此我们不需要花费全部的96字节),但是bucket指针(存储在内部桶数组中的那个)的内存仍然需要分配。所以我们另外使用8字节(一个指针)* 31072个元素。这是248576字节或0.23 MB,与丢失的内存匹配。(当然,这里仍然缺少一些字节,但是我不想在这里介绍。比如哈希表结构本身,变量等等)
神秘真的解决了。
这告诉我们什么?
PHP不是c,这就是所有这些告诉我们的。您不能期望像PHP这样的超级动态语言具有与C语言相同的高效内存使用。你不能。
但是,如果您确实想节省内存,可以考虑使用SplFixedArray处理大型静态数组。
看看这个修改后的脚本:
$startMemory = memory_get_usage(); $array = new SplFixedArray(100000); for ($i = 0; $i < 100000; ++$i) { $array[$i] = $i; } echo memory_get_usage() - $startMemory, ' bytes';
它基本上做的是相同的事情,但是如果运行它,您会注意到它只使用了“5600640字节”。这是每个元素56字节,因此比普通数组使用的每个元素144字节要少得多。这是因为一个固定的数组不需要bucket结构:所以它只需要每个元素一个zval(48字节)和一个指针(8字节),从而得到观察到的56字节。
以上是PHP數組和值到底有多大的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

會話劫持可以通過以下步驟實現:1.獲取會話ID,2.使用會話ID,3.保持會話活躍。在PHP中防範會話劫持的方法包括:1.使用session_regenerate_id()函數重新生成會話ID,2.通過數據庫存儲會話數據,3.確保所有會話數據通過HTTPS傳輸。

PHP8.1中的枚舉功能通過定義命名常量增強了代碼的清晰度和類型安全性。 1)枚舉可以是整數、字符串或對象,提高了代碼可讀性和類型安全性。 2)枚舉基於類,支持面向對象特性,如遍歷和反射。 3)枚舉可用於比較和賦值,確保類型安全。 4)枚舉支持添加方法,實現複雜邏輯。 5)嚴格類型檢查和錯誤處理可避免常見錯誤。 6)枚舉減少魔法值,提升可維護性,但需注意性能優化。

SOLID原則在PHP開發中的應用包括:1.單一職責原則(SRP):每個類只負責一個功能。 2.開閉原則(OCP):通過擴展而非修改實現變化。 3.里氏替換原則(LSP):子類可替換基類而不影響程序正確性。 4.接口隔離原則(ISP):使用細粒度接口避免依賴不使用的方法。 5.依賴倒置原則(DIP):高低層次模塊都依賴於抽象,通過依賴注入實現。

在PHPStorm中如何進行CLI模式的調試?在使用PHPStorm進行開發時,有時我們需要在命令行界面(CLI)模式下調試PHP�...

如何在系統重啟後自動設置unixsocket的權限每次系統重啟後,我們都需要執行以下命令來修改unixsocket的權限:sudo...

使用PHP的cURL庫發送JSON數據在PHP開發中,經常需要與外部API進行交互,其中一種常見的方式是使用cURL庫發送POST�...
