Neden bir dosyayı, belleğe okumaktan ve iki kez hesaplama yapmaktan iki kat daha hızlı yineliyor?


26

Aşağıdakileri karşılaştırıyorum

tail -n 1000000 stdout.log | grep -c '"success": true'
tail -n 1000000 stdout.log | grep -c '"success": false'

Takip ederek

log=$(tail -n 1000000 stdout.log)
echo "$log" | grep -c '"success": true'
echo "$log" | grep -c '"success": false'

ve şaşırtıcı bir şekilde ikinci, birinciden neredeyse 3 kat daha uzun sürer. Daha hızlı olmalı, değil mi?


İkinci çözüm, dosya içeriğinin ilk örnekte 3 kez ve yalnızca iki kez okunmasından kaynaklanabilir mi?
Laurent C.

4
İkinci örnekte En azından senin $( command substitution )is not akış. Gerisi tüm borularla aynı anda olur, ancak ikinci örnekte log=tamamlanması için beklemek zorunda . << HERE \ n $ {log = $ (command)} \ nHERE ile deneyin - ne aldığınızı görün.
mikeserv

Çok büyük dosyalar, bellekte kısıtlanan makineler veya daha fazla öğe olması durumunda, dosyanın kesinlikle yalnızca bir kez okunması grepiçin biraz hızlanma görebilirsiniz tee. cat stdout.log | tee >/dev/null >(grep -c 'true'>true.cnt) >(grep -c 'false'>false.cnt); cat true.cnt; cat false.cnt
Matt

@LaurentC., Hayır, ikinci örnekte sadece bir kez okunur. Kuyruk için sadece bir çağrı var.
psusi

Şimdi bunları karşılaştırın tail -n 10000 | fgrep -c '"success": true've yanlış.
kojiro

Yanıtlar:


11

Bir yandan, ilk yöntem tailiki kez çağırır , bu yüzden sadece bir kez yapan ikinci yöntemden daha fazla iş yapması gerekir. Öte yandan, ikinci yöntemin verileri kabuğa kopyalaması ve sonra geri çekilmesi gerekir, bu nedenle taildoğrudan içine aktarılan ilk sürümden daha fazla iş yapması gerekir grep. İlk yöntem çok işlemcili bir makinede ekstra bir avantaja sahiptir: grepparalel olarak çalışabilir tail, oysa ikinci yöntem önce tailo zaman kesinlikle seri hale getirilir grep.

Öyleyse birinin diğerinden daha hızlı olması için açık bir neden yoktur.

Neler olup bittiğini görmek istiyorsanız, kabuğun ne yaptığı sisteme bakın. Farklı kabukları da deneyin.

strace -t -f -o 1.strace sh -c '
  tail -n 1000000 stdout.log | grep "\"success\": true" | wc -l;
  tail -n 1000000 stdout.log | grep "\"success\": false" | wc -l'

strace -t -f -o 2-bash.strace bash -c '
  log=$(tail -n 1000000 stdout.log);
  echo "$log" | grep "\"success\": true" | wc -l;
  echo "$log" | grep "\"success\": true" | wc -l'

strace -t -f -o 2-zsh.strace zsh -c '
  log=$(tail -n 1000000 stdout.log);
  echo "$log" | grep "\"success\": true" | wc -l;
  echo "$log" | grep "\"success\": true" | wc -l'

Yöntem 1 ile ana aşamalar:

  1. tail okur ve başlangıç ​​noktasını bulmaya çalışır.
  2. tailgrepürettikleri kadar hızlı okuyan 4096 baytlık parçalar yazar .
  3. İkinci arama dizesi için önceki adımı tekrarlayın.

Yöntem 2 ile ana aşamalar:

  1. tail okur ve başlangıç ​​noktasını bulmaya çalışır.
  2. tail bash bir anda 128 bayt okuyan 4096 baytlık metin yazar ve zsh bir anda 4096 bayt okur.
  3. Bash veya zsh, grepüretildikleri kadar hızlı okuyan 4096 baytlık parçalar yazar .
  4. İkinci arama dizesi için önceki adımı tekrarlayın.

Bash'in 128 baytlık komutları komut yerine koyma çıktısını okurken önemli ölçüde yavaşlatır; zsh benim için yöntem 1 kadar hızlı çıkıyor. Kilometreniz CPU türüne ve numarasına, zamanlayıcı yapılandırmasına, ilgili araçların sürümlerine ve verilerin boyutuna bağlı olarak değişebilir.


