Думаю, не сделаю открытия, если скажу, что в принципе bash-скрипты штука довольно медленная. Но это обычно не критично, потому что все-таки главная задача bash-скриптов — это автоматизация тех действий, которые выполняются руками. Так что по сравнению с ручным выполнением скорость просто фантастическая. Тем не менее, бывает необходимо ускорить скрипты, если обрабатываются какие-то большие объемы данных.
Скорость выполнения скриптов
Есть определенные факторы, которые влияют на скорость выполнения скриптов. К ним можно отнести:
- Использование внешних команд
- Использование излишне сложных или просто неоптимальных регулярных выражений
- Вывод в консоль каких-то данных
- Использование лишних команд
- Использование круглых или фигурных скобок (об этом я писал отдельно)
- и т.д. (можно дополнить в комментариях)
И, соответственно, хороший скрипт
- Не использует специфические программы, которые есть не везде (требуют установки дополнительных пакетов)
- Использует минимум внешних команд
- Использует оптимизированные регулярные выражения (которые, как ни печально, многие вообще не используют), например, с минимальным использование захватывающих квантификаторов. Регулярки — это вообще отдельная интересная тема, по поводу которой могу порекомендовать книгу Джеффри Фридла «Регулярные выражения» (если ваш английский позволяет, то лучше в оригинале) и много практики.
- Не использует переменные для хранения данных, когда можно использовать пайп и запускать два процесса параллельно вместо последовательного запуска
Кроме скорости, естественно, есть еще такой фактор как совместимость. Если вы уверены, что в других оболочках скрипт выполняться не будет, или будет работать медленно, лучше откажитесь от совместимости в пользу скорости. Какой смысл писать скрипты, которые будут в 99% случаев выполняться в bash, но работать будут в 100% случаев медленно? Тем более что bash есть буквально везде, даже на смартфонах и роутерах. В наше время написание совместимых скриптов — это чаще вопрос предпочтений, чем реальной необходимости. Не очень приятного бывает осознавать, что скрипт может работать под разными оболочками, но работает 100% в баше, и, в связи с отсутствием «заточки» под баш, работает он заметно медленнее, чем мог бы.
Поэтому давайте рассмотрим некоторые моменты, касающиеся скорости скриптов на примерах.
Использование внешних команд
Вызов внешних программ работает медленнее, чем внутренних, соответственно, множественные вызовы, например, в цикле, будут работать медленнее, чем внутренние. Пример:
VAR1=123 VAR1=$(echo $VAR1 | sed 's/2.*/45/')
Переменная VAR1 примет значение «145». Но то же самое можно сделать вообще без использования sed’а. Вот так:
VAR1=123 VAR1=${VAR1/2*/45}
Такой вариант лучше не только тем, что не использует sed, но и тем, что не использует echo, и не используется вызов отдельного экземпляра интерпретатора, из которого вызываются echo и sed. Давайте посмотрим, как по времени отличаются 1000 выполнений первых двух команд и вторых двух команд. Первые:
real 0m1.136s user 0m0.052s sys 0m0.064s
Вторые:
real 0m0.004s user 0m0.000s sys 0m0.000s
Ну, тут даже комментировать нечего, циферки очень показательные. А, казалось бы, разница в одной строчке.
Использование регулярных выражений
Как я уже писал выше, тема очень интересная и достаточно специфическая, но тут, я думаю, стоит привести пример. Не исключительно связанный с самой оболочкой bash, но связанный с написанием скриптов. Простой пример. У нас есть некоторый файл, содержащий строки, из которых нам надо получить определенную информацию. Строки выглядят следующим образом: некоторое числовое значение, затем пробел и какая-то строка. Надо заменить пробел на знак подчеркивания.
Первый вариант:
#!/bin/bash sed -r 's/([0-9]) ([a-z])/\1_\2/' file.dat
И второй вариант:
#!/bin/bash sed 's/ /_/' file.dat
Первый вариант вроде как дает более наглядное представление, какой именно пробел мы заменяем (между цифрой и буквой), но у нас по условию в данных нет другого пробела, и указывать это абсолютно не нужно, кроме того, мы вычисляем два выражения для подстановки, что тоже нам не нужно. Второй вариант делает всё по существу. Взяли пробел — тупо вставили на его место знак подчеркивания. Отлично читается, и, естественно, выполняется быстрее. При 10000 записей первый скрипт:
real 0m0.071s user 0m0.068s sys 0m0.000s
Второй скрипт:
real 0m0.019s user 0m0.016s sys 0m0.000s
Что, в общем, логично. Ненужные вычисления ненужных данных. Я уже не говорю про регулярное выражение «(.*)», при использовании которого в скриптах, где обрабатывается много данных, надо обязательно подумать, надо ли использовать звездочку. В общем, у Фридла по этому поводу хорошо написано.
Вывод в консоль данных
Я, естественно, имею ввиду те данные, которые для работы скрипта никакой роли не играют. Сам вывод на экран большого количества данных может также тормозить работу скрипта и, просто отключив ненужный вывод, можно ускорить bash-скрипт. Первый скрипт:
#!/bin/bash sed -r 's/(.*) (.*)/\1_\2/' file.dat
Второй скрипт:
#!/bin/bash sed -r 's/(.*) (.*)/\1_\2/' file.dat > /dev/null
Как видите, разница только в том, что первый скрипт выводит строки в консоль, а второй в /dev/null. По времени они отличаются, естественно, вот первый скрипт:
real 0m0.155s user 0m0.064s sys 0m0.040s
А вот второй:
real 0m0.078s user 0m0.064s sys 0m0.012s
Просто запомните, что вывод на экран большого количества данных может сделать ваш скрипт медленнее. Особенно это заметно, если скрипт работает несколько часов, дублируя какие-то данные на консоль. Можно действительно ускорить скрипт в несколько раз, если просто подумать, что можно исключить, а что действительно нужно для понимания того, что происходит в данный момент.
Использование лишних команд
Это достаточно распространенный вариант, который часто связан с тем, что сначала пишут некоторый скрипт, а потом или в спешке, или по невнимательности забывают убрать лишнее. Самый частый вариант — использовали команду cat для вывода какого-то файла при дебаге парсинга в консоли, и вставили в том же виде в скрипт, хотя можно напрямую grep’у указать брать данные из файла. Или еще один пример: использование лишних команд echo. Команда внутренняя, конечно, но если она часто используется, можно ее все-таки пореже использовать, если необходима максимальная скорость.
Первый скрипт:
#!/bin/bash for i in {1..10000} do echo "1" echo "2" echo "3" done
Второй скрипт:
#!/bin/bash for i in {1..10000} do echo -e "1\n2\n3" done
И, соответственно, время работы этих двух скриптов. Первый:
real 0m0.375s user 0m0.192s sys 0m0.092s
Второй:
real 0m0.225s user 0m0.080s sys 0m0.068s
Что, в общем, логично.
Ну, а про круглые и фигурные скобки я писал отдельную статью, которую вы можете посмотреть здесь.
И, как обычно, жду вопросы, дополнения, комментарии. Наверняка у вас есть свои примеры оптимизации bash-скриптов.
Ну начнем с того, что сама тема довольно стремная — если реально много данных, и важна скорость, то может иметь смысл переписать с bash на что-то другое.
>Если вы уверены, что в других оболочках скрипт выполняться не будет, или будет работать медленно, лучше откажитесь от совместимости в пользу скорости.
Я бы предложил такюу формулировку — «если вы пишете скрипт для своей машины, то может иметь смысл отказаться от совместимости в пользу скорости»
>Тем более что bash есть буквально везде, даже на смартфонах и роутерах.
Не факт — на роутерах может быть и другой шелл — ash например, и какие-то bash-измы могут там не проканать.
>Такой вариант лучше не только тем, что не использует sed, но и тем, что не использует echo, и не используется вызов отдельного экземпляра интерпретатора, из которого вызываются echo и sed.
В свое время здорово наклолся, когда оказалось, что bash в MacOs такие вещи не воспринимает.
>Второй вариант делает всё по существу. Взяли пробел – тупо вставили на его место знак подчеркивания.
Тут стоит пощупать — а tr не быстрее окажется
>Или еще один пример: использование лишних команд echo. Команда внутренняя, конечно, но если она часто используется, можно ее все-таки пореже использовать, если необходима максимальная скорость.
Кстати если за скорость, то вместо echo рекомендуюе printf
> Не факт – на роутерах может быть и другой шелл – ash например, и какие-то bash-измы могут там не проканать.
Сдается мне, что чаще всего как раз другой. Тем не менее, порты баша для роутеров и телефонов есть
> Тут стоит пощупать – а tr не быстрее окажется
Обязательно, но тут немного о другом, о том, что сами регулярные выражения часто бывают избыточными. Sed немного притянут за уши именно для этой задачи, но как пример отлично подходит. Много раз именно с его использованием видел избыточные регулярки, которые можно упростить.