Yankı ve kedinin yürütme süresinde neden böyle bir fark var?


15

Bu soruyu cevaplamak başka bir soru sormama neden oldu:
Aşağıdaki komut dosyalarının aynı şeyi yaptığını ve ikincisinin çok daha hızlı olması gerektiğini düşündüm, çünkü birincisi catdosyayı tekrar tekrar açması gerekiyor, ancak ikincisi sadece dosyayı açıyor bir kez ve sonra sadece bir değişkeni yankılar:

(Doğru kod için güncelleme bölümüne bakın.)

İlk:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

İkinci:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

giriş yaklaşık 50 megabayttır.

Ama ikincisini denediğimde, çok, çok yavaştı çünkü değişkeni yankılamak çok ibüyük bir süreçti. Ayrıca ikinci komut dosyası ile ilgili bazı sorunlar var, örneğin çıktı dosyasının boyutu beklenenden daha düşüktü.

Ayrıca man sayfasını kontrol ettim echove catkarşılaştırmak için:

echo - bir metin satırı görüntüler

cat - dosyaları birleştirin ve standart çıktıya yazdırın

Ama fark bulamadım.

Yani:

  • İkinci senaryoda kedi neden bu kadar hızlı ve yankı bu kadar yavaş?
  • Yoksa değişkenle ilgili sorun imu var? (çünkü man sayfasında "bir metin satırı"echo görüntülendiği söyleniyor ve bu yüzden sanırım çok kısa değişkenler için değil, sadece kısa değişkenler için optimize edildi . Ancak, bu sadece bir tahmin.)i
  • Ve kullandığımda neden sorun yaşıyorum echo?

GÜNCELLEME

Yanlış seq 10yerine kullandım `seq 10`. Bu düzenlenmiş kod:

İlk:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

İkinci:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