4k rakam sayfa boyutu bağımlı mı? Demek istediğim, kuyruk ve zsh her ikisi de sadece sistemlere eşlik ediyor mu? (Muhtemelen bu yanlış terminoloji, umarım olmasa da ...) Bash farklı şekilde ne yapıyor?
mikeserv

Bu Gilles’in yerinde! Zsh ile ikinci yöntem makinemde biraz daha hızlı.
phunehehe

Harika iş Gilles, tks.
X Tian

@mikeserv Bu programların boyutunu nasıl seçtiğini görmek için kaynağa bakmadım. 4096'yı görmenin en olası nedenleri st_blksize, bu makinede 4096 olan dahili bir sabit veya borunun değeri olabilir (ve bunun MMU sayfa boyutundan kaynaklandığından emin değilim). Bash'in 128'i yerleşik bir sabit olmak zorundaydı.
Gilles 'SO- kötülük' dur

@Gilles, düşünceli cevap için teşekkürler. Son zamanlarda sayfa boyutlarını merak ettim.
mikeserv

26

Aşağıdaki testi yaptım ve sistemimde ortaya çıkan fark, ikinci senaryo için yaklaşık 100 kat daha uzun.

Dosyam, strace çıktısı bigfile

$ wc -l bigfile.log 
1617000 bigfile.log

Senaryo

xtian@clafujiu:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l

xtian@clafujiu:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l

Aslında grep için hiçbir eşleşmem yok bu yüzden son boruya hiçbir şey yazılmıyor wc -l

İşte zamanlamalar:

xtian@clafujiu:~/tmp$ time bash p1.sh
0
0

real    0m0.381s
user    0m0.248s
sys 0m0.280s
xtian@clafujiu:~/tmp$ time bash p2.sh
0
0

real    0m46.060s
user    0m43.903s
sys 0m2.176s

Bu yüzden iki senaryoyu yine strace komutuyla çalıştırdım.

strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh

İşte izlerinin sonuçları:

$ cat p1.strace 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 97.24    0.508109       63514         8         2 waitpid
  1.61    0.008388           0     84569           read
  1.08    0.005659           0     42448           write
  0.06    0.000328           0     21233           _llseek
  0.00    0.000024           0       204       146 stat64
  0.00    0.000017           0       137           fstat64
  0.00    0.000000           0       283       149 open
  0.00    0.000000           0       180         8 close
...
  0.00    0.000000           0       162           mmap2
  0.00    0.000000           0        29           getuid32
  0.00    0.000000           0        29           getgid32
  0.00    0.000000           0        29           geteuid32
  0.00    0.000000           0        29           getegid32
  0.00    0.000000           0         3         1 fcntl64
  0.00    0.000000           0         7           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    0.522525                149618       332 total

Ve p2.strace

$ cat p2.strace 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 75.27    1.336886      133689        10         3 waitpid
 13.36    0.237266          11     21231           write
  4.65    0.082527        1115        74           brk
  2.48    0.044000        7333         6           execve
  2.31    0.040998        5857         7           clone
  1.91    0.033965           0    705681           read
  0.02    0.000376           0     10619           _llseek
  0.00    0.000000           0       248       132 open
...
  0.00    0.000000           0       141           mmap2
  0.00    0.000000           0       176       126 stat64
  0.00    0.000000           0       118           fstat64
  0.00    0.000000           0        25           getuid32
  0.00    0.000000           0        25           getgid32
  0.00    0.000000           0        25           geteuid32
  0.00    0.000000           0        25           getegid32
  0.00    0.000000           0         3         1 fcntl64
  0.00    0.000000           0         6           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    1.776018                738827       293 total

analiz

Şaşırtıcı olmayan bir şekilde, her iki durumda da çoğu zaman bir işlemin tamamlanmasını beklemekle harcanır, ancak p2 p1'den 2.63 kat daha fazla bekler ve diğerlerinin de belirttiği gibi, p2.sh'de geç başlıyorsunuz.

Şimdi unutun waitpid, %sütunu yoksayın ve her iki izdeki saniye sütununa bakın.

