에코와 고양이의 실행 시간이 왜 다른가요? echo-한 줄의 텍스트를

질문에 대답 하면 또 다른 질문이
생겼습니다. 다음 스크립트는 같은 일을하고 두 번째 스크립트는 훨씬 빨라야한다고 생각했습니다. 첫 번째 스크립트는 cat반복해서 파일을 열어야하지만 두 번째 스크립트는 파일 만 열어야 하기 때문입니다. 한 번만 변수를 에코합니다.

(올바른 코드는 업데이트 섹션을 참조하십시오.)

먼저:

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

둘째:

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

입력은 약 50MB입니다.

그러나 두 번째 것을 시도했을 때 변수를 에코하는 i것이 거대한 프로세스 이기 때문에 너무 느 렸습니다 . 또한 두 번째 스크립트에 문제가 있습니다. 예를 들어 출력 파일의 크기가 예상보다 작습니다.

또한의 맨 페이지를 확인 echo하고 cat이를 비교하기를 :

echo-한 줄의 텍스트를 표시

cat-파일을 연결하고 표준 출력에 인쇄

그러나 나는 차이를 얻지 못했습니다.

그래서:

  • 왜 두 번째 스크립트에서 고양이가 너무 빠르고 에코가 너무 느립니까?
  • 아니면 변수의 문제 i입니까? (Man 페이지
    echo에는 “텍스트 라인”이 표시 되어 있기 때문에 짧은 변수에만 최적화되어 있지만 매우 긴 변수에는 최적화되지 않았다고 i생각합니다. 그러나 그것은 단지 추측에 불과합니다.)
  • 내가 왜 사용할 때 문제가 발생 echo합니까?

최신 정보

seq 10대신 `seq 10`잘못 사용 했습니다 . 이것은 편집 된 코드입니다.

먼저:

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

둘째:

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

( roaima 덕분에 특별 감사합니다 .)

그러나 문제의 핵심은 아닙니다. 루프가 한 번만 발생하더라도 동일한 문제가 발생합니다 . cat보다 훨씬 빠르게 작동합니다 echo.



답변

여기서 고려해야 할 사항이 몇 가지 있습니다.

i=`cat input`

비싸고 쉘 사이에 많은 변형이 있습니다.

이것이 명령 대체라는 기능입니다. 아이디어는 명령의 전체 출력에서 ​​후행 줄 바꿈 문자를 뺀 i값을 메모리 의 변수에 저장하는 것입니다.

이를 위해 쉘은 서브 쉘에서 명령을 분기하고 파이프 또는 소켓 쌍을 통해 출력을 읽습니다. 여기에 많은 변형이 있습니다. 여기 50MiB 파일에서 예를 들어 bash는 ksh93보다 6 배 느리지 만 zsh보다 약간 빠르며 두 배 빠릅니다 yash.

bash속도가 느려지는 주된 이유 는 파이프에서 한 번에 128 바이트를 읽고 (다른 쉘은 한 번에 4KiB 또는 8KiB를 읽는 반면) 시스템 호출 오버 헤드에 의해 불이익을 받기 때문입니다.

zshNUL 바이트를 피하기 위해 약간의 사후 처리를 수행해야하며 (다른 쉘은 NUL 바이트에서 중단됨) yash멀티 바이트 문자를 구문 분석하여 훨씬 더 많은 처리를 수행합니다.

모든 쉘은 후행 줄 바꿈 문자를 제거하여 다소 효율적으로 수행 할 수 있습니다.

일부는 다른 것보다 NUL 바이트를 더 우아하게 처리하고 존재 여부를 확인하려고 할 수 있습니다.

그런 다음 메모리에 큰 변수가 있으면이를 조작하려면 일반적으로 더 많은 메모리를 할당하고 데이터를 처리해야합니다.

여기에서는 변수의 내용을로 전달합니다 (전달하려고 함) echo.

운 좋게도 echo셸에 내장되어 있습니다. 그렇지 않으면 인수 목록이 너무 긴 오류로 인해 실행이 실패했을 수 있습니다 . 그럼에도 불구하고 인수 목록 배열을 작성하면 변수의 내용을 복사하는 것이 필요할 수 있습니다.

명령 대체 방식의 다른 주요 문제 는 변수를 인용하지 않고 split + glob 연산자 를 호출한다는 것 입니다.

