Что-то не так с моим скриптом или Bash намного медленнее чем Python?

29

Я тестировал скорость Bash и Python, выполняя цикл 1 миллиард раз.

$ cat python.py
#!/bin/python
# python v3.5
i=0;
while i<=1000000000:
    i=i+1;

Код Bash:

$ cat bash2.sh
#!/bin/bash
# bash v4.3
i=0
while [[ $i -le 1000000000 ]]
do
let i++
done

Используя timeкоманду, я обнаружил, что завершение кода Python занимает всего 48 секунд, в то время как код Bash занял более 1 часа, прежде чем я убил скрипт.

Почему это так? Я ожидал, что Баш будет быстрее. Что-то не так с моим скриптом или Bash действительно намного медленнее с этим скриптом?

Эдвард Торвальдс
источник
49
Я не совсем уверен, почему вы ожидали, что Bash будет быстрее, чем Python.
Кусалананда
9
@MatijaNalis нет, ты не можешь! Скрипт загружается в память, редактирование текстового файла, из которого он был прочитан (файл скрипта), абсолютно не влияет на работающий скрипт. Хорошая вещь, bash уже достаточно медленный, без необходимости открывать и перечитывать файл каждый раз при запуске цикла!
Тердон
4
Bash читает файл построчно по мере выполнения, но запоминает, что он читает, если снова попадает в эту строку (потому что он находится в цикле или функции). Первоначальное утверждение о перечитывании каждой итерации не соответствует действительности, но изменения в строках, которые еще не достигнуты, будут эффективными. Интересная демонстрация: создайте файл, содержащий echo echo hello >> $0и запустите его.
Майкл Гомер
3
@MatijaNalis ах, хорошо, я могу это понять. Это была идея изменить беговую петлю, которая бросила меня. Предположительно, каждая строка читается последовательно и только после завершения последней. Однако цикл обрабатывается как одна команда и будет прочитан целиком, поэтому его изменение не повлияет на запущенный процесс. Интересное отличие, тем не менее, я всегда предполагал, что весь сценарий загружается в память перед выполнением. Спасибо за указание на это!
Тердон

Ответы:

17

Это известная ошибка в bash; см. справочную страницу и выполните поиск по запросу «ОШИБКИ»:

BUGS
       It's too big and too slow.

;)


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

Самые подходящие выдержки:

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

...

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

...

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

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


Не используйте большие циклы в сценариях оболочки.

Wildcard
источник
54

Циклы оболочки медленные, а bash самые медленные. Снаряды не предназначены для тяжелой работы в петлях. Оболочки предназначены для запуска нескольких внешних оптимизированных процессов в пакетах данных.


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

#!/bin/bash

export IT=$((10**6))

echo POSIX:
for sh in dash bash ksh zsh; do
    TIMEFORMAT="%RR %UU %SS $sh"
    time $sh -c 'i=0; while [ "$IT" -gt "$i" ]; do i=$((i+1)); done'
done


echo C-LIKE:
for sh in bash ksh zsh; do
    TIMEFORMAT="%RR %UU %SS $sh"
    time $sh -c 'for ((i=0;i<IT;i++)); do :; done'
done

G=$((10**9))
TIMEFORMAT="%RR %UU %SS 1000*C"
echo 'int main(){ int i,sum; for(i=0;i<IT;i++) sum+=i; printf("%d\n", sum); return 0; }' |
   gcc -include stdio.h -O3 -x c -DIT=$G - 
time ./a.out