En büyük zaman p1, zamanının çoğunu muhtemelen anlaşılır bir şekilde okumak için harcıyor, çünkü okunacak büyük bir dosya var, ancak p2, p1'den daha 28.82 kat daha uzun zaman harcıyor. - bashbu kadar büyük bir dosyayı bir değişkene okumayı beklemiyor ve muhtemelen bir anda arabellek okuyor, satırlara bölerek ve sonra başka bir tane alıyor.

p2 okuma sayısı 705k vs p1 için 845'tir, her bir okuma bir çekirdekli alana ve tekrar dışarı bir içerik geçişi gerektirir. Okuma ve bağlam anahtarlarının sayısının yaklaşık 10 katı.

Yazma süresi p2 , yazma işleminde p1'den 41.93 kat daha fazla zaman harcıyor

yazma sayısı p1, p2, 42k ve 21k'dan daha çok yazma yapar, ancak çok daha hızlıdır.

Muhtemelen kuyruk yazma tamponlarının tersine echosatırların sonucu grep.

Dahası , p2 okunandan daha fazla zaman harcıyor, p1 tam tersi!

Diğer faktörbrk Sistem çağrılarının sayısına bakın : p2, okumadan 2.42 kat daha uzun zaman harcıyor! P1 (kayıt bile olmaz). brkProgramın adres alanını genişletmesi gerektiğinden, başlangıçta yeterince tahsis edilmemiş olması gerekir, bunun nedeni muhtemelen büyük olasılıkla bu dosyayı değişkene okumak zorunda kalması ve bu kadar büyük olmasını beklememesidir. dosya çok büyük olur, işe yaramaz bile.

tailMuhtemelen oldukça verimli bir dosya okuyucudur, çünkü yapmak için tasarlandığı şey budur, muhtemelen dosyayı sıkıştırır ve satır sonları için tarar, böylece çekirdeğin I / O'yu optimize etmesine izin verir. bash hem okuma hem de yazma için harcanan zaman kadar iyi değildir.

p2, 44ms ve 41ms arasında harcar cloneve execvbu p1 için ölçülebilir bir miktar değildir. Muhtemelen okuma ve değişken kuyruktan yaratma bash.

Sonunda, Toplamlar p1 ~ 150k sistem çağrılarını vs p2 740k (4.93 kat daha fazla) gerçekleştirir.

Bekleme süresini ortadan kaldırarak, p1 sistem çağrılarını yürütmek için 0.014416 saniye harcar, p2 0.439132 saniye (30 kez daha uzun).

Bu nedenle, p2, kullanıcı alanındaki zamanın çoğunu, sistem çağrılarının tamamlanmasını ve çekirdeğin hafızayı regüle etmesini beklemek dışında hiçbir şey yapmadan geçirdiği, p1'in daha fazla yazma yaptığı, ancak daha verimli olduğu ve önemli ölçüde daha az sistem yüküne neden olduğu ve dolayısıyla daha hızlı olduğu anlaşılmaktadır.

Sonuç

Bir bash betiği yazarken bellekten kodlama konusunda endişelenmeye asla çalışmam, bu verimli olmaya çalışmadığınızı söylemek anlamına gelmez.

tailNe yaptığını yapmak için tasarlanmıştır, muhtemelen memory mapsdosyayı okumak için verimlidir ve çekirdeğin I / O'yu optimize etmesine izin verir.

Sorununuzu optimize etmenin daha iyi bir yolu, ilk önce grep'' başarı '': 'satırlar için olabilir ve sonra gerçekleri ve yanlışları saymak, greptekrardan kaçınmak için bir sayma seçeneği vardır wc -l, hatta daha da iyisi, kuyruğu awkkesmek ve gerçekleri saymak aynı anda falses. p2 sadece uzun sürmekle kalmaz, aynı zamanda bellek brks ile karıştırılırken sisteme yük ekler.


2
TL; DR: malloc (); $ log ne kadar büyük olması gerektiğini söyleyebilseydiniz ve bir operasyonda hızlı bir şekilde yeniden tahsisat olmadan yazabiliyorsanız, muhtemelen bu kadar hızlı olurdu.
Chris K

5

Aslında ilk çözüm dosyayı da belleğe okuyor! Buna önbellek adı verilir ve işletim sistemi tarafından otomatik olarak yapılır.

Ve daha önce doğru açıklanabilir mikeserv birinci çözelti exectutes grep ise ikinci çözelti yürütür ise dosya okunurken sonra dosya ile okundu tail.

