Почему такая разница во времени выполнения echo и cat?

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

(Особая благодарность roaima .)

Однако проблема не в этом. Даже если цикл происходит только один раз, я получаю ту же проблему: catработает намного быстрее, чем echo.

Мохаммад
источник
1
а как насчет cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk
2
Это echoбыстрее. Вам не хватает того, что вы заставляете оболочку выполнять слишком много работы, не заключая в кавычки переменные при их использовании.
Ройма
Кавычки переменных не проблема; проблема заключается в самой переменной i (т. е. в ее использовании в качестве промежуточного шага между входом и выходом).
Александр
`echo $ i` - не делай этого. Используйте printf и процитируйте аргумент.
PSkocik
1
@PSkocik Я хочу сказать, что ты хочешь printf '%s' "$i", а не echo $i. @cuonglm хорошо объясняет некоторые проблемы эха в своем ответе. Почему даже в некоторых случаях с эхо недостаточно цитировать, см. Unix.stackexchange.com/questions/65803/…
PSkocik

Ответы:

24

Здесь есть несколько вещей, которые следует учитывать.

i=`cat input`

может быть дорогим, и есть много вариантов между оболочками.

Эта функция называется подстановкой команд. Идея состоит в том, чтобы сохранить весь вывод команды за вычетом символов новой строки в iпеременной в памяти.

Для этого оболочка раскладывает команду в подоболочку и считывает ее вывод через канал или пару сокетов. Вы видите много вариантов здесь. Например, в файле размером 50 МБ, например, bash работает в 6 раз медленнее, чем ksh93, но немного быстрее zsh и в два раза быстрее yash.

Основная причина bashзамедления заключается в том, что он читает из канала 128 байтов за раз (в то время как другие оболочки читают по 4 КБ или 8 КБ за раз) и подвергается штрафным расходам из-за системных вызовов.

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

Всем оболочкам нужно убрать завершающие символы новой строки, которые они могут делать более или менее эффективно.

Некоторые могут захотеть обрабатывать байты NUL более изящно, чем другие, и проверять их наличие.

Затем, когда у вас есть эта большая переменная в памяти, любая манипуляция с ней обычно включает выделение дополнительной памяти и копирование данных.

Здесь вы передаете (намеревались передать) содержимое переменной echo.

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

Другая основная проблема в вашем подходе подстановки команд заключается в том, что вы вызываете оператор split + glob (забывая процитировать переменную в кавычках).

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

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

Если вход содержит что-то вроде этого /*/*/*/../../../*/*/*/../../../*/*/*, это будет очень дорого, поскольку это означает перечисление тысяч каталогов, и это может увеличиться до нескольких сотен МБ.

Затем echoобычно делают дополнительную обработку. Некоторые реализации расширяют \xпоследовательности в полученном аргументе, что означает анализ содержимого и, возможно, другое выделение и копирование данных.

С другой стороны, OK, в большинстве оболочек catне является встроенным, так что это означает, что процесс должен быть разветвлен и выполнен (загрузка кода и библиотек), но после первого вызова этот код и содержимое входного файла будет кешироваться в памяти. С другой стороны, посредника не будет. catбудет читать большое количество за раз и записывать его сразу же без обработки, и ему не нужно выделять огромное количество памяти, только тот буфер, который он использует повторно.

