메모리에서 파일을 읽고 두 번 계산하는 것보다 파일을 두 번 빠르게 반복하는 이유는 무엇입니까?


26

나는 다음을 비교하고있다.

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

다음과 함께

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

놀랍게도 두 번째는 첫 번째보다 거의 3 배 더 오래 걸립니다. 더 빨라야하지 않습니까?


두 번째 솔루션, 파일 내용을 세 번 읽었으므로 첫 번째 예제에서 두 번만 읽었을 수 있습니까?
Laurent C.

4
적어도 두 번째 예에서 귀하 $( command substitution )는 스트리밍 되지 않습니다 . 나머지는 모두 파이프를 통해 동시에 발생하지만 두 번째 예에서는 log=이 완료 될 때까지 기다려야합니다 . << HERE \ n $ {log = $ (command)} \ nHERE로 시도하십시오-당신이 얻는 것을보십시오.
mikeserv

매우 큰 파일, 메모리 제한 시스템 또는 더 많은 항목의 경우 grep일부 속도가 향상 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., 아니요, 두 번째 예에서는 한 번만 읽습니다. 꼬리는 한 번만 호출됩니다.
psusi

이제 이것들을 tail -n 10000 | fgrep -c '"success": true'거짓과 비교하십시오 .
kojiro

답변:


11

한편으로 첫 번째 메소드는 tail두 번 호출 하므로 한 번만 수행하는 두 번째 메소드보다 더 많은 작업을 수행해야합니다. 반면에 두 번째 방법은 데이터를 셸에 복사 한 다음 다시 출력해야하므로 tail직접 파이프 된 첫 번째 버전보다 더 많은 작업을 수행해야 grep합니다. 첫 번째 방법은 멀티 프로세서 시스템에서 추가 이점을 갖는다 : grep와 병렬로 작동 할 수 tail번째 방법은 엄격 먼저 직렬화되는 반면, tail그 후, grep.

따라서 하나가 다른 것보다 빠른 이유는 없습니다.

무슨 일이 일어나고 있는지 보려면 쉘이 만드는 시스템 호출을 살펴보십시오. 다른 껍질로도 시도하십시오.

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'

방법 1의 주요 단계는 다음과 같습니다.

  1. tail 읽고 시작점을 찾으십시오.
  2. tail4096 바이트 청크를 grep작성하여 생성 된 속도만큼 읽습니다.
  3. 두 번째 검색 문자열에 대해 이전 단계를 반복하십시오.

방법 2의 주요 단계는 다음과 같습니다.

  1. tail 읽고 시작점을 찾으십시오.
  2. tail bash는 한 번에 128 바이트를 읽고 bash는 한 번에 4096 바이트를 읽습니다.
  3. Bash 또는 zsh는 4096 바이트 청크를 grep작성하여 생성 된 속도만큼 빠르게 읽습니다.
  4. 두 번째 검색 문자열에 대해 이전 단계를 반복하십시오.

명령 대체의 출력을 읽을 때 Bash의 128 바이트 청크가 크게 느려집니다. zsh는 방법 1만큼 빠르게 나옵니다. 마일리지는 CPU 유형 및 수, 스케줄러 구성, 관련 도구 버전 및 데이터 크기에 따라 달라질 수 있습니다.


4k 그림 페이지 크기는 종속적입니까? 내 말은, 꼬리와 zsh는 모두 syscall을 mmaping하고 있습니까? (아마 나는 그 말이 틀렸어도 좋지 않겠지 만 ...) 배쉬가 다르게하는 것은 무엇인가?
mikeserv

이것은 Gilles의 자리입니다! zsh를 사용하면 두 번째 방법이 내 컴퓨터에서 약간 더 빠릅니다.
phunehehe

대단한 일 Gilles, tks.
X Tian

@ mikeserv 나는이 프로그램이 크기를 선택하는 방법을보기 위해 소스를 보지 않았습니다. 4096을 볼 가능성이 가장 큰 이유는 내장 상수 또는 st_blksize파이프 의 값입니다.이 기계에서는 4096입니다 (그리고 그것이 MMU 페이지 크기이기 때문에 모르겠습니다). Bash의 128은 내장 상수 여야합니다.
Gilles 'SO- 악한 중지'

@Gilles, 신중한 답변에 감사드립니다. 최근에 페이지 크기가 궁금했습니다.
mikeserv

26

다음 테스트를 수행했으며 시스템에서 두 번째 스크립트의 결과 차이가 약 100 배 더 깁니다.

내 파일은 strace 출력입니다 bigfile

$ wc -l bigfile.log 
1617000 bigfile.log

스크립트

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

나는 실제로 grep과 일치하는 것이 없으므로 마지막 파이프에 아무것도 기록되지 않습니다. wc -l

타이밍은 다음과 같습니다.

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

그래서 strace 명령을 통해 두 스크립트를 다시 실행했습니다.

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