( Roaima'ya özel teşekkürler .)

Ancak, sorunun konusu bu değildir. Döngü sadece bir kez olsa bile, aynı sorunu alıyorum: catçok daha hızlı çalışır echo.


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

2
Daha echohızlı. Eksik olan şey, kabuğu kullandığınızda değişkenleri tırnak içine alarak çok fazla iş yapmanıza neden olmasıdır.
roaima

Değişkenlerden alıntı yapmak sorun değil; problem i değişkeninin kendisidir (yani giriş ve çıkış arasında ara adım olarak kullanmak).
Aleksander

echo $ i - bunu yapma. Printf kullanın ve argümanı alıntılayın.
PSkocik

1
@PSkocik Söylediğim istediğin printf '%s' "$i", değil echo $i. @cuonglm cevabında yankı sorunlarının bazılarını açıklıyor. Yankı olan bazı durumlarda alıntı yapmak bile neden yeterli değilse, bkz. Unix.stackexchange.com/questions/65803/…
PSkocik

Yanıtlar:


24

Burada dikkate alınması gereken birkaç nokta var.

i=`cat input`

pahalı olabilir ve mermiler arasında çok fazla varyasyon vardır.

Bu komut yerine koyma adı verilen bir özellik. Fikir, komutun tüm çıktısını eksi sondaki yeni satır karakterlerini ibellekteki değişkene depolamaktır.

Bunu yapmak için, kabuklar komutu bir alt kabukta çatallar ve çıkışını bir boru veya soket çifti aracılığıyla okur. Burada çok fazla varyasyon görüyorsunuz. Buradaki bir 50MiB dosyasında, örneğin bash'ın ksh93'ten 6 kat daha yavaş, ancak zsh'den biraz daha hızlı ve iki kat daha hızlı olduğunu görebiliyorum yash.

bashYavaş olmanın ana nedeni , bir seferde 128 bayt borudan (diğer kabuklar bir kerede 4KiB veya 8KiB okurken) okuması ve sistem çağrısı yükü tarafından cezalandırılmasıdır.

zshNUL baytlarından (diğer mermiler NUL baytlarında kırılır) kaçmak için bazı son işlemler yapması gerekir ve yashçok baytlı karakterleri ayrıştırarak daha da ağır iş işleme yapar.

Tüm mermilerin az ya da çok verimli bir şekilde yapabilecekleri son satırsonu karakterlerini soyması gerekir.

Bazıları NUL baytlarını diğerlerinden daha zarif işlemek ve varlığını kontrol etmek isteyebilir.

Daha sonra bellekte bu büyük değişkene sahip olduğunuzda, üzerindeki herhangi bir manipülasyon genellikle daha fazla bellek tahsis etmeyi ve veriyi başa çıkmayı içerir.

Burada, değişkenin içeriğini geçiyorsunuz (geçmeyi düşünüyordunuz) echo.

Neyse ki, echokabuğunuzda yerleşiktir, aksi takdirde yürütme büyük olasılıkla bir arg listesi çok uzun bir hata ile başarısız olurdu . O zaman bile, argüman listesi dizisini oluşturmak muhtemelen değişkenin içeriğini kopyalamayı içerecektir.

Komut değiştirme yaklaşımınızdaki diğer temel sorun, split + glob operatörünü çağırmanızdır (değişkeni alıntılamayı unutarak).

Bunun için, mermilerin dizgiye bir karakter dizisi olarak davranması gerekir (bazı mermiler bu bağlamda yoktur ve buggy olsa da), bu yüzden UTF-8 yerellerinde, UTF-8 dizilerini ayrıştırmak anlamına gelir (zaten olduğu gibi yapılmadıysa yash) , $IFSdizedeki karakterleri arayın . Eğer $IFSboşluk, (varsayılan olarak böyledir) sekmesi veya satır başı karakteri içeren, algoritma daha da karmaşık ve pahalıdır. Daha sonra, bu bölünmeden kaynaklanan kelimelerin tahsis edilmesi ve kopyalanması gerekir.

Glob kısmı daha pahalı olacak. Bu kelimelerin herhangi glob karakterler içeriyorsa ( *, ?, [), sonra kabuk bazı dizinlerin içeriğini okumak ve bazı pahalı desen eşleştirme yapmak zorunda olacak ( bashörneğin 'ın uygulama Şuna çok kötü bir iştir).

Girdi gibi bir şey içeriyorsa /*/*/*/../../../*/*/*/../../../*/*/*, bu binlerce dizin listelemek anlamına gelir ve bu da birkaç yüz MiB'ye kadar genişleyebilir.

Sonra echotipik olarak bazı ekstra işlemler yapar. Bazı uygulamalar \x, aldığı argümandaki dizileri genişletir , bu da içeriği ayrıştırmak ve muhtemelen verilerin başka bir tahsisi ve kopyası anlamına gelir.

Öte yandan, Tamam, çoğu kabukta catyerleşik değildir, bu nedenle bir işlemin istenmesi ve yürütülmesi (kodun ve kütüphanelerin yüklenmesi), ancak ilk çağrıldıktan sonra, bu kod ve giriş dosyasının içeriği anlamına gelir bellekte önbelleğe alınır. Öte yandan, aracı olmayacak. cattek seferde büyük miktarlar okuyacak ve işlemeden hemen yazacak ve büyük miktarda bellek ayırmasına gerek yok, sadece yeniden kullandığı bir tampon.

Ayrıca, NUL baytlarını boğmadığı ve sondaki yeni satır karakterlerini kırpmadığı (ve split + glob yapmadığı için çok daha güvenilir olduğu anlamına gelir, ancak değişkeni alıntılayarak bundan kaçınabilirsiniz ve kaçış dizisini genişletmek printfyerine echo) kullanmaktan kaçınabilirsiniz .

Yerine çağırmak, daha da optimize etmek istiyorsanız catbirkaç kez, sadece geçmesi inputiçin birkaç kez cat.

yes input | head -n 100 | xargs cat

100 yerine 3 komut çalıştırır.

Değişken sürümü daha güvenilir hale getirmek için kullanmanız gerekir zsh(diğer kabuklar NUL baytlarıyla baş edemez) ve bunu yapmanız gerekir:

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

Girişin NUL bayt içermediğini biliyorsanız, bunu güvenilir bir şekilde POSIXly yapabilirsiniz (ancak printfyerleşik olmayan yerlerde çalışmayabilir ):

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

Ancak bu, catdöngüde kullanmaktan daha verimli olmayacaktır (giriş çok küçük olmadığı sürece).


Uzun bir tartışma durumunda , hafızadan çıkabileceğinizi belirtmek gerekir . Örnek/bin/echo $(perl -e 'print "A"x999999')
cuonglm

Okuma boyutunun önemli bir etkiye sahip olduğu varsayımı ile yanılıyorsunuz, bu yüzden gerçek nedeni anlamak için cevabımı okuyun.
schily

@ schily, 128 baytlık 409600 okuma yapmak, 64k 800 okumadan daha fazla zaman (sistem zamanı) alır. Karşılaştırma dd bs=128 < input > /dev/nullile dd bs=64 < input > /dev/null. O dosyayı okumak için bass için gereken 0.6s, 0.4 readbenim testlerde bu sistem çağrıları geçirilirken, diğer kabukları orada çok daha az zaman harcar.
Stéphane Chazelas

Gerçek bir performans analizi yapmış görünmüyorsunuz. Okuma çağrısının etkisi (farklı okuma boyutları karşılaştırılırken) yaklaşıktır. Fonksiyonlar readwc() ve trim()Burne Shell'de tüm zamanın% 1'i tüm zamanın % 30'unu alır ve gprofek açıklama ile libc olmadığından büyük olasılıkla küçümsenir mbtowc().
schily

Hangisine \xgenişletilir?
Muhammed

11

Sorun değil catve echounutulmuş teklif değişkeni ile ilgili $i.

Bourne benzeri kabuk betiğinde (hariç zsh), değişkenleri unquote olarak bırakmak değişkenler glob+splitüzerinde işleçlere neden olur .

$var

aslında:

glob(split($var))

Böylece, her döngü yinelemesinde input(son satırları hariç tut) tüm içeriği genişletilir, bölünür, zonklanır. Tüm işlem dizeyi tekrar tekrar ayrıştırarak bellek ayırmak için kabuk gerektirir. Kötü performansın nedeni budur.

Sen önlemek için değişken alıntı yapabilirsiniz glob+splitama kabuk hala büyük bir dize argümanı inşa etmek ve onun içeriğini taramak için gerektiğinde beri çok yardımcı olmaz echo(yerleşiğini Değiştirme echoharici ile /bin/echoçok uzun veya bellek yetersiz size argüman listesi verecektir $iboyutuna bağlıdır ). Uygulamanın çoğu echoPOSIX uyumlu değildir, \xaldığı argümanlarda ters eğik çizgi dizilerini genişletir .

İle cat, kabuğun yalnızca her döngü yinelemesinde bir işlem üretmesi gerekir ve catkopya i / o işlemini yapar. Sistem ayrıca cat işlemini daha hızlı hale getirmek için dosya içeriğini önbelleğe alabilir.


2
@roaima: /*/*/*/*../../../../*/*/*/*/../../../../Dosya içeriğinde bulunabilecek bir şey görüntüleyerek büyük bir neden olabilecek glob kısmından bahsetmediniz . Sadece ayrıntıları belirtmek istiyorum .
cuonglm

Teşekkürler. Onsuz bile,
sıralanmamış

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

Alıntı yapmanın neden sorunu çözemediğini anlamadım. Daha fazla açıklamaya ihtiyacım var.
Muhammed

1
@ mohammad.k: Cevabımda yazdığım gibi, alıntı değişkeni glob+splitkısmı önlüyor ve while döngüsünü hızlandıracak. Ayrıca, bunun size pek yardımcı olmayacağını da belirttim. Çünkü kabuk echodavranışlarının çoğu POSIX uyumlu değildir. printf '%s' "$i"daha iyi.
cuonglm

2

Eğer ararsan

i=`cat input`

Bu, kabuk işleminizin 50 MB ila 200 MB kadar büyümesine olanak tanır (dahili geniş karakter uygulamasına bağlı olarak). Bu, kabuğunuzu yavaşlatabilir, ancak bu ana sorun değildir.

Asıl sorun, yukarıdaki komutun tüm dosyayı kabuk belleğine okuması ve echo $io dosya içeriğinde alan bölme yapılması gerektiğidir $i. Alan ayırma yapmak için, dosyadaki tüm metinlerin geniş karakterlere dönüştürülmesi gerekir ve burası çoğu zaman harcanır.

Yavaş vaka ile bazı testler yaptım ve şu sonuçları aldım:

  • En hızlı ksh93
  • Sıradaki benim Bourne Shell'im (ksh93'ten 2 kat daha yavaş)
  • Sıradaki bash (ksh93'ten 3 kat daha yavaş)
  • Sonuncusu ksh88 (ksh93'ten 7 kat daha yavaş)

Ksh93'ün en hızlı olmasının nedeni, ksh93'ün libc'den mbtowc()değil, kendi uygulamalarından biri olması gibi görünüyor .

BTW: Stephane, okuma boyutunun bir miktar etkisi olduğu konusunda yanılıyor, Bourne Kabuğunu 128 bayt yerine 4096 baytlık yığınlarda okumak için derledim ve her iki durumda da aynı performansı elde ettim.


i=`cat input`Komut bu, saha bölünmesini yapmaz echo $iyapar. Harcanan zaman tek başına karşılaştırıldığında i=`cat input`ihmal edilebilir echo $i, ancak cat inputtek başına karşılaştırılamaz ve bu durumda bash, fark bashküçük okumalar yapmanın en iyi yanıdır. 128'den 4096'ya geçişin performansı üzerinde hiçbir etkisi olmayacaktır echo $i, ancak bu benim belirttiğim nokta değildi.
Stéphane Chazelas

Ayrıca, performansın echo $igirdinin içeriğine ve dosya sistemine (IFS veya glob karakterleri içeriyorsa) bağlı olarak önemli ölçüde değişeceğini unutmayın , bu yüzden cevabımda bu konuda kabuk karşılaştırması yapmadım. Örneğin, burada yes | ghead -c50M, ksh93 en yavaş olanıdır, ancak yes | ghead -c50M | paste -sd: -en hızlısıdır.
Stéphane Chazelas

Toplam süre hakkında konuşurken, tüm uygulama hakkında konuşuyordum ve evet, elbette alan bölünmesi echo komutuyla gerçekleşiyor. ve bu, tüm zamanın çoğunun harcandığı yerdir.
schily

Elbette performans od $ i içeriğine bağlıdır.
schily

1

Her iki durumda da, döngü sadece iki kez çalıştırılır (bir kez kelime için seqve bir kez kelime için 10).

Dahası, her ikisi de bitişik boşlukları birleştirecek ve öncü / arka boşlukları bırakacak, böylece çıktı mutlaka girişin iki kopyası olmayacaktır.

İlk

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

İkinci

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

Daha echoyavaş olmasının bir nedeni, alıntılanmamış değişkeninizin boşlukta ayrı kelimelere bölünmüş olması olabilir. 50 MB için bu çok iş olacak. Değişkenleri alıntılayın!

Bu hataları düzeltmenizi ve ardından zamanlamalarınızı yeniden değerlendirmenizi öneririz.


Bunu yerel olarak test ettim. Çıktısını kullanarak 50MB dosya oluşturdum tar cf - | dd bs=1M count=50. Ben de zamanlamaları makul bir değere ölçeklendirildi böylece x100 bir faktör tarafından çalıştırmak için döngüler genişletilmiş (tüm kod etrafında başka bir döngü ekledi: for k in $(seq 100); do... done). İşte zamanlamalar:

time ./1.sh

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

time ./2.sh

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

Gördüğünüz gibi gerçek bir fark yok, ancak içeren herhangi bir şey echomarjinal olarak daha hızlı çalışıyor. Eğer tırnak kaldırmak ve kırık sürüm 2 çalıştırmak zaman iki katına, kabuk beklenen çok daha fazla iş yapmak zorunda olduğunu gösterir.

time ./2original.sh

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

Aslında döngü iki kez değil 10 kez çalışır.
fpmurphy

Söylediğin gibi yaptım, ama sorun çözülmedi. catçok, çok daha hızlı echo. İlk komut dosyası ortalama 3 saniyede çalışır, ancak ikinci komut dosyası ortalama 54 saniyede çalışır.
Muhammed

@ fpmurphy1: Hayır. Kodumu denedim. Döngü sadece iki kez çalışır, 10 kez değil.
Muhammed

@ mohammad.k üçüncü kez: değişkenlerinizi teklif ederseniz sorun ortadan kalkar.
roaima

@roaima: Komut tar cf - | dd bs=1M count=50ne yapıyor? İçinde aynı karakterlere sahip düzenli bir dosya yapıyor mu? Eğer öyleyse, benim durumumda giriş dosyası her türlü karakter ve boşluk ile tamamen düzensiz. Ve yine, kullandığın timegibi kullandım ve sonuç dediğim şeydi: 54 saniye vs 3 saniye.
Muhammed

-1

read daha hızlı cat

Herkesin bunu test edebileceğini düşünüyorum:

$ 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

cat9.372 saniye sürer. saniye echosürer .232.

readolduğu 40 kat daha hızlı .

$pEkrana yansıtıldığında ilk testim read48 kat daha hızlıydı cat.


-2

echoEkranda 1 satır koymak içindir. İkinci örnekte yaptığınız şey, dosyanın içeriğini bir değişkene koymanız ve ardından bu değişkeni yazdırmanızdır. İlkinde içeriği hemen ekrana koyarsınız.

catbu kullanım için optimize edilmiştir. echodeğil. Ayrıca 50Mb'yi ortam değişkenine koymak iyi bir fikir değildir.


Meraklı. echoMetin yazmak için neden optimize edilmesin?
roaima

2
POSIX standardında echo'nun ekrana bir satır koymak anlamına geldiğini söyleyen hiçbir şey yoktur.
fpmurphy

-2

Yankı daha hızlı olmakla değil, ne yaptığınızla ilgili:

Bir durumda, girdiden okuyup doğrudan çıktıya yazıyorsunuz. Başka bir deyişle, kediden girdiden ne okunuyorsa, stdout aracılığıyla çıktıya gider.

input -> output

Diğer durumda, girdiden bellekteki bir değişkene okuyup sonra değişkenin içeriğini çıktıya yazıyorsunuz.

input -> variable
variable -> output

İkincisi, özellikle giriş 50MB ise çok daha yavaş olacaktır.


Sanırım kedinin stdin'den kopyalayıp stdout'a yazmanın yanı sıra dosyayı açması gerektiğini de belirtmelisiniz. Bu ikinci senaryonun mükemmelliği, ama ilki toplamdan ikincisinden çok daha iyi.
Mohammad

İkinci senaryoda mükemmellik yok; cat her iki durumda da girdi dosyasını açmalıdır. İlk durumda, kedinin stdout'u doğrudan dosyaya gider. İkinci durumda, kedinin stdout'u önce bir değişkene gider ve daha sonra değişkeni çıktı dosyasına yazdırırsınız.
Aleksander

@ mohammad.k, ikinci senaryoda kesinlikle "mükemmellik" yoktur.
Joker
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.