Evet dosyaya nasıl bu kadar çabuk yazar?


58

Bir örnek vereyim:

$ 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

Burada komutun saniyede satır yesyazdığını 11504640, 1953bash forve tuşlarını kullanarak 5 saniyede sadece satır yazabileceğimi görebilirsiniz echo.

Yorumlarda önerildiği gibi, daha verimli hale getirmek için çeşitli hileler vardır, ancak hiçbiri hızını eşleştirmek için yaklaşmaz 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

Bunlar saniyede 20 bin satıra kadar yazabilir. Ve bunlar daha da geliştirilebilir:

$ 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

Bunlar bizi saniyede 40 bin hatta çıkarır. Daha iyi, ama yine yesde bir saniyede 11 milyon satır yazabilen uzak bir çığlık !

Peki dosyaya bu kadar çabuk nasıl yesyazılır?



9
İkinci örnekte, döngünün her yinelemesi için iki harici komut çağrınız vardır ve datebiraz ağırdır, ayrıca kabuğun echoher döngü yinelemesi için çıktı akışını yeniden açması gerekir . İlk örnekte, tek bir çıkış yönlendirmesi ile yalnızca bir tek komut çağırma var ve komut son derece hafif. İkisi hiçbir şekilde karşılaştırılamaz.
CVn

@ MichaelKjörling haklısın dateağır olabilir, sorumu değiştirmeye bakın.
Pandya

1
timeout 1 $(while true; do echo "GNU">>file2; done;)kullanmak yanlış bir yoldur timeout çünkü timeoutkomut ikamesi bittiğinde komut sadece başlayacaktır. Kullanın timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
Ocak'taki muru

1
Cevapların özeti: CPU write(2)çağrısını sadece sistem çağrılarına harcayarak , diğer sistem çağrılarının yükünü doldurma, baş üstü kabuk ve hatta işlem oluşturma işleminin ilk örneğinde ( datedosyaya basılan her satır için çalışır ve bekler ). Yazının bir saniyesi, disk G / Ç'de (CPU / bellek yerine), çok miktarda RAM içeren modern bir sistemde tıkanması için zar zor. Daha uzun süre çalışmasına izin verilirse, fark daha küçük olacaktır. (Bir bash uygulamasının ne kadar kötü kullandığınıza ve CPU ve diskin göreceli hızına bağlı olarak, disk G / Ç'sini bash ile doyuramayabilirsiniz).
Peter Cordes

Yanıtlar:


65

Özetle:

yestipik olarak, diğer birçok standart programları için benzer bir davranış sergiler geç a dosya akışı ile LIBC Tamponlanmış çıkış stdio . Bunlar sadece write()bazı her 4kb (16kb veya 64kb) veya BUFSIZ çıkış bloğu ne olursa olsun sistemi çağırır . echobir write()başına GNU. Bu bir var çok bir mod-anahtarlaması (a gibi maliyetli olarak, görünüşe göre değil, bağlama anahtarı ) .

Ve bu, ilk optimizasyon döngüsünün yanı sıra, yesçok basit, minik, derlenmiş bir C döngüsü olduğunu ve kabuk döngünüzün hiçbir şekilde bir derleyici optimizasyon programına benzeyebileceğinden bahsetmiyor .


ama yanılmışım:

Daha önce yeskullanılan stdio'yu kullandığımı söylediğimde, sadece yaptığını düşündüğüm için yaptım. Bu doğru değildi - davranışlarını bu şekilde taklit eder. Asıl yaptığı şey, aşağıda kabukla yaptığım şeye benzer bir şey gibidir: ilk önce argümanlarını (veya yhiçbiri yoksa) aşmadan daha fazla büyüyene kadar sınırlandırmak için döngüler BUFSIZ.

İlgili döngü durumlarından hemen önceki kaynaktan yapılan bir yorum for:

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

yeswrite()Bundan sonra kendi yapar .


konu dışı söz:

(Başlangıçta soruya dahil edilmiş ve daha önce burada yazılı olan muhtemelen bilgilendirici bir açıklamanın bağlamında saklandığı gibi) :