추적 결과는 다음과 같습니다.

$ 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

그리고 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

분석

놀랍게도, 두 경우 모두 대부분의 시간이 프로세스가 완료되기를 기다리는 데 소비되지만 p2는 p1보다 2.63 배 더 길며, 다른 사람들이 언급했듯이 p2.sh에서 늦게 시작하고 있습니다.

이제를 잊고 열을 waitpid무시하고 %두 트레이스에서 초 열을 확인하십시오.

가장 큰 시간 p1은 읽을 파일이 많기 때문에 대부분의 시간을 읽기에 소비하지만 아마도 p2는 p1보다 28.82 배 더 오래 읽습니다. - bash큰 파일을 변수로 읽을 것으로 예상하지 않고 한 번에 버퍼를 읽고 행으로 분할 한 다음 다른 파일을 가져 오는 것입니다.

읽기 카운트 p2는 705k 대 p1의 경우 84k이며, 각 읽기는 컨텍스트를 커널 공간으로 전환하고 다시 출력해야합니다. 읽기 및 컨텍스트 전환 횟수의 거의 10 배

쓰기 시간 p2는 p1보다 쓰기 시간이 41.93 배 더 길다

쓰기 카운트 p1은 p2, 42k 대 21k보다 많은 쓰기를 수행하지만 훨씬 빠릅니다.

아마도 테일 쓰기 버퍼와 반대로 echo라인이 있기 때문일 것입니다 grep.

또한 p2는 읽기보다 쓰기에 더 많은 시간을 소비하며 p1은 다른 방식으로 진행됩니다!

기타 요소brk 시스템 호출 수를 살펴보십시오 . p2는 읽는 것보다 2.42 배 더 긴 시간을 소비합니다! p1에서 (등록조차하지 않음). brk프로그램이 처음에 충분히 할당되지 않았기 때문에 프로그램이 주소 공간을 확장해야 할 때, 아마도 bash가 해당 파일을 변수로 읽어야하고 파일이 너무 클 것으로 기대하지 않기 때문에 아마도 @scai가 언급 한 것처럼 파일이 너무 커져도 작동하지 않습니다.

tail이것은 아마도 매우 효율적인 파일 판독기 일 것입니다. 이것은 이것이 의도 한 것이므로 파일을 압축하고 줄 바꿈을 스캔하여 커널이 i / o를 최적화 할 수있게합니다. bash는 읽고 쓰는 데 시간이 오래 걸리지 않습니다.

P2는 44ms와의 41ms를 보낸다 cloneexecv는 P1에 대한 측정 가능한 양이 아니다. 아마도 꼬리에서 변수를 읽고 작성하는 것 같습니다.

마지막으로 총계 p1은 ~ 150k 시스템 호출을 실행하고 p2 740k (4.93 배 더 큼)를 실행합니다.

waitpid를 제거하면 p1은 시스템 호출을 실행하는 데 0.014416 초, p2 0.439132 초 (30 배 이상)를 소비합니다.

따라서 p2는 시스템 호출이 완료되고 커널이 메모리를 재구성하기를 기다리는 것을 제외하고는 사용자 공간에서 대부분의 시간을 소비하는 것으로 보이며, p1은 더 많은 쓰기를 수행하지만보다 효율적이며 시스템로드를 크게 줄이므로 더 빠릅니다.

결론

bash 스크립트를 작성할 때 메모리를 통한 코딩에 대해 결코 걱정하지 않으려 고합니다. 효율적이지 않다고 말하는 것은 아닙니다.

tailmemory maps커널은 i / o를 읽고 효율적으로 읽을 수 있도록 파일 을 수행하도록 설계되었습니다 .

문제를 최적화하는 더 좋은 방법은 먼저 grep' "성공":'행을 찾은 다음 true와 false를 계산하고 grepcount 옵션을 사용 wc -l하여 꼬리를 피하고 더 나은 방법으로 꼬리를 통과시켜 awktrue와 count를 계산하는 것입니다. 동시에 거짓. p2는 메모리가 brks와 뒤섞이는 동안 시간이 오래 걸리고 시스템에로드를 추가합니다.


2
TL; DR : malloc (); 만약 $ log에게 그것이 얼마나 큰지 알려줄 수 있고 재 할당없이 하나의 op에 빠르게 쓸 수 있다면 그것은 아마도 빠를 것입니다.
Chris K

5

실제로 첫 번째 솔루션은 파일을 메모리로 읽습니다! 이를 캐싱 이라고 하며 운영 체제에서 자동으로 수행합니다.

mikeserv 가 이미 올바르게 설명했듯이 첫 번째 솔루션 은 파일을 읽는 grep 동안 예상 되는 반면 두 번째 솔루션은 파일을 읽은 후에 실행합니다 tail.

