Odpowiedź na to pytanie spowodowała, że zadałem kolejne pytanie:
myślałem, że poniższe skrypty robią to samo, a drugi powinien być znacznie szybszy, ponieważ pierwszy używa cat
tego, który musi otwierać plik w kółko, ale drugi otwiera tylko plik jeden raz, a potem po prostu echo zmiennej:
(Zobacz poprawny kod w sekcji aktualizacji.)
Pierwszy:
#!/bin/sh
for j in seq 10; do
cat input
done >> output
Druga:
#!/bin/sh
i=`cat input`
for j in seq 10; do
echo $i
done >> output
podczas gdy wejście wynosi około 50 megabajtów.
Ale kiedy spróbowałem drugiego, było też zbyt wolno, ponieważ echo zmiennej i
było ogromnym procesem. Mam również problemy z drugim skryptem, na przykład rozmiar pliku wyjściowego był mniejszy niż oczekiwano.
Sprawdziłem także stronę podręcznika echo
i, cat
aby je porównać:
echo - wyświetla wiersz tekstu
cat - konkatenuje pliki i drukuje na standardowym wyjściu
Ale nie dostałem różnicy.
Więc:
- Dlaczego kot jest taki szybki, a echo tak wolne w drugim skrypcie?
- A może problem ze zmienną
i
? (ponieważ na stronie podręcznikaecho
jest napisane, że wyświetla „wiersz tekstu”, więc wydaje mi się, że jest zoptymalizowany tylko dla krótkich zmiennych, a nie dla bardzo długich zmiennych, jaki
. Jednak to tylko przypuszczenie.) - I dlaczego mam problemy, kiedy używam
echo
?
AKTUALIZACJA
Użyłem seq 10
zamiast `seq 10`
niepoprawnie. To jest edytowany kod:
Pierwszy:
#!/bin/sh
for j in `seq 10`; do
cat input
done >> output
Druga:
#!/bin/sh
i=`cat input`
for j in `seq 10`; do
echo $i
done >> output
(Specjalne podziękowania dla roaima .)
Jednak nie o to chodzi w tym problemie. Nawet jeśli pętla występuje tylko jeden raz, mam ten sam problem: cat
działa znacznie szybciej niż echo
.
cat $(for i in $(seq 1 10); do echo "input"; done) >> output
? :)echo
jest szybsze. To, czego brakuje, to to, że zmuszasz powłokę do wykonywania zbyt dużej pracy, nie cytując zmiennych podczas ich używania.printf '%s' "$i"
, a nieecho $i
. @cuonglm dobrze wyjaśnia niektóre problemy echa w swojej odpowiedzi. Aby dowiedzieć się, dlaczego nawet cytowanie w niektórych przypadkach nie wystarcza z echo, zobacz unix.stackexchange.com/questions/65803/…Odpowiedzi:
Jest tu kilka rzeczy do rozważenia.
mogą być drogie i istnieje wiele odmian między pociskami.
Jest to funkcja zwana zastępowaniem poleceń. Chodzi o to, aby zapisać cały wynik polecenia minus końcowe znaki nowego wiersza w
i
zmiennej w pamięci.Aby to zrobić, powłoki rozwidlają polecenie w podpowłoce i odczytują jego dane wyjściowe przez potok lub parę gniazd. Tutaj widzisz wiele odmian. W pliku 50 MB tutaj widzę na przykład, że bash jest 6 razy wolniejszy niż ksh93, ale nieco szybszy niż zsh i dwa razy szybszy
yash
.Głównym powodem
bash
spowolnienia jest to, że czyta z potoku 128 bajtów jednocześnie (podczas gdy inne powłoki odczytują jednocześnie 4KiB lub 8KiB) i jest karany przez narzut wywołania systemowego.zsh
musi wykonać przetwarzanie końcowe, aby uniknąć bajtów NUL (inne powłoki łamią się na bajtach NUL), ayash
nawet wykonuje bardziej wymagające przetwarzanie przez analizowanie znaków wielobajtowych.Wszystkie powłoki muszą usunąć końcowe znaki nowego wiersza, które mogą wykonywać mniej lub bardziej wydajnie.
Niektórzy mogą chcieć obsługiwać bajty NUL bardziej wdzięcznie niż inni i sprawdzać ich obecność.
Następnie, gdy masz już tę dużą zmienną w pamięci, wszelkie manipulacje nią zazwyczaj wiążą się z przydzielaniem większej ilości pamięci i kopiowaniem danych.
Tutaj przekazujesz (zamierzałeś przekazać) zawartość zmiennej do
echo
.Na szczęście
echo
jest wbudowany w twoją powłokę, w przeciwnym razie wykonanie prawdopodobnie nie powiedzie się z powodu zbyt długiego błędu listy arg . Nawet wtedy zbudowanie tablicy listy argumentów prawdopodobnie będzie wymagało skopiowania zawartości zmiennej.Innym głównym problemem w metodzie zastępowania poleceń jest to, że wywołujesz operator split + glob (zapominając o cytowaniu zmiennej).
W tym celu powłoki muszą traktować ciąg znaków jako ciąg znaków (chociaż niektóre powłoki nie mają i są pod tym względem błędne), więc w ustawieniach regionalnych UTF-8 oznacza to, że parsowanie sekwencji UTF-8 (jeśli nie jest zrobione już tak jak
yash
robi) , poszukaj$IFS
znaków w ciągu. Jeśli$IFS
zawiera spację, tabulator lub znak nowej linii (co jest domyślnym przypadkiem), algorytm jest jeszcze bardziej złożony i kosztowny. Następnie słowa wynikające z tego podziału należy przypisać i skopiować.Część glob będzie jeszcze droższa. Jeśli którykolwiek z tych słów zawierać znaków glob (
*
,?
,[
), wówczas powłoka będzie musiał przeczytać zawartość niektórych katalogów i trochę drogie pasujące do wzorca (bash
„s implementacja na przykład notorycznie jest bardzo zły na to).Jeśli dane wejściowe zawierają coś podobnego
/*/*/*/../../../*/*/*/../../../*/*/*
, będzie to bardzo kosztowne, ponieważ oznacza to wyświetlenie tysięcy katalogów i może wzrosnąć do kilkuset MiB.Następnie
echo
zazwyczaj wykonuje dodatkowe przetwarzanie. Niektóre implementacje rozszerzają\x
sekwencje w otrzymywanym argumencie, co oznacza parsowanie zawartości i prawdopodobnie kolejną alokację i kopię danych.Z drugiej strony, OK, w większości powłok
cat
nie jest wbudowany, więc oznacza to rozwidlenie procesu i jego wykonanie (więc załadowanie kodu i bibliotek), ale po pierwszym wywołaniu ten kod i zawartość pliku wejściowego zostaną zapisane w pamięci podręcznej. Z drugiej strony nie będzie pośrednika.cat
odczytuje duże ilości naraz i zapisuje je od razu bez przetwarzania, i nie musi przydzielać dużej ilości pamięci, tylko ten bufor, którego ponownie używa.Oznacza to również, że jest o wiele bardziej niezawodny, ponieważ nie dusi się w bajtach NUL i nie przycina końcowych znaków nowego wiersza (i nie dzieli split + glob, chociaż można tego uniknąć, cytując zmienną, i nie rozwiń sekwencję zmiany znaczenia, ale możesz tego uniknąć, używając
printf
zamiastecho
).Jeśli chcesz dalej go optymalizować, zamiast wywoływać
cat
kilka razy, po prostu przejdźinput
kilka razy docat
.Uruchomi 3 polecenia zamiast 100.
Aby uczynić wersję zmienną bardziej niezawodną, musisz użyć
zsh
(inne powłoki nie radzą sobie z bajtami NUL) i zrobić to:Jeśli wiesz, że dane wejściowe nie zawierają bajtów NUL, możesz to niezawodnie wykonać POSIXly (choć może nie działać, jeśli
printf
nie jest wbudowane) za pomocą:Ale to nigdy nie będzie bardziej wydajne niż używanie
cat
w pętli (chyba że dane wejściowe są bardzo małe).źródło
/bin/echo $(perl -e 'print "A"x999999')
dd bs=128 < input > /dev/null
zdd bs=64 < input > /dev/null
. Z 0,6 s potrzebnych do bashowania, aby odczytać ten plik, 0,4 są wydawane na teread
wywołania systemowe w moich testach, podczas gdy inne powłoki spędzają tam znacznie mniej czasu.readwc()
itrim()
w Burne Shell zajmują 30% przez cały czas i jest to najprawdopodobniej niedoceniane, ponieważ nie ma libc zgprof
adnotacjąmbtowc()
.\x
rozwinięty?Problem nie dotyczy,
cat
aecho
dotyczy zapomnianej zmiennej cytatu$i
.W skrypcie powłoki podobnym do Bourne'a (z wyjątkiem
zsh
) pozostawienie zmiennych bez cudzysłowu powodujeglob+split
, że operatory na zmiennych.jest aktualne:
Tak więc z każdą iteracją pętli cała treść
input
(z wyłączeniem końcowych znaków nowej linii) będzie rozszerzana, dzielona, globowana. Cały proces wymaga powłoki, aby przydzielić pamięć, analizując ciąg znaków w kółko. To jest powód, dla którego masz słabą wydajność.Możesz zacytować zmienną, aby temu zapobiec,
glob+split
ale to ci niewiele pomoże, ponieważ gdy powłoka nadal musi zbudować argument dużego łańcucha i przeskanować jego zawartośćecho
(Zastąpienie wbudowanegoecho
zewnętrznego/bin/echo
argumentem spowoduje, że lista argumentów będzie za długa lub zabraknie pamięci zależy od$i
rozmiaru). Większośćecho
implementacji nie jest zgodna z POSIX, rozszerzy\x
sekwencje odwrotnego ukośnika w otrzymanych argumentach.Z
cat
, powłoka musi tylko odrodzić proces każdej iteracji pętli icat
wykona kopię we / wy. System może również buforować zawartość pliku, aby proces cat był szybszy.źródło
/*/*/*/*../../../../*/*/*/*/../../../../
może być w treści pliku. Chcę tylko wskazać szczegóły .time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s
time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
glob+split
części, a to przyspieszy pętlę while. Zauważyłem też, że to ci niewiele pomoże. Od kiedy większośćecho
zachowań powłoki nie jest zgodna z POSIX.printf '%s' "$i"
jest lepiej.Jeśli zadzwonisz
pozwala to na zwiększenie procesu powłoki o 50 MB do 200 MB (w zależności od wewnętrznej implementacji szerokiego znaku). Może to spowolnić działanie powłoki, ale nie jest to główny problem.
Głównym problemem jest to, że powyższe polecenie musi wczytać cały plik do pamięci powłoki i
echo $i
musi dokonać podziału pola na zawartość tego pliku$i
. Aby dokonać podziału pola, cały tekst z pliku należy przekonwertować na szerokie znaki i tam spędza się większość czasu.Zrobiłem kilka testów z powolnym przypadkiem i otrzymałem te wyniki:
Powodem, dla którego ksh93 jest najszybszy, wydaje się być to, że ksh93 nie korzysta
mbtowc()
z libc, ale raczej z własnej implementacji.BTW: Stephane myli się, że rozmiar odczytu ma pewien wpływ, skompilowałem powłokę Bourne'a, aby odczytać fragmenty 4096 bajtów zamiast 128 bajtów i uzyskałem taką samą wydajność w obu przypadkach.
źródło
i=`cat input`
Polecenie nie zrobić podział pola, jest toecho $i
, że robi. Czas spędzonyi=`cat input`
będzie pomijalny w porównaniu doecho $i
, ale nie w porównaniu docat input
samego, aw przypadkubash
różnicy jest w dużej mierze ze względu nabash
małe odczyty. Zmiana ze 128 na 4096 nie będzie miała wpływu na wydajnośćecho $i
, ale nie o to mi chodziło.echo $i
będzie się znacznie różnić w zależności od zawartości danych wejściowych i systemu plików (jeśli zawiera IFS lub znaki globalne), dlatego w mojej odpowiedzi nie porównałem powłok. Na przykład tutaj, na wyjściuyes | ghead -c50M
, ksh93 jest najwolniejszy ze wszystkich, ale włączonyyes | ghead -c50M | paste -sd: -
jest najszybszy.W obu przypadkach pętla zostanie uruchomiona tylko dwa razy (raz dla słowa
seq
i raz dla słowa10
).Ponadto oba będą łączyć sąsiednie białe znaki i upuszczać początkowe / końcowe białe znaki, dzięki czemu dane wyjściowe niekoniecznie będą dwiema kopiami danych wejściowych.
Pierwszy
druga
Jednym z powodów, dla których
echo
jest wolniejsze, może być to, że twoja niecytowana zmienna jest dzielona w białych znakach na osobne słowa. Za 50 MB będzie to dużo pracy. Podaj zmienne!Proponuję naprawić te błędy, a następnie ponownie ocenić swoje czasy.
Przetestowałem to lokalnie. Utworzyłem plik 50 MB przy użyciu danych wyjściowych
tar cf - | dd bs=1M count=50
. Rozszerzyłem również pętle, aby działały x razy tak, że czasy zostały skalowane do rozsądnej wartości (dodałem kolejną pętlę wokół całego kodu:for k in $(seq 100); do
...done
). Oto czasy:Jak widać, nie ma prawdziwej różnicy, ale jeśli cokolwiek zawiera wersja,
echo
działa nieznacznie szybciej. Jeśli usunę cytaty i uruchomię zepsutą wersję 2, czas się podwoi, pokazując, że powłoka musi wykonać znacznie więcej pracy, niż można się spodziewać.źródło
cat
jest bardzo, bardzo szybszy niżecho
. Pierwszy skrypt działa średnio 3 sekundy, ale drugi średnio 54 sekundy.tar cf - | dd bs=1M count=50
? Czy tworzy zwykły plik zawierający te same znaki? Jeśli tak, w moim przypadku plik wejściowy jest całkowicie nieregularny ze wszystkimi rodzajami znaków i białych znaków. I znowu użyłemtime
tak, jak ty użyłeś, a wynik był taki, który powiedziałem: 54 sekundy vs 3 sekundy.read
jest znacznie szybszy niżcat
Myślę, że każdy może to przetestować:
cat
zajmuje 9,372 sekundy.echo
zajmuje.232
sekundy.read
jest 40 razy szybszy .Mój pierwszy test, kiedy
$p
echo pokazało ekran,read
był 48 razy szybszy niżcat
.źródło
Ma
echo
to na celu umieszczenie 1 linii na ekranie. W drugim przykładzie robisz to, że umieszczasz zawartość pliku w zmiennej, a następnie drukujesz tę zmienną. W pierwszym od razu umieszczasz zawartość na ekranie.cat
jest zoptymalizowany do tego zastosowania.echo
nie jest. Również umieszczenie 50 Mb w zmiennej środowiskowej nie jest dobrym pomysłem.źródło
echo
byłby zoptymalizowany do pisania tekstu?Nie chodzi o to, aby echo było szybsze, chodzi o to, co robisz:
W jednym przypadku czytasz od wejścia i piszesz bezpośrednio do wyjścia. Innymi słowy, cokolwiek jest czytane z wejścia przez cat, przechodzi do wyjścia przez standardowe wyjście.
W innym przypadku czytasz dane wejściowe do zmiennej w pamięci, a następnie zapisujesz zawartość zmiennej wyjściowej.
Ten ostatni będzie znacznie wolniejszy, szczególnie jeśli wejście ma 50 MB.
źródło