Bash медленный или я делаю что-то неправильно?

Недавно у меня возникла необходимость просмотреть журналы моего сервера Minecraft. Мне нужно было распаковать несколько файлов журналов, заархивированных в формате gzip, и свернуть их в один огромный журнал. А затем я использовал grep для поиска полезной информации (однако я решил это сделать).

Поэтому я решил написать bash-скрипт:

  dir = "/home/minecraft/spigot-1.20.2/logs"

  logs=$(ls $dir)

  start=$SECONDS
  sum = ""

  for log in $logs
  do
      s=$SECONDS
      log_date=$(echo $log | cut -b 1-10)
      unzipped_log = "$(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"$'\n'
      while read -r line
      do
          echo "$line"
      done <<< "$unzipped_log"
      echo The file $log $(du -h $dir/$log) processed in $(($SECONDS - $s))
  done

  s=$SECONDS
  last = "$(cat $dir/latest.log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //")"$'\n'
  date_today=$(date +%Y-%m-%d)
  while read -r line
  do
      echo "$date_today $line"
  done <<< "$last"
  echo The file latest.log processed in $(($SECONDS - $s))

  echo Script worked in $(($SECONDS-$start)) seconds

После запуска source sum_logs.sh | tac > all_logs.txt тайминг составил 60 секунд.

Затем я переписал код на Python:

  import os, gzip, time, re
  from datetime import datetime

  dir = "/home/minecraft/spigot-1.20.2/logs"

  start = time.time()
  time.sleep(1)

  logs = os.listdir(dir)

  for log in logs:
      log_path = os.path.join(dir, log)
      log_date = log[:10]
      try:
          with gzip.open(log_path, 'rt') as f:
              for line in f.readlines():
                  line = re.sub(r'\[Async Chat Thread - #\d+/INFO]: ', '', line)
                  print(log_date, line.replace('\n', ''))
      except:
          with open(log_path, 'r') as f:
              for line in f.readlines():
                  line = re.sub(r'\[Async Chat Thread - #\d+/INFO]: ', '', line)
                  print(log_date, line.replace('\n', ''))

  elapsed = round(time.time() - start)

  print(f'Script worked in {elapsed} seconds')

Время 9 секунд.

Я понимаю, что bash медленнее Python. Но я понятия не имел, что разница в 6 раз. Действительно ли bash такой медленный или есть способ ускорить мой код?

Редактировать.

dir = "/home/minecraft/spigot-1.20.2/logs"

start=$SECONDS
sum = ""