따라서 다양한 최적화로 인해 첫 번째 솔루션이 더 빠릅니다. 그러나 이것이 항상 사실 일 필요는 없습니다. OS가 캐시하지 않기로 결정한 실제로 큰 파일의 경우 두 번째 솔루션이 더 빨라질 수 있습니다. 그러나 메모리에 맞지 않는 더 큰 파일의 경우 두 번째 솔루션이 전혀 작동하지 않습니다.


3

가장 큰 차이점은 echo . 이걸 고려하세요:

$ 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

위에서 볼 수 있듯이 시간이 걸리는 단계는 데이터를 인쇄하는 것입니다. 단순히 새 파일로 리디렉션하고 그 파일을 통해 grep하면 하면 파일을 한 번만 읽을 때 훨씬 빠릅니다.


요청에 따라 here 문자열을 사용하십시오.

 $ 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

here 문자열이 모든 데이터를 하나의 긴 줄에 연결하고 있기 때문에 속도가 느려질 것입니다. 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

분할이 발생하지 않도록 변수를 따옴표로 묶으면 조금 더 빠릅니다.

 $ 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

그러나 속도 제한 단계가 데이터를 인쇄하기 때문에 여전히 느립니다.


왜 시도하지 않는 <<<것이 그 차이가 있는지 확인하기 위해 흥미로운 일이 될 것이다.
Graeme

3

나도 이것에 갔다 ... 먼저, 나는 파일을 만들었다 :

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

위의 내용을 직접 실행하면 /tmp/log2 : 1 비율의 "success": "true"선으로 150 만 줄을 만들어야 합니다."success": "false" 선.

다음으로 몇 가지 테스트를 수행했습니다. 프록시를 통해 모든 테스트를 실행 sh했으므로time 단일 프로세스 만 관찰하면되므로 전체 작업에 대해 단일 결과를 표시 할 수 있습니다.

두 번째 파일 설명자를 추가하고 tee,이유를 설명 할 수 있다고 생각 하지만 가장 빠릅니다 .

    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

첫 번째는 다음과 같습니다.

    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

그리고 두 번째 :

    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

내 테스트에서 변수를 읽을 때 속도가 3 * 이상 차이가 있음을 알 수 있습니다.

그 중 일부는 쉘 변수를 읽을 때 쉘 변수가 분할되고 처리되어야한다는 것입니다. 파일이 아닙니다.

A는 here-document다른 한편으로는, 모든 의도와 목적을 위해, A는 file-file descriptor, 어쨌든. 그리고 우리 모두 알다시피 유닉스는 파일로 작업 합니다.

무엇이 나에게 가장 흥미로운 것은 약 here-docs당신이 그들의 조작 할 수 있다는 것입니다 file-descriptors직선으로 - |pipe- 하고이를 실행합니다. 원하는 |pipe위치를 지정할 때 좀 더 자유 로워 질 수 있으므로 매우 편리합니다 .

나는에 있었다teetail 첫 있기 때문에 grep먹는 here-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

훨씬 빠릅니다.

내가없이 실행할 때 I 성공적으로 배경 첫 번째 프로세스는 완전히이를 동시에 실행 할 수 없습니다. 여기에 완전히 배경이 없습니다.. 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

하지만 내가 추가하면 &:

    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 `|'

아직도, 그 차이는 적어도 나에게는 수백 분의 1 초 밖에되지 않는 것 같습니다.

어쨌든, 더 빠르게 실행되는 이유 tee는 두 greps번의 호출로 동시에 실행되어 tail. tee파일을 복제하고 두 번째 파일로 분리하기 때문입니다.grep 프로세스 모든 인스 트림 스트림으로 모든 것이 처음부터 끝까지 한 번에 실행되므로 모두 같은 시간에 끝납니다.

첫 번째 예제로 돌아가십시오.

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

그리고 두 번째 :

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

그러나 입력을 나누고 동시에 프로세스를 실행할 때 :

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

1
+1하지만 마지막 테스트가 구문 오류로 사망했지만 시간이 정확하지 않다고 생각합니다. :)
terdon

@ terdon 그들은 잘못되었을 수 있습니다-나는 그것이 죽었다는 것을 지적하고있었습니다. &와 no &의 차이점을 보여주었습니다. 추가하면 쉘이 화를냅니다. 그러나 한 두 가지를 엉망으로 만들었으므로 복사 / 붙여 넣기 작업을 많이 수행했지만 모두 문제가없는 것 같습니다.
mikeserv

sh : line 2 : 예기치 않은 토큰`| '근처의 구문 오류
terdon

@terdon Yeah that- "완전히 동시에 실행할 수있는 첫 번째 프로세스를 성공적으로 수행 할 수 없습니다. 참조?" 첫 번째는 배경 지식이 없지만 "예기치 않은 토큰"을 추가하려고 &를 추가하려고 할 때. 내가 때. &를 사용할 수있는 heredoc 소스.
mikeserv
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.