มีบางอย่างผิดปกติกับสคริปต์ของฉันหรือ Bash นั้นช้ากว่า Python มาก?


29

ฉันกำลังทดสอบความเร็วของ Bash และ Python โดยใช้การวนซ้ำ 1 พันล้านครั้ง

$ 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 ไม่คุณไม่สามารถ! สคริปต์ถูกโหลดเข้าสู่หน่วยความจำการแก้ไขไฟล์ข้อความที่อ่านจาก (ไฟล์สคริปต์) จะไม่มีผลกับสคริปต์ที่ใช้งาน สิ่งที่ดีเช่นกันการทุบตีช้าพอโดยไม่ต้องเปิดและอ่านไฟล์ใหม่ทุกครั้งที่มีการวนซ้ำ!
terdon


4
Bash อ่านไฟล์ทีละบรรทัดขณะที่มันดำเนินการ แต่จำสิ่งที่มันอ่านถ้ามันมาถึงบรรทัดนั้นอีกครั้ง (เพราะมันอยู่ในวงหรือฟังก์ชั่น) การอ้างสิทธิ์ดั้งเดิมเกี่ยวกับการอ่านซ้ำแต่ละครั้งไม่เป็นความจริง แต่การแก้ไขบรรทัดที่ยังไม่สามารถเข้าถึงจะมีผล การสาธิตที่น่าสนใจ: สร้างไฟล์ที่มีecho echo hello >> $0และเรียกใช้
Michael Homer

3
@MatijaNalis อ่าฉันเข้าใจได้ มันเป็นความคิดที่จะเปลี่ยนลูปการวิ่งที่ทำให้ฉัน สันนิษฐานว่าแต่ละบรรทัดอ่านตามลำดับและหลังจากบรรทัดสุดท้ายเสร็จสิ้น อย่างไรก็ตามการวนซ้ำถือว่าเป็นคำสั่งเดียวและจะถูกอ่านอย่างครบถ้วนดังนั้นการเปลี่ยนแปลงจะไม่ส่งผลกระทบต่อกระบวนการทำงาน ความแตกต่างที่น่าสนใจแม้ว่าฉันคิดเสมอว่าสคริปต์ทั้งหมดโหลดลงในหน่วยความจำก่อนที่จะดำเนินการ ขอบคุณที่ชี้นำ!
terdon

คำตอบ:


17

นี่เป็นข้อผิดพลาดที่รู้จักกันดีในการทุบตี; ดูหน้าคนและค้นหา "BUGS":

BUGS
       It's too big and too slow.

;)


สำหรับไพรเมอร์ที่ยอดเยี่ยมเกี่ยวกับความแตกต่างทางแนวคิดระหว่างการเขียนสคริปต์เชลล์และภาษาการเขียนโปรแกรมอื่น ๆ ฉันขอแนะนำให้อ่าน:

ข้อความที่ตัดตอนมาที่เกี่ยวข้องมากที่สุด:

เชลล์เป็นภาษาระดับสูงกว่า บางคนอาจพูดว่าไม่ใช่ภาษา พวกเขาอยู่ก่อนล่ามบรรทัดคำสั่งทั้งหมด งานจะทำโดยคำสั่งเหล่านั้นที่คุณเรียกใช้และเชลล์นั้นมีไว้เพื่อติดตั้งให้เท่านั้น

...

โดยเฉพาะอย่างยิ่งในการประมวลผลข้อความคุณเรียกใช้ยูทิลิตี้น้อยที่สุดเท่าที่จะเป็นไปได้และให้พวกเขาร่วมมือกับงานไม่ใช่เรียกใช้เครื่องมือหลายพันตัวตามลำดับที่รอให้แต่ละตัวเริ่มต้นรันทำความสะอาดก่อนเรียกใช้งานถัดไป

...

ดังที่ได้กล่าวไว้ก่อนหน้านี้การรันหนึ่งคำสั่งมีค่าใช้จ่าย ค่าใช้จ่ายมากถ้าคำสั่งนั้นไม่ได้สร้างขึ้น แต่ถึงแม้ว่าพวกเขาจะสร้างขึ้นในราคาที่มีขนาดใหญ่

และเชลล์ไม่ได้ถูกออกแบบมาให้ทำงานเช่นนั้นพวกเขาไม่มีข้ออ้างที่จะเป็นภาษาโปรแกรมนักแสดง พวกเขาไม่ใช่พวกเขาเป็นเพียงตัวแปลบรรทัดคำสั่ง ดังนั้นการเพิ่มประสิทธิภาพเล็ก ๆ น้อย ๆ ได้ดำเนินการในหน้านี้


อย่าใช้ลูปขนาดใหญ่ในการเขียนสคริปต์เชลล์


54

เชลล์วนรอบช้าและทุบตีช้าที่สุด 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: version sh (การวิจัยของ AT&T) 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

จากผลลัพธ์:

