Jak „tak” pisze do pliku tak szybko?

58

Podam przykład:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

Tutaj możesz zobaczyć, że polecenie yeszapisuje 11504640linie w sekundę, podczas gdy ja mogę pisać tylko 1953linie w 5 sekund przy użyciu bash fori echo.

Jak sugerowano w komentarzach, istnieją różne sztuczki, aby uczynić go bardziej wydajnym, ale żadne z nich nie jest zbliżone do prędkości yes:

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

Mogą napisać do 20 tysięcy wierszy na sekundę. Można je dodatkowo ulepszyć, aby:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

To daje nam do 40 tysięcy linii na sekundę. Lepiej, ale wciąż daleko, z yesktórego można napisać około 11 milionów wierszy na sekundę!

Więc jak yeszapisuje się do pliku tak szybko?

Pandya
źródło
9
W drugim przykładzie masz dwa zewnętrzne wywołania poleceń dla każdej iteracji pętli i datejest to nieco ciężkie, a ponadto powłoka musi ponownie otworzyć strumień wyjściowy echodla każdej iteracji pętli. W pierwszym przykładzie jest tylko jedno wywołanie polecenia z jednym przekierowaniem wyjścia, a polecenie jest bardzo lekkie. Oba nie są w żaden sposób porównywalne.
CVn
@ MichaelKjörling masz rację datemoże być ciężki, zobacz edytuj moje pytanie.
Pandya
1
timeout 1 $(while true; do echo "GNU">>file2; done;)jest niewłaściwym sposobem użycia, timeout ponieważ timeoutpolecenie uruchomi się dopiero po zakończeniu zastępowania polecenia. Użyj timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
muru
1
podsumowanie odpowiedzi: spędzając czas procesora tylko na write(2)wywołaniach systemowych, a nie na obciążeniach innymi syscallami, narzutami powłoki, a nawet tworzeniem procesów w pierwszym przykładzie (który działa i czeka datena każdą linię wydrukowaną w pliku). Jedna sekunda pisania to zaledwie tyle, ile wynosi wąskie gardło we / wy dysku (zamiast procesora / pamięci) w nowoczesnym systemie z dużą ilością pamięci RAM. Gdyby pozwolić na dłuższą pracę, różnica byłaby mniejsza. (W zależności od tego, jak źle używasz implementacji bash oraz względnej prędkości procesora i dysku, możesz nawet nie nasycić dysku we / wy dyskiem bash).
Peter Cordes,

Odpowiedzi:

65

łupina od orzecha:

yeswykazuje podobne zachowanie do większości innych standardowych narzędzi, które zazwyczaj zapisują do STREAMU PLIKÓW, a dane wyjściowe są buforowane przez libC przez stdio . Są tylko zrobić syscall write()każdy niektóre 4KB (16KB lub 64KB) lub cokolwiek wyjście bloku bufsiz jest. echojest write()na GNU. To jest dużo z trybu przełączania (co nie jest, jak widać, jak kosztowne jako kontekst przełącznika ) .

I wcale nie wspominając, że poza początkową pętlą optymalizacji yesjest to bardzo prosta, niewielka, skompilowana pętla C, a pętla powłoki nie jest w żaden sposób porównywalna z programem zoptymalizowanym pod kątem kompilatora.


ale byłem w błędzie:

Kiedy powiedziałem wcześniej, że yesużyłem stdio, założyłem, że tak, ponieważ zachowuje się bardzo podobnie do tych, które to robią. To nie było poprawne - tylko naśladuje ich zachowanie w ten sposób. To, co faktycznie robi, jest bardzo podobne do tego, co zrobiłem poniżej z powłoką: najpierw zapętla się, by złączyć swoje argumenty (lub yjeśli ich nie ma), dopóki nie wzrosną bez przekroczenia BUFSIZ.

Komentarz ze źródła bezpośrednio poprzedzający odpowiednie forstany pętli:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yeswrite()potem robi swoje .


dygresja:

(Jak pierwotnie uwzględniono w pytaniu i zachowano w kontekście możliwego pouczającego wyjaśnienia już tutaj napisanego) :

