Menjawab pertanyaan ini membuat saya mengajukan pertanyaan lain:
Saya pikir skrip berikut melakukan hal yang sama dan yang kedua harus jauh lebih cepat, karena yang pertama menggunakan cat
yang perlu membuka file berulang kali tetapi yang kedua hanya membuka file satu kali dan kemudian hanya menggemakan variabel:
(Lihat bagian pembaruan untuk kode yang benar.)
Pertama:
#!/bin/sh
for j in seq 10; do
cat input
done >> output
Kedua:
#!/bin/sh
i=`cat input`
for j in seq 10; do
echo $i
done >> output
sedangkan input sekitar 50 megabita.
Tetapi ketika saya mencoba yang kedua, itu terlalu, terlalu lambat karena menggemakan variabel i
adalah proses besar. Saya juga mendapat beberapa masalah dengan skrip kedua, misalnya ukuran file output lebih rendah dari yang diharapkan.
Saya juga memeriksa halaman manual echo
dan cat
membandingkannya:
echo - menampilkan satu baris teks
cat - menyatukan file dan mencetak pada output standar
Tetapi saya tidak mendapatkan perbedaannya.
Begitu:
- Mengapa kucing sangat cepat dan gema sangat lambat dalam naskah kedua?
- Atau masalah dengan variabel
i
? (karena di halaman manualecho
dikatakan menampilkan "satu baris teks" dan jadi saya kira itu dioptimalkan hanya untuk variabel pendek, bukan untuk variabel yang sangat panjang sepertii
. Namun, itu hanya dugaan.) - Dan mengapa saya mendapat masalah saat menggunakan
echo
?
MEMPERBARUI
Saya menggunakan seq 10
bukannya `seq 10`
salah. Ini adalah kode yang diedit:
Pertama:
#!/bin/sh
for j in `seq 10`; do
cat input
done >> output
Kedua:
#!/bin/sh
i=`cat input`
for j in `seq 10`; do
echo $i
done >> output
(Terima kasih khusus kepada roaima .)
Namun, bukan itu masalahnya. Bahkan jika loop hanya terjadi satu kali, saya mendapatkan masalah yang sama: cat
bekerja jauh lebih cepat daripada echo
.
cat $(for i in $(seq 1 10); do echo "input"; done) >> output
? :)echo
lebih cepat. Apa yang Anda lewatkan adalah bahwa Anda membuat shell melakukan terlalu banyak pekerjaan dengan tidak mengutip variabel ketika Anda menggunakannya.printf '%s' "$i"
, bukanecho $i
. @cuonglm menjelaskan beberapa masalah gema dengan baik dalam jawabannya. Untuk alasan mengapa bahkan mengutip tidak cukup dalam beberapa kasus dengan gema, lihat unix.stackexchange.com/questions/65803/…Jawaban:
Ada beberapa hal yang perlu dipertimbangkan di sini.
bisa mahal dan ada banyak variasi di antara cangkang.
Itu fitur yang disebut substitusi perintah. Idenya adalah untuk menyimpan seluruh output dari perintah dikurangi karakter baris baru ke dalam
i
variabel dalam memori.Untuk melakukan itu, shells fork perintah dalam subkulit dan membaca outputnya melalui pipa atau soketpair. Anda melihat banyak variasi di sini. Pada file 50MiB di sini, saya dapat melihat misalnya bash menjadi 6 kali lebih lambat dari ksh93 tetapi sedikit lebih cepat dari zsh dan dua kali lebih cepat
yash
.Alasan utama untuk
bash
menjadi lambat adalah bahwa ia membaca dari 128 byte pipa sekaligus (sementara shell lain membaca 4KiB atau 8KiB pada suatu waktu) dan dihukum oleh overhead panggilan sistem.zsh
perlu melakukan beberapa post-processing untuk keluar dari NUL byte (shell lain memecah NUL bytes), danyash
melakukan lebih banyak tugas berat dengan mem-parsing karakter multi-byte.Semua shell perlu menghapus karakter baris baru yang mungkin mereka lakukan lebih atau kurang efisien.
Beberapa mungkin ingin menangani byte NUL lebih anggun daripada yang lain dan memeriksa keberadaan mereka.
Kemudian setelah Anda memiliki variabel besar dalam memori, manipulasi apa pun pada umumnya melibatkan mengalokasikan lebih banyak memori dan mengatasi data.
Di sini, Anda mengirim (bermaksud untuk mengirimkan) konten variabel ke
echo
.Untungnya,
echo
sudah ada di dalam shell Anda, jika tidak eksekusi akan gagal dengan daftar argumen yang terlalu panjang . Bahkan kemudian, membangun array daftar argumen mungkin akan melibatkan menyalin konten variabel.Masalah utama lainnya dalam pendekatan substitusi perintah Anda adalah bahwa Anda menjalankan operator split + glob (dengan lupa mengutip variabel).
Untuk itu, shell perlu memperlakukan string sebagai string karakter (meskipun beberapa shell tidak dan bermasalah dalam hal itu) sehingga dalam lokal UTF-8, itu berarti menguraikan urutan UTF-8 (jika tidak dilakukan sudah seperti yang
yash
dilakukan) , cari$IFS
karakter dalam string. Jika$IFS
berisi spasi, tab, atau baris baru (yang merupakan kasus secara default), algoritme itu bahkan lebih kompleks dan mahal. Kemudian, kata-kata yang dihasilkan dari pemisahan itu perlu dialokasikan dan disalin.Bagian gumpalan akan lebih mahal. Jika ada kata-kata berisi karakter gumpal (
*
,?
,[
), maka shell akan harus membaca isi dari beberapa direktori dan melakukan beberapa pencocokan pola mahal (bash
's pelaksanaan misalnya ini sangat sangat buruk pada saat itu).Jika input berisi sesuatu seperti
/*/*/*/../../../*/*/*/../../../*/*/*
, itu akan sangat mahal karena itu berarti daftar ribuan direktori dan yang dapat berkembang hingga beberapa ratus MiB.Maka
echo
biasanya akan melakukan beberapa pemrosesan tambahan. Beberapa implementasi memperluas\x
urutan dalam argumen yang diterimanya, yang berarti mengurai konten dan mungkin alokasi lain dan menyalin data.Di sisi lain, OK, dalam kebanyakan shell
cat
tidak built-in, sehingga itu berarti forking proses dan mengeksekusinya (jadi memuat kode dan pustaka), tetapi setelah doa pertama, kode itu dan isi file input akan di-cache dalam memori. Di sisi lain, tidak akan ada perantara.cat
akan membaca sejumlah besar dalam satu waktu dan menulisnya langsung tanpa diproses, dan tidak perlu mengalokasikan sejumlah besar memori, hanya satu buffer yang digunakan kembali.Ini juga berarti bahwa itu jauh lebih dapat diandalkan karena tidak tersedak NUL byte dan tidak memotong karakter baris baru (dan tidak melakukan split + glob, meskipun Anda dapat menghindarinya dengan mengutip variabel, dan tidak perluas urutan escape meskipun Anda bisa menghindarinya dengan menggunakan
printf
alih-alihecho
).Jika Anda ingin mengoptimalkannya lebih lanjut, alih-alih memohon
cat
beberapa kali, berikan sajainput
beberapa kalicat
.Akan menjalankan 3 perintah, bukan 100.
Untuk membuat versi variabel lebih dapat diandalkan, Anda harus menggunakan
zsh
(shell lain tidak dapat mengatasi byte NUL) dan melakukannya:Jika Anda tahu input tidak mengandung byte NUL, maka Anda dapat melakukannya dengan POSIXly (meskipun mungkin tidak berfungsi di tempat
printf
yang tidak dibangun) dengan:Tapi itu tidak akan pernah lebih efisien daripada menggunakan
cat
dalam loop (kecuali inputnya sangat kecil).sumber
/bin/echo $(perl -e 'print "A"x999999')
dd bs=128 < input > /dev/null
dengandd bs=64 < input > /dev/null
. Dari 0,6s yang dibutuhkan untuk bash untuk membaca file itu, 0,4 dihabiskan dalamread
panggilan sistem dalam tes saya, sementara shell lain menghabiskan lebih sedikit waktu di sana.readwc()
dantrim()
di Burne Shell mengambil 30% dari seluruh waktu dan ini kemungkinan besar diremehkan karena tidak ada libc dengangprof
anotasi untukmbtowc()
.\x
diperluas?Masalahnya bukan tentang
cat
danecho
, ini tentang variabel kutipan yang terlupakan$i
.Dalam skrip shell Bourne-like (kecuali
zsh
), membiarkan variabel tanda kutip menyebabkanglob+split
operator pada variabel.sebenarnya:
Jadi dengan setiap iterasi loop, seluruh konten
input
(tidak termasuk baris baru) akan diperluas, dipecah, di-globbing. Seluruh proses membutuhkan shell untuk mengalokasikan memori, mengurai string berulang-ulang. Itulah alasan Anda mendapatkan kinerja yang buruk.Anda dapat mengutip variabel untuk mencegah
glob+split
tetapi tidak akan banyak membantu Anda, karena ketika shell masih perlu membangun argumen string besar dan memindai isinya untukecho
(Mengganti builtinecho
dengan eksternal/bin/echo
akan memberi Anda daftar argumen terlalu lama atau kehabisan memori tergantung pada$i
ukuran). Sebagian besarecho
implementasi tidak sesuai dengan POSIX, itu akan memperluas\x
urutan backslash dalam argumen yang diterimanya.Dengan
cat
, shell hanya perlu menelurkan proses setiap loop iterasi dancat
akan melakukan copy i / o. Sistem juga dapat men-cache konten file untuk membuat proses kucing lebih cepat.sumber
/*/*/*/*../../../../*/*/*/*/../../../../
bisa ada dalam konten file. Hanya ingin menunjukkan detailnya .time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s
time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
glob+split
bagian, dan itu akan mempercepat loop while. Dan saya juga mencatat bahwa itu tidak akan banyak membantu Anda. Karena ketika sebagian besarecho
perilaku shell tidak mematuhi POSIX.printf '%s' "$i"
lebih baik.Jika Anda menelepon
ini memungkinkan proses shell Anda tumbuh hingga 50MB hingga 200MB (tergantung pada implementasi karakter lebar internal). Ini mungkin membuat shell Anda lambat tetapi ini bukan masalah utama.
Masalah utama adalah bahwa perintah di atas perlu membaca seluruh file menjadi memori shell dan
echo $i
kebutuhan untuk melakukan pemisahan bidang pada konten file di$i
. Untuk melakukan pemisahan bidang, semua teks dari file perlu dikonversi menjadi karakter lebar dan ini adalah tempat sebagian besar waktu dihabiskan.Saya melakukan beberapa tes dengan case lambat dan mendapatkan hasil ini:
Alasan mengapa ksh93 adalah yang tercepat tampaknya karena ksh93 tidak menggunakan
mbtowc()
dari libc melainkan implementasi sendiri.BTW: Stephane keliru bahwa ukuran baca memiliki pengaruh, saya mengkompilasi Bourne Shell untuk membaca 4096 byte, bukan 128 byte dan mendapatkan kinerja yang sama dalam kedua kasus.
sumber
i=`cat input`
perintah tidak melakukan membelah lapangan, ituecho $i
yang tidak. Waktu yang dihabiskani=`cat input`
akan diabaikan dibandingkan denganecho $i
, tetapi tidak dibandingkan dengancat input
sendirian, dan dalam kasusbash
, perbedaannya adalah untuk bagian terbaik karenabash
melakukan pembacaan kecil. Mengubah dari 128 ke 4096 tidak akan memiliki pengaruh pada kinerjaecho $i
, tapi bukan itu yang saya maksudkan.echo $i
akan sangat bervariasi tergantung pada konten input dan sistem file (jika mengandung karakter IFS atau glob), itulah sebabnya saya tidak melakukan perbandingan shell pada jawaban saya. Sebagai contoh, di sini pada output dariyes | ghead -c50M
, ksh93 adalah yang paling lambat dari semua, tetapiyes | ghead -c50M | paste -sd: -
, ini yang tercepat.Dalam kedua kasus, loop akan dijalankan hanya dua kali (satu kali untuk kata
seq
dan satu kali untuk kata10
).Futhermore keduanya akan menggabungkan spasi putih yang berdekatan, dan drop spasi putih memimpin / tertinggal, sehingga output tidak harus dua salinan input.
Pertama
Kedua
Salah satu alasan mengapa
echo
ini lebih lambat mungkin karena variabel Anda yang tidak dikutip sedang dibagi di spasi putih menjadi kata-kata yang terpisah. Untuk 50MB itu akan banyak pekerjaan. Kutip variabelnya!Saya sarankan Anda memperbaiki kesalahan ini dan kemudian mengevaluasi kembali timing Anda.
Saya sudah menguji ini secara lokal. Saya membuat file 50MB menggunakan output dari
tar cf - | dd bs=1M count=50
. Saya juga memperpanjang loop untuk dijalankan oleh faktor x100 sehingga timingnya diskalakan ke nilai yang masuk akal (saya menambahkan loop lebih lanjut di sekitar seluruh kode Anda:for k in $(seq 100); do
...done
). Berikut ini waktunya:Seperti yang Anda lihat tidak ada perbedaan nyata, tetapi jika ada versi yang dikandungnya
echo
berjalan sedikit lebih cepat. Jika saya menghapus tanda kutip dan menjalankan versi 2 Anda yang rusak waktu berlipat ganda, menunjukkan bahwa shell harus melakukan lebih banyak pekerjaan yang harus diharapkan.sumber
cat
sangat, sangat cepat daripadaecho
. Script pertama berjalan dalam rata-rata 3 detik, tetapi yang kedua berjalan dalam rata-rata 54 detik.tar cf - | dd bs=1M count=50
? Apakah itu membuat file biasa dengan karakter yang sama di dalamnya? Jika demikian, dalam kasus saya, file input benar-benar tidak beraturan dengan semua jenis karakter dan spasi putih. Dan lagi, saya menggunakantime
seperti yang Anda gunakan, dan hasilnya adalah yang saya katakan: 54 detik vs 3 detik.read
jauh lebih cepat daripadacat
Saya pikir semua orang dapat menguji ini:
cat
membutuhkan 9,372 detik.echo
butuh beberapa.232
detik.read
adalah 40 kali lebih cepat .Tes pertama saya ketika
$p
bergema ke layar mengungkapkanread
adalah 48 kali lebih cepat daricat
.sumber
Ini
echo
dimaksudkan untuk meletakkan 1 baris di layar. Apa yang Anda lakukan dalam contoh kedua adalah Anda meletakkan konten file dalam variabel dan kemudian Anda mencetak variabel itu. Di yang pertama Anda langsung meletakkan konten di layar.cat
dioptimalkan untuk penggunaan ini.echo
tidak. Juga menempatkan 50MB di variabel lingkungan bukan ide yang baik.sumber
echo
dioptimalkan untuk menulis teks?Ini bukan tentang gema yang lebih cepat, ini tentang apa yang Anda lakukan:
Dalam satu kasus Anda membaca dari input dan menulis ke output secara langsung. Dengan kata lain, apa pun yang dibaca dari input melalui cat, pergi ke output melalui stdout.
Dalam kasus lain Anda membaca dari input ke dalam variabel di memori dan kemudian menulis isi dari variabel dalam output.
Yang terakhir akan jauh lebih lambat, terutama jika inputnya 50MB.
sumber