Apakah ada yang salah dengan skrip saya atau Bash jauh lebih lambat dari Python?

29

Saya sedang menguji kecepatan Bash dan Python dengan menjalankan loop 1 miliar kali.

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

Kode bash:

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

Menggunakan timeperintah saya menemukan bahwa kode Python hanya membutuhkan waktu 48 detik untuk menyelesaikannya sementara kode Bash mengambil lebih dari 1 jam sebelum saya membunuh skrip.

Kenapa begitu? Saya berharap bahwa Bash akan lebih cepat. Apakah ada yang salah dengan skrip saya atau Bash jauh lebih lambat dengan skrip ini?

Edward Torvalds
sumber
49
Saya tidak yakin mengapa Anda mengharapkan Bash lebih cepat dari Python.
Kusalananda
9
@MatijaNalis tidak, Anda tidak bisa! Script dimuat ke dalam memori, mengedit file teks yang dibacanya (file skrip) sama sekali tidak akan berpengaruh pada skrip yang sedang berjalan. Untung juga, bash sudah cukup lambat tanpa harus membuka dan membaca kembali file setiap kali loop dijalankan!
terdon
4
Bash membaca file baris demi baris saat dijalankan, tetapi ia mengingat apa yang dibaca jika kembali ke baris itu (karena itu dalam satu lingkaran, atau suatu fungsi). Klaim asli tentang membaca kembali setiap iterasi tidak benar, tetapi modifikasi pada garis yang belum dijangkau akan efektif. Demonstrasi yang menarik: buat file berisi echo echo hello >> $0, dan jalankan.
Michael Homer
3
@MatijaNalis ah, oke, saya bisa mengerti itu. Gagasan untuk mengubah loop berjalan yang melemparkan saya. Agaknya, setiap baris dibaca secara berurutan dan hanya setelah yang terakhir selesai. Namun, loop diperlakukan sebagai satu perintah dan akan dibaca secara keseluruhan, jadi mengubahnya tidak akan memengaruhi proses yang sedang berjalan. Perbedaan yang menarik, saya selalu berasumsi bahwa seluruh skrip dimuat ke dalam memori sebelum dieksekusi. Terima kasih telah menunjukkannya!
terdon

Jawaban:

17

Ini adalah bug yang dikenal di bash; lihat halaman manual dan cari "BUGS":

BUGS
       It's too big and too slow.

;)


Untuk primer yang bagus tentang perbedaan konseptual antara skrip shell dan bahasa pemrograman lainnya, saya sangat merekomendasikan membaca:

Kutipan yang paling relevan:

Kerang adalah bahasa tingkat yang lebih tinggi. Orang mungkin mengatakan itu bahkan bukan bahasa. Mereka di depan semua penerjemah baris perintah. Pekerjaan dilakukan oleh perintah-perintah yang Anda jalankan dan shell hanya dimaksudkan untuk mengaturnya.

...

TKI, dalam shell, terutama untuk memproses teks, Anda memanggil utilitas sesedikit mungkin dan meminta mereka bekerja sama untuk tugas tersebut, tidak menjalankan ribuan alat secara berurutan menunggu masing-masing untuk memulai, menjalankan, membersihkan sebelum menjalankan yang berikutnya.

...

Seperti yang dikatakan sebelumnya, menjalankan satu perintah memiliki biaya. Biaya besar jika perintah itu tidak dibangun, tetapi bahkan jika mereka dibangun, biayanya besar.

Dan shell tidak dirancang untuk berjalan seperti itu, mereka tidak berpura-pura menjadi bahasa pemrograman yang tampil. Mereka bukan, mereka hanya penafsir baris perintah. Jadi, sedikit optimasi telah dilakukan di bagian depan ini.


Jangan gunakan loop besar dalam skrip shell.

Wildcard
sumber
54

Loop shell lambat dan bash adalah yang paling lambat. Kerang tidak dimaksudkan untuk melakukan pekerjaan berat di loop. Shell dimaksudkan untuk meluncurkan beberapa proses eksternal dan dioptimalkan pada kumpulan data.


Ngomong-ngomong, saya penasaran bagaimana perbandingan shell jadi saya membuat sedikit patokan:

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

