Почему открытие файла происходит быстрее, чем чтение содержимого переменной?

36

В bashскрипте мне нужны различные значения из /proc/файлов. До сих пор у меня есть десятки строк, которые напрямую копируют файлы:

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

Чтобы сделать это более эффективным, я сохранил содержимое файла в переменной и добавил следующее:

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

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

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

То же самое относится и к dashи zsh. Я подозревал особое состояние /proc/файлов как причину, но когда я копирую содержимое /proc/meminfoв обычный файл и использую, результаты совпадают:

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

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

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

Почему открытие файла происходит быстрее, чем чтение того же содержимого из переменной?

Десерт
источник
@ l0b0 Это предположение не ошибочно, вопрос показывает, как я пришел к нему, и ответы объясняют, почему это так. Ваше редактирование теперь дает ответы, больше не отвечая на заглавный вопрос: они не говорят, так ли это.
десерт
ОК, уточнил. Поскольку заголовок был неправильным в подавляющем большинстве случаев, просто не для определенной памяти отображались специальные файлы.
10
@ l0b0 Нет, вот что я спрашиваю здесь: «Я подозревал особое состояние /proc/файлов как причину, но когда я копирую содержимое /proc/meminfoв обычный файл и использую, что результаты совпадают:« Это не специально для /proc/файлы, чтение обычных файлов также быстрее!
десерт

Ответы:

47

Здесь речь идет не об открытии файла, а о прочтении содержимого переменной, а скорее о том, чтобы создать дополнительный процесс или нет.

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfoразветвляется процесс, который запускается, grepкоторый открывается /proc/meminfo(виртуальный файл, в памяти, без дискового ввода-вывода) читает его и соответствует регулярному выражению.

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

Часть о чтении /proc/meminfoнесущественна по сравнению с тем, что ядру не нужно много времени, чтобы генерировать информацию, и не grepнужно много времени, чтобы ее прочитать.

Если вы выполните strace -cэто, вы увидите, что один open()и один read()системные вызовы, используемые для чтения, /proc/meminfo- это арахис по сравнению со всем, что grepнужно для запуска ( strace -cне считая разветвления).

В:

a=$(</proc/meminfo)

В большинстве оболочек, которые поддерживают этот $(<...)оператор ksh, оболочка просто открывает файл и читает его содержимое (и удаляет завершающие символы новой строки). bashотличается и намного менее эффективен в том, что он разветвляет процесс, выполняющий это чтение, и передает данные в родительский канал по каналу. Но здесь это делается один раз, поэтому это не имеет значения.

В:

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

Оболочка должна порождать два процесса, которые работают одновременно, но взаимодействуют друг с другом через канал. Создание этой трубы, ее разрушение, а также запись и чтение из нее имеют небольшую стоимость. Гораздо большая стоимость порождает дополнительный процесс. Планирование процессов также оказывает определенное влияние.

Вы можете обнаружить, что использование <<<оператора zsh делает его немного быстрее:

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

В zsh и bash это делается путем записи содержимого $aво временный файл, что обходится дешевле, чем запуск дополнительного процесса, но, вероятно, не даст вам никакой выгоды по сравнению с получением данных сразу /proc/meminfo. Это все еще менее эффективно, чем ваш подход, который копирует /proc/meminfoна диск, так как запись временного файла выполняется на каждой итерации.

dashне поддерживает здесь-строки, но его heredocs реализованы с помощью канала, который не требует создания дополнительного процесса. В:

 grep '^MemFree' << EOF
 $a
 EOF

Оболочка создает трубу, разветвляет процесс. Дочерний элемент выполняется grepсо своим стандартным вводом-выводом в качестве конца чтения канала, а родительский элемент записывает содержимое на другом конце канала.

Но такая обработка каналов и синхронизация процессов все еще могут быть более дорогостоящими, чем просто получение данных /proc/meminfo.

Содержание /proc/meminfoкороткое и не занимает много времени для производства. Если вы хотите сохранить некоторые циклы ЦП, вы хотите удалить дорогостоящие детали: разветвление процессов и запуск внешних команд.

Подобно:

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

Избегайте, bashчье сопоставление с образцом очень неэффективно. С помощью zsh -o extendedglobвы можете сократить его до:

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

Обратите внимание, что ^это особенность во многих оболочках (Bourne, fish, rc, es и zsh, по крайней мере, с параметром extendedglob), я бы рекомендовал процитировать его. Также обратите внимание, что echoне может использоваться для вывода произвольных данных (отсюда мое использование printfвыше).

Стефан Шазелас
источник
4
В случае с printfвами говорят, что оболочка должна порождать два процесса, но разве printfоболочка не встроена?
Дэвид Конрад
6
@DavidConrad Это так, но большинство оболочек не пытаются проанализировать конвейер, для каких частей он может работать в текущем процессе. Он просто разветвляется и позволяет детям понять это. В этом случае родительский процесс разветвляется дважды; потом ребенок с левой стороны видит встроенное и выполняет его; ребенок с правой стороны видит grepи исполняет.
chepner
1
@DavidConrad, канал представляет собой механизм IPC, поэтому в любом случае обеим сторонам придется работать в разных процессах. Хотя A | Bесть некоторые оболочки, такие как AT & T ksh или zsh, которые выполняются Bв текущем процессе оболочки, если это встроенная, составная или функциональная команда, я не знаю ни одной, которая запускается Aв текущем процессе. Во всяком случае, чтобы сделать это, им придется обрабатывать SIGPIPE сложным образом, как если бы Aон выполнялся в дочернем процессе и без завершения оболочки, чтобы поведение не было слишком удивительным при Bраннем выходе. Намного легче работать Bв родительском процессе.
Стефан Шазелас
Bash поддерживает<<<
Д. Бен Нобл
1
@ D.BenKnoble, я не имел в виду, bashчто не поддерживает <<<, просто что оператор пришел, zshкак $(<...)пришел из ksh.
Стефан Шазелас
6

В первом случае вы просто используете утилиту grep и находите что-то из файла /proc/meminfo, /procэто виртуальная файловая система, поэтому /proc/meminfoфайл находится в памяти, и для его извлечения требуется очень мало времени.

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

Разница заключается в /proc(потому что это в памяти) и трубе, см. Пример ниже:

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
источник
1

Вы вызываете внешнюю команду в обоих случаях (grep). Внешний вызов требует подоболочки. Создание этой оболочки является основной причиной задержки. Оба случая одинаковы, поэтому: одинаковая задержка.

Если вы хотите прочитать внешний файл только один раз и использовать его (из переменной) несколько раз, не выходите из оболочки:

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

Что занимает всего около 0,1 секунды вместо полных 1 секунды для вызова grep.

Исаак
источник