私のスクリプトに何か問題がありますか、それともBashはPythonよりずっと遅いですか?


29

ループを10億回実行して、BashとPythonの速度をテストしていました。

$ 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の方が高速になると期待していました。私のスクリプトに何か問題がありますか、またはこのスクリプトでBashが本当に遅くなりますか?


49
BashがPythonよりも高速であると期待した理由がよくわかりません。
クサラナナンダ

9
@MatijaNalisいいえ、できません!スクリプトはメモリに読み込まれ、読み込まれたテキストファイル(スクリプトファイル)を編集しても、実行中のスクリプトにはまったく影響しません。また、bashはループが実行されるたびにファイルを開いて再読み取りすることなく、すでに十分に低速です!
テルドン


4
Bashは実行時にファイルを1行ずつ読み取りますが、(ループまたは関数内にあるため)その行に戻ったときに読み取った内容を記憶します。各反復の再読み取りに関する元の主張は真実ではありませんが、まだ到達していない行に対する変更は有効です。興味深いデモ:を含むファイルを作成し、echo echo hello >> $0実行します。
マイケルホーマー

3
@MatijaNalisああ、わかりました、理解できます。実行中のループを変更するというアイデアが私を投げました。おそらく、各行は、最後の行が終了した後にのみ連続して読み取られます。ただし、ループは単一のコマンドとして扱われ、ループ全体が読み取られるため、ループを変更しても実行中のプロセスには影響しません。しかし興味深い違いは、スクリプト全体が実行前にメモリにロードされると常に想定していたことです。それを指摘してくれてありがとう!
テルドン

回答:


17

これはbashの既知のバグです。manページを参照して、「バグ」を検索してください。

BUGS
       It's too big and too slow.

;)


シェルスクリプトと他のプログラミング言語の概念的な違いに関する優れた入門書として、以下を読むことを強くお勧めします。

最も適切な抜粋:

シェルは高レベルの言語です。それは言語でさえないと言うかもしれません。すべてのコマンドラインインタープリターの前にあります。ジョブは実行するコマンドによって実行され、シェルはそれらを調整することのみを目的としています。

...

IOW、特にテキストを処理するシェルでは、できるだけ少ないユーティリティを呼び出してタスクに協力させます。数千のツールを順番に実行して、各ツールが起動、実行、クリーンアップされてから次のツールを実行するのを待ちません。

...

前述したように、1つのコマンドを実行するにはコストがかかります。そのコマンドが組み込まれていない場合、莫大な費用がかかりますが、たとえそれらが組み込まれていても、費用は大きいです。

そして、シェルはそのように動作するように設計されておらず、パフォーマンスの高いプログラミング言語であるというふりをしていません。これらはコマンドラインインタープリターではありません。したがって、この面ではほとんど最適化が行われていません。


シェルスクリプトで大きなループを使用しないでください。


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

詳細:

  • CPU:Intel(R)Core(TM)i5 CPU M 430 @ 2.27GHz
  • ksh:バージョンsh(AT&T Research)93u + 2012-08-01
  • bash:GNU bash、バージョン4.3.11(1)-release(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のようなforループも使用できます。次に、Cのようにforループを使用します。while [同じシェルの-loopsの約2倍の速さです。

  • kshには、反復あたりfor (2.7µsの最速ループがあります
  • ダッシュボードには反復あたりwhile [5.8µsの最速ループがあります

C forループは、小数点以下3桁から4桁高速です。(トーバルズはCが大好きだと聞きました)。

最適化されたC forループは、bashのwhile [ループ(最も遅いシェルループ)より56500 倍速く、kshのfor (ループ(最も速いシェルループ)よりも6750倍高速です。


繰り返しますが、シェルの典型的なパターンは、外部の最適化されたプログラムのいくつかのプロセスにオフロードすることですので、シェルの速度はそれほど重要ではありません。

このパターンを使用すると、シェルを使用すると、Pythonスクリプトよりも優れたパフォーマンスでスクリプトを記述しやすくなります(前回チェックしたとき、Pythonでプロセスパイプラインを作成するのはかなり面倒でした)。

考慮すべきもう1つのことは、起動時間です。

time python3 -c ' '

PCでは30〜40ミリ秒かかりますが、シェルは約3ミリ秒かかります。多くのスクリプトを起動すると、これはすぐに追加され、Pythonが開始するのにかかる余分な27〜37ミリ秒で非常に多くのことができます。小さなスクリプトは、その時間枠内で何度か終了できます。

(NodeJsはおそらく、開始するのに約100ミリ秒かかるため、この部門ではおそらく最悪のスクリプトランタイムです(開始した後でも、スクリプト言語の中から優れたパフォーマンスを見つけるのは難しいでしょう)。


kshのためには、(AT&T実装を指定したい場合ksh88、AT&T ksh93pdkshmkshそれらの間の変動のかなり多くがありますと...)。の場合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オプティマイザーがループを完全に排除していると思いました。そうではありませんが、それはまだ面白い最適化をやっている:それは250000へのループの繰り返し回数を減らす、並列に追加4を行うにはSIMD命令を使用しています
マーク・Plotnick

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

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

この最後の重要な部分は、LC_ALL = Cのエクスポートです。これを使用すると、特に正規表現関数の場合、多くのbash操作が大幅に高速になることがわかりました。また、{}および:をノーオペレーションとして使用するための、ドキュメント化されていない構文も示しています。


3
LC_ALL提案の+1、私はそれを知りませんでした。
アインポクルム-モニカを

+1がの[[場合よりもはるかに高速である点が興味深い[。LC_ALL = C(エクスポートする必要のないところで)が違いを生むことを知りませんでした。
PSkocik

@PSkocik私の知る限り[[、bash ビルトインであり、[実際/bin/[には/bin/test、外部プログラムと同じです。これが、thayが遅い理由です。
-tomsmeding

@tomsmending [は、すべての一般的なシェルに組み込まれています(try type [)。外部プログラムは現在ほとんど使用されていません。
PSkocik

10

シェルは、設計された目的に使用する場合に効率的です(ただし、シェルで探す効率はめったにありません)。

シェルはコマンドラインインタープリターであり、コマンドを実行し、タスクに協力させるように設計されています。

あなたが1000000000にカウントしたい場合は、カウントする(1)コマンドを呼び出すなどseqbcawkまたは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実際にはそのコマンドラインを解釈するためにシェルを呼び出すことになります!


あなたの答えが大好きです。他の多くの回答では、改善された「方法」テクニックについて説明しますが、OPのアプローチの方法論におけるエラーに対処する「理由」と「知覚しない」の両方をカバーします。
greg.arnott



2

コメントは別として、コードを少し最適化できます。例えば

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

このコードは少し時間がかかります。

しかし、明らかに実際に使用できるほど速くはありません。


-3

論理的に同等の「while」および「until」式の使用とbashの劇的な違いに気付きました。

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
=割り当てに使用しています。すぐにtrueを返します。ループは発生しません。
ワイルドカード

1
実際に以前にBashを使用したことがありますか?:)
LinuxSecurityFreak
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.