`ใช่ 'เขียนไฟล์อย่างรวดเร็วอย่างไร?


58

ขอยกตัวอย่าง:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

ที่นี่คุณสามารถเห็นได้ว่าคำสั่งyesเขียน11504640เส้นในครั้งที่สองในขณะที่ฉันสามารถเขียนเพียง1953บรรทัดใน 5 วินาทีโดยใช้ทุบตีของและforecho

ตามที่แนะนำในข้อคิดเห็นมีเทคนิคต่าง ๆ เพื่อให้มีประสิทธิภาพมากขึ้น แต่ไม่มีใครเข้ามาใกล้เคียงกับความเร็วของyes:

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

สิ่งเหล่านี้สามารถเขียนได้มากถึง 20,000 บรรทัดในหนึ่งวินาที และพวกเขาสามารถปรับปรุงเพิ่มเติมเพื่อ:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

สิ่งเหล่านี้ทำให้เราได้มากถึง 40,000 บรรทัดในไม่กี่วินาที ดีกว่า แต่ก็ยังเป็นหนทางไกลyesที่สามารถเขียนได้ 11 ล้านบรรทัดในหนึ่งวินาที!

ดังนั้นการyesเขียนไปยังไฟล์อย่างรวดเร็วได้อย่างไร



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

@ MichaelKjörlingคุณพูดถูกdateอาจจะมีน้ำหนักมากให้ดูที่การแก้ไขคำถามของฉัน
Pandya

1
timeout 1 $(while true; do echo "GNU">>file2; done;)เป็นวิธีที่ไม่ถูกต้องที่จะใช้timeout เนื่องจากtimeoutคำสั่งจะเริ่มต้นเมื่อการแทนที่คำสั่งเสร็จสิ้น timeout 1 sh -c 'while true; do echo "GNU">>file2; done'ใช้
muru

1
สรุปคำตอบ: โดยใช้เวลา CPU เพียงอย่างเดียวในการwrite(2)เรียกระบบไม่ใช่บน boatloads ของ syscalls อื่น ๆ โอเวอร์เฮดของเชลล์หรือแม้แต่การสร้างกระบวนการในตัวอย่างแรกของคุณ (ซึ่งจะทำงานและรอdateให้ทุกบรรทัดที่พิมพ์ไปยังไฟล์) หนึ่งวินาทีของการเขียนนั้นเพียงพอที่จะทำให้เกิดปัญหาคอขวดบนดิสก์ I / O (แทนที่จะเป็น CPU / หน่วยความจำ) ในระบบที่ทันสมัยพร้อม RAM จำนวนมาก หากอนุญาตให้ทำงานได้นานกว่าความแตกต่างก็จะเล็กลง (ขึ้นอยู่กับว่าการใช้ bash นั้นแย่เพียงใดและความเร็วที่สัมพันธ์กันของ CPU และดิสก์คุณอาจไม่ได้ใช้ I / O ของดิสก์ที่อิ่มตัวด้วย bash)
Peter Cordes

คำตอบ:


65

สรุป:

yesแสดงพฤติกรรมคล้ายกับสาธารณูปโภคมาตรฐานอื่น ๆ ส่วนใหญ่ซึ่งมักจะเขียนไปยังกระแสไฟกับการส่งออกบัฟเฟอร์โดย libc ผ่านstdio สิ่งเหล่านี้ทำ syscall write()ทุก ๆ 4kb (16kb หรือ 64kb)หรืออะไรก็ตามที่บล็อกเอาต์พุตของBUFSIZคืออะไร echoเป็นต่อwrite() GNUนั่นเป็นจำนวนมากของโหมดสลับ (ซึ่งไม่เห็นได้ชัดว่าเป็นค่าใช้จ่ายเป็นบริบทสวิทช์ )

และนั่นไม่ได้เป็นการพูดถึงเลยว่านอกเหนือจากลูปการเพิ่มประสิทธิภาพเริ่มต้นแล้วyesมันเป็นคอมไพล์ C เล็ก ๆ ที่เรียบง่ายเล็ก ๆ ที่คอมไพล์และเชลล์ลูปของคุณนั้นไม่มีทางเทียบได้กับคอมไพเลอร์ที่ดีที่สุด


แต่ฉันผิด:

เมื่อฉันพูดก่อนหน้านี้ว่าyesใช้ stdio ฉันคิดว่ามันทำเพราะมันทำตัวเหมือนคนที่ทำ สิ่งนี้ไม่ถูกต้อง - มันเลียนแบบพฤติกรรมของพวกเขาด้วยวิธีนี้เท่านั้น สิ่งที่ไม่จริงเป็นอย่างมากเช่นอนาล็อกเพื่อสิ่งที่ผมทำอยู่ด้านล่างด้วยเปลือก: มันเป็นครั้งแรกลูป conflate อาร์กิวเมนต์(หรือyถ้าไม่มี)BUFSIZจนกว่าพวกเขาอาจจะเติบโตไม่มากไม่เกิน

ความคิดเห็นจากแหล่งที่มาก่อนหน้าforสถานะลูปที่เกี่ยวข้องทันที:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yesไม่มันทำเองwrite()หลังจากนั้น


พูดนอกเรื่อง:

(ตามเดิมรวมอยู่ในคำถามและเก็บไว้สำหรับบริบทเพื่อคำอธิบายข้อมูลที่อาจเขียนไว้แล้วที่นี่) :

ฉันพยายามtimeout 1 $(while true; do echo "GNU">>file2; done;)แต่ไม่สามารถหยุดลูปได้

timeoutปัญหาที่คุณมีกับทดแทนคำสั่ง - ฉันคิดว่าฉันได้รับมันในขณะนี้และสามารถอธิบายได้ว่าทำไมมันไม่ได้หยุด timeoutไม่เริ่มเพราะบรรทัดคำสั่งไม่เคยทำงาน เชลล์ของคุณใช้เปลือกลูกเปิดท่อบน stdout แล้วอ่านมัน มันจะหยุดอ่านเมื่อเด็กจบการทำงานแล้วมันจะแปลความหมายทั้งหมดที่เด็กเขียน$IFSmangling และการขยาย glob และกับผลลัพธ์ที่ได้ก็จะเข้ามาแทนที่ทุกอย่างจากการจับคู่$()

แต่ถ้าเด็กเป็นลูปที่ไม่มีที่สิ้นสุดที่ไม่เคยเขียนไปที่ไพพ์เด็กก็จะไม่หยุดลูปและtimeoutบรรทัดคำสั่งของมันก็ไม่เคยเสร็จสมบูรณ์มาก่อน(อย่างที่ฉันเดา)คุณทำCTRL-Cและฆ่าลูปลูก ดังนั้นไม่timeoutสามารถ ฆ่าลูปที่ต้องทำให้เสร็จก่อนที่จะเริ่มได้


อื่น ๆtimeout:

... ก็ไม่เกี่ยวข้องกับปัญหาประสิทธิภาพการทำงานของคุณเป็นระยะเวลาที่โปรแกรมเชลล์ของคุณต้องใช้การสลับระหว่างผู้ใช้และโหมดเคอร์เนลเพื่อจัดการผลผลิต timeoutแม้ว่าจะไม่ยืดหยุ่นเท่ากับเชลล์ที่อาจใช้เพื่อจุดประสงค์นี้: โดยที่ shells excel อยู่ในความสามารถในการรวมอาร์กิวเมนต์และจัดการกระบวนการอื่น ๆ

ดังที่ได้กล่าวไว้ที่อื่นการย้ายการ[fd-num] >> named_fileเปลี่ยนเส้นทางของคุณไปยังเป้าหมายเอาต์พุตของลูปแทนที่จะส่งเฉพาะการส่งเอาต์พุตที่นั่นสำหรับคำสั่งที่วนซ้ำสามารถปรับปรุงประสิทธิภาพได้อย่างopen()มาก นอกจากนี้ยังทำด้านล่างด้วย|ไพพ์ที่มีเป้าหมายเป็นเอาต์พุตสำหรับลูปด้านใน


เปรียบเทียบโดยตรง:

คุณอาจชอบ:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

ซึ่งเป็นชนิดเช่นความสัมพันธ์ของคำสั่งย่อยก่อนที่จะอธิบาย แต่มีท่อไม่มีและเด็กจะ backgrounded จนกว่ามันฆ่าแม่ ในyesกรณีที่ผู้ปกครองถูกแทนที่จริง ๆ ตั้งแต่เด็กวางไข่ แต่เปลือกเรียกyesโดยการซ้อนกระบวนการของตัวเองด้วยอันใหม่ดังนั้น PID จึงยังคงเหมือนเดิมและเด็กซอมบี้ของมันก็ยังรู้ว่าใครจะฆ่าหลังจากทั้งหมด


บัฟเฟอร์ที่ใหญ่กว่า:

ตอนนี้ให้ดูเกี่ยวกับการเพิ่มwrite()บัฟเฟอร์ของเชลล์

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

ฉันเลือกตัวเลขนั้นเพราะสตริงเอาท์พุทที่ยาวเกิน 1kb จะถูกแบ่งออกเป็นส่วนแยกwrite()สำหรับฉัน และนี่คือการวนซ้ำอีกครั้ง:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

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


ที่เกี่ยวข้อง:

ตามที่ร้องขอมีคำอธิบายอย่างละเอียดมากกว่าความคิดเห็นของโค้ดเพียงสิ่งที่ทำที่นี่ในลิงค์นี้


@ heemayl - อาจจะ? ฉันไม่แน่ใจทั้งหมดฉันเข้าใจสิ่งที่คุณถาม? เมื่อโปรแกรมใช้ stdio ที่จะเขียนออกมันจะมีอย่างใดอย่างหนึ่งบัฟเฟอร์(เช่น stderr ค่าเริ่มต้น)หรือสายบัฟเฟอร์(กับขั้วโดยค่าเริ่มต้น)หรือบล็อกบัฟเฟอร์(สิ่งอื่น ๆ โดยทั่วไปมากที่สุดคือการกำหนดโดยค่าเริ่มต้น) ฉันไม่ชัดเจนเล็กน้อยเกี่ยวกับสิ่งที่กำหนดขนาดของบัฟเฟอร์ส่งออก - แต่โดยทั่วไปจะเป็น 4kb บางส่วน และฟังก์ชัน stdio lib จะรวบรวมผลลัพธ์ของพวกเขาจนกว่าพวกเขาจะสามารถเขียนบล็อกทั้งหมด ddเป็นเครื่องมือมาตรฐานเดียวที่ไม่ใช้ stdio แน่นอน คนอื่น ๆ ส่วนใหญ่ทำ
mikeserv

3
รุ่นของเชลล์กำลังทำงานopenอยู่ (ที่มีอยู่) writeและclose(ซึ่งฉันเชื่อว่ายังคงรอการล้างข้อมูล) และสร้างกระบวนการใหม่และดำเนินการdateสำหรับแต่ละลูป
dave_thompson_085

@ dave_thompson_085 - ไปที่/ dev / แชท และสิ่งที่คุณพูดไม่จำเป็นต้องเป็นความจริงอย่างที่คุณเห็น ยกตัวอย่างเช่นการทำที่wc -lห่วงด้วยbashสำหรับผมที่ได้รับ 1/5 ของการส่งออกshห่วงไม่ - bashการบริหารจัดการน้อยกว่า 100k writes()เพื่อdash's 500k
mikeserv

ขอโทษฉันคลุมเครือ ฉันหมายถึงรุ่นของเชลล์ในคำถามซึ่งในขณะที่ฉันอ่านมันมีเพียงเวอร์ชันดั้งเดิมที่มีการfor((sec0=`date +%S`;...ควบคุมเวลาและการเปลี่ยนเส้นทางในลูปไม่ใช่การปรับปรุงที่ตามมา
dave_thompson_085

@ dave_thompson_085 - ไม่เป็นไร คำตอบนั้นผิดเกี่ยวกับประเด็นพื้นฐานบางอย่างและควรจะถูกต้องมากในตอนนี้ตามที่หวังไว้
mikeserv

20

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

  • เปิดไฟล์สำหรับเขียน
  • เรียกใช้ฟังก์ชั่นที่ได้รับการปรับปรุงและรวบรวมสำหรับการเขียนไปยังกระแส
  • กระแสข้อมูลถูกบัฟเฟอร์ดังนั้น syscall (สวิตช์ราคาแพงไปยังโหมดเคอร์เนล) เกิดขึ้นน้อยมากในกลุ่มก้อนขนาดใหญ่
  • ปิดไฟล์

สคริปต์ของคุณทำอะไร:

  • อ่านในบรรทัดของรหัส
  • ตีความรหัสทำให้มีการดำเนินการพิเศษมากมายในการแยกวิเคราะห์อินพุตของคุณและคิดว่าจะทำอย่างไร
  • สำหรับการวนซ้ำแต่ละรอบของ while (ซึ่งอาจไม่ถูกในภาษาที่แปล):
    • เรียกdateคำสั่งภายนอกและเก็บเอาท์พุทของมัน (เฉพาะในรุ่นเดิม - ในรุ่นที่แก้ไขคุณได้รับปัจจัย 10 โดยไม่ทำเช่นนี้)
    • ทดสอบว่าตรงตามเงื่อนไขการเลิกจ้างของลูปหรือไม่
    • เปิดไฟล์ในโหมดต่อท้าย
    • echoคำสั่งparse รู้จัก (ด้วยรหัสการจับคู่รูปแบบ) เป็น shell builtin การขยายพารามิเตอร์การโทรและทุกอย่างอื่นในอาร์กิวเมนต์ "GNU" และสุดท้ายเขียนบรรทัดลงในไฟล์ที่เปิด
    • ปิดไฟล์อีกครั้ง
    • ทำซ้ำกระบวนการ

ชิ้นส่วนที่มีราคาแพง: การตีความทั้งหมดมีราคาแพงมาก (ทุบตีกำลังทำการประมวลผลอินพุตทั้งหมดไว้อย่างน่ากลัวมาก - สตริงของคุณอาจมีการทดแทนตัวแปรการทดแทนกระบวนการการขยายส่วนขยายรั้งอักขระหลบหนีและอื่น ๆ ) อาจเป็นคำสั่ง switch ที่มีการเปลี่ยนเส้นทางไปยังฟังก์ชันที่เกี่ยวข้องกับ builtin และที่สำคัญคุณเปิดและปิดไฟล์สำหรับแต่ละบรรทัดของเอาต์พุต คุณสามารถใส่>> fileวงวนด้านนอกเพื่อทำให้เร็วขึ้นมากแต่คุณยังคงอยู่ในภาษาที่ตีความ คุณโชคดีมากechoเป็น shell builtin ไม่ใช่คำสั่งภายนอกมิฉะนั้นลูปของคุณจะเกี่ยวข้องกับการสร้างกระบวนการใหม่ (fork & exec) ในการวนซ้ำทุกครั้ง ซึ่งจะบดกระบวนการให้หยุดชะงัก - คุณเห็นว่ามีค่าใช้จ่ายเท่าใดเมื่อคุณมีdateคำสั่งในลูป


11

คำตอบอื่น ๆ ได้กล่าวถึงประเด็นหลักแล้ว ในหมายเหตุด้านข้างคุณสามารถเพิ่มทรูพุตของ while while ของคุณโดยการเขียนไปยังไฟล์เอาต์พุตเมื่อสิ้นสุดการคำนวณ เปรียบเทียบ:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

กับ

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s

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