Dlaczego otwieranie pliku jest szybsze niż czytanie zmiennej treści?

36

W bashskrypcie potrzebuję różnych wartości z /proc/plików. Do tej pory mam kilkadziesiąt wierszy bezpośrednio w plikach:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo

Aby uczynić to bardziej wydajnym, zapisałem zawartość pliku w zmiennej i grep, że:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'

Zamiast otwierać plik wiele razy, powinno to po prostu otworzyć go raz i grepować zawartość zmiennej, co, jak zakładam, będzie szybsze - ale w rzeczywistości jest wolniejsze:

bash 4.4.19 $ time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null
real    0m0.803s
user    0m0.619s
sys     0m0.232s
bash 4.4.19 $ a=$(</proc/meminfo)
bash 4.4.19 $ time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null
real    0m1.182s
user    0m1.425s
sys     0m0.506s

To samo dotyczy dashi zsh. Podejrzewałem specjalny stan /proc/plików jako przyczynę, ale kiedy kopiuję zawartość /proc/meminfodo zwykłego pliku i używam, że wyniki są takie same:

bash 4.4.19 $ cat </proc/meminfo >meminfo
bash 4.4.19 $ time for i in $(seq 1 1000);do grep ^MemFree meminfo; done >/dev/null
real    0m0.790s
user    0m0.608s
sys     0m0.227s

Użycie ciągu tutaj do zapisania potoku sprawia, że ​​jest on nieco szybszy, ale nadal nie tak szybki jak w przypadku plików:

bash 4.4.19 $ time for i in $(seq 1 1000);do <<<"$a" grep ^MemFree; done >/dev/null
real    0m0.977s
user    0m0.758s
sys     0m0.268s

Dlaczego otwieranie pliku jest szybsze niż czytanie tej samej treści ze zmiennej?

deser
źródło
@ l0b0 To założenie nie jest błędne, pytanie pokazuje, jak to wymyśliłem, a odpowiedzi wyjaśniają, dlaczego tak jest. Twoja edycja sprawia, że ​​odpowiedzi nie odpowiadają już na pytanie tytułowe: Nie mówią, czy tak jest.
deser
OK, wyjaśnione. Ponieważ nagłówek był niepoprawny w zdecydowanej większości przypadków, po prostu nie dla niektórych plików specjalnych odwzorowanych w pamięci.
l0b0
@ l0b0 Nie, to co pytam tutaj: „Podejrzewałem szczególny stan /proc/plików jako powód, ale kiedy skopiować zawartość /proc/meminfodo pliku regularnego i stosowania, że wyniki są takie same:” To nie specjalny do /proc/plików, czytanie zwykłych plików jest również szybsze!
deser

Odpowiedzi:

47

Tutaj nie chodzi o otwarcie pliku, a nie o odczytanie zawartości zmiennej, ale o wymyślenie dodatkowego procesu lub nie.

grep -oP '^MemFree: *\K[0-9]+' /proc/meminforozwidla proces, który wykonuje grep, który otwiera /proc/meminfo(plik wirtualny, w pamięci, brak dysku I / O zaangażowany) odczytuje ją i pasuje do wyrażenia regularnego.

Najdroższą częścią jest rozwidlenie procesu i załadowanie narzędzia grep i jego zależności od biblioteki, wykonanie dynamicznego łączenia, otwarcie bazy danych ustawień regionalnych, dziesiątek plików na dysku (ale prawdopodobnie buforowanych w pamięci).

Część dotycząca czytania /proc/meminfojest nieznaczna w porównaniu z tym, że jądro potrzebuje niewiele czasu na wygenerowanie informacji i greppotrzebuje czasu na ich odczytanie.

Jeśli się strace -cna tym uruchomisz , zobaczysz, że jeden open()i jeden read()wywołania systemowe używane do odczytu /proc/meminfoto orzeszki ziemne w porównaniu do wszystkiego, co greprobi na początku ( strace -cnie liczy forkingu).

W:

a=$(</proc/meminfo)

W większości powłok obsługujących tego $(<...)operatora ksh powłoka po prostu otwiera plik i odczytuje jego zawartość (i usuwa końcowe znaki nowego wiersza). bashjest inny i znacznie mniej wydajny, ponieważ powoduje, że proces dokonuje odczytu i przekazuje dane do elementu nadrzędnego za pomocą potoku. Ale tutaj zrobiono to raz, więc to nie ma znaczenia.

W:

printf '%s\n' "$a" | grep '^MemFree'

Powłoka musi odrodzić dwa procesy, które działają jednocześnie, ale współdziałają ze sobą za pomocą potoku. Tworzenie fajki, burzenie, pisanie i czytanie z niej ma niewielki koszt. Znacznie większy koszt to pojawienie się dodatkowego procesu. Planowanie procesów również ma pewien wpływ.

Może się okazać, że użycie <<<operatora zsh sprawia, że ​​jest to nieco szybsze:

grep '^MemFree' <<< "$a"

