"예"를 사용하면 파일에 어떻게 그렇게 빨리 쓸 수 있나요?

"예"를 사용하면 파일에 어떻게 그렇게 빨리 쓸 수 있나요?

예를 들어 보겠습니다.

$ 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

yes여기서는 명령이 1초 안에 줄을 쓰는 반면, 나는 bash를 사용하고 115046405초 안에 줄을 쓸 수 있다는 것을 볼 수 있습니다 .1953forecho

의견에서 제안한 대로 효율성을 향상시키는 다양한 방법이 있지만 그 속도에 근접한 방법은 없습니다 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

초당 최대 20,000개의 행을 쓸 수 있습니다. 다음과 같이 더욱 개선될 수 있습니다.

$ 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

이를 통해 초당 40,000개의 행을 얻을 수 있습니다. 더 좋지만 yes여전히 초당 1,100만 줄을 쓰는 것과는 거리가 멀습니다!

그래서,yes파일이 왜 그렇게 빨리 작성되나요?

답변1

간단히 말해서:

yes일반적으로 대부분의 다른 표준 유틸리티와 유사한 동작을 나타냅니다.쓰다파일 스트림출력은 다음을 통해 libC에 의해 버퍼링됩니다.stdio. 이들은 시스템 호출만 수행합니다.write()각각 약 4kb(16kb 또는 64kb)또는 출력 블록BUFSIZ예. echowrite()GNU. 그건많은~의모드 전환 (물론 그런건 아니지만컨텍스트 스위치).

말할 것도 없이, 초기 최적화 루프 외에는 yes매우 간단하고 작은 컴파일된 C 루프이므로 쉘 루프는 컴파일러 최적화 프로그램과 비교할 수 없습니다.


그러나 나는 틀렸다:

내가 이전에 yesuse 라고 말했을 때 stdio, 나는 그것이 그렇게 하는 사람들과 매우 유사하게 행동하기 때문에 그랬다고 가정했습니다. 그것은 사실이 아닙니다. 단지 그들의 행동을 그런 식으로 흉내낼 뿐입니다. 실제로 수행하는 작업은 쉘을 사용하여 아래에서 수행하는 작업과 매우 유사합니다. 먼저 인수 병합을 반복합니다.(또는 y그렇지 않은 경우)더 이상 성장하지 않을 때까지는 그렇지 않습니다 BUFSIZ.

님의 댓글원천관련 루프 상태 직전 for:

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

yes그 후에는 자신의 write()작업을 수행하십시오.


여담:

(원래 질문에 포함되어 있으며 여기에 작성된 잠재적으로 유익한 설명에 대한 맥락으로 유지됩니다.):

시도했지만 timeout 1 $(while true; do echo "GNU">>file2; done;)루프를 멈출 수 없습니다.

명령 대체 문제 timeout- 이제 이해하고 왜 멈추지 않는지 설명할 수 있을 것 같습니다. timeout명령줄이 실행되지 않았기 때문에 시작되지 않습니다. 쉘은 서브쉘을 분기하고 표준 출력에서 ​​파이프를 열고 이를 읽습니다. 하위 프로세스가 종료되면 읽기를 중지한 다음 $IFS재구성 및 전역 확장을 위해 작성된 모든 하위 프로세스를 해석하고 결과에 따라 $(에서 일치하는 모든 항목을 바꿉니다 ).

그러나 하위 프로세스가 파이프에 쓰지 않는 무한 루프인 경우 하위 프로세스는 루프를 멈추지 않으며 timeout명령줄도 결코 멈추지 않습니다.(내가 추측한 대로)Ctrl+를 수행 C하고 하위 루프를 종료합니다. 그래서 timeout확인 안 돼요시작하기 전에 완료해야 하는 루프를 종료합니다.


기타 timeout:

...쉘 프로그램이 출력을 처리하기 위해 사용자 모드와 커널 모드 사이를 전환하는 데 소요되는 시간만큼 성능 문제와 관련이 없습니다. timeout그러나 쉘만큼 유연하지는 않습니다. 쉘의 장점은 매개변수를 처리하고 다른 프로세스를 관리하는 능력에 있습니다.

다른 곳에서 지적했듯이[fd-num] >> named_file단순히 루프 명령의 출력을 지시하는 대신 루프의 출력 대상으로 리디렉션하면 성능이 크게 향상될 수 있습니다.open()시스템 호출은 한 번만 완료하면 됩니다. |이 작업은 대상이 내부 루프의 출력인 파이프를 사용하여 아래에서도 수행 됩니다.


