Senaryomda bir sorun mu var yoksa Bash Python'dan daha mı yavaş?


29

Bash ve Python'un hızını 1 milyar kez bir döngü çalıştırarak test ediyordum.

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

Bash kodu:

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

timeKomutu kullanarak Python kodunun bitirmesi sadece 48 saniye sürdü, Bash kodu ise senaryoyu öldürmeden önce 1 saatten fazla sürdü.

Bu neden böyle? Bash'in daha hızlı olacağını umuyordum. Senaryomda bir sorun mu var yoksa Bash bu senaryoda gerçekten çok mu yavaş?


49
Bash'in neden Python'dan daha hızlı olmasını beklediğinizden emin değilim .
Kusalananda

9
@MatijaNalis hayır yapamazsınız! Komut dosyası belleğe yüklenir, okunduğu metin dosyasını (komut dosyası dosyası) düzenlemenin çalışan komut dosyası üzerinde kesinlikle bir etkisi olmaz. İyi bir şey, bash zaten bir döngü çalıştırıldığında bir dosyayı açıp yeniden okumak zorunda kalmadan zaten yeterince yavaş!
terdon


4
Bash dosyayı yürütürken satır satır okur, ancak o satıra tekrar gelirse ne okuduğunu hatırlar (çünkü bir döngüde veya bir fonksiyondadır). Her yinelemenin yeniden okunması hakkındaki orijinal iddia doğru değildir, ancak henüz ulaşılmamış satırlardaki değişiklikler etkili olacaktır. İlginç bir gösteri: içeren bir dosya oluşturun echo echo hello >> $0ve çalıştırın.
Michael Homer

3
@MatijaNalis ah, tamam, bunu anlayabiliyorum. Beni fırlatan koşu döngüsünü değiştirme fikriydi. Muhtemelen, her satır sırayla okunur ve sadece sonuncusu bittikten sonra. Bununla birlikte, bir döngü tek bir komut olarak değerlendirilir ve bütünüyle okunur, bu nedenle değiştirmek çalışan işlemi etkilemez. İlginç bir ayrım olsa da, yürütmeden önce tüm betiğin belleğe yüklendiğini her zaman varsaydım. Gösterdiğin için teşekkürler!
terdon

Yanıtlar:


17

Bu, bash'ta bilinen bir hatadır; man sayfasına bakın ve "HATA" ifadesini arayın:

BUGS
       It's too big and too slow.

;)


Kabuk yazımı ve diğer programlama dilleri arasındaki kavramsal farklar üzerine mükemmel bir başlangıç ​​için, okumanı şiddetle tavsiye ederim:

En uygun alıntılar:

Kabuklar daha yüksek bir dildir. Bir dilin bile olmadığını söyleyebilir. Tüm komut satırı tercümanlarından önce. İş, çalıştırdığınız komutlar tarafından yapılır ve kabuk sadece onları düzenlemek içindir.

...

IOW, kabuklarda, özellikle de metni işlemek için, mümkün olduğunca az yardımcı program başlattınız ve bir sonrakiini çalıştırmadan önce her birinin başlamasını, çalıştırılmasını ve temizlenmesini bekleyen sıralı binlerce aracı çalıştırmamalarını sağlayın.

...

Daha önce de belirtildiği gibi, bir komut çalıştırmanın bir maliyeti vardır. Bu komut yerleşik değilse büyük bir maliyet, ancak yerleşik olsalar bile maliyet büyüktür.

Ve kabuklar bu şekilde çalışacak şekilde tasarlanmadı, performans programlama dilleri yapmayı önemsemediler. Onlar değil, sadece komut satırı tercümanları. Böylece bu cephede çok az optimizasyon yapıldı.


Kabuk komut dosyalarında büyük döngüler kullanmayın.


54

Kabuk döngüleri yavaş ve bash en yavaş. Mermilerin mermilerde ağır iş yapması gerekmiyor. Kabuklar, birkaç veri grubu üzerinde birkaç harici, optimize edilmiş işlem başlatmak içindir.


