내 스크립트에 문제가 있습니까 아니면 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보다 빠를 것으로 예상 했는지 잘 모르겠습니다 .
Kusalananda

9
@MatijaNalis 안돼! 스크립트는 메모리에로드되어 읽은 텍스트 파일 (스크립트 파일)을 편집해도 실행중인 스크립트에는 전혀 영향을 미치지 않습니다. 좋은 점은, bash는 루프가 실행될 때마다 파일을 열고 다시 읽지 않아도 이미 충분히 느리다는 것입니다!
terdon


4
Bash는 파일이 실행될 때 한 줄씩 파일을 읽습니다. 그러나 루프 나 함수 때문에 해당 줄에 다시 올 경우 읽은 내용을 기억합니다. 각 반복을 다시 읽는 것에 대한 원래의 주장은 사실이 아니지만 아직 도달하지 않은 행에 대한 수정은 효과적입니다. 흥미로운 데모 :를 포함하는 파일을 echo echo hello >> $0만들고 실행하십시오.
Michael Homer

3
@MatijaNalis 아, 알았어요. 이해할 수 있습니다. 나를 던지는 러닝 루프를 변경하는 아이디어였습니다. 아마도 각 행은 마지막 행이 완료된 후에 만 ​​순차적으로 읽 힙니다. 그러나 루프는 단일 명령으로 취급되며 전체를 읽으므로 변경하면 실행중인 프로세스에 영향을 미치지 않습니다. 그러나 흥미로운 차이점은 항상 전체 스크립트가 실행 전에 메모리에로드되는 것으로 가정했습니다. 지적 해 주셔서 감사합니다!
terdon

답변:


17

이것은 bash의 알려진 버그입니다. 매뉴얼 페이지를 참조하여 "BUGS"를 검색하십시오.

BUGS
       It's too big and too slow.

;)


쉘 스크립팅과 다른 프로그래밍 언어의 개념적 차이점에 대한 훌륭한 입문서를 보려면 다음을 읽는 것이 좋습니다.

가장 적절한 발췌 :

쉘은 고급 언어입니다. 언어조차도 말할 수 없습니다. 그들은 모든 명령 줄 인터프리터 앞에 있습니다. 작업은 사용자가 실행하는 명령으로 수행되며 셸은 명령을 조정하기위한 것입니다.

...

쉘, 특히 텍스트를 처리하기 위해 IOW는 가능한 한 적은 유틸리티를 호출하고 작업에 협력하게하고 다음 도구를 실행하기 전에 각 도구가 시작, 실행, 정리 될 때까지 수천 개의 도구를 순서대로 실행하지 않습니다.

...

앞에서 언급했듯이 하나의 명령을 실행하면 비용이 발생합니다. 해당 명령이 내장되어 있지 않으면 막대한 비용이 들지만 내장되어 있어도 비용이 큽니다.

그리고 쉘은 그런 식으로 실행되도록 설계되지 않았으며, 성능이 뛰어난 프로그래밍 언어가 될 수 없습니다. 그들은 단지 명령 줄 해석 기일뿐입니다. 따라서이 부분에서는 최적화가 거의 이루어지지 않았습니다.


쉘 스크립팅에서 큰 루프를 사용하지 마십시오.


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 (i) 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-4 우분투 1

)

(축약 된) 결과 (반복 당 시간)는 다음과 같습니다.

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 for 루프를 사용하십시오. while [동일한 쉘에서 -loops의 약 2 배 빠릅니다 .

  • ksh 는 반복 당 for (2.7µs 에서 가장 빠른 루프를 갖습니다.
  • 대시 는 반복 당 while [5.8µs 에서 가장 빠른 루프를 갖습니다.

C for 루프는 10-4 배 더 빠를 수 있습니다. (토발즈가 C를 좋아한다고 들었습니다).

최적화 된 C for 루프는 bash while [루프 (가장 느린 쉘 루프)보다 56500 배 빠르며 ksh for (루프 (가장 빠른 쉘 루프) 보다 6750 배 빠릅니다 .


쉘의 일반적인 패턴은 외부의 최적화 된 프로그램의 몇 가지 프로세스로 오프로드하는 것이기 때문에 쉘의 속도 저하는 중요하지 않습니다.

이 패턴을 사용하면 쉘은 종종 파이썬 스크립트보다 우수한 성능의 스크립트를 작성하는 것을 훨씬 쉽게 만듭니다 (마지막으로 확인했을 때 파이썬에서 프로세스 파이프 라인을 만드는 것은 다소 어색했습니다).

고려해야 할 또 다른 사항은 시작 시간입니다.

time python3 -c ' '

내 PC에서 30 ~ 40ms가 걸리고 쉘은 약 3ms가 걸립니다. 많은 스크립트를 실행하면 빠르게 추가되고 파이썬이 시작하는 데 걸리는 27-37ms의 추가 시간에 매우 많은 일을 할 수 있습니다. 작은 스크립트는 해당 시간 프레임에서 여러 번 끝낼 수 있습니다.

(NodeJ는 아마도 시작하는 데 약 100ms가 걸리기 때문에이 부서에서 가장 최악의 스크립팅 런타임 일 것입니다.


KSH를 들어, (AT & T 구현을 지정할 수 있습니다 ksh88, AT & T ksh93, pdksh, mksh그들 사이의 변화의 꽤 많은 거기로 ...). 의 경우 bash버전을 지정할 수 있습니다. 최근에 일부 진전이 이루어졌습니다 (다른 쉘에도 적용됨).
Stéphane Chazelas

@ StéphaneChazelas 감사합니다. 사용 된 소프트웨어 및 하드웨어 버전을 추가했습니다.
PSkocik

참고로 : 파이썬에서 프로세스 파이프 라인을 만들려면 다음과 같이해야합니다 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']).
Bakuriu

1
아마도 gcc 최적화 프로그램이 루프를 완전히 제거하고 있다고 생각했습니다. 그렇지는 않지만 여전히 흥미로운 최적화를하고 있습니다. SIMD 명령어를 사용하여 4 번의 추가를 병렬로 수행하여 루프 반복 횟수를 250000으로 줄입니다.
Mark 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.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

test4 : 26.69 초

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

test5 : 12.79 초

#!/bin/bash
export LC_ALL=C

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

이 마지막 부분에서 중요한 부분은 내보내기 LC_ALL = C입니다. 많은 bash 작업, 특히 정규식 함수를 사용하면 훨씬 빨리 종료됩니다. 또한 {} 및 :를 no-op로 사용하는 구문에 대한 문서화되지 않은 문서를 표시합니다.


3
LC_ALL 제안에 대해 +1, 나는 그것을 몰랐습니다.
einpoklum-복원 Monica Monica

+1 [[보다 빠른 방법에 관심 이 [있습니다. LC_ALL = C (내보낼 필요가없는 BTW)에 차이가 있다는 것을 몰랐습니다.
PSkocik

@PSkocik 내가 아는 한, [[bash 내장 [이며 실제로 는 외부 프로그램 /bin/[과 동일 /bin/test합니다. 그래서 thay가 더 느린 이유입니다.
tomsmeding

@tomsmending [은 모든 일반적인 쉘에 내장되어 있습니다 (try 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실제로는 해당 명령 행을 해석하기 위해 쉘을 호출합니다!


나는 당신의 대답을 좋아합니다. 다른 많은 답변들도 개선 된 "어떻게"기술에 대해 논의하는 반면, 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)).
Tomasz

2
=과제에 사용 하고 있습니다. 즉시 true를 반환합니다. 루프가 발생하지 않습니다.
와일드 카드

1
실제로 Bash를 사용한 적이 있습니까? :)
LinuxSecurityFreak
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.