직접 비교:

당신은 다음을 좋아할 것입니다:

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

이것은유형앞에서 설명한 명령-하위 관계와 비슷하지만 파이프와 하위 프로세스가 없으면 상위 프로세스가 종료될 때까지 백그라운드에 있습니다. 이 yes경우 부모 프로세스는 자식이 생성된 이후 실제로 교체되었지만 yes자체 프로세스를 새 프로세스로 덮어써 쉘이 호출되므로 PID는 동일하게 유지되고 좀비 자식은 여전히 ​​누구를 죽일지 알고 있습니다.


더 큰 버퍼:

이제 쉘의 버퍼를 늘리는 방법을 살펴보겠습니다 write().

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

1kb보다 큰 출력 문자열은 별도의 조각으로 분할되기 때문에 이 숫자를 선택했습니다 write(). 그래서 이것은 또 다른 루프입니다:

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

이번 테스트에서는 쉘이 동시에 쓴 데이터의 양이 이전 테스트의 300배에 달했다. 너무 초라하지 않습니다. 그러나 그것은 진실이 아니다 yes.


비용은 다음과 같습니다.

요청 시 단순한 코드 주석보다 더 포괄적인 설명을 여기에서 볼 수 있습니다.이 링크.

답변2

더 나은 질문은 쉘이 파일 쓰기 속도가 너무 느린 이유입니다. 파일 쓰기 시스템 호출을 책임감 있게 사용하는 독립 실행형 컴파일러(모든 문자를 한 번에 플러시하는 대신)는 이를 상당히 빠르게 수행합니다. 당신이 하고 있는 일은설명했다언어 (쉘), 추가로 당신은많은불필요한 입력 및 출력 작업. 무엇 yes인가요:

  • 쓰기 위해 파일 열기
  • 최적화되고 컴파일된 함수를 호출하여 스트림에 쓰기
  • 스트림은 버퍼링되므로 시스템 호출(커널 모드로의 값비싼 전환)이 큰 덩어리로 거의 발생하지 않습니다.
  • 파일을 닫다

스크립트의 기능은 다음과 같습니다.

  • 코드 한 줄 읽기
  • 코드를 해석하고, 입력 내용을 실제로 구문 분석하고 수행할 작업을 파악하기 위해 많은 추가 작업을 수행합니다.
  • while 루프의 각 반복에 대해(통역 언어에서는 저렴하지 않을 수 있음):
    • 외부 명령을 호출 date하고 그 출력을 저장합니다(원래 버전에서만 가능 - 수정된 버전에서는 이렇게 하지 않으면 10배의 이득을 얻게 됩니다)
    • 루프 종료 조건이 충족되는지 테스트
    • 열려 있는추가 모드의 파일
    • 명령을 구문 분석하고 echo, 이를 쉘 내장으로 식별하고(일부 패턴 일치 코드를 사용하여) 인수 확장 및 인수 "GNU"의 다른 모든 것을 호출하고, 마지막으로 열린 파일에 행을 작성합니다.
    • 폐쇄파일을 다시
    • 이 과정을 반복하세요

비용이 많이 드는 부분: 전체 해석 비용이 매우 비쌉니다(bash는 모든 입력에 대해 많은 전처리를 수행합니다. 문자열에는 변수 대체, 프로세스 대체, 중괄호 확장, 이스케이프 문자 등이 포함될 수 있습니다). 그리고 내장 함수에 대한 모든 호출이 필요합니다. 내장 함수를 처리하는 함수로 리디렉션하는 스위치 문일 수 있으며 각 출력 줄에 대해 파일을 열고 닫는 것이 매우 중요합니다. >> filewhile 루프 외부에 배치하면 이 작업을 수행 할 수 있습니다.훨씬 더 빨리, 하지만 여전히 통역된 언어를 사용하고 있습니다. 이것이 echo외부 명령이 아닌 내장 셸이라는 점은 운이 좋습니다 . 그렇지 않으면 루프가 각 반복마다 새 프로세스(포크 및 실행)를 생성하게 됩니다. 이로 인해 프로세스가 중단됩니다. date루프에서 명령을 사용할 때 이것이 얼마나 비용이 많이 드는지 알 수 있습니다.

답변3

다른 답변이 요점에 도달했습니다. 참고로, 계산이 끝날 때 출력 파일에 기록하여 while 루프의 처리량을 늘릴 수 있습니다. 비교하다:

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

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

그리고

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

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

관련 정보