Denedim timeout 1 $(while true; do echo "GNU">>file2; done;)ama döngüyü durduramadım.

timeoutBen şimdi anladım düşünüyorum ve o durmuyor neden açıklayabilir - Sorun komut ikamesi ile var. timeoutbaşlamadı çünkü komut satırı hiç çalıştırılmadı. Kabuğunuz bir çocuk kabuğu çatallıyor, stdout'unda bir boru açıyor ve okuyor. Çocuk kapanıyor zaman okuma durur ve daha sonra tüm alt için yazdığı yorumlayacak $IFSgenişlemeleri bozma ve glob ve sonuçları ile bu kadar her şeyi değiştirecektir $(eşleme ).

Ancak, çocuk asla boruya yazmayan sonsuz bir döngü ise, o zaman çocuk asla döngüyü kesmez ve timeoutkomut satırı daha önce hiç bitmez (sanırım), siz CTRL-Cçocuk döngüsünü yapıp öldürürsünüz. Yani timeoutolabilir asla o başlamadan önce tamamlaması gereken döngü öldürür.


diğer timeouts:

... kabuk programınızın çıktıyı ele almak için kullanıcı ve çekirdek modu arasında geçiş yapması gereken süre kadar performans sorunlarınızla ilgili değil. timeoutBununla birlikte, bu amaç için bir kabuk kadar esnek değildir: mermilerin mükemmel olduğu durumlarda argümanları yönetme ve diğer işlemleri yönetme yetenekleri vardır.

Başka bir yerde de belirtildiği gibi, [fd-num] >> named_fileyönlendirme işleminizi yalnızca döngü için komut vermek için yalnızca çıkışı yönlendirmek yerine döngünün çıktı hedefine kaydırmak, performansı önemli ölçüde artırabilir çünkü bu şekilde en azından open()sistem çağrısı yalnızca bir kez yapılmalıdır. Bu ayrıca |iç döngüler için çıkış olarak hedeflenen boru ile aşağıda yapılır .


doğrudan karşılaştırma:

Beğenebilirsin:

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

Hangi tür arasında daha önce açıklanan komut alt ilişkisi gibi, ama orada hiçbir boru ve o üst öldürene kadar çocuk arka plana. In yesdurumda çocuk kökenli beri ebeveyn aslında yerini almıştır, ancak kabuk aramalar yesyenisi ve kendi sürecini kaplayan bunu PID aynı kalır ve onun zombi çocuk hala sonuçta öldürmek için kim bilir.


daha büyük tampon:

Şimdi kabuğun write()tamponunu arttırmayı görelim .

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

Bu sayıyı seçtim, çünkü 1kb'den daha uzun çıkış dizeleri write()benim için ayrı bölümlere ayrılıyordu . Ve işte yine döngü:

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

Bu test, kabuk tarafından yazılan veri miktarının, bu test için bir öncekiyle aynı sürede yazılmasını sağlar. O kadar da eski püskü değil. Ama öyle değil yes.


ilgili:

İstenildiği gibi, bu bağlantıda burada ne yapıldığına dair sadece kod yorumlarından daha ayrıntılı bir açıklama vardır .


@heemayl - belki? Ne istediğini anladığımdan emin değilim. Bir program çıktıyı yazmak için stdio kullandığında, tamponlama (varsayılan olarak stderr gibi) veya satır tamponlama (varsayılan olarak terminallere) veya blok tamponlama (temelde çoğu diğer şeyler varsayılan olarak bu şekilde ayarlanır) yapar . im çıktı tamponunun boyutunu neyin belirlediği konusunda net değil - fakat genellikle 4 kb’lık bir değerdir. ve böylece stdio lib fonksiyonları bir blok yazana kadar çıktılarını toplayacaktır. ddÖrneğin, kesinlikle stdio kullanmayan standart bir araçtır. diğerleri çoğu.
mikeserv

3
Kabuk sürümü open(var) writeve close(hala yıkama için beklediğine inanıyorum ) yapıyor ve dateher döngü için yeni bir işlem oluşturup yürütüyor .
dave_thompson_085

