Bagaimana `ya` menulis ke file begitu cepat?

58

Izinkan saya memberi contoh:

$ 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

Di sini Anda dapat melihat bahwa perintah yesmenulis 11504640baris dalam sedetik sementara saya hanya dapat menulis 1953baris dalam 5 detik menggunakan bash's fordan echo.

Seperti yang disarankan dalam komentar, ada berbagai trik untuk membuatnya lebih efisien tetapi tidak ada yang mendekati kecepatan 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

Ini dapat menulis hingga 20 ribu baris dalam satu detik. Dan mereka dapat ditingkatkan lebih lanjut ke:

$ 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

Ini memberi kita hingga 40 ribu baris dalam satu detik. Lebih baik, tetapi masih jauh dari yesyang bisa menulis sekitar 11 juta baris dalam satu detik!

Jadi, bagaimana yesmenulis ke file begitu cepat?

Pandya
sumber
9
Dalam contoh kedua, Anda memiliki dua permintaan perintah eksternal untuk setiap iterasi dari loop, dan dateagak berat, ditambah shell harus membuka kembali aliran output untuk echountuk setiap iterasi loop. Dalam contoh pertama, hanya ada satu perintah perintah dengan redirection output tunggal, dan perintah ini sangat ringan. Keduanya sama sekali tidak sebanding.
CVn
@ MichaelKjörling Anda benar datemungkin berat, lihat edit pertanyaan saya.
Pandya
1
timeout 1 $(while true; do echo "GNU">>file2; done;)adalah cara yang salah untuk digunakan timeout karena timeoutperintah hanya akan mulai setelah substitusi perintah selesai. Gunakan timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
muru
1
ringkasan jawaban: dengan hanya menghabiskan waktu CPU pada write(2)panggilan sistem, bukan pada muatan kapal dari syscall lain, overhead shell, atau bahkan proses pembuatan dalam contoh pertama Anda (yang berjalan dan menunggu dateuntuk setiap baris yang dicetak ke file). Satu detik penulisan hampir tidak cukup untuk bottleneck pada disk I / O (bukan CPU / memori), pada sistem modern dengan banyak RAM. Jika dibiarkan berjalan lebih lama, selisihnya akan lebih kecil. (Tergantung pada seberapa buruk implementasi bash yang Anda gunakan, dan kecepatan relatif CPU dan disk, Anda mungkin bahkan tidak memenuhi I / O disk dengan bash).
Peter Cordes

Jawaban:

65

singkatnya:

yesmenunjukkan perilaku yang mirip dengan sebagian besar utilitas standar lainnya yang biasanya menulis ke STREAM FILE dengan output buffered oleh libC via stdio . Ini hanya melakukan syscall write()setiap 4kb (16kb atau 64kb) atau apa pun blok output BUFSIZ . echoadalah write()per GNU. Itu banyak dari modus-switching (yang tidak, tampaknya, sebagai mahal sebagai konteks-switch ) .

Dan itu sama sekali tidak menyebutkan bahwa, selain loop optimasi awal, yesadalah loop C sangat kecil, dikompilasi dan loop shell Anda sama sekali tidak sebanding dengan program yang dioptimalkan kompiler.


tapi saya salah:

Ketika saya mengatakan sebelumnya bahwa yesstdio yang digunakan, saya hanya berasumsi itu terjadi karena berperilaku seperti yang dilakukan. Ini tidak benar - hanya meniru perilaku mereka dengan cara ini. Apa yang sebenarnya dilakukannya sangat mirip dengan analog dengan hal yang saya lakukan di bawah ini dengan shell: pertama-tama loop untuk mengacaukan argumennya (atau yjika tidak ada) sampai mereka tidak dapat tumbuh lagi tanpa melebihi BUFSIZ.

Komentar dari sumber segera sebelum status forloop yang relevan :

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

yesapakah itu sendiri write()setelah itu.


penyimpangan:

(Seperti awalnya termasuk dalam pertanyaan dan disimpan untuk konteks untuk penjelasan yang mungkin informatif sudah ditulis di sini) :