W Zsh i Bash odbywa się to poprzez zapisanie zawartości $apliku tymczasowego, który jest tańszy niż tworzenie dodatkowego procesu, ale prawdopodobnie nie przyniesie żadnych korzyści w porównaniu z natychmiastowym pobraniem danych /proc/meminfo. Jest to nadal mniej wydajne niż podejście, które kopiuje się /proc/meminfona dysk, ponieważ zapisywanie pliku tymczasowego odbywa się przy każdej iteracji.

dashnie obsługuje ciągów tutaj, ale jego heredoki są implementowane za pomocą potoku, który nie wymaga odrodzenia dodatkowego procesu. W:

 grep '^MemFree' << EOF
 $a
 EOF

Powłoka tworzy potok, rozwidla proces. Dziecko wykonuje grepze swoim stdin jako końcem odczytu potoku, a rodzic zapisuje zawartość na drugim końcu potoku.

Ale obsługa rur i synchronizacja procesów nadal prawdopodobnie będzie droższa niż zwykłe uzyskiwanie danych /proc/meminfo.

Treść /proc/meminfojest krótka i zajmuje niewiele czasu. Jeśli chcesz zapisać niektóre cykle procesora, chcesz usunąć kosztowne części: rozwidlanie procesów i uruchamianie zewnętrznych poleceń.

Lubić:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}

Unikaj bashjednak tego, czy dopasowanie wzorca jest bardzo nieskuteczne. Za pomocą zsh -o extendedglobmożesz skrócić go do:

memfree=${${"$(</proc/meminfo)"##*MemFree: #}%%$'\n'*}

Zauważ, że ^jest wyjątkowy w wielu powłokach (Bourne, fish, rc, es i zsh przynajmniej z opcją Extendedglob), polecam zacytowanie go. Zauważ też, że echonie można go użyć do wyprowadzenia dowolnych danych (stąd moje użycie printfpowyżej).

Stéphane Chazelas
źródło
4
W przypadku, ze printfmówisz powłoka musi tarło dwa procesy, ale nie jest printfwbudowanym poleceniem powłoki?
David Conrad
6
@DavidConrad Jest, ale większość powłok nie próbuje analizować potoku, dla którego części mógłby działać w bieżącym procesie. Po prostu się rozwidla i pozwala dzieciom to rozgryźć. W takim przypadku proces nadrzędny rozwidla się dwukrotnie; dziecko po lewej stronie widzi wbudowane i wykonuje je; dziecko po prawej stronie widzi grepi wykonuje.
chepner
1
@DavidConrad, rura jest mechanizmem IPC, więc w każdym przypadku obie strony będą musiały działać w różnych procesach. Podczas gdy wewnątrz A | Bsą pewne powłoki, takie jak AT&T ksh lub zsh, które działają Bw bieżącym procesie powłoki, jeśli jest to polecenie wbudowane, złożone lub funkcyjne, nie znam żadnej, która działałaby Aw bieżącym procesie. Aby to zrobić, musieliby obsługiwać SIGPIPE w skomplikowany sposób, tak jakby Adziałał w procesie potomnym i bez przerywania powłoki, aby zachowanie nie było zbyt zaskakujące przy Bwcześniejszym wyjściu. Znacznie łatwiej jest uruchomić Bproces nadrzędny.
Stéphane Chazelas
Obsługa Bash<<<
D. Ben Knoble
1
@ D.BenKnoble, nie chciałem się sugerować, bashnie wspiera <<<, tak że operator pochodziły z zshjak $(<...)przyszedł z ksh.
Stéphane Chazelas
6

W pierwszym przypadku po prostu używasz narzędzia grep i znajdujesz coś z pliku /proc/meminfo, /procjest to wirtualny system plików, więc /proc/meminfoplik znajduje się w pamięci i pobranie jego zawartości zajmuje bardzo mało czasu.

Ale w drugim przypadku tworzysz potok, a następnie przekazujesz wynik pierwszego polecenia do drugiego polecenia za pomocą tego potoku, co jest kosztowne.

Różnica wynika z /proc(ponieważ jest w pamięci) i potoku, zobacz poniższy przykład:

time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null

real    0m0.914s
user    0m0.032s
sys     0m0.148s


cat /proc/meminfo > file
time for i in {1..1000};do grep ^MemFree file;done >/dev/null

real    0m0.938s
user    0m0.032s
sys     0m0.152s


time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null

real    0m1.016s
user    0m0.040s
sys     0m0.232s
Prvt_Yadav
źródło
1

W obu przypadkach wywołujesz zewnętrzne polecenie (grep). Wywołanie zewnętrzne wymaga podpowłoki. Rozwidlenie tej skorupy jest podstawową przyczyną opóźnienia. Oba przypadki są podobne, a zatem: podobne opóźnienie.

Jeśli chcesz odczytać plik zewnętrzny tylko raz i użyć go (ze zmiennej) wiele razy, nie wychodź z powłoki:

meminfo=$(< /dev/meminfo)    
time for i in {1..1000};do 
    [[ $meminfo =~ MemFree:\ *([0-9]*)\ *.B ]] 
    printf '%s\n' "${BASH_REMATCH[1]}"
done

Co zajmuje tylko około 0,1 sekundy zamiast pełnego 1 sekundy dla połączenia grep.

Izaak
źródło