Estou lendo um arquivo enorme para armazenar dados em um hash muito grande. Estou tentando manter o uso de RAM o menor possível.
Eu tenho um MWE que mostra um comportamento estranho em Perl:
#!/usr/bin/env perl
use 5.038;
use warnings FATAL => 'all';
use autodie ':default';
use DDP {output => 'STDOUT', array_max => 10, show_memsize => 1}; # pretty print with "p"
my @l = split /\s+/, 'OC Pimascovirales; Iridoviridae; Betairidovirinae; Iridovirus.';
p @l;
$_ =~ s/[\.;]$// foreach @l; # single line keeps code shorter
p @l;
que tem saída:
[
[0] "OC",
[1] "Pimascovirales;",
[2] "Iridoviridae;",
[3] "Betairidovirinae;",
[4] "Iridovirus."
] (356B)
[
[0] "OC",
[1] "Pimascovirales",
[2] "Iridoviridae",
[3] "Betairidovirinae",
[4] "Iridovirus"
] (400B)
Embora este exemplo seja trivialmente pequeno, farei isso muitas vezes, portanto, o gerenciamento de RAM é importante.
como diminuir o comprimento da string aumentou o tamanho da RAM dessa matriz de 356B para 400B?
Se possível, posso evitar aumentos como esse?
É uma consequência do Copy on Write. Em outras palavras, até você começar a alterar as strings, Perl apenas sabe onde procurar na string original para encontrá-las, mas não as copia.
Use Devel::Peek para ver:
Antes da substituição:
Depois:
Todos os elementos (exceto o primeiro ) tinham originalmente a
IsCOW
bandeira.Por que a estrutura de dados pós-modificação do OP usa mais memória?
s///
cria novos escalares em vez de modificar a string no local, es///
acontece que cria novos escalares com buffers de string maiores do quesplit
no exemplo do OP.Eu explico ambos com mais detalhes abaixo, mas é realmente isso.
Por que não
s///
modifica a string no local?Pelo menos nas formas que importam para esta postagem, os dois trechos a seguir são equivalentes desde 5.20:
O ponto chave aqui é que os escalares existentes estão sendo substituídos por novos.
Mas nem sempre foi assim. Era uma vez, Perl simplesmente reduzia o tamanho usado do buffer ao removê-lo de seu final using
s///
, resultando em nenhuma memória adicional usada. Isto é demonstrado pelo seguinte programa simples:Observe que o buffer de string está
0x55da3c08e7c0
antes e depois. Somente a quantidade usada do buffer (CUR
) foi alterada.Avance para 5.20 e você terá algo diferente.
Observe que o buffer de string foi movido de
0x55ee06d4d530
para0x55ee06d3acf0
.Uma cópia do buffer está sendo feita, resultando no uso adicional de memória pelo menos temporário.
O que mudou é que o 5.20 introduziu o mecanismo copy-on-write ("COW"). Graças a esse mecanismo, cópias de escalares contendo strings não copiam mais o buffer de strings. Somente o ponteiro para o buffer é copiado e o buffer de string é sinalizado como compartilhado com o
IsCOW
sinalizador.Quando você executa uma correspondência de regex, é feita uma cópia do escalar que está sendo correspondido. Esta cópia é anexada através de magia a todos os vars de captura aplicáveis (
$1
, etc), incluindo$&
e similares. Mas graças ao novo mecanismo COW, nenhuma cópia é feita do buffer de string. Tanto o original quanto a cópia compartilham o mesmo buffer de string até que um seja alterado.No nosso cenário, um deles é alterado um momento depois, já que estamos realizando uma substituição no local.
$_
portanto, obtém um novo buffer para armazenar o valor modificado. É isso que nos dá a equivalência que descrevi no início desta resposta.Podemos ver o mecanismo COW em ação se evitarmos alterar o escalar original.
Observe que o escalar tem o
IsCOW
sinalizador definido após a correspondência da regex. Seu buffer(0x55b9dacf5790
) está sendo compartilhado com um escalar associado a$&
.O uso do COW para variáveis de captura tornou o código mais limpo, corrigiu bugs e melhorou o desempenho.
A memória usada pela cópia da string correspondente será liberada na próxima vez que você fizer uma correspondência no mesmo escopo, para que a memória "perdida" nesta cópia não seja acumulada. Isso significa que a memória perdida não está relacionada ao comprimento do
@l
exemplo do OP.Por que
s///
cria escalares com buffers de string maiores quesplit
?Porque
s///
"constrói" a string, ondesplit
conhece as strings que deseja retornar antes de criar os escalares para elas.Perl favorece a velocidade em detrimento da memória (geralmente de quantidades substanciais). Uma maneira de fazer isso é alocando buffers de string maiores que o necessário. Neste caso, os novos escalares estão sendo criados com buffers maiores.
split
não "construi" a string. Ele sabe o comprimento exato da string que deseja colocar no escalar quando cria o escalar.s///r
não sabe o comprimento final da string que retornará antecipadamente. Ele "constrói" anexando a um escalar que criou. À medida que o buffer de string do escalar fica cheio, ele sofre expansões de tamanho.Essa diferença na forma como a string é construída explica as diferenças no tamanho dos buffers.
split
aloca escalares com buffers de tamanho 16, 17, 16, 19, 16 no exemplo do OP.s///
aloca escalares com buffers de tamanho 16, 40, 16, 40, 16 no exemplo do OP.Para responder à segunda parte da sua pergunta: você pode usar
split /[;.\s]+/
e o array resultante será 354B e conterá os valores desejados sem necessidade de pós-processamento (e sem cópia de string).Isso pressupõe que não haja ponto e vírgula ou pontos em nenhum lugar, exceto no final das palavras; se isso não for verdade, você pode usar o menos bonito (e provavelmente um pouco mais lento)
split /(?:[;.](?=\s))?\s+/
.