Saya sudah mencoba timeout 1 $(while true; do echo "GNU">>file2; done;)tetapi tidak dapat menghentikan loop.

The timeoutmasalah Anda dengan substitusi perintah - saya pikir saya mendapatkannya sekarang, dan dapat menjelaskan mengapa itu tidak berhenti. timeouttidak memulai karena baris perintahnya tidak pernah dijalankan. Cangkang Anda mencangkang cangkang anak, membuka pipa di stdout-nya, dan membacanya. Ini akan berhenti membaca ketika anak berhenti, dan kemudian akan menafsirkan semua anak menulis untuk $IFSekspansi mangling dan glob, dan dengan hasilnya ia akan mengganti semuanya dari $(yang cocok ).

Tetapi jika anak adalah loop tanpa akhir yang tidak pernah menulis ke pipa, maka anak tidak pernah berhenti looping, dan timeoutbaris perintah tidak pernah selesai sebelumnya (seperti yang saya duga) Anda lakukan CTRL-Cdan bunuh loop anak. Jadi tidak pernahtimeout bisa mematikan loop yang harus diselesaikan sebelum bisa mulai.


timeouts lainnya :

... sama sekali tidak relevan dengan masalah kinerja Anda karena jumlah waktu yang harus dihabiskan oleh program shell Anda untuk beralih antara mode pengguna dan kernel untuk menangani output. timeout, bagaimanapun, tidak sefleksibel mungkin untuk tujuan ini: di mana shell unggul dalam kemampuan mereka untuk mengurai argumen dan mengelola proses lainnya.

Seperti dicatat di tempat lain, hanya memindahkan [fd-num] >> named_fileredirection Anda ke target keluaran loop daripada hanya mengarahkan output di sana untuk perintah looped secara substansial dapat meningkatkan kinerja karena dengan cara itu setidaknya open()syscall hanya perlu dilakukan sekali. Ini juga dilakukan di bawah dengan |pipa yang ditargetkan sebagai output untuk loop dalam.


perbandingan langsung:

Anda mungkin suka:

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

Yang jenis seperti perintah sub hubungan dijelaskan sebelumnya, tetapi tidak ada pipa dan anak dilatarbelakangi sampai membunuh orang tua. Dalam yeskasus orang tua sebenarnya telah diganti sejak anak itu lahir, tetapi shell memanggil yesdengan overlay prosesnya sendiri dengan yang baru dan sehingga PID tetap sama dan anak zombie-nya masih tahu siapa yang harus dibunuh setelah semua.


buffer yang lebih besar:

Sekarang mari kita lihat tentang meningkatkan write()buffer shell .

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

Saya memilih nomor itu karena string keluaran lebih dari 1 kb sedang dibagi menjadi terpisah write()untuk saya. Dan inilah loop lagi:

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

Itu 300 kali jumlah data yang ditulis oleh shell dalam jumlah waktu yang sama untuk tes ini daripada yang terakhir. Tidak terlalu buruk. Tapi ternyata tidak yes.


terkait:

Seperti yang diminta, ada deskripsi yang lebih menyeluruh daripada sekadar komentar kode tentang apa yang dilakukan di sini di tautan ini .