( Detail:

  • CPU: Intel (R) Core (TM) i5 CPU M 430 @ 2.27GHz
  • ksh: versi sh (Penelitian AT&T) 93u + 2012-08-01
  • bash: GNU bash, versi 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • tanda hubung: 0.5.7-4ubuntu1

)

Hasil (disingkat) (waktu per iterasi) adalah:

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

Dari hasil:

Jika Anda ingin loop shell sedikit lebih cepat, maka jika Anda memiliki [[sintaks dan Anda ingin loop shell cepat, Anda berada di shell maju dan Anda juga memiliki C-seperti untuk loop. Gunakan C suka untuk loop, lalu. Mereka bisa sekitar 2 kali lebih cepat dari while [-Loop di shell yang sama.

  • ksh memiliki for (loop tercepat sekitar 2,7 μs per iterasi
  • dash memiliki while [loop tercepat sekitar 5,8 μs per iterasi

C untuk loop dapat menjadi 3-4 desimal dari besarnya lebih cepat. (Saya mendengar cinta Torvalds C).

C untuk loop yang dioptimalkan adalah 56500 kali lebih cepat dari while [loop bash (loop shell paling lambat) dan 6750 kali lebih cepat dari for (loop ksh (loop shell tercepat).


Sekali lagi, lambatnya shell seharusnya tidak terlalu menjadi masalah, karena pola khas shell adalah untuk membongkar beberapa proses program eksternal yang dioptimalkan.

Dengan pola ini, kerang sering membuatnya jauh lebih mudah untuk menulis skrip dengan kinerja lebih unggul daripada skrip python (terakhir kali saya memeriksa, membuat proses pipa dengan python agak canggung).

Hal lain yang perlu dipertimbangkan adalah waktu startup.

time python3 -c ' '

membutuhkan 30 hingga 40 ms pada PC saya sedangkan cangkang memakan waktu sekitar 3ms. Jika Anda meluncurkan banyak skrip, ini dengan cepat bertambah dan Anda dapat melakukan sangat banyak dalam 27-37 ms ekstra yang dibutuhkan python hanya untuk memulai. Skrip kecil dapat diselesaikan beberapa kali dalam jangka waktu tersebut.

(NodeJs mungkin runtime scripting terburuk di departemen ini karena dibutuhkan sekitar 100 ms hanya untuk memulai (meskipun begitu sudah dimulai, Anda akan sulit sekali menemukan pemain yang lebih baik di antara bahasa scripting)).

PSkocik
sumber
Untuk ksh, Anda mungkin ingin menentukan pelaksanaan (AT & T ksh88, AT & T ksh93, pdksh, mksh...) karena ada cukup banyak variasi di antara mereka. Sebab bash, Anda mungkin ingin menentukan versi. Ini membuat beberapa kemajuan akhir-akhir ini (yang berlaku juga untuk shell lain).
Stéphane Chazelas
@ StéphaneChazelas Terima kasih. Saya menambahkan versi perangkat lunak dan perangkat keras yang digunakan.
PSkocik
Untuk referensi: untuk membuat pipa proses python Anda harus melakukan sesuatu seperti: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Ini memang canggung, tetapi seharusnya tidak sulit untuk mengkodekan pipelinefungsi yang melakukan ini untuk Anda untuk sejumlah proses, yang menghasilkan pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu
1
Saya pikir mungkin pengoptimal gcc benar-benar menghilangkan loop. Tidak, tetapi masih melakukan optimasi yang menarik: menggunakan instruksi SIMD untuk melakukan 4 penambahan secara paralel, mengurangi jumlah iterasi loop ke 250000.
Mark Plotnick
1
@PSkocik: Ini tepat di ujung apa yang dapat dilakukan pengoptimal pada 2016. Sepertinya C ++ 17 akan mengamanatkan bahwa kompiler harus dapat menghitung ekspresi yang sama pada waktu kompilasi (bahkan bukan sebagai optimasi). Dengan kemampuan C ++ di tempat, GCC dapat mengambilnya sebagai optimisasi untuk C juga.
MSalters
18

Saya melakukan sedikit pengujian, dan pada sistem saya menjalankan yang berikut - tidak ada yang membuat urutan percepatan yang diperlukan untuk menjadi kompetitif, tetapi Anda dapat membuatnya lebih cepat:

Tes 1: 18.233s

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

test2: 20.45s

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

test3: 17.64s

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

test4: 26.69s

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

test5: 12.79s

#!/bin/bash
export LC_ALL=C

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

Bagian penting dalam yang terakhir ini adalah ekspor LC_ALL = C. Saya telah menemukan bahwa banyak operasi bash berakhir secara signifikan lebih cepat jika ini digunakan, khususnya fungsi regex apa pun. Itu juga memperlihatkan sintaks yang tidak didokumentasikan untuk menggunakan {} dan: sebagai no-op.

Erik Brandsberg
sumber
3
+1 untuk saran LC_ALL, saya tidak tahu itu.
einpoklum - mengembalikan Monica
+1 Menarik betapa [[jauh lebih cepat daripada [. Saya tidak tahu LC_ALL = C (BTW Anda tidak perlu mengekspornya) membuat perbedaan.
PSkocik
@PSkocik Sejauh yang saya tahu, [[adalah bash builtin, dan [benar-benar /bin/[, yang sama dengan /bin/test- program eksternal. Itu sebabnya thay lebih lambat.
tomsmeding
@tomsmending [adalah builtin di semua shell umum (coba type [). Program eksternal sebagian besar tidak digunakan sekarang.
PSkocik
10

Sebuah shell efisien jika Anda menggunakannya untuk apa yang telah dirancang untuknya (meskipun efisiensi jarang apa yang Anda cari dalam shell).

Shell adalah interpreter baris perintah, ia dirancang untuk menjalankan perintah dan meminta mereka bekerja sama untuk suatu tugas.

Jika Anda ingin menghitung sampai 1000000000, Anda memanggil (satu) perintah untuk menghitung, seperti seq, bc, awkatau python/ perl... Menjalankan 1000000000 [[...]]perintah dan 1000000000 letperintah pasti akan sangat tidak efisien, terutama dengan bashyang merupakan shell yang paling lambat dari semua.

Dalam hal itu, shell akan jauh lebih cepat:

$ 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

Meskipun tentu saja, sebagian besar pekerjaan dilakukan oleh perintah yang diminta shell, sebagaimana mestinya.

Sekarang, Anda tentu saja dapat melakukan hal yang sama dengan python:

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

Tapi itu tidak benar-benar bagaimana Anda akan melakukan hal-hal pythonseperti pythonitu terutama bahasa pemrograman, bukan penerjemah baris perintah.

Perhatikan bahwa Anda dapat melakukan:

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

Tapi, pythonsebenarnya akan memanggil shell untuk menafsirkan baris perintah itu!

Stéphane Chazelas
sumber
Saya suka jawaban Anda. Begitu banyak jawaban lain yang membahas peningkatan teknik "bagaimana", sementara Anda membahas "mengapa" dan juga "mengapa tidak" mengatasi kesalahan dalam metodologi pendekatan OP.
greg.arnott
7

Jawaban: Bash jauh lebih lambat daripada Python.

Salah satu contoh kecil adalah dalam kinerja posting blog beberapa bahasa .

steve
sumber
3

Tidak ada yang salah (kecuali harapan Anda) karena python sangat cepat untuk bahasa yang tidak dikompilasi, lihat https://wiki.python.org/moin/PythonSpeed

Matija Nalis
sumber
1
Saya agak tidak menyarankan jawaban seperti ini, ini milik komentar IMHO.
LinuxSecurityFreak
2

Selain komentar, Anda bisa mengoptimalkan kode kecil yang , misalnya

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

Kode ini seharusnya memakan waktu lebih sedikit .

Tapi jelas tidak cukup cepat untuk bisa digunakan.

LinuxSecurityFreak
sumber
-3

Saya perhatikan perbedaan dramatis dalam bash dari penggunaan ekspresi "sementara" dan "hingga" yang secara logika setara:

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

Bukannya itu benar-benar memiliki relevansi yang luar biasa dengan pertanyaan, selain itu mungkin kadang-kadang perbedaan kecil membuat perbedaan besar, meskipun kami berharap mereka akan setara.

penguin pemberani
sumber
6
Coba yang ini ((i==900000)).
Tomasz
2
Anda menggunakan =untuk tugas. Ini akan segera kembali benar. Tidak akan ada loop yang terjadi.
Wildcard
1
Apakah Anda pernah menggunakan Bash sebelumnya? :)
LinuxSecurityFreak