>  기사  >  백엔드 개발  >  PHP 배열과 값은 얼마나 큽니까?

PHP 배열과 값은 얼마나 큽니까?

藏色散人
藏色散人원래의
2019-01-28 16:03:333369검색

이 기사는 PHP 5 메모리 사용에 관한 것입니다. 이 기사에 설명된 상황의 경우 PHP 7에서는 메모리 사용량이 약 3배 더 낮습니다.

PHP 배열과 값은 얼마나 큽니까?

이 게시물에서는 다음 스크립트를 예로 들어 PHP 배열(및 일반적인 값)의 메모리 사용량을 연구하고 싶습니다. 이 스크립트는 100000개의 고유한 정수 배열 요소를 생성하고 결과를 측정합니다. 메모리 사용량:

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

무엇을 원하시나요? 간단히 말해서 정수는 8바이트(64비트 유닉스 시스템에서 long 유형 사용)이고 100,000개의 정수를 얻으므로 당연히 800000바이트가 필요합니다.

이제 위 코드를 실행해 보세요. 이는 14649024바이트를 제공합니다. 네, 맞습니다. 13.97MB로 예상보다 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.73MB가 되며 이는 실제 숫자와 매우 가깝습니다. 나머지 대부분은 초기화되지 않은 버킷에 대한 포인터이지만 이 문제는 나중에 논의하겠습니다. .

이제 위에서 언급한 값에 대한 더 자세한 분석을 원하시면 계속 읽어주세요 :)

zvalue_value Alliance

먼저 PHP가 값을 저장하는 방법을 살펴보세요. 아시다시피, PHP는 약한 유형의 언어이므로 유형 간에 빠르게 전환할 수 있는 방법이 필요합니다. PHP는 이를 위해 다음과 같이 zend에 정의된 Union을 사용합니다.

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를 사용하면 값은 해시 테이블(예: 배열)에 대한 포인터로 해석됩니다.

하지만 여기서 너무 자세히 설명하지는 마세요. 우리에게 중요한 유일한 것은 공용체의 크기가 가장 큰 구성 요소의 크기와 같다는 것입니다. 여기서 가장 큰 구성 요소는 문자열 구조입니다(zend_object_value 구조는 str 구조와 크기가 동일하지만 단순화를 위해 생략하겠습니다). 문자열 구조체는 포인터(8바이트)와 정수(4바이트), 총 12바이트를 저장합니다. 메모리 정렬로 인해(12바이트 구조는 64비트/8바이트의 배수가 아니기 때문에 좋지 않음) 구조의 전체 크기는 16바이트가 되며 이는 전체 공용체의 크기이기도 합니다.

이제 우리는 PHP의 동적 타이핑으로 인해 각 값이 8바이트가 아니라 16바이트일 필요가 있다는 것을 알았습니다. 100000개의 값을 곱하면 1600000바이트, 즉 1.53MB가 되지만 실제 값은 13.97MB이므로 아직 구할 수 없습니다.

zval의 구조

이것은 매우 논리적입니다. 공용체는 값 자체만 저장하지만 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개의 요소를 24바이트에 저장하면 총합은 2,400,000, 즉 2.29MB가 되고 간격은 줄어들지만 실제 값은 여전히 ​​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는 여기에 두 개의 포인터로 구성된 공용체를 추가합니다. 공용체의 크기는 가장 큰 구성 요소의 크기라는 점을 기억하시기 바랍니다. 두 공용체 구성 요소는 모두 포인터이므로 둘 다 크기가 8바이트입니다. 따라서 공용체의 크기도 8바이트입니다.

이것을 24바이트에 추가하면 이미 32바이트가 됩니다. 100000개의 요소를 곱하면 3.05MB의 메모리 사용량이 발생합니다.

Zend MM Allocator

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

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

위 내용은 PHP 배열과 값은 얼마나 큽니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.