mikeserv
sumber
@ heemayl - mungkin? Saya tidak sepenuhnya yakin saya mengerti apa yang Anda minta? ketika sebuah program menggunakan stdio untuk menulis keluaran, ia melakukannya tanpa buffering (seperti stderr secara default) atau line buffering (ke terminal secara default) atau block buffering (pada dasarnya sebagian besar hal lain diatur dengan cara ini secara default) . im sedikit tidak jelas tentang apa yang mengatur ukuran buffer output - tetapi biasanya beberapa 4kb. dan fungsi stdio lib akan mengumpulkan output mereka sampai mereka dapat menulis seluruh blok. ddadalah salah satu alat standar yang pasti tidak menggunakan stdio, misalnya. kebanyakan orang lain melakukannya.
mikeserv
3
Versi shell sedang melakukan open(sudah ada) writeDAN close(yang saya percaya masih menunggu flush), DAN membuat proses baru dan mengeksekusi date, untuk setiap loop.
dave_thompson_085
@ dave_thompson_085 - pergi ke / dev / chat . dan apa yang Anda katakan belum tentu benar, seperti yang Anda lihat di sana. Sebagai contoh, melakukan wc -lloop dengan bashsaya mendapatkan 1/5 dari output shloop - bashmengelola sedikit lebih dari 100rb writes()sampai dash500rb.
mikeserv
Maaf saya ambigu; Maksud saya versi shell dalam pertanyaan, yang pada saat saya baca itu hanya memiliki versi asli dengan for((sec0=`date +%S`;...untuk mengontrol waktu dan pengalihan dalam loop, bukan perbaikan selanjutnya.
dave_thompson_085
@ dave_thompson_085 - tidak masalah. tetap saja jawabannya salah tentang beberapa poin mendasar, dan seharusnya sudah cukup benar sekarang, seperti yang saya harapkan.
mikeserv
20

Pertanyaan yang lebih baik adalah mengapa shell Anda menulis file dengan sangat lambat. Setiap program terkompilasi mandiri yang menggunakan syscalls menulis file secara bertanggung jawab (tidak menyiram setiap karakter pada suatu waktu) akan melakukannya dengan cepat. Apa yang Anda lakukan, adalah menulis baris dalam bahasa yang ditafsirkan (shell), dan di samping itu Anda melakukan banyak operasi input output yang tidak perlu. Apa yang yesdilakukan:

  • membuka file untuk ditulis
  • panggilan fungsi yang dioptimalkan dan dikompilasi untuk menulis ke aliran
  • aliran buffered, jadi syscall (pergantian yang mahal ke mode kernel) jarang terjadi, dalam potongan besar
  • menutup file

Apa yang dilakukan skrip Anda:

  • membaca dalam satu baris kode
  • menafsirkan kode, membuat banyak operasi tambahan untuk benar-benar menguraikan input Anda dan mencari tahu apa yang harus dilakukan
  • untuk setiap iterasi loop sementara (yang mungkin tidak murah dalam bahasa yang ditafsirkan):
    • panggil dateperintah eksternal dan simpan hasilnya (hanya dalam versi asli - dalam versi revisi Anda mendapatkan faktor 10 dengan tidak melakukan ini)
    • menguji apakah kondisi terminasi loop terpenuhi
    • buka file dalam mode tambahkan
    • echoperintah parse , mengenalinya (dengan beberapa kode kode yang cocok) sebagai shell builtin, memanggil parameter ekspansi dan segala sesuatu yang lain pada argumen "GNU", dan akhirnya menulis baris ke file yang terbuka
    • tutup file lagi
    • ulangi prosesnya

Bagian yang mahal: keseluruhan interpretasinya sangat mahal (bash melakukan banyak sekali preprocessing dari semua input - string Anda berpotensi mengandung substitusi variabel, proses substitusi, ekspansi brace, karakter pelarian dan banyak lagi), setiap panggilan dari sebuah builtin adalah mungkin pernyataan beralih dengan pengalihan ke fungsi yang berkaitan dengan builtin, dan yang sangat penting, Anda membuka dan menutup file untuk setiap baris output. Anda bisa meletakkan di >> fileluar loop sementara untuk membuatnya lebih cepat , tetapi Anda masih dalam bahasa yang ditafsirkan. Anda cukup beruntungechoadalah shell builtin, bukan perintah eksternal - jika tidak, loop Anda akan melibatkan pembuatan proses baru (fork & exec) pada setiap iterasi tunggal. Yang akan menghentikan proses ini - Anda melihat betapa mahalnya itu ketika Anda memiliki dateperintah dalam loop.

orion
sumber
11

Jawaban lain telah membahas poin utama. Di samping catatan, Anda dapat meningkatkan throughput loop sementara Anda dengan menulis ke file output di akhir perhitungan. Membandingkan:

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

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

dengan

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

real    0m0.030s
user    0m0.019s
sys     0m0.011s
Apoorv Gupta
sumber
Ya, ini penting dan kecepatan menulis (setidaknya) menggandakan dalam kasus saya
Pandya