หากคุณต้องการเชลล์เชลล์ที่เร็วขึ้นเล็กน้อยจากนั้นถ้าคุณมี[[ไวยากรณ์และคุณต้องการเชลล์เชลล์ที่รวดเร็วคุณอยู่ในเชลล์ขั้นสูงและคุณมี C-like สำหรับลูปด้วยเช่นกัน ใช้ C like for loop แล้ว พวกเขาสามารถเร็วประมาณ 2 ครั้งเท่ากับwhile [-loops ในเชลล์เดียวกัน

  • kshมีfor (ลูปที่เร็วที่สุดที่ประมาณ2.7µsต่อการวนซ้ำ
  • เส้นประมีwhile [วงที่เร็วที่สุดที่ประมาณ5.8 per ต่อการวนซ้ำ

C สำหรับลูปสามารถเรียงลำดับทศนิยมได้เร็วกว่า 3-4 (ฉันได้ยิน Torvalds รัก C)

C ที่ปรับให้เหมาะสมสำหรับลูปคือ 56500 เท่าเร็วกว่าwhile [ลูปของ bash (เชลล์เชลล์ที่ช้าที่สุด) และ 6750 เท่าเร็วกว่าfor (ลูปksh (เชลล์วงที่เร็วที่สุด)


อีกครั้งความช้าของเชลล์ไม่ควรมีความสำคัญมากนักเนื่องจากรูปแบบทั่วไปที่มีเชลล์คือการถ่ายโอนไปยังโปรเซสภายนอกภายนอก

ด้วยรูปแบบนี้เชลล์มักทำให้การเขียนสคริปต์ง่ายขึ้นด้วยประสิทธิภาพที่เหนือกว่าสคริปต์ python (ครั้งสุดท้ายที่ฉันตรวจสอบการสร้างไพพ์ไลน์ของกระบวนการในไพ ธ อนค่อนข้างเงอะงะ)

อีกสิ่งที่ควรพิจารณาคือเวลาเริ่มต้น

time python3 -c ' '

ใช้เวลา 30 ถึง 40 ms บนพีซีของฉันในขณะที่เชลล์ใช้เวลาประมาณ 3 มิลลิวินาที หากคุณเปิดใช้งานสคริปต์จำนวนมากสิ่งนี้จะเพิ่มขึ้นอย่างรวดเร็วและคุณสามารถทำได้มากใน 27-37 ms พิเศษที่ python ใช้เพื่อเริ่มต้นเท่านั้น สคริปต์ขนาดเล็กสามารถเสร็จสิ้นได้หลายครั้งในช่วงเวลานั้น

(NodeJs อาจเป็นสคริปต์รันไทม์ที่เลวร้ายที่สุดในแผนกนี้เนื่องจากใช้เวลาประมาณ 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.45s

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

ทดสอบ 4: 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 ฉันพบว่าการดำเนินการทุบตีจำนวนมากจบลงเร็วขึ้นอย่างมากหากมีการใช้งานโดยเฉพาะอย่างยิ่งฟังก์ชัน regex ใด ๆ นอกจากนี้ยังแสดงให้เห็นว่าไม่มีเอกสารสำหรับไวยากรณ์ในการใช้ {} และ: เป็น no-op


3
+1 สำหรับคำแนะนำ LC_ALL ฉันไม่ทราบ
einpoklum - คืนสถานะโมนิก้า

+1 น่าสนใจว่า[[มันเร็วกว่ามากแค่[ไหน ฉันไม่รู้ LC_ALL = C (BTW คุณไม่จำเป็นต้องส่งออก) สร้างความแตกต่าง
PSkocik

@PSkocik เท่าที่ฉันรู้[[เป็นตัวทุบตีและ[เป็นจริง/bin/[ซึ่งเป็นเช่นเดียวกับ/bin/test- โปรแกรมภายนอก ซึ่งเป็นสาเหตุที่ช้าลง
tomsmeding

@tomsmending [เป็น builtin ในเชลล์ทั่วไปทั้งหมด (ลอง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

7

คำตอบ: Bash ช้ากว่า Python มาก

หนึ่งในตัวอย่างเล็ก ๆ น้อย ๆ อยู่ในโพสต์บล็อกการปฏิบัติงานของหลายภาษา


3

ไม่มีอะไรผิดปกติ (ยกเว้นความคาดหวังของคุณ) เนื่องจาก python ค่อนข้างเร็วสำหรับภาษาที่ไม่ได้คอมไพล์โปรดดูที่https://wiki.python.org/moin/PythonSpeed


1
ฉันค่อนข้างท้อจากคำตอบเช่นนี้นี่เป็นความเห็นของ IMHO
LinuxSecurityFreak

2

นอกเหนือจากความคิดเห็นคุณสามารถเพิ่มประสิทธิภาพรหัสเล็กน้อยเช่น

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

รหัสนี้ควรใช้เวลาสักหน่อยเวลาที่น้อยลง

แต่เห็นได้ชัดว่าไม่เร็วพอที่จะใช้งานได้จริง


-3

ฉันสังเกตเห็นความแตกต่างอย่างมากในการทุบตีจากการใช้คำที่เทียบเท่า "ในขณะที่" และ "จนถึง" จนถึง ":

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
คุณกำลังใช้=งานที่ได้รับมอบหมาย มันจะกลับมาจริงทันที จะไม่มีการวนซ้ำ
สัญลักษณ์แทน

1
คุณเคยใช้ Bash มาก่อนหรือไม่ :)
LinuxSecurityFreak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.