이를 위해, 쉘의 문자열로 문자열을 처리 할 필요가 문자를 (이미 같은 수행하지 않을 경우 수단이 UTF-8 시퀀스를 구문 분석하는 것으로, (일부 쉘하지 않는 그 점에서 버그가 있지만) 그래서 UTF-8 로켈에서 yash수행) $IFS문자열에서 문자를 찾으십시오 . 경우 $IFS공간 (기본값의 경우) 탭 또는 줄 바꿈을 포함, 알고리즘은 훨씬 더 복잡하고 비싸다. 그런 다음 그 분리로 인한 단어를 할당하고 복사해야합니다.

글로브 부분은 훨씬 비쌉니다. 그 단어 중 하나라도 글로브 문자가 포함 된 경우 ( *, ?, [), 다음 쉘이 일부 디렉토리의 내용을 읽고 비싼 패턴 매칭을해야 할 것 ( bash예를 들어 ‘의 구현은 아주 나쁜 악명입니다).

입력에와 같은 것이 포함되어 있으면 /*/*/*/../../../*/*/*/../../../*/*/*수천 개의 디렉토리를 나열하고 수백 MiB로 확장 할 수 있으므로 매우 비쌉니다.

그런 다음 echo일반적으로 추가 처리를 수행합니다. 일부 구현 \x은 수신하는 인수에서 시퀀스를 확장 합니다. 이는 컨텐츠와 데이터의 다른 할당 및 사본을 구문 분석하는 것을 의미합니다.

반면에 OK는 대부분의 셸 cat에 내장되어 있지 않으므로 프로세스를 실행하고 실행하여 코드와 라이브러리를로드하지만 첫 번째 호출 후 해당 코드와 입력 파일의 내용을 의미합니다 메모리에 캐시됩니다. 반면에 중개자는 없습니다. cat한 번에 많은 양을 읽고 처리하지 않고 바로 쓸 수 있으며 대량의 메모리를 할당 할 필요가 없으며 재사용하는 하나의 버퍼 만 필요합니다.

또한 NUL 바이트를 질식시키지 않고 후행 줄 바꿈 문자를 자르지 않기 때문에 훨씬 더 안정적임을 의미합니다 (그리고 split + glob를하지 마십시오. 변수를 인용하여 피할 수는 있지만 printf대신 이스케이프 시퀀스를 확장하면 피할 수 있습니다 echo.

대신 호출하는, 더를 최적화하려면 cat여러 번, 단지 통과 input에 여러 번 cat.

yes input | head -n 100 | xargs cat

100 대신 3 개의 명령을 실행합니다.

변수 버전을보다 안정적으로 만들려면 zsh다른 쉘이 NUL 바이트를 처리 할 수 ​​없어서 사용해야 합니다.

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

입력에 NUL 바이트가 포함되어 있지 않다는 것을 알고 있다면 POSIXly ( printf내장되지 않은 곳에서는 작동하지 않을 수 있음)를 안정적으로 수행 할 수 있습니다 .

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

그러나 cat입력이 매우 작은 경우를 제외하고는 루프에서 사용하는 것보다 결코 효율적이지 않습니다.


답변

문제에 대해하지 않습니다 catecho는 잊어 인용 변수에 관하여, $i.

Bourne과 유사한 쉘 스크립트 (제외 zsh)에서는 변수를 인용 부호없이 남겨두면 glob+split연산자가 변수에 영향을 미칩니다 .

$var

실제로 :

glob(split($var))

따라서 각 루프 반복마다 input(후행 줄 바꿈 제외) 의 전체 내용 이 확장되고 분할되고 globbing됩니다. 전체 프로세스는 셸에서 메모리를 할당하고 문자열을 반복해서 구문 분석해야합니다. 그것이 당신이 나쁜 성능을 얻은 이유입니다.

glob+split쉘은 여전히 ​​큰 문자열 인수를 작성하고 그 내용을 스캔해야 할 때 변수를 인용 할 수는 있지만 크게 도움이되지는 않습니다 echo(내장 변수 echo를 외부로 바꾸면 /bin/echo인수 목록이 너무 길거나 메모리에 없음) $i크기 에 따라 다름). 대부분의 echo구현은 POSIX를 준수하지 않으며 \x수신 한 인수로 백 슬래시 시퀀스를 확장합니다 .

를 사용 cat하면 쉘은 각 루프 반복마다 프로세스를 생성하기 만하면 cat되며 복사 I / O를 수행합니다. 시스템은 파일 프로세스를 캐시하여 cat 프로세스를 더 빠르게 만들 수 있습니다.


답변

전화하면

i=`cat input`

이를 통해 쉘 프로세스는 내부 와이드 문자 구현에 따라 50MB에서 최대 200MB까지 증가 할 수 있습니다. 이것은 쉘을 느리게 만들 수 있지만 주요 문제는 아닙니다.

주요 문제는 위의 명령이 전체 파일을 셸 메모리로 읽어야하고 echo $i해당 파일 내용에서 필드 분할을 수행해야한다는 것입니다 $i. 필드 분할을 수행하려면 파일의 모든 텍스트를 넓은 문자로 변환해야하며 대부분의 시간이 소요됩니다.

느린 경우로 몇 가지 테스트를 수행 한 결과는 다음과 같습니다.

  • 가장 빠른 ksh93
  • 다음은 Bourne Shell입니다 (ksh93보다 2 배 느림)
  • 다음은 bash입니다 (ksh93보다 3 배 느림)
  • 마지막은 ksh88 (ksh93보다 7 배 느림)

ksh93이 가장 빠른 이유는 ksh93이 mbtowc()libc에서 사용되지 않고 자체 구현이기 때문입니다.

BTW : Stephane은 읽기 크기가 약간의 영향을 미쳤다고 생각합니다. Bourne Shell을 컴파일하여 128 바이트 대신 4096 바이트 청크로 읽었으며 두 경우 모두 동일한 성능을 얻었습니다.


답변

두 경우 모두 루프는 두 번만 실행됩니다 (단어에 seq대해 한 번, 단어에 대해 한 번 10).

또한 둘 다 인접한 공백을 병합하고 선행 / 후행 공백을 삭제하여 출력이 입력의 두 복사본이 될 필요는 없습니다.

먼저

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

둘째

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

echo느려진 이유 중 하나 는 인용되지 않은 변수가 공백에서 별도의 단어로 분리되어 있기 때문일 수 있습니다. 50MB의 경우 많은 작업이 필요합니다. 변수를 인용하십시오!

이러한 오류를 수정 한 후 타이밍을 다시 평가하시기 바랍니다.


나는 이것을 로컬에서 테스트했습니다. 의 출력을 사용하여 50MB 파일을 만들었습니다 tar cf - | dd bs=1M count=50. 또한 타이밍이 합리적인 값으로 조정되도록 x100 배로 루프를 확장했습니다 (전체 코드 주위에 루프를 추가했습니다 : for k in $(seq 100); dodone). 타이밍은 다음과 같습니다.

time ./1.sh

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

time ./2.sh

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

보시다시피 실제 차이는 없지만 포함 된 버전 echo이 약간 더 빨리 실행됩니다. 따옴표를 제거하고 깨진 버전 2를 실행하면 시간이 두 배가되어 쉘이 훨씬 더 많은 작업을 수행해야 함을 보여줍니다.

time ./2original.sh

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


답변

read 보다 훨씬 빠르다 cat

모든 사람들이 이것을 테스트 할 수 있다고 생각합니다.

$ 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 초가 걸립니다. 몇 초가 echo걸립니다 .232.

read이다 40 배 빠른 .

$p화면에 반향 될 때의 첫 번째 테스트 는 read보다 48 배 빠릅니다 cat.


답변

echo화면에 한 줄을 추가하기위한 것입니다. 두 번째 예에서는 파일의 내용을 변수에 넣은 다음 해당 변수를 인쇄합니다. 첫 번째 내용은 즉시 화면에 내용을 넣습니다.

cat이 사용법에 최적화되어 있습니다. echo아니다. 또한 환경 변수에 50Mb를 넣는 것은 좋지 않습니다.


답변

에코가 빨라지는 것이 아니라 내가하고있는 일에 관한 것입니다.

어떤 경우에는 입력에서 읽고 쓰고 직접 출력합니다. 즉, cat을 통해 입력에서 읽은 내용은 stdout을 통해 출력됩니다.

input -> output

다른 경우에는 입력을 메모리의 변수로 읽은 다음 변수의 내용을 출력에 씁니다.

input -> variable
variable -> output

특히 입력이 50MB 인 경우 후자가 훨씬 느려집니다.