Dlaczego jest taka różnica w czasie wykonania echa i kota?


15

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 cattego, 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 ibył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 echoi, cataby 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ęcznika echojest 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, jak i. Jednak to tylko przypuszczenie.)
  • I dlaczego mam problemy, kiedy używam echo?

AKTUALIZACJA

Użyłem seq 10zamiast `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: catdziała znacznie szybciej niż echo.


1
i co cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
To echojest szybsze. To, czego brakuje, to to, że zmuszasz powłokę do wykonywania zbyt dużej pracy, nie cytując zmiennych podczas ich używania.
roaima,

Cytowanie zmiennych nie jest problemem; problemem jest sama zmienna i (tj. użycie jej jako kroku pośredniego między wejściem a wyjściem).
Aleksander

`echo $ i` - nie rób tego. Użyj printf i zacytuj argument.
PSkocik,

1
@PSkocik Mówię, że chcesz printf '%s' "$i", a nie echo $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/…
PSkocik,

Odpowiedzi:


24

Jest tu kilka rzeczy do rozważenia.

i=`cat input`

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 izmiennej 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 bashspowolnienia 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.

zshmusi wykonać przetwarzanie końcowe, aby uniknąć bajtów NUL (inne powłoki łamią się na bajtach NUL), a yashnawet 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 echojest 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 yashrobi) , poszukaj $IFSznaków w ciągu. Jeśli $IFSzawiera 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 echozazwyczaj wykonuje dodatkowe przetwarzanie. Niektóre implementacje rozszerzają \xsekwencje 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 catnie 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. catodczytuje 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 printfzamiast echo).

Jeśli chcesz dalej go optymalizować, zamiast wywoływać catkilka razy, po prostu przejdź inputkilka razy do cat.

yes input | head -n 100 | xargs cat

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:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

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 printfnie jest wbudowane) za pomocą:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Ale to nigdy nie będzie bardziej wydajne niż używanie catw pętli (chyba że dane wejściowe są bardzo małe).


Warto wspomnieć, że w przypadku długich kłótni możesz stracić pamięć . Przykład/bin/echo $(perl -e 'print "A"x999999')
cuonglm,

Mylisz się z założeniem, że rozmiar odczytu ma znaczący wpływ, więc przeczytaj moją odpowiedź, aby zrozumieć prawdziwy powód.
schily

@schily, wykonanie 409600 odczytów 128 bajtów zajmuje więcej czasu (czas systemowy) niż 800 odczytów 64k. Porównaj dd bs=128 < input > /dev/nullz dd bs=64 < input > /dev/null. Z 0,6 s potrzebnych do bashowania, aby odczytać ten plik, 0,4 są wydawane na te readwywołania systemowe w moich testach, podczas gdy inne powłoki spędzają tam znacznie mniej czasu.
Stéphane Chazelas,

Wydaje się, że nie przeprowadziłeś prawdziwej analizy wydajności. Wpływ wywołania odczytu (przy porównywaniu różnych rozmiarów odczytu) wynosi około. 1% przez cały czas, podczas gdy funkcje readwc() i trim()w Burne Shell zajmują 30% przez cały czas i jest to najprawdopodobniej niedoceniane, ponieważ nie ma libc z gprofadnotacją mbtowc().
schily

Do którego jest \xrozwinięty?
Mohammad,

11

Problem nie dotyczy, cata echodotyczy zapomnianej zmiennej cytatu $i.

W skrypcie powłoki podobnym do Bourne'a (z wyjątkiem zsh) pozostawienie zmiennych bez cudzysłowu powoduje glob+split, że operatory na zmiennych.

$var

jest aktualne:

glob(split($var))

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+splitale to ci niewiele pomoże, ponieważ gdy powłoka nadal musi zbudować argument dużego łańcucha i przeskanować jego zawartość echo(Zastąpienie wbudowanego echozewnętrznego /bin/echoargumentem spowoduje, że lista argumentów będzie za długa lub zabraknie pamięci zależy od $irozmiaru). Większość echoimplementacji nie jest zgodna z POSIX, rozszerzy \xsekwencje odwrotnego ukośnika w otrzymanych argumentach.

Z cat, powłoka musi tylko odrodzić proces każdej iteracji pętli i catwykona kopię we / wy. System może również buforować zawartość pliku, aby proces cat był szybszy.


2
@roaima: Nie wspomniałeś o części glob, która może być ogromnym powodem, obrazując coś, co /*/*/*/*../../../../*/*/*/*/../../../../może być w treści pliku. Chcę tylko wskazać szczegóły .
cuonglm,

Muszę ci podziękować. Nawet bez tego taktowanie podwaja się, gdy używa się zmiennej
niecytowanej

1
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
netmonk

Nie rozumiem, dlaczego cytowanie nie może rozwiązać problemu. Potrzebuję więcej opisu.
Mohammad,

1
@ mohammad.k: Jak napisałem w mojej odpowiedzi, cytat zmiennej zapobiega glob+splitczęści, a to przyspieszy pętlę while. Zauważyłem też, że to ci niewiele pomoże. Od kiedy większość echozachowań powłoki nie jest zgodna z POSIX. printf '%s' "$i"jest lepiej.
cuonglm,

2

Jeśli zadzwonisz

i=`cat input`

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 $imusi 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:

  • Najszybszy jest ksh93
  • Dalej jest moja Bourne Shell (2x wolniej niż ksh93)
  • Dalej jest bash (3x wolniejszy niż ksh93)
  • Ostatni to ksh88 (7x wolniejszy niż ksh93)

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.


i=`cat input`Polecenie nie zrobić podział pola, jest to echo $i, że robi. Czas spędzony i=`cat input`będzie pomijalny w porównaniu do echo $i, ale nie w porównaniu do cat inputsamego, aw przypadku bashróżnicy jest w dużej mierze ze względu na bashmał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.
Stéphane Chazelas

Zauważ też, że wydajność echo $ibę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ściu yes | ghead -c50M, ksh93 jest najwolniejszy ze wszystkich, ale włączony yes | ghead -c50M | paste -sd: -jest najszybszy.
Stéphane Chazelas,

Mówiąc o całkowitym czasie, mówiłem o całej implementacji i tak, oczywiście podział pola odbywa się za pomocą polecenia echo. i tutaj spędza się większość czasu.
schily

Oczywiście masz rację, że wydajność zależy od zawartości $ i.
schily

1

W obu przypadkach pętla zostanie uruchomiona tylko dwa razy (raz dla słowa seqi raz dla słowa 10).

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

#!/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

Jednym z powodów, dla których echojest 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:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Jak widać, nie ma prawdziwej różnicy, ale jeśli cokolwiek zawiera wersja, echodział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ć.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

W rzeczywistości pętla działa 10 razy, a nie dwa razy.
fpmurphy

Zrobiłem tak, jak powiedziałeś, ale problem nie został rozwiązany. catjest bardzo, bardzo szybszy niż echo. Pierwszy skrypt działa średnio 3 sekundy, ale drugi średnio 54 sekundy.
Mohammad

@ fpmurphy1: Nie. Próbowałem mojego kodu. Pętla działa tylko dwa razy, a nie 10 razy.
Mohammad,

@ mohammad.k po raz trzeci: jeśli podasz swoje zmienne, problem zniknie.
roaima,

@roaima: Do czego służy polecenie 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łem timetak, jak ty użyłeś, a wynik był taki, który powiedziałem: 54 sekundy vs 3 sekundy.
Mohammad,

-1

read jest znacznie szybszy niż cat

Myślę, że każdy może to przetestować:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

catzajmuje 9,372 sekundy. echozajmuje .232sekundy.

readjest 40 razy szybszy .

Mój pierwszy test, kiedy $pecho pokazało ekran, readbył 48 razy szybszy niż cat.


-2

Ma echoto 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.

catjest zoptymalizowany do tego zastosowania. echonie jest. Również umieszczenie 50 Mb w zmiennej środowiskowej nie jest dobrym pomysłem.


Ciekawy. Dlaczego nie echobyłby zoptymalizowany do pisania tekstu?
roaima,

2
W standardzie POSIX nic nie mówi, że echo ma na celu umieszczenie jednej linii na ekranie.
fpmurphy

-2

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.

input -> output

W innym przypadku czytasz dane wejściowe do zmiennej w pamięci, a następnie zapisujesz zawartość zmiennej wyjściowej.

input -> variable
variable -> output

Ten ostatni będzie znacznie wolniejszy, szczególnie jeśli wejście ma 50 MB.


Myślę, że musisz wspomnieć, że kot musi otworzyć plik oprócz kopiowania ze standardowego wejścia i zapisywania go na standardowe wyjście. To doskonałość drugiego skryptu, ale pierwszy jest znacznie lepszy niż drugi w ogóle.
Mohammad,

W drugim skrypcie nie ma doskonałości; W obu przypadkach cat musi otworzyć plik wejściowy. W pierwszym przypadku stdout cat przechodzi bezpośrednio do pliku. W drugim przypadku stdout cat najpierw przechodzi do zmiennej, a następnie drukujesz zmienną do pliku wyjściowego.
Aleksander

@ mohammad.k, w drugim skrypcie zdecydowanie nie ma „doskonałości”.
Wildcard
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.