Próbowałem, timeout 1 $(while true; do echo "GNU">>file2; done;)ale nie mogłem zatrzymać pętli.

timeoutProblem masz z podstawienia polecenia - Chyba się to teraz, a może wyjaśnić, dlaczego nie zatrzymać. timeoutnie uruchamia się, ponieważ jego linia poleceń nigdy nie jest uruchamiana. Twoja skorupa rozwidla powłokę potomną, otwiera rurkę na swoim standardowym wyjściu i czyta ją. Przestanie czytać, gdy dziecko zakończy pracę, a następnie zinterpretuje wszystko, co napisało dziecko dla $IFSmanglingu i ekspansji globu, a rezultatem zastąpi wszystko, od $(dopasowywania ).

Ale jeśli dziecko jest nieskończoną pętlą, która nigdy nie zapisuje do potoku, to dziecko nigdy nie przestaje zapętlać, a timeoutlinia poleceń nigdy nie jest uzupełniana (jak sądzę) , robisz CTRL-Ci zabijasz pętlę potomną. Dlatego nigdy nietimeout można zabić pętli, która musi zostać ukończona, zanim będzie mogła się rozpocząć.


inne timeouts:

... po prostu nie są tak istotne dla twoich problemów z wydajnością, jak ilość czasu, którą Twój program powłoki musi poświęcić na przełączanie między trybem użytkownika a trybem jądra, aby obsłużyć dane wyjściowe. timeoutnie jest jednak tak elastyczny, jak powłoka może być w tym celu: gdzie powłoki wyróżniają się zdolnością do łączenia argumentów i zarządzania innymi procesami.

Jak zauważono gdzie indziej, po prostu przeniesienie [fd-num] >> named_fileprzekierowania do celu wyjściowego pętli, a nie tylko kierowanie tam wyjścia dla zapętlonego polecenia, może znacznie poprawić wydajność, ponieważ w ten sposób przynajmniej open()syscall musi być zrobiony tylko raz. Odbywa się to również poniżej, gdy |rura jest kierowana jako wyjście dla wewnętrznych pętli.


bezpośrednie porównanie:

Możesz zrobić jak:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

To trochę przypomina opisaną wcześniej relację podrzędną polecenia, ale nie ma potoku, a dziecko jest w tle, dopóki nie zabije rodzica. W yesprzypadku, gdy rodzic faktycznie został zastąpiony od momentu odrodzenia dziecka, ale powłoka wywołuje yes, nakładając własny proces na nowy, więc PID pozostaje ten sam, a jego dziecko zombie wciąż wie, kogo zabić.


większy bufor:

Teraz zobaczmy, jak zwiększyć write()bufor powłoki .

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

Wybrałem tę liczbę, ponieważ ciągi wyjściowe dłuższe niż 1kb były write()dla mnie dzielone na osobne . I oto znowu pętla:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

To 300 razy więcej danych zapisanych przez powłokę w tym samym czasie dla tego testu niż w ostatnim. Nieźle. Ale tak nie jest yes.


związane z:

Zgodnie z prośbą istnieje bardziej dokładny opis niż zwykłe komentarze do kodu dotyczące tego, co zostało zrobione tutaj pod tym linkiem .

