Zrobiłem następujący test i w moim systemie wynikowa różnica jest około 100 razy dłuższa dla drugiego skryptu.
Mój plik jest wyjściem strace o nazwie bigfile
$ wc -l bigfile.log
1617000 bigfile.log
Skrypty
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
Właściwie nie mam żadnych dopasowań dla grep, więc nic nie jest zapisywane do ostatniego potoku do wc -l
Oto czasy:
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
Uruchomiłem więc dwa skrypty ponownie za pomocą polecenia strace
strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh
Oto wyniki ze śladów:
$ 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
I 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
Analiza
Nic dziwnego, że w obu przypadkach większość czasu spędza się na oczekiwaniu na zakończenie procesu, ale p2 czeka 2,63 razy dłużej niż p1, a jak wspomnieli inni, zaczynasz późno w p2.sh.
Więc teraz zapomnij o waitpid
, zignoruj %
kolumnę i spójrz na kolumnę sekund na obu śladach.
Największy czas p1 spędza większość czasu na czytaniu prawdopodobnie zrozumiale, ponieważ jest duży plik do odczytu, ale p2 spędza 28,82 razy dłużej w czytaniu niż p1. - bash
nie spodziewa się odczytać tak dużego pliku do zmiennej i prawdopodobnie odczytuje bufor jednocześnie, dzieląc go na linie, a następnie otrzymując inny.
liczba odczytów p2 wynosi 705k vs 84k dla p1, każdy odczyt wymaga przełączenia kontekstu w przestrzeń jądra i ponownie. Prawie 10-krotna liczba odczytów i przełączników kontekstu.
Czas zapisu p2 spędza 41,93 razy dłużej na zapisie niż p1
liczba zapisów p1 robi więcej zapisów niż p2, 42k vs 21k, jednak są one znacznie szybsze.
Prawdopodobnie z powodu echo
linii do grep
w przeciwieństwie do buforów zapisu ogona.
Co więcej , p2 spędza więcej czasu na pisaniu niż na czytaniu, p1 jest na odwrót!
Inny czynnik Spójrz na liczbę brk
wywołań systemowych: p2 wydaje 2,42 razy dłużej na łamanie niż na czytanie! W p1 (nawet się nie rejestruje). brk
dzieje się, gdy program musi rozszerzyć przestrzeń adresową, ponieważ początkowo nie przydzielono wystarczającej ilości miejsca, prawdopodobnie wynika to z bashu konieczności odczytu tego pliku do zmiennej i nie spodziewania się, że będzie on tak duży, i jak wspomniał @scai, jeśli plik staje się zbyt duży, nawet to nie działałoby.
tail
jest prawdopodobnie dość wydajnym czytnikiem plików, ponieważ właśnie do tego został przeznaczony, prawdopodobnie mapuje plik i skanuje pod kątem przerwania linii, umożliwiając w ten sposób jądrze zoptymalizowanie operacji wejścia / wyjścia. bash nie jest tak dobry na czas spędzony na czytaniu i pisaniu.
p2 spędza 44 ms i 41 ms clone
i execv
nie jest to wymierna ilość dla p1. Prawdopodobnie bash czyta i tworzy zmienną z ogona.
Wreszcie Totals p1 wykonuje ~ 150 tys. Wywołań systemowych w porównaniu z p2 740 tys. (4,93 razy więcej).
Eliminując oczekiwanie, p1 spędza 0,014416 sekund na wykonywaniu wywołań systemowych, p2 0,439132 sekund (30 razy dłużej).
Wygląda więc na to, że p2 spędza większość czasu w przestrzeni użytkownika, nie robiąc nic poza czekaniem na zakończenie wywołań systemowych i zreorganizowaniem pamięci przez jądro, p1 wykonuje więcej zapisów, ale jest bardziej wydajny i powoduje znacznie mniejsze obciążenie systemu, a zatem jest szybszy.
Wniosek
Nigdy nie próbowałbym się martwić kodowaniem przez pamięć podczas pisania skryptu bash, co nie znaczy, że nie próbujesz być wydajny.
tail
jest zaprojektowany do robienia tego, co robi, prawdopodobnie jest memory maps
to plik, dzięki czemu jest efektywny w czytaniu i pozwala jądru zoptymalizować operacje we / wy.
Lepszym sposobem na zoptymalizowanie problemu może być najpierw grep
„sukces”: „linie”, a następnie liczyć trues i falses, grep
ma opcję zliczania, która ponownie pozwala uniknąć wc -l
, a nawet lepiej, przepuszczać ogon awk
i liczyć true i załamuje się jednocześnie. p2 nie tylko długo trwa, ale dodaje obciążenie do systemu, gdy pamięć jest tasowana przy pomocy brk.