Eu fiz o teste a seguir e no meu sistema a diferença resultante é cerca de 100 vezes mais longa para o segundo script.
Meu arquivo é uma saída strace chamada bigfile
$ wc -l bigfile.log
1617000 bigfile.log
Scripts
xtian@clafujiu:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l
xtian@clafujiu:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l
Na verdade, não tenho correspondências para o grep, então nada é gravado no último canal até wc -l
Aqui estão os horários:
xtian@clafujiu:~/tmp$ time bash p1.sh
0
0
real 0m0.381s
user 0m0.248s
sys 0m0.280s
xtian@clafujiu:~/tmp$ time bash p2.sh
0
0
real 0m46.060s
user 0m43.903s
sys 0m2.176s
Então, eu executei os dois scripts novamente através do comando strace
strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh
Aqui estão os resultados dos rastreamentos:
$ cat p1.strace
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
97.24 0.508109 63514 8 2 waitpid
1.61 0.008388 0 84569 read
1.08 0.005659 0 42448 write
0.06 0.000328 0 21233 _llseek
0.00 0.000024 0 204 146 stat64
0.00 0.000017 0 137 fstat64
0.00 0.000000 0 283 149 open
0.00 0.000000 0 180 8 close
...
0.00 0.000000 0 162 mmap2
0.00 0.000000 0 29 getuid32
0.00 0.000000 0 29 getgid32
0.00 0.000000 0 29 geteuid32
0.00 0.000000 0 29 getegid32
0.00 0.000000 0 3 1 fcntl64
0.00 0.000000 0 7 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 0.522525 149618 332 total
E p2.strace
$ cat p2.strace
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
75.27 1.336886 133689 10 3 waitpid
13.36 0.237266 11 21231 write
4.65 0.082527 1115 74 brk
2.48 0.044000 7333 6 execve
2.31 0.040998 5857 7 clone
1.91 0.033965 0 705681 read
0.02 0.000376 0 10619 _llseek
0.00 0.000000 0 248 132 open
...
0.00 0.000000 0 141 mmap2
0.00 0.000000 0 176 126 stat64
0.00 0.000000 0 118 fstat64
0.00 0.000000 0 25 getuid32
0.00 0.000000 0 25 getgid32
0.00 0.000000 0 25 geteuid32
0.00 0.000000 0 25 getegid32
0.00 0.000000 0 3 1 fcntl64
0.00 0.000000 0 6 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 1.776018 738827 293 total
Análise
Não é de surpreender que, em ambos os casos, a maior parte do tempo seja gasta aguardando a conclusão de um processo, mas p2 espera 2,63 vezes mais que p1 e, como outros já mencionaram, você está começando tarde no p2.sh.
Então agora esqueça o waitpid
, ignore a %
coluna e observe a coluna dos segundos nos dois rastreamentos.
O maior tempo em que p1 passa a maior parte do tempo na leitura provavelmente é compreensível, porque há um arquivo grande para ler, mas o p2 gasta 28,82 vezes mais em leitura do que o p1. - bash
não espera ler um arquivo tão grande em uma variável e provavelmente está lendo um buffer de cada vez, dividindo-se em linhas e, em seguida, obtendo outro.
a contagem de leituras p2 é 705k vs 84k para a p1, cada leitura exigindo uma alternância de contexto para o espaço do kernel e saindo novamente. Quase 10 vezes o número de leituras e alternâncias de contexto.
O tempo na gravação p2 passa 41,93 vezes mais na gravação do que na p1
o número de gravações p1 faz mais gravações que p2, 42k vs 21k, porém são muito mais rápidos.
Provavelmente por causa das echo
linhas em grep
oposição aos buffers de escrita de cauda.
Além disso , p2 gasta mais tempo na gravação do que na leitura, p1 é o contrário!
Outro fator Veja o número de brk
chamadas do sistema: o p2 gasta 2,42 vezes mais tempo do que a leitura! Na p1 (ele nem se registra). brk
é quando o programa precisa expandir seu espaço de endereço porque o suficiente não foi alocado inicialmente, isso provavelmente se deve ao bash ter que ler esse arquivo na variável e não esperar que ele seja tão grande e, como o @scai mencionou, se o o arquivo fica muito grande, mesmo isso não funcionaria.
tail
é provavelmente um leitor de arquivos bastante eficiente, porque é para isso que ele foi projetado para fazer, provavelmente cria um mapa do arquivo e procura por quebras de linha, permitindo que o kernel otimize a E / S. bash não é tão bom, tanto no tempo gasto lendo e escrevendo.
O p2 gasta 44ms e 41ms clone
e execv
não é um valor mensurável para o p1. Provavelmente bash lendo e criando a variável a partir da cauda.
Finalmente, o total p1 executa ~ 150k chamadas do sistema vs p2 740k (4,93 vezes maior).
Eliminando waitpid, p1 gasta 0,014416 segundos na execução de chamadas do sistema, p2 0,439132 segundos (30 vezes mais).
Portanto, parece que o p2 passa a maior parte do tempo no espaço do usuário sem fazer nada, exceto aguardando a conclusão das chamadas do sistema e o kernel para reorganizar a memória, o p1 realiza mais gravações, mas é mais eficiente e causa uma carga do sistema significativamente menor e, portanto, é mais rápido.
Conclusão
Eu nunca tentaria me preocupar com a codificação através da memória ao escrever um script bash, isso não significa dizer que você não tenta ser eficiente.
tail
foi projetado para fazer o que faz, provavelmente memory maps
o arquivo para que seja eficiente para ler e permita que o kernel otimize a E / S.
Uma maneira melhor de otimizar seu problema pode ser o primeiro grep
para '' sucesso '': as linhas e depois contar as verdades e falsidades, grep
tem uma opção de contagem que novamente evita wc -l
, ou melhor ainda, canaliza a cauda awk
e conta verdades e falsifica simultaneamente. O p2 não apenas demora, mas adiciona carga ao sistema enquanto a memória está sendo embaralhada com brks.