for f in $dir/*
do
    s=$SECONDS
    log=$(echo $f | cut -b 36-)
    log_date=$(echo $log | cut -b 1-10)
    unzipped_log = "$(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"$'\n'
    echo "$unzipped_log"
    echo The file $log $(du -h $dir/$log) processed in $(($SECONDS - $s))
done

s=$SECONDS
date_today=$(date +%Y-%m-%d)
last = "$(cat $dir/latest.log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$date_today /g")"$'\n'
echo $last
echo The file latest.log processed in $(($SECONDS - $s))

echo Script worked in $(($SECONDS-$start)) seconds

Приведенный выше код выполняется за 23 секунды!
Я думаю, основная проблема заключалась во вложенных циклах. Я понятия не имел, что мне пригодится s/^/$date/g.

Редактировать 2.

dir = "/home/minecraft/spigot-1.20.2/logs"

start=$SECONDS
sum = ""

for f in $dir/*
do
    s=$SECONDS
    log=$(echo $f | cut -b 36-)
    log_date=$(echo $log | cut -b 1-10)
    zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g"
    echo The file $log $(du -h $dir/$log) processed in $(($SECONDS - $s))
done

s=$SECONDS
date_today=$(date +%Y-%m-%d)
cat $dir/latest.log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$date_today /g"
echo The file latest.log processed in $(($SECONDS - $s))

echo Script worked in $(($SECONDS-$start)) seconds

Хорошо, теперь это 5 секунд :0. Удалил ненужные переменные, как предлагалось. Я в большом удивлении...

Вы делаете что-то не так, но это не источник вашей проблемы: не используйте ls в скриптах

tripleee 10.05.2024 18:40

@tripleee Понятно, спасибо за совет. Каждый файл здесь имеет либо 2024-05-09-1.log.gz, либо latest.log. Так что в моем случае, я думаю, с ls проблем нет. Но приятно знать.

upconett 10.05.2024 18:55
unzipped_log = "$(zcat ... | sed ...)"; while/read ... echo ... done <<< "$unzipped_log" можно заменить на zcat ... | sed -e ... -e "s/^/$log_date /"; аналогично для 2-го цикла
markp-fuso 10.05.2024 18:56

во втором цикле у вас есть echo "$(date +%Y-%m-%d) $line", который вызывает (дорогой) подад для получения текущей даты; здесь большой удар по производительности, поскольку эта подоболочка вызывается при каждом проходе цикла; еще хуже, каждая подоболочка возвращает одно и то же; вам лучше вызвать эту подоболочку один раз перед циклом (сохранив результат в переменной), а затем сослаться на переменную в echo (еще лучше добавить дополнительное предложение sed -e, чтобы отдать предпочтение всем строкам с $last_date - см. предыдущий комментарий о переписывании 1-го петля)

markp-fuso 10.05.2024 19:02

@markp-fuso О, здорово! Мне нравится ярлык. Это похоже на замену начала строки ^ на date? Я думал, что это будет типа s/^/&$log_date/. Но ваш вариант работает)

upconett 10.05.2024 19:04

поскольку мы говорим о начале строки, то & лишний (в данном случае); несмотря ни на что... when in doubt, try it out

markp-fuso 10.05.2024 19:07

@markp-fuso Что касается второго цикла, я вижу ошибку. Однако цикл работает только с небольшим файлом текущего журнала. Так что в худшем случае это может занять около 5-10 дополнительных секунд. А остальные 50...

upconett 10.05.2024 19:08

есть ли у вас сроки обработки каждого файла? если нет, то добавьте некоторые тайминги (например, date перед обработкой каждого файла; разница между двумя выборками $SECONDS и т. д.); эти вызовы подоболочки (для получения того же date результата) требуют больших затрат времени; в зависимости от количества строк и вашей ОС... эти вызовы $(date ...) могут привести к тому, что последняя обработка файла журнала будет занимать слишком большую часть общего времени

markp-fuso 10.05.2024 19:20

@markp-fuso вау, я не знал о $SECONDS. Переделал все с ним. Однако производительность та же: 65 секунд. Я добавил тайминги в файлы. И большинство из них обрабатываются немедленно. Хотя есть такие и с 27/25/7 секундами. Это большие файлы размером более 400 КБ (остальные ~4 КБ). Обновил рассматриваемый код.

upconett 10.05.2024 19:40

В чем смысл цикла while read? Вы можете просто echo "$unzipped_log" с гораздо меньшими накладными расходами или позволить вашему zcat | sed | ... конвейеру записывать данные напрямую в стандартный вывод и вообще никогда не сохранять содержимое в переменной unzipped_log.

Charles Duffy 10.05.2024 20:02

Как человек, который много пишет как на bash, так и на Python (помимо других языков), я бы посчитал код bash, производительность которого на 1/6 ниже производительности эквивалентного Python, вполне конкурентоспособным. Люди, которые совершают обычные ошибки (например, создают ненужные подоболочки или внешние исполняемые файлы в тесном внутреннем цикле), часто делают сценарии оболочки более чем в 100 раз медленнее, чем эквивалентные Python; на самом деле вы делаете некоторые умеренно неэффективные вещи, но (по большей части, исключая ненужный цикл while read line; do echo "$line"; done) делаете их только один раз для каждого файла, а не один раз для каждой строки.

Charles Duffy 10.05.2024 20:04

Тем не менее, я хотел бы призвать вас не использовать кроличью нору «все имена моих файлов соответствуют этому шаблону, поэтому код, который их неправильно обрабатывает, в порядке». Худшее событие потери данных, с которым я когда-либо лично присутствовал, было вызвано тем, что кто-то сделал такое предположение - затем была добавлена ​​новая программа, создающая файлы в том же каталоге, и библиотека C, используемая этой программой Python («безопасная для памяти» , ха!) имело переполнение буфера, которое сбрасывало случайный мусор в строку, используемую для имен файлов.

Charles Duffy 10.05.2024 20:09

Конечным результатом было то, что мы получили имя файла с * в нем, окруженным пробелами, и когда небрежно написанный сценарий оболочки попытался удалить этот файл, он удалил все наши резервные копии, используемые для поддержки выставления счетов. Это не то место, где вы хотите быть; Просто скажите «нет» расширениям без кавычек, чтобы попытаться обрабатывать строки в переменных оболочки, как если бы они были массивами.

Charles Duffy 10.05.2024 20:09

(также избегайте cat foo | ... в пользу <foo ... или ... <foo - не большая разница, когда ... равен sed, но когда это что-то вроде tail или sort или wc -c, которое можно оптимизировать при наличии доступного для поиска файлового дескриптора, вы можете упустить очень реальный прирост производительности ).

Charles Duffy 10.05.2024 20:12

(и заведите привычку запускать свой код через shellcheck.net - он не найдет ничего, что явно ориентировано на производительность, но доступно множество небольших оптимизаций корректности).

Charles Duffy 10.05.2024 20:15
ls $dir собирается забрать latest.log, так что вам придется обрабатывать latest.log дважды, да? Это то что ты хочешь?
markp-fuso 10.05.2024 20:29

Спасибо всем за поддержку! Я думаю, что код можно оптимизировать дальше, но я рад, что разница уже х2,5, а не х6.

upconett 10.05.2024 21:01

@markp-fuso Этого не происходит, latels.log только один раз

upconett 10.05.2024 21:02

из последнего обновления - unzipped_log = "$(zcat ... | sed ...)" ; echo "$unzipped_log" - зачем нести накладные расходы на а) вызов подоболочки zcat | sed и б) сохранение всех результатов в памяти только для echo ввода следующей строки кода; как уже указывалось в комментариях и принятом ответе... устраните эти накладные расходы, заменив эти 2 строки кода одной строкой zcat | sed; то же самое касается last = "$(zcat | sed)" ; echo "$last"

markp-fuso 10.05.2024 21:06

@markp-fuso Для меня не было очевидным, что переменные будут настолько убийственны для производительности: 0. Спасибо!

upconett 11.05.2024 04:26

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

Charles Duffy 11.05.2024 04:33

Имейте в виду, что еще хуже то, что echo не гарантирует, что ваше содержимое будет побайтно идентично исходным значениям, но это совершенно другая тема. (Если вас волнует надежность, вместо этого используйте printf — см. unix.stackexchange.com/a/65819).

Charles Duffy 11.05.2024 04:35

это не большая проблема с производительностью, но cat здесь не нужен - cat $dir/latest.log | sed -e ... -e ... - поскольку sed способен читать непосредственно из файла; вместо этого рассмотрите sed -e ... -e ... "$dir/latest.log" (также возьмите за привычку заключать ссылки на переменные в двойные кавычки (запустите тест, чтобы увидеть, что произойдет, если dir содержит пробелы и вы не используете двойные кавычки); во втором скрипте у вас есть echo $last, но если $last встроено переводы строк (\n), затем посмотрите на разницу вывода между echo $last и echo "$last"

markp-fuso 11.05.2024 14:59

у вас должна быть возможность сократить еще немного свободного времени, заменив неэффективные конструкции $(echo ... | cut ...); та же проблема, которая уже была избита до смерти... исключите ненужные подоболочки, особенно внутри цикла; Я добавил раздел к (принятому) ответу с более подробной информацией. устранение этих дополнительных 4 подоболочек (для каждого прохода цикла) должно немного сократить общее время; Кстати, вы можете получить немного более детальный тайминг с помощью time for f in "$dir"/*; do ... done

markp-fuso 11.05.2024 15:22
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
25
118
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

С последним обновлением вопроса кажется, что комментарии не имеют достаточного смысла, повторяя предложения...

unzipped_log = "$(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"
while read -r line
do
    echo "$line"
done <<< "$unzipped_log"

Устраните сохранение всех результатов zcat ... | sed ... в переменной и вместо этого передавайте их непосредственно в цикл while/read:

while read -r line
do
    echo "$line"
done < <(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"

Но поскольку единственное, что делает цикл, это «прочитать строку, повторить строку, прочитать строку, повторить строку», мы можем просто исключить цикл:

zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g"

Тот же самый дизайн кода можно использовать для замены второго цикла while/read.

ПРИМЕЧАНИЕ. переместите $date_today в сценарий sed так же, как это было сделано для $log_date.


Конструкция var1=$(echo $var2 | cut -b start-length) слишком сложна и требует двух дополнительных (дорогих) подоболочек. Тот факт, что эта конструкция используется дважды внутри цикла while, усиливает снижение производительности.

С помощью bash 4.2+ мы можем использовать встроенную возможность подстроки:

${var:start:len}

Имейте в виду, что нумерация позиций bash начинается с 0, поэтому, если вы начинаете с x=123456 и хотите, чтобы первые 3 символа: ${x:1:3} => 234 while ${x:0:3} => 123

Используя эту информацию, мы можем изменить текущий код OP следующим образом:

### replace this:

log=$(echo $f | cut -b 36-)
log_date=$(echo $log | cut -b 1-10)

### with this:

log = "${f:35}"
log_date = "${log:0:10}"

ПРИМЕЧАНИЕ. Я предполагаю, что OP имеет дело с однобайтовыми символами, поэтому cut -b и cut -c будут эквивалентны; если OP имеет дело с многобайтовыми символами, то код замены может быть немного более сложным


Конструкцию logs=$(ls $dir) / for log in $logs можно улучшить, но было бы полезно иметь образец реальных имен файлов в каталоге $dir.

Отдельно от разглагольствований в комментариях — имейте в виду, что когда вы делаете sed ..., например, в Bash, вы запускаете целый подпроцесс — подпроцесс, поддерживаемый ОС, с большими накладными расходами — в то время как в Python вызов функции просто запустит пару коды операций. То же самое касается ls и почти всех «команд», за исключением нескольких встроенных. Есть исключения, которые со временем стали встроенными, например echo.

Люди, владеющие Bash, обычно знают, как избежать этих ловушек, и просто не вызывают подпроцесс в циклах. Поэтому люди в комментариях советуют вам «никогда не использовать ls в скрипте».

С другой стороны, такой язык, как Python, имеет стандартный синтаксис, все будет выполняться в одном процессе, и если нужно запустить что-то во внешнем процессе, это совершенно очевидно: вызовы функций будут занимать в 1000 раз меньше ресурсов ОС, чем внешний процесс. процесс запущен.

echo является встроенным, поэтому, если кто-то не заставит /usr/bin/echo, он не попадает в ту же категорию, что ls или sed. (Тем не менее, встроенное echo в bash менее эффективно, чем print() или sys.stdout.write() в Python, поскольку оно должно сбрасываться после каждой операции, тогда как запись или печать в Python могут откладывать сброс, когда стандартный вывод полностью буферизован).
Charles Duffy 10.05.2024 20:25

Спасибо за информацию - я обновляю ее.

jsbueno 10.05.2024 20:30

То, что это подпроцесс, не является причиной отказа от ls в сценариях.

Shawn 10.05.2024 21:53

Другие вопросы по теме