Щось з моїм сценарієм щось не так, або Bash набагато повільніше, ніж Python?


29

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

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

Баш-код:

$ 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.
Kusalananda

9
@MatijaNalis ні, ти не можеш! Сценарій завантажується в пам'ять, редагування текстового файлу, з якого він був прочитаний (файл сценарію), абсолютно не вплине на запущений сценарій. Хороша річ, bash вже досить повільний, без необхідності відкривати та перечитувати файл кожного разу, коли цикл запускається!
terdon


4
Bash читає файл по рядку під час його виконання, але він запам'ятовує, що він читав, якщо він знову доходив до цього рядка (тому що він знаходиться в циклі чи функції). Оригінальна заява про повторне читання кожної ітерації не відповідає дійсності, але модифікації ще не досягнутих ліній будуть ефективними. Цікава демонстрація: зробіть файл, що містить echo echo hello >> $0, і запустіть його.
Майкл Гомер

3
@MatijaNalis ах, добре, я можу це зрозуміти. Мене кинула ідея змінити ходову петлю. Імовірно, кожен рядок читається послідовно і лише після закінчення останнього. Однак цикл трактується як одна команда і буде прочитаний у повному обсязі, тому зміна не вплине на процес запуску. Цікава відмінність, але я завжди вважав, що весь сценарій завантажений у пам'ять перед виконанням. Дякуємо, що вказали на це!
terdon

Відповіді:


17

Це відома помилка в баші; перегляньте сторінку чоловіка та шукайте "КУРИ":

BUGS
       It's too big and too slow.

;)


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

Найбільш доречні уривки:

Оболонки - мова вищого рівня. Можна сказати, що це навіть не мова. Вони перед усіма перекладачами командного рядка. Завдання виконується тими командами, які ви виконуєте, і оболонка призначена лише для їх оркестрування.

...

IOW, в оболонках, особливо для обробки тексту, ви викликаєте якомога менше утиліт і змушуєте їх співпрацювати із завданням, а не запускати тисячі інструментів послідовно, чекаючи, коли кожен запуститься, запустіть, очистіть перед запуском наступного.

...

Як було сказано раніше, виконання однієї команди має вартість. Величезна вартість, якщо ця команда не вбудована, але навіть якщо вони вбудовані, вартість велика.

І оболонки не були розроблені так, щоб вони не працювали так, вони не претендують на те, що вони виконують мови програмування. Вони не є, вони просто перекладачі командного рядка. Отже, на цьому фронті зроблено невелику оптимізацію.


Не використовуйте великі петлі в сценарії оболонок.


54

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


У всякому разі, мені було цікаво, як порівнюють петлі оболонки, тому я зробив невеликий орієнтир:

#!/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

( Детальніше:

  • CPU: Intel (R) Core (TM) i5 CPU M 430 при 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-подібний цикл. Тоді використовуйте C як для циклу. Вони можуть бути приблизно в 2 рази while [швидшими, ніж -крутки в одній оболонці.

  • ksh має найшвидший for (цикл приблизно в 2,7 мкс за ітерацію
  • тире має найшвидший while [цикл приблизно 5,8 мкс за ітерацію

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

Оптимізований C для циклу в 56500 разів швидший, ніж while [цикл bash (найповільніший цикл оболонок) і в 6750 разів швидший, ніж for (цикл ksh (найшвидший цикл оболонки).


Знову ж таки, повільність оболонок не повинна мати великого значення, тому що типова схема з оболонками полягає у перевантаженні на кілька процесів зовнішніх, оптимізованих програм.

За допомогою цього шаблону оболонки часто полегшують написання сценаріїв із ефективністю, що перевершує сценарії python (останній раз, коли я перевіряв, створення технологічних конвеєрів у python було досить незграбним).

Ще одна річ, яку слід врахувати - час запуску.

time python3 -c ' '

на моєму ПК займає від 30 до 40 мс, тоді як оболонки займають близько 3 мс. Якщо ви запускаєте багато сценаріїв, це швидко додається, і ви можете зробити дуже багато за додаткові 27-37 мс, які пітон потребує лише для початку. Невеликі сценарії можуть бути завершені кілька разів у цей період часу.

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


Для 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

тест3: 17,64с

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

тест4: 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 (BTW вам не потрібно його експортувати) змінився.
PSkocik

@PSkocik Наскільки я знаю, [[це bash вбудований і [є насправді /bin/[, що це те саме, що /bin/test- зовнішня програма. Ось чому таї повільніше.
Tomsmeding

@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



2

Крім коментарів, ви можете трохи оптимізувати код , наприклад

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

Цей код повинен зайняти трохи менше часу.

Але очевидно недостатньо швидко, щоб бути фактично корисним.


-3

Я помітив різку різницю в bash від використання логічно еквівалентних виразів "while" і "till":

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
Ви використовуєте =для призначення. Це повернеться правдою негайно. Жодна петля не відбудеться.
Wildcard

1
Ви раніше використовували Bash? :)
LinuxSecurityFreak
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.