sábado, 17 de dezembro de 2011

Quão grande são arrays PHP (e valores) realmente?

Neste post eu quero para investigar o uso de memória de matrizes PHP (e valores em geral) usando o seguinte script como um exemplo, que cria 100 mil elementos do array inteiro exclusivo e mede o uso de memória resultante:


<Php?
$ startMemory  =  memory_get_usage ();
$ array  =  gama ( 1 ,  100 mil );
echo  memory_get_usage ()  -  $ startMemory ,  'bytes' ;
O quanto você esperaria que fosse? Simples, um inteiro é de 8 bytes (em uma máquina Unix de 64 bits e usando o tempo tipo) e você tem 100 mil números inteiros, então obviamente você vai precisar 800000 bytes. Isso é algo como 0,76 MBs.

Agora tente e executar o código acima. Você pode fazê-lo online, se quiser. Isso me dá 14649024 bytes . Sim, você ouviu direito, que é 13,97 MB - eightteen vezes mais do que estimado.

Então, onde é que isso factor extra de 18 vem?

Sumário
Para aqueles que não querem saber a história completa, aqui está um rápido resumo do uso de memória dos diferentes componentes envolvidos:

                             | 64 bits | 32 bits
-------------------------------------------------- -
zval | 24 bytes | 16 bytes
+ Cíclica GC info | 8 bytes | 4 bytes
+ Cabeçalho da alocação | 16 bytes | 8 bytes
================================================== =
zval (valor) total | 48 bytes | 28 bytes
================================================== =
balde | 72 bytes | 36 bytes
+ Cabeçalho da alocação | 16 bytes | 8 bytes
+ Pointer | 8 bytes | 4 bytes
================================================== =
balde (elemento da matriz) total | 96 bytes | 48 bytes
================================================== =
total total | 144 bytes | 76 bytes
Os números acima irão variar dependendo do seu sistema operacional, o compilador e suas opções de compilação. Por exemplo, se você compilar o PHP com debug ou com fio de segurança, você vai ter números diferentes. Mas eu acho que os tamanhos acima são o que você vai ver em uma produção de 64 bits média de construir PHP 5.3 no Linux.

Se você multiplicar os 144 bytes por nossos 100.000 elementos que você começa 14400000 bytes, que é 13,73 MB. Isso é muito perto do número real - o resto é principalmente ponteiros para baldes não inicializada, mas vou cobrir isso mais tarde.

Agora, se você quiser ter uma análise mais detalhada dos valores mencionados acima, continue a ler:)

O zvalue_value união
Dê uma olhada em valores como o PHP lojas. Como você sabe PHP é uma linguagem fracamente tipada, por isso precisa de alguma forma para alternar entre os vários tipos rápido. PHP usa uma união para este, que é definido como segue em zend.h # 307 (o meu comentário):

typedef  união  _zvalue_value  {
    longo  lVal ;                 / / Para inteiros e booleanos
    dupla  dval ;               / / Para floats (dobra)
    struct  {                   / / Para cadeias
        de char  * val ;             / / consiste na própria string
        int  len ;               / / e seu comprimento
    }  str ;
    HashTable  * ht ;             / / Para matrizes (tabelas hash)
    zend_object_value  obj ;     / / Para objetos
}  zvalue_value ;
Se você não sabe C, que não é um problema como o código é bastante simples: A união é um meio de fazer algum valor acessível como vários tipos. Por exemplo, se você fizer uma zvalue_value-> lVal você terá o valor interpretado como um inteiro. Se você usar zvalue_value-> ht por outro lado, o valor será interpretado como um ponteiro para uma hashtable (array aka).

Mas não vamos ficar muito para esta aqui. Importante para nós só é que o tamanho de uma união é igual ao tamanho de seu maior componente. O maior componente aqui é a seqüência de struct (o zend_object_value struct tem o mesmo tamanho que o str struct, mas vou deixar isso para simplificar). A cadeia de lojas struct um ponteiro (8 bytes) e um inteiro (4 bytes), que é de 12 bytes no total. Devido ao alinhamento de memória (structs com 12 bytes não são legais porque eles não são um múltiplo de 64 bits / 8 bytes), o tamanho total da estrutura vai ser de 16 bytes embora e que também será o tamanho da união como um todo .

Portanto, agora sabemos que não precisamos de 8 bytes para cada valor, mas 16 - devido a tipagem dinâmica do PHP. Multiplicando por 100 mil valores nos dá 1.600.000 bytes, ou seja, 1,53 MB. Mas o valor real é 13,97 MB, por isso não podemos estar lá ainda.

