Maison  >  Article  >  développement back-end  >  Quelle est la taille des tableaux et des valeurs PHP ?

Quelle est la taille des tableaux et des valeurs PHP ?

藏色散人
藏色散人original
2019-01-28 16:03:333344parcourir

Cet article concerne l'utilisation de la mémoire PHP 5. Pour la situation décrite dans cet article, l'utilisation de la mémoire est environ 3 fois inférieure dans PHP 7.

Quelle est la taille des tableaux et des valeurs PHP ?

Dans cet article, je souhaite étudier l'utilisation de la mémoire des tableaux PHP (et des valeurs en général) en utilisant le script suivant comme exemple, qui crée 100 000 Tableau entier unique éléments et mesuré l'utilisation de la mémoire qui en résulte :

$startMemory = memory_get_usage();
$array = range(1, 100000);
echo memory_get_usage() - $startMemory, ' bytes';

Que voulez-vous que ce soit ? En termes simples, un entier fait 8 octets (en utilisant le type long sur une machine Unix 64 bits), vous obtenez 100 000 entiers, vous avez donc évidemment besoin de 800 000 octets.

Essayez maintenant d'exécuter le code ci-dessus. Cela nous donne 14649024 octets. Oui, vous avez bien entendu, 13,97 Mo, soit 18 fois plus que ce que nous avions estimé.

Alors, d'où vient le facteur supplémentaire de 18 ?

Résumé

Pour ceux qui ne veulent pas connaître toute l'histoire, voici celui impliqué. Un résumé rapide de l'utilisation de la mémoire des différents composants :

                             |  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

Les chiffres ci-dessus varient en fonction de votre système d'exploitation, de votre compilateur et des options de compilation. Par exemple, si vous compilez PHP avec debug ou thread safety, vous obtiendrez des nombres différents. Mais je pense que la taille indiquée ci-dessus correspond à ce que vous verrez dans une version de production 64 bits de PHP 5.3 sous Linux.

Si vous multipliez ces 144 octets par 100 000 éléments, vous obtenez 14 400 000 octets, soit 13,73 Mo, ce qui est très proche du nombre réel - la plupart du reste est constitué de pointeurs de compartiment non initialisés, mais j'en discuterai plus tard.

Maintenant, si vous souhaitez une analyse plus détaillée des valeurs mentionnées ci-dessus, continuez à lire :)

zvalue_value alliance

Regardez d'abord comment PHP stocke les valeurs de. Comme vous le savez, PHP est un langage faiblement typé, il a donc besoin d'un moyen de basculer rapidement entre les types. PHP utilise pour cela l'union, qui est définie dans zend comme suit.

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;

Si vous ne connaissez pas le C, ce n'est pas un problème car le code est très simple : l'union est un moyen de rendre certaines valeurs accessibles sous différents types. Par exemple, si vous effectuez zvalue_value->lval, vous obtiendrez une valeur interprétée comme un entier. D'un autre côté, si vous utilisez zvalue_value->ht, la valeur sera interprétée comme un pointeur vers une table de hachage (c'est-à-dire un tableau).

Mais n’entrons pas trop dans les détails ici. Pour nous, la seule chose qui compte, c’est que la taille d’un syndicat soit égale à la taille de sa plus grande composante. Le composant le plus important ici est la structure de chaîne (la structure zend_object_value a la même taille que la structure str, mais je vais l'omettre pour plus de simplicité). La structure de chaîne stocke un pointeur (8 octets) et un entier (4 octets), soit un total de 12 octets. En raison de l'alignement de la mémoire (les structures de 12 octets ne sont pas cool car elles ne sont pas des multiples de 64 bits/8 octets), la taille totale de la structure sera de 16 octets, ce qui correspond également à la taille de l'union dans son ensemble.

Nous savons maintenant qu'en raison du typage dynamique de PHP, chaque valeur n'a pas besoin de 8 octets, mais de 16 octets. La multiplication par 100 000 valeurs nous donne 1 600 000 octets, soit 1,53 Mo, mais la valeur réelle est de 13,97 Mo, nous ne pouvons donc pas l'obtenir pour l'instant.

La structure de zval

C'est très logique - l'union ne stocke que la valeur elle-même, mais PHP a évidemment aussi besoin du type de stockage et de certaines informations de garbage collection. La structure qui contient ces informations s'appelle un zval, dont vous avez peut-être entendu parler. Pour plus d'informations sur les raisons pour lesquelles PHP en a besoin, je vous recommande de lire un article de Sara Golemon. Quoi qu'il en soit, cette structure est définie comme suit :

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 (&)
};

La taille de la structure est déterminée par la somme des tailles de ses composants : 16 octets pour zvalue_value (calculé comme ci-dessus), 4 octets pour zend_uint, et 1 pour l'octet zend_uchars. Le total est de 22 octets. En raison de l'alignement de la mémoire, la taille réelle sera de 24 octets.

Donc, si nous stockons 100 000 éléments sur 24 octets, alors le total est de 2 400 000, soit 2,29 Mo, l'écart se réduit, mais la valeur réelle est toujours plus de 6 fois l'originale.

Collecteur circulaire (à partir de PHP 5.3)

PHP 5.3 introduit un nouveau garbage collector de référence circulaire. Pour ce faire, PHP doit stocker quelques données supplémentaires. Je ne veux pas expliquer ici comment fonctionne cet algorithme, vous pouvez le lire sur la page liée du manuel. Ce qui est important pour notre calcul de taille, c'est que PHP enveloppera chaque zval dans zval_gc_info :

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

Comme vous pouvez le voir, Zend ajoute simplement une union par-dessus, qui se compose de deux pointeurs. J'espère que vous vous souvenez que la taille d'une union est la taille de son plus grand composant : les deux composants de l'union sont des pointeurs, ils ont donc tous deux une taille de 8 octets. La taille de l’union est donc également de 8 octets.

Si nous ajoutons cela à 24 octets, nous avons déjà 32 octets. Multiplié par 100 000 éléments, nous obtenons une utilisation de mémoire de 3,05 Mo.

Zend MM Allocator

C, contrairement à PHP, ne gère pas la mémoire à votre place. Vous devez suivre vous-même vos allocations. Pour ce faire, PHP utilise un gestionnaire de mémoire personnalisé optimisé spécifiquement pour ses besoins : le Zend Memory Manager. Zend MM est basé sur le malloc de Doug Lea et ajoute quelques optimisations et fonctionnalités spécifiques à PHP (telles que les limites de mémoire, le nettoyage après chaque requête, etc.).

Ce qui est important pour nous ici, c'est que MM ajoute un en-tête d'allocation pour chaque allocation effectuée via lui. La définition est la suivante :

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, &#39; bytes&#39;;

它基本上做的是相同的事情,但是如果运行它,您会注意到它只使用了“5600640字节”。这是每个元素56字节,因此比普通数组使用的每个元素144字节要少得多。这是因为一个固定的数组不需要bucket结构:所以它只需要每个元素一个zval(48字节)和一个指针(8字节),从而得到观察到的56字节。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn