Có gì đó không đúng với kịch bản của tôi hay Bash chậm hơn Python nhiều?


29

Tôi đã kiểm tra tốc độ của Bash và Python bằng cách chạy vòng lặp 1 tỷ lần.

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

Mã Bash:

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

Sử dụng timelệnh tôi phát hiện ra rằng mã Python chỉ mất 48 giây để hoàn thành trong khi mã Bash mất hơn 1 giờ trước khi tôi giết tập lệnh.

Tại sao cái này rất? Tôi đã dự đoán rằng Bash sẽ nhanh hơn. Có điều gì đó sai với kịch bản của tôi hay Bash thực sự chậm hơn nhiều với kịch bản này?


49
Tôi không chắc tại sao bạn mong đợi Bash nhanh hơn Python.
Kusalananda

9
@MatijaNalis không bạn không thể! Tập lệnh được tải vào bộ nhớ, chỉnh sửa tệp văn bản được đọc từ (tệp tập lệnh) sẽ hoàn toàn không có tác dụng đối với tập lệnh đang chạy. Một điều tốt nữa, bash đã đủ chậm mà không cần phải mở và đọc lại một tập tin mỗi khi một vòng lặp được chạy!
terdon


4
Bash đọc từng dòng tệp khi nó thực thi, nhưng nó nhớ những gì nó đọc nếu đến dòng đó một lần nữa (vì nó nằm trong một vòng lặp hoặc một hàm). Khiếu nại ban đầu về việc đọc lại mỗi lần lặp là không đúng, nhưng sửa đổi các dòng chưa đạt được sẽ có hiệu lực. Một minh chứng thú vị: tạo một tệp có chứa echo echo hello >> $0và chạy nó.
Michael Homer

3
@MatijaNalis ah, OK, tôi có thể hiểu điều đó. Đó là ý tưởng thay đổi một vòng lặp chạy đã ném tôi. Có lẽ, mỗi dòng được đọc tuần tự và chỉ sau khi dòng cuối cùng kết thúc. Tuy nhiên, một vòng lặp được coi là một lệnh duy nhất và sẽ được đọc toàn bộ, do đó việc thay đổi nó sẽ không ảnh hưởng đến quá trình chạy. Tuy nhiên, sự khác biệt thú vị là tôi luôn cho rằng toàn bộ tập lệnh được tải vào bộ nhớ trước khi thực thi. Cảm ơn đã chỉ ra điều đó!
terdon

Câu trả lời:


17

Đây là một lỗi đã biết trong bash; xem trang người đàn ông và tìm kiếm "BUG":

BUGS
       It's too big and too slow.

;)


Đối với một mồi tuyệt vời về sự khác biệt về khái niệm giữa kịch bản shell và các ngôn ngữ lập trình khác, tôi khuyên bạn nên đọc:

Các trích đoạn thích hợp nhất:

Vỏ là một ngôn ngữ cấp cao hơn. Người ta có thể nói nó thậm chí không phải là một ngôn ngữ. Họ trước tất cả các thông dịch viên dòng lệnh. Công việc được thực hiện bởi những lệnh bạn chạy và shell chỉ nhằm mục đích sắp xếp chúng.

...

IOW, trong shell, đặc biệt là để xử lý văn bản, bạn gọi càng ít tiện ích càng tốt và để chúng hợp tác với nhiệm vụ, không chạy hàng ngàn công cụ theo trình tự chờ từng cái bắt đầu, chạy, dọn sạch trước khi chạy cái tiếp theo.

...

Như đã nói trước đó, chạy một lệnh có chi phí. Một chi phí rất lớn nếu lệnh đó không được dựng sẵn, nhưng ngay cả khi chúng được dựng sẵn, chi phí vẫn rất lớn.

Và shell không được thiết kế để chạy như vậy, chúng không có ý định trở thành ngôn ngữ lập trình biểu diễn. Họ không phải, họ chỉ là thông dịch viên dòng lệnh. Vì vậy, ít tối ưu hóa đã được thực hiện trên mặt trận này.


Đừng sử dụng các vòng lặp lớn trong kịch bản shell.


54

Các vòng lặp Shell chậm và bash là chậm nhất. Vỏ không có nghĩa là làm công việc nặng trong các vòng lặp. Shell có nghĩa là để khởi chạy một vài quy trình bên ngoài, được tối ưu hóa trên các lô dữ liệu.


Dù sao, tôi đã tò mò làm thế nào so sánh các vòng vỏ để tôi thực hiện một điểm chuẩn nhỏ:

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

( Chi tiết:

  • CPU: Intel (R) Core (TM) i5 CPU M 430 @ 2.27GHz
  • ksh: phiên bản sh (Nghiên cứu AT & T) 93u + 2012-08-01
  • bash: GNU bash, phiên bản 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • dấu gạch ngang: 0,5,7-4ubfox1

)

Các kết quả (viết tắt) (thời gian mỗi lần lặp) là:

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

Từ kết quả:

Nếu bạn muốn một vòng lặp shell nhanh hơn một chút, thì nếu bạn có [[cú pháp và bạn muốn một vòng lặp shell nhanh, thì bạn đang ở trong một trình bao nâng cao và bạn cũng có vòng lặp giống như C. Sử dụng vòng lặp C like cho, sau đó. Chúng có thể nhanh gấp khoảng 2 lần so với while [-loops trong cùng một lớp vỏ.

  • kshfor (vòng lặp nhanh nhất với khoảng 2,7 Lời mỗi lần lặp
  • dashwhile [vòng lặp nhanh nhất với khoảng 5,8 Cước mỗi lần lặp

C cho các vòng lặp có thể là 3-4 bậc thập phân có độ lớn nhanh hơn. (Tôi nghe nói Torvalds yêu C).

Vòng lặp C được tối ưu hóa nhanh hơn 56500 lần so với while [vòng lặp của bash (vòng lặp shell chậm nhất) và nhanh hơn 6750 lần so với for (vòng lặp của ksh (vòng lặp shell nhanh nhất).


Một lần nữa, sự chậm chạp của shell không quan trọng lắm, bởi vì mẫu điển hình với shell là giảm tải cho một vài quy trình của các chương trình tối ưu hóa bên ngoài.

Với mẫu này, shell thường giúp việc viết tập lệnh dễ dàng hơn nhiều so với tập lệnh python (lần trước tôi đã kiểm tra, việc tạo các đường dẫn quy trình trong python khá vụng về).

Một điều khác cần xem xét là thời gian khởi động.

time python3 -c ' '

mất 30 đến 40 ms trên PC của tôi trong khi shell mất khoảng 3ms. Nếu bạn khởi chạy rất nhiều tập lệnh, điều này sẽ nhanh chóng tăng lên và bạn có thể làm rất nhiều trong 27-37 ms mà python chỉ cần bắt đầu. Các kịch bản nhỏ có thể được hoàn thành nhiều lần trong khung thời gian đó.

(NodeJs có lẽ là thời gian chạy kịch bản tồi tệ nhất trong bộ phận này vì chỉ mất khoảng 100ms để bắt đầu (mặc dù khi nó đã bắt đầu, bạn khó có thể tìm thấy một trình diễn tốt hơn giữa các ngôn ngữ kịch bản)).


Đối với ksh, bạn có thể muốn xác định việc thực hiện (AT & T ksh88, AT & T ksh93, pdksh, mksh...) như có khá nhiều sự thay đổi giữa chúng. Đối với bash, bạn có thể muốn chỉ định phiên bản. Nó đã thực hiện một số tiến bộ gần đây (cũng áp dụng cho các vỏ khác).
Stéphane Chazelas

@ StéphaneChazelas Cảm ơn. Tôi đã thêm các phiên bản của phần mềm và phần cứng được sử dụng.
PSkocik

Để tham khảo: để tạo một đường dẫn quy trình trong python, bạn phải làm một cái gì đó như : from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Điều này thực sự vụng về, nhưng không khó để mã hóa một pipelinechức năng thực hiện điều này cho bạn cho bất kỳ số lượng quy trình, dẫn đến pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu

1
Tôi nghĩ có lẽ trình tối ưu hóa gcc đã hoàn toàn loại bỏ vòng lặp. Không phải vậy, nhưng nó vẫn đang thực hiện một tối ưu hóa thú vị: nó sử dụng các hướng dẫn SIMD để thực hiện song song 4 lần, giảm số lần lặp lại xuống 250000.
Đánh dấu Plotnick

1
@PSkocik: Nó nằm ngay bên cạnh những gì trình tối ưu hóa có thể làm trong năm 2016. Có vẻ như C ++ 17 sẽ bắt buộc các trình biên dịch phải có thể tính toán các biểu thức tương tự tại thời gian biên dịch (thậm chí không phải là tối ưu hóa). Với khả năng C ++ đó, ​​GCC cũng có thể chọn nó làm tối ưu hóa cho C.
MSalters

18

Tôi đã thực hiện một chút thử nghiệm và trên hệ thống của tôi đã chạy như sau - không có thứ tự tăng tốc độ nào cần thiết để cạnh tranh, nhưng bạn có thể làm cho nó nhanh hơn:

Kiểm tra 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

kiểm tra 3: 17,64

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

kiểm tra5: 12,79

#!/bin/bash
export LC_ALL=C

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

Phần quan trọng trong phần cuối cùng này là xuất LC_ALL = C. Tôi đã thấy rằng nhiều hoạt động bash kết thúc nhanh hơn đáng kể nếu điều này được sử dụng, đặc biệt là bất kỳ chức năng regex nào. Nó cũng hiển thị một cú pháp không có giấy tờ để sử dụng {} và: as-op.


3
+1 cho đề xuất LC_ALL, tôi không biết điều đó.
einpoklum - phục hồi Monica

+1 Thú vị làm [[sao nhanh hơn nhiều [. Tôi không biết LC_ALL = C (BTW bạn không cần xuất nó) đã tạo ra sự khác biệt.
PSkocik

@PSkocik Theo như tôi biết, [[là một bash dựng sẵn, và [thực sự /bin/[, nó giống như /bin/test- một chương trình bên ngoài. Đó là lý do tại sao thay thế chậm hơn.
tomsmeding

@tomsmending [là một nội dung trong tất cả các shell thông thường (thử type [). Chương trình bên ngoài hầu hết không được sử dụng.
PSkocik

10

Một shell là hiệu quả nếu bạn sử dụng nó cho những gì nó đã được thiết kế (mặc dù hiệu quả hiếm khi là những gì bạn tìm kiếm trong một shell).

Shell là một trình thông dịch dòng lệnh, nó được thiết kế để chạy các lệnh và để chúng hợp tác với một tác vụ.

Nếu bạn muốn đếm đến 1000000000, bạn gọi một (một) lệnh để đếm, như seq, bc, awkhoặc python/ perl... Chạy 1000000000 [[...]]lệnh và 1000000000 letlệnh chắc chắn là khủng khiếp không hiệu quả, đặc biệt là với bashđó là vỏ chậm nhất của tất cả.

Về vấn đề đó, một cái vỏ sẽ nhanh hơn rất nhiều:

$ 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

Mặc dù tất nhiên, hầu hết các công việc được thực hiện bởi các lệnh mà shell gọi, như nó phải vậy.

Bây giờ, tất nhiên bạn có thể làm tương tự với python:

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

Nhưng đó không phải là thực sự như thế nào bạn muốn làm những việc trong pythonkhi pythonchủ yếu là một ngôn ngữ lập trình, không phải là một thông dịch dòng lệnh.

Lưu ý rằng bạn có thể làm:

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

Nhưng, pythonthực sự sẽ gọi một cái vỏ để diễn giải dòng lệnh đó!


Tôi thích câu trả lời của bạn. Vì vậy, nhiều câu trả lời khác thảo luận về các kỹ thuật "làm thế nào" được cải thiện, trong khi bạn đề cập đến cả "tại sao" và nhận thức về "tại sao không" giải quyết lỗi trong phương pháp tiếp cận của OP.
greg.arnott



2

Ngoài các ý kiến, bạn có thể tối ưu hóa mã một chút , ví dụ:

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

Mã này sẽ mất ít thời gian hơn một chút .

Nhưng rõ ràng là không đủ nhanh để thực sự có thể sử dụng.


-3

Tôi đã nhận thấy một sự khác biệt đáng kể trong bash từ việc sử dụng các biểu thức "while" và "Until" tương đương logic:

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

Không phải là nó thực sự có liên quan rất lớn đến câu hỏi, ngoài ra đôi khi những khác biệt nhỏ có thể tạo ra sự khác biệt lớn, mặc dù chúng ta mong đợi chúng tương đương nhau.


6
Hãy thử với cái này ((i==900000)).
Tomasz

2
Bạn đang sử dụng =để chuyển nhượng. Nó sẽ trở lại đúng ngay lập tức. Không có vòng lặp sẽ diễn ra.
tự đại diện

1
Bạn đã thực sự sử dụng Bash trước đây? :)
LinuxSecurityFreak
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.