O zval struct
E isso é bastante lógico - a união só armazena o valor em si, mas PHP, obviamente, também precisa armazenar seu tipo e algumas informações de coleta de lixo. A estrutura da holding essas informações é chamado de zval e você pode ter já ter ouvido falar dele. Para mais informações sobre por que o PHP precisa que eu recomendaria a ler um artigo escrito por Sara Golemon . De qualquer forma, esta estrutura é definida da seguinte forma :

struct  _zval_struct  {
    zvalue_value  valor ;      / / O valor
    zend_uint  refcount__gc ;  / / O número de referências a este valor (para GC)
    zend_uchar  tipo ;         / / O tipo
    zend_uchar  is_ref__gc ;   / / Se este valor é uma referência (&)
};
O tamanho de uma estrutura é determinado pela soma dos tamanhos de seus componentes: O zvalue_value é de 16 bytes (como calculado acima), o zend_uint é de 4 bytes eo zend_uchar s são 1 byte cada. Isso é um total de 22 bytes. Mais uma vez devido ao alinhamento de memória do tamanho real será de 24 bytes embora.

Então, se nós armazenamos 100.000 elementos de 24 bytes que seriam 2,4 milhões no total, que é 2,29 MB. A diferença está diminuindo, mas o valor real é ainda mais de seis vezes maior.

O coletor de ciclos (a partir do PHP 5.3)
PHP 5.3 introduziu um novo coletor de lixo de referências cíclicas . Para que isso funcione PHP tem de armazenar alguns dados adicionais. Eu não quero explicar como o algoritmo funciona aqui, você pode ler isso na página vinculada a partir do manual. Importante para nossos cálculos de tamanho é que o PHP irá envolver todos os zval em um zval_gc_info :

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;
Como você pode ver Zend só acrescenta uma união em cima dela, que consiste de dois ponteiros. Como você espera lembrar o tamanho de uma união é o tamanho de sua maior componente: Ambos os componentes união são ponteiros, assim, ambos têm um tamanho de 8 bytes. Assim, a dimensão da união é de 8 bytes também.

Se acrescentarmos que no topo dos 24 bytes já temos temos 32 bytes. Multiplique isso pelos 100 mil elementos e temos uma utilização da memória de 3,05 MB.

O alocador MM Zend
C ao contrário de PHP não gerenciar a memória para você. Você precisa manter o controle de sua atribuição a si mesmo. Para este efeito, PHP usa um gerenciador de memória personalizado que é otimizado que especificamente para suas necessidades: O Gerenciador de memória Zend . O MM Zend é baseado em malloc Doug Lea e adiciona algumas otimizações PHP e características específicas (como o limite de memória, limpeza após cada pedido e coisas assim).

O que é importante para nós aqui é que o MM adiciona um cabeçalho de alocação para cada alocação feito por ele. É definido da seguinte forma :

typedef  struct  _zend_mm_block  {
    zend_mm_block_info  informação ;
# if ZEND_DEBUG
    unsigned  int  magia ;
# 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 ;  / / tamanho da atribuição
    size_t  _prev ;  / / bloco anterior (não sei exatamente o que isso é)
}  zend_mm_block_info ;
Como você pode ver as definições são desordenados com lotes de cheques opção de compilação. Então, se uma dessas opções é definir o cabeçalho de alocação será maior e vai ser maior se você compilar o PHP com proteção heap, multi-threading debug, e cookies MM.

Para este exemplo que vamos assumir que todas essas opções estão desativadas. Nesse caso, a única coisa que resta são os dois size_t s _size e _prev . A size_t tem 8 bytes (em 64 bit), de modo que o cabeçalho da alocação tem um tamanho total de 16 bytes - e que cabeçalho é adicionado em cada atribuição.

Então, agora precisamos ajustar nossas zval tamanho novamente. Na realidade, não é de 32 bytes, mas é 48, devido a esse cabeçalho de alocação. Multiplicado por 100 mil elementos que o nosso é 4,58 MB. O valor real é 13,97 MB, por isso, já tem cerca de coberto um terço.