Bu yüzden ilk çözüm, çeşitli optimizasyonlar nedeniyle daha hızlı. Ancak bu her zaman doğru olmak zorunda değildir. İşletim sisteminin önbelleğe almamaya karar verdiği büyük dosyalar için ikinci çözüm daha hızlı olabilir. Ancak, belleğinize sığmayan daha büyük dosyalar için ikinci çözümün hiç işe yaramayacağını unutmayın.


3

Bence asıl fark çok basit ki echoyavaş. Bunu düşün:

$ time (tail -n 1000000 foo | grep 'true' | wc -l; 
        tail -n 1000000 foo | grep 'false' | wc -l;)
666666
333333

real    0m0.999s
user    0m1.056s
sys     0m0.136s

$ time (log=$(tail -n 1000000 foo); echo "$log" | grep 'true' | wc -l; 
                                    echo "$log" | grep 'false' | wc -l)
666666
333333

real    0m4.132s
user    0m3.876s
sys     0m0.468s

$ time (tail -n 1000000 foo > bb;  grep 'true' bb | wc -l; 
                                   grep 'false' bb | wc -l)
666666
333333

real    0m0.568s
user    0m0.512s
sys     0m0.092s

Yukarıda da görebileceğiniz gibi, zaman alan adım verileri yazdırmaktır. Eğer sadece yeni bir dosyaya yönlendirirseniz ve onun üzerinden geçerseniz dosyayı yalnızca bir kez okurken çok daha hızlı olur.


Ve talep edildiği gibi, burada bir dize ile:

 $ time (log=$(tail -n 1000000 foo); grep 'true' <<< $log | wc -l; 
                                     grep 'false' <<< $log | wc -l  )
1
1

real    0m7.574s
user    0m7.092s
sys     0m0.516s

Bu daha yavaş, muhtemelen burada çünkü dize tüm verileri bir uzun satırda birleştiriyor ve bu da yavaşlayacak grep:

$ tail -n 1000000 foo | (time grep -c 'true')
666666

real    0m0.500s
user    0m0.472s
sys     0m0.000s

$ tail -n 1000000 foo | perl -pe 's/\n/ /' | (time grep -c 'true')
1

real    0m1.053s
user    0m0.048s
sys     0m0.068s

Değişken, bölünme oluşmayacak şekilde belirtilirse, işler biraz daha hızlı olur:

 $ time (log=$(tail -n 1000000 foo); grep 'true' <<< "$log" | wc -l; 
                                     grep 'false' <<< "$log" | wc -l  )
666666
333333

real    0m6.545s
user    0m6.060s
sys     0m0.548s

Fakat yine de yavaş çünkü hız sınırlayıcı adım veriyi yazdırıyor.


Neden denemiyorsunuz, <<<bunun bir fark yaratıp yaratmadığını görmek ilginç olacak.
Graeme

3

Ben de buna devam ettim ... İlk önce dosyayı yaptım:

printf '"success": "true"
        "success": "true"
        "success": "false"
        %.0b' `seq 1 500000` >|/tmp/log

Yukarıdakileri kendiniz çalıştırırsanız, /tmp/log2: 1 "success": "true"satır / satır oranına sahip 1,5 milyon satır bulunmalıdır "success": "false".

Yapacağım bir sonraki şey bazı testler yapmaktı. Tüm testleri bir proxy üzerinden yaptım, shbu yüzden timesadece tek bir işlemi izlemeliydim - bu nedenle tüm iş için tek bir sonuç gösterebilirdi.

Bu en hızlı gibi görünüyor, ikinci bir dosya tanımlayıcısı eklese bile ve tee,bunun nedenini açıklayabileceğimi düşünüyorum:

    time sh <<-\CMD
        . <<HD /dev/stdin | grep '"success": "true"' | wc -l
            tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
                grep '"success": "false"' |\
                    wc -l 1>&2 & } 3>&1 &
        HD
    CMD
666666
333334
sh <<<''  0.11s user 0.08s system 84% cpu 0.224 total

İşte ilk:

    time sh <<\CMD
        tail -n 1000000 /tmp/log | grep '"success": "true"' | wc -l
        tail -n 1000000 /tmp/log | grep '"success": "false"' | wc -l
    CMD

666666
333334
sh <<<''  0.31s user 0.17s system 148% cpu 0.323 total

