Как `да` пишет в файл так быстро?

58

Позвольте мне привести пример:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

Здесь вы можете видеть, что команда yesзаписывает 11504640строки в секунду, а я могу писать только 1953строки за 5 секунд, используя bash forи echo.

Как предложено в комментариях, есть несколько хитростей, чтобы сделать его более эффективным, но ни один из них не приблизился к скорости yes:

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

Они могут записывать до 20 тысяч строк в секунду. И они могут быть улучшены до:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

Это дает нам до 40 тысяч строк в секунду. Лучше, но все же далеко, из yesкоторого можно написать около 11 миллионов строк в секунду!

Итак, как yesзаписать в файл так быстро?

Pandya
источник
9
Во втором примере у вас есть два внешних вызова команд для каждой итерации цикла, и dateон несколько тяжелый, плюс оболочка должна повторно открывать выходной поток echoдля каждой итерации цикла. В первом примере есть только один вызов команды с одним перенаправлением вывода, и команда является чрезвычайно легкой. Два ни в коем случае не сопоставимы.
CVn
@ MichaelKjörling, ты прав, dateможет быть, тяжеловес, см. Правку на мой вопрос.
Пандя
1
timeout 1 $(while true; do echo "GNU">>file2; done;)это неправильный способ использования, timeout так как timeoutкоманда будет запускаться только после завершения подстановки команды. Использование timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
Муру
1
Сводка ответов: только тратя процессорное время на write(2)системные вызовы, а не на загрузку других системных вызовов, издержки оболочки или даже создание процесса в самом первом примере (который запускается и ожидает dateкаждой строки, напечатанной в файле). Одной секунды записи едва хватает для узкого места на дисковых операциях ввода-вывода (а не на процессоре / памяти) в современной системе с большим объемом оперативной памяти. Если разрешить работать дольше, разница будет меньше. (В зависимости от того, насколько плохую реализацию bash вы используете, и относительной скорости процессора и диска, вы можете даже не насыщать дисковый ввод-вывод bash).
Питер Кордес

Ответы:

65

скорлупа:

yesдемонстрирует поведение, аналогичное большинству других стандартных утилит, которые обычно записывают в FILE STREAM с выводом, буферизованным libC через stdio . Они только делают системный вызов write()каждые 4 КБ (16 КБ или 64 КБ) или независимо от выходного блока BUFSIZ . echoэто write()пер GNU. Это много в режиме коммутации (который не является, по- видимому, столь же дорогостоящим , как контекстно-переключатель ) .

И это вовсе не yesзначит, что, помимо начального цикла оптимизации, это очень простой, крошечный, скомпилированный цикл C, и ваш цикл оболочки ни в коей мере не сравним с программой, оптимизированной для компилятора.


но я был неправ:

Когда я говорил, что yesраньше использовал stdio, я предполагал, что это так, потому что он во многом похож на те, которые используют. Это было не правильно - это только имитирует их поведение таким образом. То, что он на самом деле делает, очень похоже на то, что я сделал ниже с оболочкой: сначала он зацикливается, чтобы сопоставить свои аргументы (или, yесли их нет), пока они не могут больше расти, не превышая BUFSIZ.

Комментарий от источника, непосредственно предшествующего соответствующим forсостояниям цикла:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yesпосле этого делает свой собственный write().


отступление:

(Первоначально включено в вопрос и сохранено для контекста в возможно информативном объяснении, уже написанном здесь) :

Я пытался, timeout 1 $(while true; do echo "GNU">>file2; done;)но не смог остановить цикл.

timeoutПроблема у вас есть с заменой команды - Я думаю , что я получаю сейчас, и могу объяснить , почему он не останавливается. timeoutне запускается, потому что его командная строка никогда не запускается. Ваша оболочка разветвляется на дочернюю оболочку, открывает канал на своем стандартном выводе и читает его. Он прекратит чтение, когда ребенок уйдет, а затем интерпретирует все, что написал ребенок для $IFSискажений и глобальных расширений, и с результатами он заменит все, начиная $(с сопоставления ).

Но если дочерний элемент представляет собой бесконечный цикл, который никогда не записывает в канал, дочерний элемент никогда не прекращает цикл, и timeoutкомандная строка никогда не завершается до того, как (как я полагаю) вы это делаете CTRL-Cи уничтожаете дочерний цикл. Так что никогда неtimeout можете убить цикл, который необходимо завершить, прежде чем он может начаться.


другие timeoutс:

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

Как отмечалось в другом месте, простое перемещение вашего [fd-num] >> named_fileперенаправления на выходную цель цикла, а не только направление вывода туда для зацикленной команды, может существенно улучшить производительность, потому что таким образом, по крайней мере, open()системный вызов должен быть выполнен только один раз. Это также делается ниже с помощью |трубы, предназначенной для вывода на внутренние петли.


прямое сравнение:

Вы могли бы сделать как:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

