Mengapa ada perbedaan waktu eksekusi gema dan kucing?


15

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 catyang 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 iadalah proses besar. Saya juga mendapat beberapa masalah dengan skrip kedua, misalnya ukuran file output lebih rendah dari yang diharapkan.

Saya juga memeriksa halaman manual echodan catmembandingkannya:

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 manual echodikatakan menampilkan "satu baris teks" dan jadi saya kira itu dioptimalkan hanya untuk variabel pendek, bukan untuk variabel yang sangat panjang seperti i. Namun, itu hanya dugaan.)
  • Dan mengapa saya mendapat masalah saat menggunakan echo?

MEMPERBARUI

Saya menggunakan seq 10bukannya `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: catbekerja jauh lebih cepat daripada echo.


1
dan bagaimana cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
Ini echolebih cepat. Apa yang Anda lewatkan adalah bahwa Anda membuat shell melakukan terlalu banyak pekerjaan dengan tidak mengutip variabel ketika Anda menggunakannya.
roaima

Mengutip variabel bukanlah masalah; masalahnya adalah variabel i sendiri (yaitu menggunakannya sebagai langkah perantara antara input dan output).
Aleksander

`echo $ i` - jangan lakukan ini. Gunakan printf dan kutip argumennya.
PSkocik

1
@ PSkik Apa yang saya katakan adalah Anda inginkan printf '%s' "$i", bukan echo $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/…
PSkocik

Jawaban:


24

Ada beberapa hal yang perlu dipertimbangkan di sini.

i=`cat input`

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 ivariabel 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 bashmenjadi 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.

zshperlu melakukan beberapa post-processing untuk keluar dari NUL byte (shell lain memecah NUL bytes), dan yashmelakukan 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, echosudah 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 yashdilakukan) , cari $IFSkarakter dalam string. Jika $IFSberisi 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 echobiasanya akan melakukan beberapa pemrosesan tambahan. Beberapa implementasi memperluas \xurutan dalam argumen yang diterimanya, yang berarti mengurai konten dan mungkin alokasi lain dan menyalin data.

Di sisi lain, OK, dalam kebanyakan shell cattidak 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. catakan 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 printfalih-alih echo).

Jika Anda ingin mengoptimalkannya lebih lanjut, alih-alih memohon catbeberapa kali, berikan saja inputbeberapa kali cat.

yes input | head -n 100 | xargs cat

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:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

Jika Anda tahu input tidak mengandung byte NUL, maka Anda dapat melakukannya dengan POSIXly (meskipun mungkin tidak berfungsi di tempat printfyang tidak dibangun) dengan:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Tapi itu tidak akan pernah lebih efisien daripada menggunakan catdalam loop (kecuali inputnya sangat kecil).


Perlu disebutkan bahwa dalam kasus argumen panjang, Anda bisa mendapatkan memori . Contoh/bin/echo $(perl -e 'print "A"x999999')
cuonglm

Anda salah dengan asumsi bahwa ukuran membaca memiliki pengaruh yang signifikan, jadi bacalah jawaban saya untuk memahami alasan sebenarnya.
schily

@ Schily, melakukan 40.900 bacaan 128 byte membutuhkan lebih banyak waktu (waktu sistem) dari 800 bacaan 64k. Bandingkan dd bs=128 < input > /dev/nulldengan dd bs=64 < input > /dev/null. Dari 0,6s yang dibutuhkan untuk bash untuk membaca file itu, 0,4 dihabiskan dalam readpanggilan sistem dalam tes saya, sementara shell lain menghabiskan lebih sedikit waktu di sana.
Stéphane Chazelas

Yah, sepertinya Anda tidak menjalankan analisis kinerja nyata. Pengaruh panggilan baca (saat membandingkan berbagai ukuran baca) adalah aprox. 1% dari seluruh waktu sementara fungsi readwc() dan trim()di Burne Shell mengambil 30% dari seluruh waktu dan ini kemungkinan besar diremehkan karena tidak ada libc dengan gprofanotasi untuk mbtowc().
schily

Ke mana \xdiperluas?
Mohammad

11

Masalahnya bukan tentang catdan echo, ini tentang variabel kutipan yang terlupakan $i.

Dalam skrip shell Bourne-like (kecuali zsh), membiarkan variabel tanda kutip menyebabkan glob+splitoperator pada variabel.

$var

sebenarnya:

glob(split($var))

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+splittetapi tidak akan banyak membantu Anda, karena ketika shell masih perlu membangun argumen string besar dan memindai isinya untuk echo(Mengganti builtin echodengan eksternal /bin/echoakan memberi Anda daftar argumen terlalu lama atau kehabisan memori tergantung pada $iukuran). Sebagian besar echoimplementasi tidak sesuai dengan POSIX, itu akan memperluas \xurutan backslash dalam argumen yang diterimanya.

Dengan cat, shell hanya perlu menelurkan proses setiap loop iterasi dan catakan melakukan copy i / o. Sistem juga dapat men-cache konten file untuk membuat proses kucing lebih cepat.


2
@roaima: Anda tidak menyebut bagian glob, yang bisa menjadi alasan besar, membayangkan sesuatu yang /*/*/*/*../../../../*/*/*/*/../../../../bisa ada dalam konten file. Hanya ingin menunjukkan detailnya .
cuonglm

Gotcha terima kasih. Bahkan tanpa itu, waktunya akan berlipat ganda ketika menggunakan variabel yang tidak
dikutip

1
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
netmonk

Saya tidak mengerti mengapa mengutip tidak bisa menyelesaikan masalah. Saya perlu penjelasan lebih lanjut.
Mohammad

1
@ mohammad.k: Seperti yang saya tulis dalam jawaban saya, variabel kutipan mencegah glob+splitbagian, dan itu akan mempercepat loop while. Dan saya juga mencatat bahwa itu tidak akan banyak membantu Anda. Karena ketika sebagian besar echoperilaku shell tidak mematuhi POSIX. printf '%s' "$i"lebih baik.
cuonglm

2

Jika Anda menelepon

i=`cat input`

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 $ikebutuhan 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:

  • Tercepat adalah ksh93
  • Berikutnya adalah Bourne Shell saya (2x lebih lambat ksh93)
  • Berikutnya adalah bash (3x lebih lambat dari ksh93)
  • Terakhir adalah ksh88 (7x lebih lambat dari ksh93)

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.


The i=`cat input`perintah tidak melakukan membelah lapangan, itu echo $iyang tidak. Waktu yang dihabiskan i=`cat input`akan diabaikan dibandingkan dengan echo $i, tetapi tidak dibandingkan dengan cat inputsendirian, dan dalam kasus bash, perbedaannya adalah untuk bagian terbaik karena bashmelakukan pembacaan kecil. Mengubah dari 128 ke 4096 tidak akan memiliki pengaruh pada kinerja echo $i, tapi bukan itu yang saya maksudkan.
Stéphane Chazelas

Juga perhatikan bahwa kinerja echo $iakan 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 dari yes | ghead -c50M, ksh93 adalah yang paling lambat dari semua, tetapi yes | ghead -c50M | paste -sd: -, ini yang tercepat.
Stéphane Chazelas

Ketika berbicara tentang total waktu, saya berbicara tentang keseluruhan implementasi dan ya, tentu saja pemisahan bidang terjadi dengan perintah echo. dan di sinilah sebagian besar waktu dihabiskan.
schily

Anda tentu saja benar bahwa kinerjanya tergantung pada konten od $ i.
schily

1

Dalam kedua kasus, loop akan dijalankan hanya dua kali (satu kali untuk kata seqdan satu kali untuk kata 10).

Futhermore keduanya akan menggabungkan spasi putih yang berdekatan, dan drop spasi putih memimpin / tertinggal, sehingga output tidak harus dua salinan input.

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

Salah satu alasan mengapa echoini 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:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Seperti yang Anda lihat tidak ada perbedaan nyata, tetapi jika ada versi yang dikandungnya echoberjalan 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.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

Sebenarnya loop berjalan 10 kali, bukan dua kali.
fpmurphy

Saya melakukan seperti yang Anda katakan, tetapi masalahnya belum terpecahkan. catsangat, sangat cepat daripada echo. Script pertama berjalan dalam rata-rata 3 detik, tetapi yang kedua berjalan dalam rata-rata 54 detik.
Mohammad

@ fpmurphy1: Tidak. Saya mencoba kode saya. Loop hanya berjalan dua kali, bukan 10 kali.
Mohammad

@ mohammad.k untuk ketiga kalinya: jika Anda mengutip variabel Anda, masalahnya hilang.
roaima

@roaima: Apa yang dilakukan perintah 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 menggunakan timeseperti yang Anda gunakan, dan hasilnya adalah yang saya katakan: 54 detik vs 3 detik.
Mohammad

-1

read jauh lebih cepat daripada cat

Saya pikir semua orang dapat menguji ini:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

catmembutuhkan 9,372 detik. echobutuh beberapa .232detik.

readadalah 40 kali lebih cepat .

Tes pertama saya ketika $pbergema ke layar mengungkapkan readadalah 48 kali lebih cepat dari cat.


-2

Ini echodimaksudkan 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.

catdioptimalkan untuk penggunaan ini. echotidak. Juga menempatkan 50MB di variabel lingkungan bukan ide yang baik.


Ingin tahu. Mengapa tidak echodioptimalkan untuk menulis teks?
roaima

2
Tidak ada dalam standar POSIX yang mengatakan gema dimaksudkan untuk meletakkan satu baris di layar.
fpmurphy

-2

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.

input -> output

Dalam kasus lain Anda membaca dari input ke dalam variabel di memori dan kemudian menulis isi dari variabel dalam output.

input -> variable
variable -> output

Yang terakhir akan jauh lebih lambat, terutama jika inputnya 50MB.


Saya pikir Anda harus menyebutkan bahwa kucing harus membuka file selain menyalin dari stdin dan menulisnya ke stdout. Ini adalah keunggulan dari skrip kedua, tetapi yang pertama sangat lebih baik daripada yang kedua secara total.
Mohammad

Tidak ada keunggulan dalam naskah kedua; cat perlu membuka file input dalam kedua kasus. Dalam kasus pertama, stdout kucing langsung menuju ke file. Dalam kasus kedua, stdout of cat pergi terlebih dahulu ke variabel, dan kemudian Anda mencetak variabel ke file output.
Aleksander

@ mohammad.k, dengan tegas tidak ada "keunggulan" dalam naskah kedua.
Wildcard
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.