Чому існує така різниця у часі виконання ехо та кота?


15

Відповідаючи на це запитання, я змусив поставити ще одне запитання:
я вважав, що наступні сценарії роблять те саме, а другий повинен бути набагато швидшим, тому що перший використовує catте, що потрібно відкривати файл знову і знову, але другий відкриває файл лише один раз, а потім просто повторює змінну:

(Правильний код див. У розділі оновлення.)

Спочатку:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

Друге:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

при цьому вхід становить близько 50 мегабайт.

Але коли я спробував другий, він був занадто повільним, оскільки повторення змінної iбуло масовим процесом. У мене також виникли деякі проблеми з другим сценарієм, наприклад, розмір вихідного файлу був меншим, ніж очікувалося.

Я також перевірив довідкову сторінку echoта catпорівняв їх:

echo - відображення рядка тексту

cat - об'єднати файли та друкувати на стандартному виході

Але я не зрозумів різниці.

Так:

  • Чому кішка така швидка, а відлуння так повільне у другому сценарії?
  • Або проблема зі змінною i? (тому що на сторінці чоловіка на echoньому сказано, що він відображає "рядок тексту", і тому, я думаю, він оптимізований лише для коротких змінних, а не для дуже дуже довгих змінних, наприклад i. Однак це лише здогад.)
  • І чому у мене виникли проблеми під час використання echo?

ОНОВЛЕННЯ

Я використовував seq 10замість `seq 10`неправильно. Це відредагований код:

Спочатку:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

Друге:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

(Особлива подяка роайма .)

Однак це не суть проблеми. Навіть якщо цикл виникає лише один раз, я отримую ту ж проблему: catпрацює набагато швидше, ніж echo.


1
а про що cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
Чим echoшвидше. Що вам не вистачає, це те, що ви змушуєте оболонку робити занадто багато роботи, не цитуючи змінні, коли ви їх використовуєте.
roaima

Цитування змінних не є проблемою; проблема - сама змінна i (тобто використання її як проміжного кроку між входом і виходом).
Олександр

`echo $ i` - не робіть цього. Використовуйте printf і цитуйте аргумент.
PSkocik

1
@PSkocik Що я кажу, ти хочеш printf '%s' "$i", ні echo $i. @cuonglm у своїй відповіді пояснює деякі проблеми ехо. Чому навіть цитування недостатньо в деяких випадках із відлунням, дивіться unix.stackexchange.com/questions/65803/…
PSkocik

Відповіді:


24

Тут слід розглянути кілька речей.

i=`cat input`

може бути дорогим, і між оболонками може бути багато варіацій.

Це особливість, яка називається підміна команд. Ідея полягає у збереженні всього виводу команди за мінусом знаків нового рядка у iзмінну в пам'яті.

Для цього оболонки розщеплюють команду в підпакеті і читають її вихід через трубу або сокетпар. Ви бачите тут багато варіацій. У файлі 50MiB тут я бачу, наприклад, що bash у 6 разів повільніше ksh93, але трохи швидше, ніж zsh і вдвічі швидше yash.

Основна причина bashповільності полягає в тому, що він читає з труби 128 байтів одночасно (в той час як інші оболонки читають 4KiB або 8KiB одночасно) і карається накладними викликами системи.

zshпотрібно виконати деяку післяобробку, щоб уникнути байтів NUL (інші оболонки розбиваються на байти NUL), а yashще більше обтяжує обробку, аналізуючи багатобайтові символи.

Усі оболонки повинні знімати символи нового рядка, які вони можуть робити більш-менш ефективно.

Деякі можуть захотіти обробляти байти NUL більш витончено, ніж інші, і перевірити їх наявність.

Потім, коли у вас є така велика змінна пам'ять, будь-яка маніпуляція з нею, як правило, передбачає розподіл більше пам'яті та копіювання даних поперек.

Тут ви передаєте (мали намір передати) вміст змінної в echo.

На щастя, echoвін вбудований у вашу оболонку, інакше виконання, ймовірно, не вдалося б із списком аргументів занадто довгою помилкою. Вже тоді побудова масиву списку аргументів, можливо, передбачає копіювання вмісту змінної.