Это что- то вроде отношения подчиненных команд, описанного ранее, но нет никакого канала, и потомок остается на заднем плане, пока не убьет родителя. В том yesслучае, если родительский объект был фактически заменен с момента появления потомка, оболочка вызывает yes, накладывая свой собственный процесс на новый, поэтому PID остается прежним, а его дочерний элемент-зомби все еще знает, кого убивать в конце концов.


больший буфер:

Теперь давайте посмотрим, как увеличить write()буфер оболочки .

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

Я выбрал это число, потому что выходные строки длиной более 1 КБ делятся на отдельные write()для меня. И вот снова цикл:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

Это в 300 раз превышает объем данных, записанных оболочкой, за то же время, что и в предыдущем тесте. Не слишком потрепанный. Но это не так yes.


Связанный:

В соответствии с запросом, здесь есть более подробное описание, чем просто комментарии к коду о том, что делается здесь по этой ссылке .

mikeserv
источник
@heemayl - может быть? Я не совсем уверен, что понимаю, о чем ты спрашиваешь? когда программа использует stdio для записи вывода, она делает это либо без буферизации (как stderr по умолчанию), либо с буферизацией строки (для терминалов по умолчанию), либо с блокировкой буфера (в основном большинство других вещей устанавливается таким образом по умолчанию) . Мне немного непонятно, что определяет размер выходного буфера, но обычно он составляет 4 КБ. и поэтому функции stdio lib будут собирать свои выходные данные до тех пор, пока они не смогут записать целый блок. ddэто один стандартный инструмент, который определенно не использует, например, stdio. большинство других делают.
mikeserv
3
Версия оболочки выполняет open(существующее) writeИ close(которое, я считаю, все еще ожидает сброса), И создает новый процесс и выполняет его dateдля каждого цикла.
dave_thompson_085
@ dave_thompson_085 - перейти в / dev / chat . и то, что вы говорите, не обязательно верно, как вы можете видеть там. Например, делая это wc -lцикл с bashдля меня получает 1/5 - ый из вывода shконтура делает - bashуправляет чуть более 100k writes()к dash«s 500k.
mikeserv
Извините, я был неоднозначен; Я имел в виду версию оболочки в этом вопросе, которая на момент прочтения имела только оригинальную версию с for((sec0=`date +%S`;...временем управления и перенаправлением в цикле, а не последующими улучшениями.
dave_thompson_085
@ dave_thompson_085 - хорошо. в любом случае, в некоторых фундаментальных моментах ответ был неверным, и, как я надеюсь, он должен быть в значительной степени правильным.
mikeserv
20

Лучший вопрос - почему ваша оболочка пишет файл так медленно. Любая автономная скомпилированная программа, которая ответственно использует системные вызовы записи файлов (не сбрасывая каждый символ за раз), сделает это достаточно быстро. То, что вы делаете, это написание строк на интерпретируемом языке (оболочке), и, кроме того, вы делаете много ненужных операций ввода-вывода. Что yesделает:

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

Что делает ваш скрипт:

  • читает в строке кода
  • интерпретирует код, делая много дополнительных операций, чтобы фактически проанализировать ваш ввод и выяснить, что делать
  • для каждой итерации цикла while (что, вероятно, недешево в интерпретируемом языке):
    • вызовите dateвнешнюю команду и сохраните ее вывод (только в оригинальной версии - в пересмотренной версии вы получаете коэффициент 10, не делая этого)
    • проверить, выполнено ли условие завершения цикла
    • открыть файл в режиме добавления
    • echoКоманда parse , распознает ее (с некоторым кодом соответствия шаблона) как встроенную оболочку, вызывает расширение параметра и все остальное в аргументе «GNU», и, наконец, записывает строку в открытый файл
    • закройте файл снова
    • повторить процесс

Дорогие части: вся интерпретация чрезвычайно дорогая (bash делает очень большую предварительную обработку всех входных данных - ваша строка может потенциально содержать подстановку переменных, подстановку процессов, расширение фигурных скобок, escape-символы и т. Д.), Каждый вызов встроенной функции возможно, оператор switch с редиректом на функцию, которая имеет дело со встроенной функцией, и, что очень важно, вы открываете и закрываете файл для каждой строки вывода. Вы можете выйти >> fileиз цикла while, чтобы сделать его намного быстрее , но вы все еще на интерпретируемом языке. Вам очень повезло, чтоechoэто встроенная оболочка, а не внешняя команда, иначе ваш цикл будет включать создание нового процесса (fork & exec) на каждой отдельной итерации. Что остановило бы процесс - вы видели, насколько это дорого, когда у вас была dateкоманда в цикле.

Орион
источник
11

Другие ответы касались основных моментов. Кстати, вы можете увеличить пропускную способность цикла while, записав в выходной файл в конце вычисления. Для сравнения:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

с участием

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s
Апурв Гупта
источник
Да, это важно, и скорость записи (по крайней мере) удваивается в моем случае
Pandya