Это также означает, что он намного более надежен, так как не душит байты NUL и не обрезает завершающие символы новой строки (и не использует split + glob, хотя вы можете избежать этого, заключив переменную в кавычки, и не расширить escape-последовательность, хотя вы можете избежать этого, используя 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, выполнение 409600 операций чтения из 128 байтов занимает больше времени (системное время), чем 800 операций чтения из 64 КБ. Сравните dd bs=128 < input > /dev/nullс dd bs=64 < input > /dev/null. Из 0,6 с, необходимых для bash, чтобы прочитать этот файл, 0,4 тратятся на эти readсистемные вызовы в моих тестах, в то время как другие оболочки проводят там гораздо меньше времени.
Стефан Шазелас
Что ж, похоже, вы не провели реальный анализ производительности. Влияние вызова чтения (при сравнении различных размеров чтения) является приблизительным. 1% всего времени, в то время как функции readwc() и trim()в Burne Shell занимают 30% всего времени, и это, скорее всего, недооценено, поскольку нет libc с gprofаннотацией для mbtowc().
15:30
На что \xраспространяется?
Мухаммед
11

Проблема не в том, catа echoв переменной забытой кавычки $i.

В Bourne-подобном сценарии оболочки (кроме zsh), оставляя переменные без кавычек, вызывайте glob+splitоператоры переменных.

$var

на самом деле:

glob(split($var))

Таким образом, с каждой итерацией цикла все содержимое input(исключая завершающие символы новой строки) будет расширено, разделено, выделено. Весь процесс требует оболочки для выделения памяти, разбирая строку снова и снова. Вот почему у вас плохая производительность.

Вы можете заключить переменную в кавычки, чтобы предотвратить, glob+splitно это вам не очень поможет, так как, когда оболочке все еще нужно создать аргумент большой строки и проверить его содержимое echo(замена встроенной функции echoexternal /bin/echoдаст вам список аргументов слишком длинный или нехватки памяти зависит от $iразмера). Большая часть echoреализации не совместима с POSIX, она расширит \xпоследовательности обратной косой черты в полученных аргументах.

При catэтом оболочке нужно только порождать процесс на каждой итерации цикла и catвыполнять копирование ввода-вывода. Система также может кэшировать содержимое файла, чтобы ускорить процесс cat.

cuonglm
источник
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часть, и это ускорит цикл while. И я также отметил, что это вам не сильно поможет. С тех пор, когда большая часть echoповедения оболочки не соответствует POSIX. printf '%s' "$i"лучше.
Cuonglm
2

Если вы позвоните

i=`cat input`

это позволяет вашему процессу оболочки увеличиваться на 50 МБ до 200 МБ (в зависимости от внутренней реализации широких символов). Это может замедлить работу вашей оболочки, но это не главная проблема.

Основная проблема заключается в том, что приведенная выше команда должна прочитать весь файл в память оболочки и echo $iвыполнить деление полей в содержимом этого файла $i. Чтобы разделить поля, весь текст из файла необходимо преобразовать в широкие символы, и именно на это тратится большая часть времени.

Я сделал несколько тестов с медленным случаем и получил эти результаты:

  • Самый быстрый ksh93
  • Далее мой Bourne Shell (в 2 раза медленнее, чем ksh93)
  • Далее идет bash (в 3 раза медленнее, чем ksh93)
  • Последний - ksh88 (в 7 раз медленнее, чем ksh93)

Причина, по которой ksh93 является самой быстрой, заключается в том, что ksh93 использует не mbtowc()libc, а собственную реализацию.

КСТАТИ: Стефан ошибается, что размер чтения имеет некоторое влияние, я скомпилировал оболочку Bourne для чтения 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 -c50Mksh93 самый медленный из всех, но yes | ghead -c50M | paste -sd: -он самый быстрый.
Стефан Шазелас
Говоря об общем времени, я говорил обо всей реализации и, конечно, разделение полей происходит с помощью команды echo. и именно здесь проводится большая часть всего времени.
Щили
Вы, конечно, правы, что производительность зависит от содержания od $ i.
Щил
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это медленнее, может быть то, что ваша переменная без кавычек разделяется в пробеле на отдельные слова. Для 50 МБ это будет много работы. Цитировать переменные!

Я предлагаю вам исправить эти ошибки, а затем пересмотреть время.


Я проверил это локально. Я создал файл размером 50 МБ, используя выводtar cf - | dd bs=1M count=50 . Я также расширил циклы, чтобы они выполнялись с коэффициентом х100, чтобы время было уменьшено до разумного значения (я добавил еще один цикл вокруг всего кода: 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
roaima
источник
На самом деле цикл выполняется 10 раз, а не дважды.
fpmurphy
Я сделал, как вы сказали, но проблема не была решена. catочень, очень быстро, чем echo. Первый скрипт выполняется в среднем за 3 секунды, а второй - за 54 секунды.
Мухаммед
@ fpmurphy1: нет. Я попробовал свой код. Цикл выполняется только дважды, а не 10 раз.
Мухаммед
@ mohammad.k в третий раз: если вы процитируете свои переменные, проблема исчезнет.
Ройма
@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.

WinEunuuchs2Unix
источник
-2

echo поставить 1 строку на экране. Во втором примере вы делаете то, что вы помещаете содержимое файла в переменную, а затем печатаете эту переменную. В первом вы сразу же выводите контент на экран.

catоптимизирован для этого использования. echoне является. Также не рекомендуется помещать 50 Мбайт в переменную окружения.

Marco
источник
Любопытно. Почему бы не echoбыть оптимизированным для написания текста?
Ройма
2
В стандарте POSIX нет ничего, что гласит, что echo предназначен для размещения одной строки на экране.
fpmurphy
-2

Дело не в том, что эхо быстрее, а в том, что вы делаете:

В одном случае вы читаете с ввода и пишете на вывод напрямую. Другими словами, все, что читается с ввода через cat, отправляется на вывод через stdout.

input -> output

В другом случае вы читаете из ввода в переменную в памяти и затем записываете содержимое переменной в выводе.

input -> variable
variable -> output

Последнее будет намного медленнее, особенно если ввод 50 МБ.

Александр
источник
Я думаю, вы должны упомянуть, что cat должен открывать файл в дополнение к копированию из stdin и записи его в stdout. Это превосходство второго сценария, но первый лучше всего второго.
Мухаммед
Во втором сценарии нет превосходства; кошка должна открыть входной файл в обоих случаях. В первом случае стандартный вывод cat идет прямо в файл. Во втором случае стандартный вывод cat идет сначала в переменную, а затем вы выводите переменную в выходной файл.
Александр
@ mohammad.k, во втором сценарии категорически нет «совершенства».
Wildcard