Baldes
Até agora temos considerado apenas valores únicos. Mas as estruturas array em PHP também ocupam muito espaço: "Array" na verdade é um termo mal escolhido aqui. Matrizes são tabelas de hash PHP / dicionários em realidade. Assim como tabelas hash trabalho? Basicamente, para cada chave um hash é gerado e que hash é usado como um deslocamento em uma matriz C "real". Como os hashes podem colidir, todos os elementos que têm o mesmo hash são armazenados em uma lista vinculada. Ao acessar um elemento PHP primeiro calcula o hash, olha para a direita eo balde atravessa a lista de links, comparando a chave exata, elemento por elemento. Um balde é definido da seguinte forma (ver zend_hash.h # 54 ):

typedef  struct  balde  {
    ulong  h ;                   / / O hash (ou para as chaves int chave)
    uint  nKeyLength ;           / / O comprimento da chave (para as chaves string)
    nula  * pData ;               / / Os dados reais
    vazio  * pDataPtr ;            / /? ? O que é isso??
    struct  balde  * pListNext ;  / / PHP arrays estão ordenados. Isto dá o próximo elemento nessa ordem
    struct  balde  * pListLast ;  / / e isso dá o elemento anterior
    struct  balde  * pNext ;      / / O elemento seguinte neste (duplamente) ligados lista
    struct  balde  * plast ;      / / O elemento anterior neste (duplamente) ligados lista
    const  de char  * arKey ;         / / A chave (para as chaves string)
}  Bucket ;
Como você pode ver a pessoa precisa para armazenar cargas de dados para obter o tipo de estrutura de dados abstrata matriz que usa PHP (PHP arrays são matrizes, dicionários e listas ligadas ao mesmo tempo, que se precisa de muita informação). Os tamanhos dos componentes individuais são 8 bytes para o unsigned long, 4 bytes para o int unsigned e 7 vezes 8 bytes para os ponteiros. Isso é um total de 68. Adicionar alinhamento e você terá 72 bytes.

Baldes como zvals precisam ser alocados na cabeça, por isso precisamos adicionar os 16 bytes para o cabeçalho de alocação de novo, dando-nos 88 bytes. Também precisamos armazenar ponteiros para os baldes na "real" array C ( Bucket ** arBuckets; ) que eu mencionei acima, o que acrescenta mais 8 bytes por elemento. Então, tudo em todos balde todas as necessidades de armazenamento de 96 bytes.

Então, se nós precisamos de um balde para cada valor, que é 96 bytes para o balde e 48 bytes para o zval, que é de 144 bytes no total. Para 100 mil elementos que é 14400000 bytes aka 13,73 MB.

Mistério resolvido.

Espere, há uma outra 0.24 MB esquerda!
Os últimos 0.24 MB são devidos a baldes não inicializada: O tamanho da matriz C reais armazenar os baldes deveriam idealmente ser aproximadamente o mesmo que o número de elementos do array armazenado. Desta forma, você tem o menor número de colisões (. A menos que você quer desperdiçar muita memória) Mas PHP, obviamente, não pode realocar o array inteiro cada vez que um elemento é adicionado - o que seria reeeally lento. Em vez disso PHP sempre dobra o tamanho da matriz balde interno, se ele atinge o limite. Assim, o tamanho da matriz é sempre uma potência de dois.

No nosso caso, é 2 ^ 17 = 131072 . Mas precisamos de apenas 100 mil desses baldes, por isso estamos deixando 31.072 baldes não utilizados. Os baldes não serão alocados (por isso não precisa gastar o total 96 bytes), mas a memória para o ponteiro do balde (a armazenada na matriz balde interno) ainda precisa ser alocado. Então, nós utilizar adicionalmente 8 bytes (um ponteiro) * 31072 elementos. Isso é 248.576 bytes ou 0,23 MB. Que coincide com a memória em falta. (Claro, ainda existem alguns bytes em falta, mas eu realmente não quero para cobrir ali. São coisas como a estrutura da tabela de hash em si, variáveis, etc)

Mistério realmente resolvido.

O que isso nos diz?
PHP não é C . Isso é tudo isso deve nos dizer. Você não pode esperar que uma linguagem super-dinâmicas como PHP tem o uso da memória mesmo altamente eficiente que C tem. Você simplesmente não pode.

Mas se você quer economizar memória você poderia considerar o uso de um SplFixedArray para grandes arrays estáticos.

Ter um olhar um script esta modificação:

    bytes ' ;

Ele basicamente faz a mesma coisa, mas se você executá-lo , você notará que ele usa "apenas" 5600640 bytes. Isso é 56 bytes por elemento e, portanto, muito menos do que os 144 bytes por elemento de uma matriz normal usa. Isso ocorre porque uma matriz fixa não precisa da estrutura de balde: Então, ele requer apenas uma zval (48 bytes) e um ponteiro (8 bytes) para cada elemento, dando-nos a observar 56 bytes.

Nenhum comentário :

Postar um comentário

Total de visualizações de página