我的脚本有问题吗?还是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已经足够慢,而不必在每次运行循环时都打开并重新读取文件!
terdon


4
Bash在执行时逐行读取文件,但它会记住如果再次遇到该行(因为它在循环或函数中)而读取的内容。关于重新读取每个迭代的原始主张是不正确的,但对尚未达到的行进行修改将是有效的。一个有趣的演示:制作一个包含的文件echo echo hello >> $0,然后运行它。
迈克尔·荷马

3
@MatijaNalis啊,好的,我可以理解。改变运行循环的想法使我感到震惊。据推测,只有最后一行结束后,才逐行读取每一行。但是,循环被视为单个命令,并且将完整读取它,因此更改它不会影响正在运行的进程。但是有趣的区别是,我一直假设整个脚本在执行之前就已加载到内存中。感谢您指出!
terdon

Answers:


17

这是bash中的一个已知错误;请参见手册页并搜索“ BUGS”:

BUGS
       It's too big and too slow.

;)


对于外壳脚本和其他编程语言之间的概念差异的出色入门,我强烈建议阅读:

最相关的摘录:

Shell是一种高级语言。有人可能说这甚至不是一门语言。它们位于所有命令行解释器之前。该工作由您运行的那些命令完成,而shell仅用于编排它们。

...

在外壳程序中,尤其是在处理文本时,IOW会调用尽可能少的实用程序并使它们配合任务,而不是依次运行数千个工具来等待每个工具启动,运行,清理,然后再运行下一个工具。

...

如前所述,运行一个命令是有代价的。如果该命令不是内置的,则成本很高,但是即使内置了该命令,代价也很大。

而且,shell还没有设计成可以像这样运行,它们没有成为高性能编程语言的幌子。它们不是,它们只是命令行解释器。因此,在这方面很少进行优化。


不要在shell脚本中使用大循环。


54

Shell循环慢,而bash循环最慢。Shell并不是要循环执行繁重的工作。Shell旨在对批量数据启动一些外部的,优化的过程。


无论如何,我很好奇壳循环如何比较,所以我做了一个基准测试:

#!/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)-发行版(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

从结果:

如果您想要一个稍微快一点的Shell循环,那么如果您拥有[[语法并且想要一个快速的Shell循环,那么您就在高级Shell中,并且您也拥有类似于C的for循环。然后使用C like for循环。while [在同一shell中,它们的速度大约是-loop的2倍。

  • ksh具有最快的for (循环,每次迭代约2.7µs
  • 破折号具有最快的while [循环,每次迭代约5.8µs

C for循环可以快3-4个十进制数量级。(我听说Torvalds爱C)。

优化的C for循环比bash while [循环(最慢的Shell循环)快56500倍,比ksh for (循环(最快的Shell循环)快6750倍。


再说一次,shell的缓慢性无关紧要,因为使用shell的典型模式是将一些优化的外部程序卸载。

使用这种模式,shell通常使编写脚本的性能要比python脚本好得多(我上次检查时,在python中创建进程管道相当笨拙)。

要考虑的另一件事是启动时间。

time python3 -c ' '

我的PC需要30到40毫秒,而外壳需要3毫秒。如果您启动了许多脚本,这很快就会加起来,并且您可以在python启动额外的27-37毫秒内完成很多工作。小脚本可以在该时间范围内完成多次。

(NodeJs可能是该部门中最糟糕的脚本运行时,因为它仅需100毫秒即可启动(即使启动后,您仍很难在脚本语言中找到性能更好的执行器))。


对于KSH,您可能要指定执行(AT&T ksh88,AT&T ksh93pdkshmksh...),因为有相当多的它们之间的变化。对于bash,您可能要指定版本。最近它取得了一些进展(这也适用于其他shell)。
斯特凡Chazelas

@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
马克Plotnick

1
@PSkocik:这恰好是优化器在2016年可以做的事情。C++ 17似乎要求编译器必须能够在编译时计算相似的表达式(即使不是优化)。有了该C ++功能,GCC也可以将其作为C的优化。
MSalters '16

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
对于LC_ALL建议+1,我不知道。
einpoklum-恢复莫妮卡

+1有趣的[[是速度比快得多[。我不知道LC_ALL = C(顺便说一句,您不需要导出它)会有所不同。
PSkocik

据我所知,@PSkocik [[是一个bash内置[函数/bin/[,实际上是/bin/test-与外部程序相同。这就是为什么放慢速度的原因。
tomsmeding

@tomsmending [是所有常见shell中的内置函数(try type [)。外部程序现在几乎未使用。
PSkocik

10

如果将外壳用于设计用途,则外壳是有效的(尽管效率很少是您在外壳中寻找的)。

Shell是命令行解释器,旨在运行命令并使它们配合完成任务。

如果要算1000000000,你调用一个(一个)命令来算,像seqbcawkpython/ perl...运行十亿[[...]]命令1000000000个let命令注定是非常低效的,尤其是bash这是所有的最慢的外壳。

在这方面,shell会快很多:

$ 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

当然,大多数工作是由Shell调用的命令完成的,应该如此。

现在,您当然可以使用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实际上是在调用shell来解释该命令行!


我喜欢你的回答。许多其他答案都讨论了改进的“如何”技术,同时涵盖了“为什么”和“为什么不”这两个方面,以解决OP的方法论中的错误。
greg.arnott



2

除了注释之外,您可以稍微优化代码,例如

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

此代码应采取一点的时间更少。

但是显然不够快,无法实际使用。


-3

我注意到bash与逻辑上等效的“ while”和“ until”表达式的使用存在显着差异:

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))
Tomasz

2
您正在=用于分配。它将立即返回true。没有循环发生。
2016年

1
您以前真的使用过Bash吗?:)
LinuxSecurityFreak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.