Her neyse, kabuk döngülerinin nasıl karşılaştırıldığını merak ettim, bu yüzden küçük bir değerlendirme yaptım:

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

( Ayrıntılar:

  • İşlemci: Intel (R) Çekirdek (TM) i5 CPU M 430 @ 2.27GHz
  • ksh: sürüm sh (AT&T Araştırması) 93u + 2012-08-01
  • bash: GNU bash, sürüm 4.3.11 (1) - yayın (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-bilinmeyen-linux-gnu)
  • tire: 0.5.7-4ubuntu1

)

(Kısaltılmış) sonuçlar (yineleme başına zaman):

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

Sonuçlardan:

Biraz daha hızlı bir kabuk döngüsü istiyorsanız, [[sözdizimine sahipseniz ve hızlı bir kabuk döngüsü istiyorsanız, gelişmiş bir kabuktasınız ve döngü için de C benzeri bir sisteminiz var. O zaman döngü için C benzeri kullanın. while [Aynı kabuktaki-döngülerden 2 kat daha hızlı olabilirler .

  • ksh , yineleme başına for (yaklaşık 2,7µs'de en hızlı döngüye sahiptir
  • çizgi her yineleme başına while [yaklaşık 5.8µs en hızlı döngü vardır

Döngüler için C, 3-4 ondalık büyüklükte daha hızlı olabilir. (Torvalds'ın C'yi sevdiğini duydum).

Döngü için en iyi duruma getirilmiş C, bash while [döngüsünden (en yavaş kabuk döngüsü) 56500 kat daha hızlı ve ksh for (döngüsünden (en hızlı kabuk döngüsü) 6750 kat daha hızlıdır .


Yine, kabukların yavaşlığı çok da önemli olmamalıdır, çünkü kabukları olan tipik kalıp dış, optimize edilmiş programların birkaç işlemine boşaltmaktır.

Bu kalıpla, kabuklar genellikle python komut dosyalarından daha üstün performansa sahip komut dosyaları yazmayı çok daha kolaylaştırır (en son kontrol ettiğimde python'da işlem boru hatları oluşturmak oldukça sakardı).

Dikkate alınması gereken başka bir şey, başlangıç ​​zamanı.

time python3 -c ' '

bilgisayarımda 30 ila 40 ms sürüyor, kabukları ise 3 ms. Çok fazla komut dosyası başlatırsanız, bu hızlı bir şekilde eklenir ve python'un daha yeni başlaması için gerekli olan 27-37 msn'de çok fazla şey yapabilirsiniz. Küçük komut dosyaları bu zaman dilimi içinde birkaç kez tamamlanabilir.

(DüğümJ'ler muhtemelen bu bölümdeki en kötü komut dosyası çalıştırma zamanıdır, çünkü yalnızca başlatılması yaklaşık 100ms sürer (bir kez başlasa bile, betik dilleri arasında daha iyi bir performans bulmak için zorlanırsınız)).


Ksh için, (AT & T uygulanmasını belirtmek isteyebilirsiniz ksh88, AT & T ksh93, pdksh, mksharalarında varyasyon oldukça çok şey var olarak ...). Çünkü bashsürümünü belirtmek isteyebilirsiniz. Son zamanlarda bazı ilerlemeler kaydetti (bu, diğer mermiler için de geçerlidir).
Stéphane Chazelas,

@ StéphaneChazelas Teşekkürler. Kullanılan yazılımın ve donanımın versiyonlarını ekledim.
PSkocik

Başvuru için: Eğer böyle bir şey yapmak zorunda Python süreç boru hattını oluşturmak için: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Bu gerçekten sakardır, ancak bunun pipelineiçin herhangi bir işlem için sizin için bunu yapan bir işlevi kodlamak zor olmamalıdır pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu

1
Belki de gcc optimizerinin döngüyü tamamen ortadan kaldırdığını düşündüm. Değil, ama yine de ilginç bir optimizasyon yapıyor: 4 ekleme paralel yapmak için SIMD komutlarını kullanıyor ve döngü yineleme sayısını
250000'e düşürüyor

1
@PSkocik: 2016'da optimizerlerin yapabileceklerinin hemen yanı başında. C ++ 17, derleyicilerin derleme zamanında benzer ifadeleri hesaplayabilmesi gerektiğini zorunlu kılıyor (bir optimizasyon olarak değil). Bu C ++ özelliği mevcut olduğunda, GCC onu C için de bir optimizasyon olarak algılayabilir.
MSalters,

18

Biraz test yaptım ve sistemimde aşağıdakiler yapıldı: hiçbiri rekabetçi olmak için gereken büyüklük hızını vermedi, ancak daha hızlı yapabilirsiniz:

Test 1: 18.233'ler

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

test2: 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 s

#!/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++)) { 
:
}

Bu sonuncunun önemli kısmı ihracat LC_ALL = C'dir. Birçok bash işleminin, özellikle de herhangi bir regex işlevinin kullanılması durumunda çok daha hızlı sonuçlandığını gördüm. Ayrıca, {} ve: no-op işlevini kullanmak için sözdiziminin belgesiz olduğunu gösterir.


3
LC_ALL önerisi için +1, bunu bilmiyordum.
einpoklum - Monica

+1 İlginç [[olandan çok daha hızlı olması [. LC_ALL = C (BTW'yi dışa aktarmanıza gerek yok) farketmedim.
PSkocik

@ PSkocik Bildiğim kadarıyla [[, bir bash yerleşiktir ve [gerçekten /bin/[de aynıdır /bin/test- harici bir program. Bu yüzden burası daha yavaş.
16'da

@tomsending [, tüm genel kabuklarda yerleşiktir (deneyin type [). Dış program çoğunlukla şu anda kullanılmamaktadır.
PSkocik

10

Bir kabuk, tasarlandığı amaç için kullanırsanız verimlidir (ancak, verimlilik nadiren bir kabukta aradığınız şeydir).

Bir kabuk bir komut satırı yorumlayıcısıdır, komutları çalıştırmak ve bir göreve işbirliği yapmalarını sağlamak için tasarlanmıştır.

Eğer 1000000000'dan saymak isterseniz, gibi saymak için (bir) komutunu çağırmak seq, bc, awkveya python/ perl... 1000000000 Koşu [[...]]komutları ve 1000000000 letözellikle birlikte, komutları korkunç verimsiz olacağı kesindir bashtüm en yavaş kabuk olan.

Bu bakımdan, bir kabuk çok daha hızlı olacaktır:

$ 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

Tabii ki, işin çoğu, kabuğun yapması gereken emirleri olduğu gibi yapıyor.

Şimdi, elbette aynı şeyi yapabilirsiniz python:

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

Ancak bu, aslında bir programlama dili olduğu pythongibi python, bir komut satırı yorumlayıcısı olarak yapabileceğiniz şeyler değildir.

Yapabileceğini unutmayın:

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

Ancak, pythonaslında bu komut satırını yorumlamak için bir kabuk çağırıyor!


Cevabını seviyorum. Pek çok başka cevap, "yaklaşım" tekniklerini geliştirirken, hem "neden" hem de algısal olarak "neden olmasın" konusunu ele alırken, OP'nin yaklaşım metodolojisindeki hatayı ele alıyor.
greg.arnott 30:18



2

Yorumların yanı sıra kodu biraz da optimize edebilirsiniz , örneğin

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

Bu kod biraz daha az zaman almalıdır .

Ama açıkçası aslında kullanılabilir olacak kadar hızlı değil.


-3

Bash'te mantıksal olarak eşdeğer "while" ve "ifadeler" ifadelerinin kullanılmasından belirgin bir fark gördüm:

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

Bunun soru ile gerçekten büyük bir ilgisi olmadığını değil, bunun dışında belki de küçük farklılıklar eşdeğer olmasını beklememize rağmen büyük bir fark yaratıyorlar.


6
Bununla dene ((i==900000)).
Tomasz,

2
=Görev için kullanıyorsun . Hemen gerçek olacak. Hiçbir döngü gerçekleşmeyecek.
Wildcard

1
Bash'i daha önce kullandın mı? :)
LinuxSecurityFreak
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.