Ve ikinci:

    time sh <<\CMD
        log=$(tail -n 1000000 /tmp/log)
        echo "$log" | grep '"success": "true"' | wc -l
        echo "$log" | grep '"success": "false"' | wc -l
    CMD
666666
333334
sh <<<''  2.12s user 0.46s system 108% cpu 2.381 total

Testlerimde yaptığınız gibi bir değişkene okurken hızda 3 kattan fazla bir fark olduğunu görebiliyorsunuz.

Bunun bir kısmı, kabuk değişkeninin okunurken kabuk tarafından bölünmesi ve ele alınması gerektiğidir - bu bir dosya değildir.

here-documentÖte yandan A , tüm niyet ve amaçlar için, yine de bir file- birfile descriptor, şeydir. Ve hepimizin bildiği gibi - Unix dosyaları ile çalışır .

Benim için en ilginç here-docsolanı, onların file-descriptors- düz olarak |pipe- manipüle edip onları uygulayabilmeniz . Bu, |pipeistediğiniz yere işaret etmede size biraz daha fazla özgürlük tanıdığı için çok kullanışlıdır .

Ben zorundaydı ilk Çünkü yediği saniye sol ve orada hiçbir şeydir okumak için. Ama ben içine almak ve onu geçmek için tekrar aldı o kadar önemli değildi. Eğer diğerleri kadar tavsiye ederseniz :teetailgrephere-doc |pipe|piped/dev/fd/3>&1 stdout,grep -c

    time sh <<-\CMD
        . <<HD /dev/stdin | grep -c '"success": "true"'
            tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
                grep -c '"success": "false"' 1>&2 & } 3>&1 &
        HD
    CMD
666666
333334
sh <<<''  0.07s user 0.04s system 62% cpu 0.175 total

Daha da hızlı.

Ben olmadan çalıştırabilir Ama I başarıyla arka plan ilk işlem tamamen aynı zamanda bunları çalıştırmak için olamaz. İşte tamamen arkaplanı olmadan:. sourcingheredoc

    time sh <<\CMD
        tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
            grep -c '"success": "true"' 1>&2 & } 3>&1 |\
                grep -c '"success": "false"'
    CMD
666666
333334
sh <<<''  0.10s user 0.08s system 109% cpu 0.165 total

Ama ben eklediğimde &:

    time sh <<\CMD
        tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
            grep -c '"success": "true"' 1>&2 & } 3>&1 & |\
                grep -c '"success": "false"'
    CMD
sh: line 2: syntax error near unexpected token `|'

Yine de, farkın en azından benim için saniyenin sadece birkaçda biri gibi göründüğü için istediğin gibi yap.

Her neyse, bunun daha hızlı çalışmasının teenedeni, her ikisinin grepsde aynı anda yalnızca bir tail. teeçoğaltma çağrısı ile çalıştırılması , dosyayı bizim için çoğaltıyor ve greptüm akış içindeki ikinci işleme ayırıyor - her şey bir anda baştan sona koşuyor. hepsi de aynı anda bitiyor.

Öyleyse ilk örneğinize geri dönelim:

    tail | grep | wc #wait til finished
    tail | grep | wc #now we're done

Ve ikinci:

    var=$( tail ) ; #wait til finished
    echo | grep | wc #wait til finished
    echo | grep | wc #now we're done

Ancak girdilerimizi böldüğümüzde ve süreçlerimizi aynı anda yürüttüğümüzde:

          3>&1  | grep #now we're done
              /        
    tail | tee  #both process together
              \  
          >&1   | grep #now we're done

1
+1 ancak son testiniz bir sözdizimi hatasıyla öldü, orada saatlerin doğru olduğunu sanmıyorum :)
terdon

@ terdon Yanılıyor olabilirler - Öldüğünü işaret ediyordum. Ben & hayır ile arasındaki farkı gösterdim - eklediğinizde, kabuk bozulur. Fakat çok fazla kopya / yapıştırma işlemi yaptım, böylece bir veya iki
tanesini bozmuş

sh: line 2: beklenmedik belirteci `| 'yakınındaki sözdizimi hatası
terdon

@ terdon Evet, - "Tamamen eşzamanlı olarak çalıştırmak için ilk işlemi başarıyla arka planlayamıyorum. Bakın?" İlki arka plan değil, ama "beklenmedik belirteç." Ne zaman ben . Kullanabileceğim heredoc kaynağı.
mikeserv
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.