Інша основна проблема у вашому підході до заміни команди полягає в тому, що ви викликаєте оператор split + glob (забувши процитувати змінну).

Для цього оболонки повинні сприймати рядок як рядок символів (хоча деякі оболонки не мають і є помилковими в цьому відношенні), так що в локалі UTF-8, це означає розбір послідовностей UTF-8 (якщо це не зроблено вже так, як yashце робиться) , шукайте $IFSсимволів у рядку. Якщо $IFSміститься пробіл, вкладка або новий рядок (що за замовчуванням є), алгоритм є ще складнішим і дорогішим. Потім слова, отримані в результаті розщеплення, потрібно виділити та скопіювати.

Глобальна частина буде ще дорожчою. Якщо будь-які з цих слів містять Глоби символи ( *, ?, [), то оболонка буде читати вміст деяких каталогів і зробити деякий дорогою по шаблоном ( bashреалізація «s, наприклад , як відомо , дуже погано в цьому).

Якщо вхід містить щось подібне /*/*/*/../../../*/*/*/../../../*/*/*, це буде надзвичайно дорогим, оскільки це означає перелік тисяч каталогів і може розширитися до декількох сотень МіБ.

Тоді echoзазвичай виконується додаткова обробка. Деякі реалізації розширюють \xпослідовності аргументу, який він отримує, що означає розбір вмісту і, можливо, інше розподілення та копіювання даних.

З іншого боку, гаразд, у більшості оболонок catне вбудований, тобто означає розблокувати процес та виконати його (таким чином завантаження коду та бібліотек), але після першого виклику цей код та вміст вхідного файлу збережеться в пам'яті. З іншого боку, посередника не буде. catбуде читати великі обсяги одночасно і записувати його відразу, не обробляючи, і йому не потрібно виділяти величезну кількість пам'яті, лише той буфер, який він повторно використовує.

Це також означає, що це набагато надійніше, оскільки він не задихається на байтах NUL і не обрізає символи нового рядка (і не робить розділення + глобул, хоча ви можете цього уникнути, цитуючи змінну, і не розгорніть послідовність втечі, хоча ви можете уникнути цього, використовуючи printfзамість echo).

Якщо ви хочете додатково оптимізувати це, замість того, щоб викликати catкілька разів, просто перейдіть inputдо цього cat.

yes input | head -n 100 | xargs cat

Запустить 3 команди замість 100.

Щоб зробити змінну версію більш надійною, вам потрібно буде скористатися zsh(інші оболонки не можуть впоратися з байтами NUL) і зробити це:

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

Якщо ви знаєте, що вхід не містить байтів NUL, то ви можете надійно зробити це POSIXly (хоча він може не працювати там, де printfне вбудований) за допомогою:

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

Але це ніколи не буде більш ефективним, ніж використання catв циклі (якщо тільки вхід не дуже малий).


Варто згадати, що в разі тривалої аргументації ви можете вийти з пам'яті . Приклад/bin/echo $(perl -e 'print "A"x999999')
cuonglm

Ви помиляєтесь з припущенням, що розмір читання має значний вплив, тому прочитайте мою відповідь, щоб зрозуміти справжню причину.
schily

@schily, 409600 читання 128 байт вимагає більше часу (системний час), ніж 800 читання 64 кб. Порівняйте dd bs=128 < input > /dev/nullз dd bs=64 < input > /dev/null. Із 0,6, потрібних для того, щоб прочитати цей файл, 0,4 витрачається на ті readсистемні виклики в моїх тестах, а інші оболонки проводять там набагато менше часу.
Стефан Шазелас

Що ж, ви, здається, не провели реального аналізу продуктивності. Вплив читання дзвінка (при порівнянні різних розмірів читання) є приблизно. 1% усього часу, поки функції readwc() і trim()в оболонці Берна займають 30% всього часу, і це, швидше за все, недооцінено, оскільки немає libc з gprofанотацією mbtowc().
шилі

До чого \xрозширюється?
Мохаммед

11

Проблема не в тому, catа echoв забутій змінній цитати $i.

У скрипті оболонки, подібному до Борна (крім zsh), залишаючи змінними без котировки причину glob+splitоператорів змінних.

$var

насправді:

glob(split($var))

Таким чином, з кожною ітерацією циклу весь вміст input(виключаючи зворотні нові рядки) буде розширюватися, розщеплюватися, глобалізуватися. Увесь процес вимагає, щоб оболонка виділяла пам'ять, розбираючи рядок знову і знову. Ось чому у вас погана робота.

Ви можете процитувати змінну для запобігання, glob+splitале вона не допоможе вам сильно, оскільки коли оболонці все-таки потрібно створити аргумент великого рядка і просканувати його вміст на echoпредмет (Заміна вбудованого echoна зовнішній /bin/echoдасть вам список аргументів занадто довгий або не має пам’яті залежать від $iрозміру). Більшість echoреалізацій не сумісні з POSIX, вони розширять \xпослідовність зворотних косої риски в отриманих аргументах.

З cat, оболонці потрібно лише породжувати обробку кожної ітерації циклу і catвиконувати копію введення-виводу. Система також може кешувати вміст файлів, щоб зробити процес кішки швидшим.


2
@roaima: Ви не згадали про глобальну частину, що може бути величезною причиною, уявляючи щось, що /*/*/*/*../../../../*/*/*/*/../../../../може бути у вмісті файлу. Просто хочу вказати на деталі .
cuonglm

Маю спасибі Навіть без цього терміни
збільшуються

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

Я не зрозумів, чому котирування не може вирішити проблему. Мені потрібно більше опису.
Мохаммед

1
@ mohammad.k: Як я писав у своїй відповіді, цитата змінна запобігає glob+splitчастині, і це прискорить цикл у той час. І я також зазначив, що це вам не дуже допоможе. З тих пір, як більшість echoповедінки оболонки не відповідає POSIX. printf '%s' "$i"краще.
cuonglm

2

Якщо ви телефонуєте

i=`cat input`

це дозволяє вашому процесу оболонки зрости на 50 Мб до 200 МБ (залежно від внутрішньої широкої реалізації символів). Це може зробити вашу оболонку повільною, але це не головна проблема.

Основна проблема полягає в тому, що команда, подана вище, повинна прочитати весь файл в пам'яті оболонки, і echo $iпотрібно зробити розділення поля на вміст цього файлу в $i. Для того, щоб зробити розбиття полів, весь текст з файлу потрібно перетворити на широкі символи, і саме тут витрачається більша частина часу.

Я зробив кілька тестів з повільним випадком і отримав такі результати:

  • Найшвидший - ksh93
  • Далі - мій Борн Шелл (у 2 рази повільніше, ніж кш93)
  • Далі йде баш (у 3 рази повільніше, ніж ksh93)
  • Остання - ksh88 (на 7 разів повільніше, ніж ksh93)

Причина, чому ksh93 є найшвидшим, здається, в тому, що ksh93 використовується не mbtowc()від libc, а з власної реалізації.

BTW: Стефан помиляється, що розмір читання має певний вплив, я склав оболонку Борна для читання в 4096 байтових фрагментах замість 128 байт і отримав однакову продуктивність в обох випадках.


i=`cat input`Команда робить поділ полів, це echo $iщо робить. Час, витрачений на те, i=`cat input`буде незначним порівняно з echo $i, але не порівняно з cat inputпоодинкою, і, у випадку bash, різниця в кращому випадку через bashневеликі читання. Зміна з 128 на 4096 не матиме впливу на продуктивність echo $i, але я це не робив.
Стефан Шазелас

Також зауважте, що продуктивність роботи echo $iбуде значно відрізнятися в залежності від вмісту вхідного файлу та файлової системи (якщо вона містить IFS або глобальні символи), тому я не робив порівняння оболонок із цим у своїй відповіді. Наприклад, тут, на виході yes | ghead -c50M, ksh93 є найповільнішим з усіх, але на yes | ghead -c50M | paste -sd: -, він найшвидший.
Стефан Шазелас

Коли я говорив про загальний час, я говорив про всю реалізацію, і так, звичайно, розділення поля відбувається за допомогою команди echo. і саме тут проводиться більша частина всього часу.
шилі

Ви, звичайно, вірні, що продуктивність залежить від вмісту $ i.
schily

1

В обох випадках цикл буде виконуватися всього двічі (один раз для слова seqі один раз для слова 10).

Крім того, обидва об'єднають сусідній пробіл та відкинуть пробіл / кінець пробілу, так що вихід не обов'язково має дві копії вводу.

Спочатку

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

Друге

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

Однією з причин того, чому echoповільніше, може бути те, що ваша котируемая змінна розділяється на пробіли на окремі слова. Для 50MB це буде багато роботи. Цитуйте змінні!

Я пропоную вам виправити ці помилки, а потім переоцінити свої терміни.


Я перевірив це локально. Я створив файл 50MB, використовуючи вихідний файл tar cf - | dd bs=1M count=50. Я також розширив цикли для виконання з коефіцієнтом x100, щоб тимчасові позначки були масштабовані до розумного значення (я додав ще цикл навколо всього вашого коду: for k in $(seq 100); do... done). Ось терміни:

time ./1.sh

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

time ./2.sh

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

Як бачите, немає реальної різниці, але якщо що-небудь, то версія, яка містить echo, працює трохи швидше. Якщо я видаляю лапки і запускаю вашу розбиту версію 2, час подвоюється, показуючи, що оболонці доведеться зробити набагато більше роботи, якої слід очікувати.

time ./2original.sh

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

Насправді цикл працює 10 разів, а не двічі.
fpmurphy

Я зробив, як ви сказали, але проблема не вирішена. catдуже, дуже швидше, ніж echo. Перший сценарій працює в середньому 3 секунди, а другий - в середньому 54 секунди.
Мохаммед

@ fpmurphy1: Ні. Я спробував свій код. Цикл працює лише двічі, а не 10 разів.
Мохаммед

@ mohammad.k втретє: якщо ви цитуєте свої змінні, проблема усувається.
roaima

@roaima: Що робить команда tar cf - | dd bs=1M count=50? Чи робить це звичайний файл із однаковими символами всередині нього? Якщо так, у моєму випадку вхідний файл абсолютно нерегулярний із усіма видами символів та пробілів. І знову я використовував так, timeяк ви звикли, і результат був такий, який я сказав: 54 секунди проти 3 секунд.
Мохаммед

-1

read набагато швидше, ніж cat

Я думаю, що кожен може перевірити це:

$ 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

catзаймає 9,372 секунди. echoзаймає .232секунди.

readв 40 разів швидше .

Моє перше випробування, коли $pлунало на екрані, readбуло в 48 разів швидше, ніж cat.


-2

Ціль echoпризначена для розміщення 1 рядка на екрані. Що ви робите у другому прикладі, це те, що ви вміщуєте файл у змінну, а потім друкуєте цю змінну. У першому ви негайно викладете вміст на екран.

catоптимізовано для цього використання. echoне. Також розміщення 50Mb у змінній середовища не є хорошою ідеєю.


Цікавий. Чому б не echoбуло оптимізовано для написання тексту?
roaima

2
У стандарті POSIX немає нічого, що говорить про те, що ехо має на меті поставити одну лінію на екран.
fpmurphy

-2

Справа не в тому, що швидке відлуння, а в тому, що ти робиш:

В одному випадку ви читаєте з введення та запису на вихід безпосередньо. Іншими словами, все, що читається з введення через кішку, йде на вихід через stdout.

input -> output

В іншому випадку ви читаєте з введення змінної в пам'ять, а потім записуєте вміст змінної у вихідний.

input -> variable
variable -> output

Останнє буде набагато повільніше, особливо якщо вхід становить 50 Мб.


Я думаю, ви повинні згадати, що кіт повинен відкрити файл, крім копіювання зі stdin та написання його в stdout. Це досконалість другого сценарію, але перший є дуже кращим, ніж другий загалом.
Мохаммед

У другому сценарії немає досконалості; cat потрібно відкрити вхідний файл в обох випадках. У першому випадку stdout cat переходить безпосередньо до файлу. У другому випадку stdout cat переходить спочатку до змінної, а потім ви друкуєте змінну у вихідний файл.
Олександр

@ mohammad.k, у другому сценарії немає чіткої «досконалості».
Wildcard
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.