( Подробности:

  • Процессор: Intel® Core ™ TM Процессор M530 с тактовой частотой 2,27 ГГц
  • ksh: версия sh (AT & T Research) 93u + 2012-08-01
  • bash: GNU bash, версия 4.3.11 (1) -релиз (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • тире: 0.5.7-4ubuntu1

)

(Сокращенные) результаты (время на итерацию):

POSIX:
5.8 µs  dash
8.5 µs ksh
14.6 µs zsh
22.6 µs bash

C-LIKE:
2.7 µs ksh
5.8 µs zsh
11.7 µs bash

C:
0.4 ns C

Из результатов:

Если вам нужен немного более быстрый цикл оболочки, то если у вас есть [[синтаксис и вы хотите быстрый цикл оболочки, вы находитесь в расширенной оболочке и у вас также есть цикл C-like for. Тогда используйте C как для цикла. Они могут быть примерно в 2 раза быстрее, чем while [-loops в одной и той же оболочке.

  • У ksh самый быстрый for (цикл - около 2,7 мкс за итерацию
  • У dash самый быстрый while [цикл - около 5,8 мкс за итерацию

C для циклов может быть на 3-4 десятичных порядка быстрее. (Я слышал, что Торвальдс любит C).

Оптимизированный цикл C for в 56500 раз быстрее, чем while [цикл bash (самый медленный цикл оболочки), и в 6750 раз быстрее, чем for (цикл ksh (самый быстрый цикл оболочки).


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

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

Еще одна вещь, чтобы рассмотреть это время запуска.

time python3 -c ' '

занимает от 30 до 40 мс на моем ПК, тогда как оболочка занимает около 3 мс Если вы запускаете много скриптов, это быстро складывается, и вы можете очень много сделать за дополнительные 27-37 мс, которые питон берет, чтобы начать. Небольшие сценарии могут быть завершены несколько раз за этот период времени.

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

PSkocik
источник
Для KSH, Вы можете указать реализацию (AT & T ksh88, AT & T ksh93, pdksh, mksh...) , поскольку есть довольно много вариаций между ними. Для bash, вы можете указать версию. В последнее время достигнут определенный прогресс (это относится и к другим оболочкам).
Стефан Шазелас
@ StéphaneChazelas Спасибо. Я добавил версии используемого программного и аппаратного обеспечения.
PSkocik
Для справки: чтобы создать трубопровод процесса в Python , вы должны сделать что - то вроде: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Это действительно неуклюже, но не должно быть сложно написать pipelineфункцию, которая сделает это за вас для любого числа процессов, что приведет к pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Бакуриу
1
Я подумал, что, возможно, оптимизатор gcc полностью исключил петлю. Это не так, но он все еще выполняет интересную оптимизацию: он использует SIMD-инструкции для параллельного выполнения 4-х операций добавления, сокращая количество итераций цикла до 250000.
Марк Плотник
1
@PSkocik: Это прямо на пороге того, что оптимизаторы могут сделать в 2016 году. Похоже, что C ++ 17 потребует, чтобы компиляторы могли вычислять подобные выражения во время компиляции (даже не в качестве оптимизации). Имея эту возможность C ++, GCC может использовать ее как оптимизацию для C.
MSalters
18

Я провел небольшое тестирование, и в моей системе запустилось следующее - ни один из них не увеличил на порядок, что было бы необходимо для конкурентоспособности, но вы можете сделать это быстрее:

Тест 1: 18.233 с

#!/bin/bash
i=0
while [[ $i -le 4000000 ]]
do
    let i++
done

тест2: 20,45 с

#!/bin/bash
i=0
while [[ $i -le 4000000 ]]
do 
    i=$(($i+1))
done

test3: 17.64 с

#!/bin/bash
i=0
while [[ $i -le 4000000 ]]; do let i++; done

test4: 26,69 с

#!/bin/bash
i=0
while [ $i -le 4000000 ]; do let i++; done

тест5: 12,79 с

#!/bin/bash
export LC_ALL=C

for ((i=0; i != 4000000; i++)) { 
:
}

Важной частью этого последнего является экспорт LC_ALL = C. Я обнаружил, что многие операции bash заканчиваются значительно быстрее, если это используется, в частности, любая функция регулярного выражения. Он также показывает недокументированный синтаксис для использования {} и: в качестве запрета.

Эрик Брандсберг
источник
3
+1 за предложение LC_ALL, я этого не знал.
einpoklum - восстановить Монику
+1 Интересно, как [[это намного быстрее, чем [. Я не знал, что LC_ALL = C (кстати, вам не нужно его экспортировать) имеет значение.
PSkocik
@PSkocik Насколько я знаю, [[это встроенный bash, и [он действительно /bin/[такой же, как /bin/testи внешняя программа. Вот почему это медленнее.
Томсминг
@tomsmending [является встроенным во все общие оболочки (попробуйте type [). Внешняя программа сейчас в основном не используется.
PSkocik
10

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

Оболочка является интерпретатором командной строки, она предназначена для запуска команд и обеспечения их взаимодействия с задачей.

Если вы хотите , чтобы рассчитывать на 1000000000, вы вызываете команду (один) , чтобы рассчитывать, как seq, bc, awkили python/ perl... Запуск 1000000000 [[...]]команды и 1000000000 letкоманды обязано быть очень неэффективными, особенно с bashэто самой медленной оболочкой всех.

В этом отношении оболочка будет намного быстрее:

$ time sh -c 'seq 100000000' > /dev/null
sh -c 'seq 100000000' > /dev/null  0.77s user 0.03s system 99% cpu 0.805 total
$ time python -c 'i=0
> while i <= 100000000: i=i+1'
python -c 'i=0 while i <= 100000000: i=i+1'  12.12s user 0.00s system 99% cpu 12.127 total

Хотя, конечно, большая часть работы выполняется командами, которые вызывает оболочка, как и должно быть.

Теперь вы можете сделать то же самое с python:

python -c '
import os
os.dup2(os.open("/dev/null", os.O_WRONLY), 1);
os.execlp("seq", "seq", "100000000")'

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

Обратите внимание, что вы можете сделать:

python -c 'import os; os.system("seq 100000000 > /dev/null")'

Но на pythonсамом деле будет вызывать оболочку для интерпретации этой командной строки!

Стефан Шазелас
источник
Мне нравится твой ответ. Во многих других ответах обсуждаются улучшенные методы «как», в то время как вы рассматриваете как «почему», так и воспринимаемо «почему нет», обращаясь к ошибке в методологии подхода ОП.
greg.arnott
3

Нет ничего плохого (кроме ваших ожиданий), так как python действительно довольно быстр для некомпилированного языка, см. Https://wiki.python.org/moin/PythonSpeed

Матия Налис
источник
1
Я скорее отговариваю от подобных ответов, это относится к комментариям ИМХО.
LinuxSecurityFreak
2

Помимо комментариев, вы можете немного оптимизировать код , например,

#!/bin/bash
for (( i = 0; i <= 1000000000; i++ ))
do
: # null command
done

Этот код должен занять немного меньше времени.

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

LinuxSecurityFreak
источник
-3

Я заметил существенное отличие bash от использования логически эквивалентных выражений while и while:

time (i=0 ; while ((i<900000)) ; do  i=$((i+1)) ; done )

real    0m5.339s
user    0m5.324s
sys 0m0.000s

time (i=0 ; until ((i=900000)) ; do  i=$((i+1)) ; done )

real    0m0.000s
user    0m0.000s
sys 0m0.000s

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

бесстрашный пингвин
источник
6
Попробуйте с этим ((i==900000)).
Томас
2
Вы используете =для назначения. Это вернет истину немедленно. Никакой петли не будет.
Подстановочный
1
Вы действительно использовали Bash раньше? :)
LinuxSecurityFreak