@ dave_thompson_085 - / dev / chat adresine gidin . ve söyledikleriniz, orada gördüğünüz gibi mutlaka doğru değildir. Örneğin, bunu yaparken wc -lile döngü bashbeni çıkışı 1 / 5'ini alır için sh- döngü yapar bash100k biraz üzerinde yönetir writes()için dash'ın 500k.
mikeserv

Üzgünüm belirsizdim; Sorudaki kabuk versiyonunu kastetmiştim, okuduğumda for((sec0=`date +%S`;...sonraki gelişmeleri değil, sadece zamanı ve döngüdeki yönlendirmeyi kontrol eden orijinal versiyona sahipti .
dave_thompson_085

@ dave_thompson_085 - iyi. Cevap yine de bazı temel noktalar konusunda yanlıştı ve umarım, şimdi hemen hemen doğru olmalı.
mikeserv

20

Daha iyi bir soru, neden kabuğunuzun dosyayı bu kadar yavaş yazdığıdır. Dosya yazma sistemlerini sorumlu bir şekilde kullanan (her seferinde her karakteri temizlemeyen) bağımsız bir şekilde derlenmiş herhangi bir program bunu oldukça çabuk yapar. Yaptığınız şey, yorumlanmış bir dilde (kabuk) satırlar yazmak ve ayrıca birçok gereksiz girdi çıktı işlemi de gerçekleştiriyorsunuz. Ne yesyapar:

  • yazmak için bir dosya açar
  • Bir akıma yazmak için optimize edilmiş ve derlenmiş işlevleri çağırır
  • Akış tamponlanır, bu nedenle büyük parçalar halinde bir çağrı sistemi (çekirdek moduna pahalı bir geçiş) çok nadiren olur
  • bir dosyayı kapatır

Komut dosyanızın ne yaptığı:

  • kod satırında okur
  • Giriş kodunu yorumlar, girdilerinizi ayrıştırmak ve ne yapmanız gerektiğini anlamak için çok fazla ek işlem yapar
  • while döngüsünün her bir yinelemesi için (ki bu muhtemelen yorumlanmış bir dilde ucuz değildir):
    • dateharici komutu arayın ve çıktısını saklayın (yalnızca orijinal sürümde - gözden geçirilmiş sürümde bunu yapmadan 10 faktör kazanırsınız)
    • döngünün sonlandırma koşulunun karşılanıp karşılanmadığını test edin
    • açmak ekleme modunda bir dosyayı
    • parse echokomutu, bir kabuk yerleşimi olarak tanımlayın (bazı desen eşleme kodlarıyla), parametre genişletme işlevini ve "GNU" argümanındaki her şeyi çağırın ve son olarak açık dosyaya satırı yazın
    • dosyayı tekrar kapat
    • işlemi tekrarla

Pahalı kısımlar: bütün yorumlama çok pahalıdır (bash bütün girdilerin çok fazla ön işleme tabi tutulmasıdır - stringiniz potansiyel olarak değişken ikame, işlem ikame, küme genişletme, kaçış karakterleri ve daha fazlasını içerebilir) Büyük olasılıkla, yerleşik yapıyla ilgilenen bir işleve yönlendiren bir switch ifadesidir ve çok önemlisi, her çıktı satırı için bir dosyayı açar ve kapatırsınız. Süreyi daha hızlı>> file hale getirmek için while döngüsünün dışına koyabilirsiniz , ancak yine de yorumlanmış bir dildesiniz. Çok şanslısın kiechoharici bir komut değil, bir kabuk yerleşimidir - aksi takdirde, döngünüz her yinelemede yeni bir işlem (fork & exec) oluşturmayı içerir. Süreci durdurmak için öğütecek olan - datedöngüde komut varken bunun ne kadar maliyetli olduğunu gördünüz .


11

Diğer cevaplar ana noktaları ele almıştır. Bir yandan, hesaplama sonunda çıktı dosyasına yazarak while döngüsünün verimini artırabilirsiniz. Karşılaştırmak:

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

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

ile

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

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

Evet, bu önemli ve yazma hızı (en azından) benim durumumda iki katına çıkıyor
Pandya
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.