mikeserv
źródło
@heemayl - może? nie jestem całkowicie pewien, czy rozumiem, o co pytasz? gdy program używa stdio do zapisywania danych wyjściowych, robi to bez buforowania (domyślnie stderr) lub buforowania linii (domyślnie do terminali) lub buforowania bloków (w zasadzie większość innych rzeczy jest domyślnie ustawiona w ten sposób) . jestem trochę niejasny, co ustawia rozmiar bufora wyjściowego - ale zwykle jest to 4 kb. dlatego funkcje stdio lib będą zbierać dane wyjściowe, dopóki nie będą w stanie napisać całego bloku. ddjest standardowym narzędziem, które zdecydowanie nie używa na przykład stdio. większość innych.
mikeserv
3
Wersja powłoki robi open(istnieje) writeORAZ close(co, jak sądzę, nadal czeka na opróżnienie), ORAZ tworzy nowy proces i wykonuje datedla każdej pętli.
dave_thompson_085
@ dave_thompson_085 - przejdź do / dev / chat . a to, co mówisz, niekoniecznie musi być prawdą, jak to widać. Na przykład, że robi wc -lpętlę bashdla mnie dostaje 1/5 dnia wyprowadzenia shpętli robi - bashzarządza nieco ponad 100k writes()do dash„s 500k.
mikeserv
Przepraszam, byłem dwuznaczny; W pytaniu miałem na myśli wersję powłoki, która w momencie jej czytania miała tylko wersję oryginalną z for((sec0=`date +%S`;...kontrolą czasu i przekierowaniem w pętli, a nie kolejnymi ulepszeniami.
dave_thompson_085
@ dave_thompson_085 - w porządku. odpowiedź i tak była błędna w niektórych podstawowych kwestiach i, jak mam nadzieję, powinna być całkiem poprawna.
mikeserv
20

Lepszym pytaniem byłoby, dlaczego powłoka tak wolno zapisuje plik. Każdy samodzielny skompilowany program, który w sposób odpowiedzialny używa syscalls do zapisywania plików (nie opróżniając wszystkich znaków na raz), zrobiłby to dość szybko. To, co robisz, to pisanie wierszy w interpretowanym języku (powłoce), a ponadto wykonujesz wiele niepotrzebnych operacji wprowadzania danych wyjściowych. Co yesrobi:

  • otwiera plik do pisania
  • wywołuje zoptymalizowane i skompilowane funkcje do zapisu do strumienia
  • strumień jest buforowany, więc wywołanie systemowe (kosztowne przejście do trybu jądra) zdarza się bardzo rzadko, w dużych porcjach
  • zamyka plik

Co robi twój skrypt:

  • wczytuje wiersz kodu
  • interpretuje kod, wykonując wiele dodatkowych operacji, aby właściwie przeanalizować dane wejściowe i dowiedzieć się, co zrobić
  • dla każdej iteracji pętli while (która prawdopodobnie nie jest tania w interpretowanym języku):
    • wywołaj datezewnętrzne polecenie i zapisz jego dane wyjściowe (tylko w wersji oryginalnej - w wersji poprawionej zyskujesz współczynnik 10, nie robiąc tego)
    • sprawdzić, czy warunek zakończenia pętli jest spełniony
    • otwórz plik w trybie dołączania
    • parsuj echopolecenie, rozpoznaj go (z pewnym kodem dopasowującym wzór) jako wbudowaną powłokę, wywołaj interpretację parametrów i wszystko inne w argumencie „GNU”, a na koniec zapisz wiersz do otwartego pliku
    • zamknij plik ponownie
    • powtórz proces

Drogie części: cała interpretacja jest niezwykle kosztowna (bash wykonuje strasznie dużo przetwarzania wstępnego wszystkich danych wejściowych - twój łańcuch może potencjalnie zawierać podstawianie zmiennych, podstawianie procesów, rozwijanie nawiasów klamrowych, znaki ucieczki i więcej), każde wywołanie wbudowanego jest prawdopodobnie instrukcja switch z przekierowaniem do funkcji, która zajmuje się wbudowanym programem, i co bardzo ważne, otwierasz i zamykasz plik dla każdego wiersza wyniku. Możesz >> filewyłączyć pętlę while, aby uczynić ją znacznie szybszą , ale nadal jesteś w tłumaczonym języku. Masz to szczęścieechojest wbudowaną powłoką, a nie zewnętrznym poleceniem - w przeciwnym razie twoja pętla wymagałaby tworzenia nowego procesu (fork & exec) przy każdej iteracji. Co zatrzymałoby proces - zatrzymałeś się - zobaczyłeś, ile to kosztuje, gdy masz datepolecenie w pętli.

orion
źródło
11

Pozostałe odpowiedzi dotyczyły głównych punktów. Na marginesie, możesz zwiększyć przepustowość pętli while, pisząc do pliku wyjściowego na końcu obliczeń. Porównać:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

z

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s
Apoorv Gupta
źródło
Tak, to ma znaczenie, a szybkość pisania (przynajmniej) podwaja się w moim przypadku
Pandya