echo와 cat의 실행 시간에 왜 이렇게 큰 차이가 있나요?

echo와 cat의 실행 시간에 왜 이렇게 큰 차이가 있나요?

응답이것질문으로 인해 또 다른 질문이 생겼습니다.
다음 스크립트는 동일한 작업을 수행한다고 생각합니다. 첫 번째 스크립트는 cat파일을 계속해서 열어야 하기 때문에 두 번째 스크립트가 더 빨라야 하지만 두 번째 스크립트는 파일을 한 번만 연 다음 Echo만 엽니다. 변수:

(올바른 코드는 업데이트된 섹션을 참조하세요.)

첫 번째:

#!/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 - 파일을 연결하고 표준 출력으로 인쇄

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

그래서:

  • 두 번째 스크립트에서 cat은 왜 그렇게 빠르고 echo는 그렇게 느린가요?
  • 아니면 변수에 문제가 있는 걸까요 i? (맨페이지에 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

(특별히 감사함로에마.)

그러나 이것이 요점이 아닙니다. 루프가 한 번만 발생하더라도 동일한 문제가 있습니다 . cat.echo

답변1

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

i=`cat input`

비용이 많이 들 수 있으며 케이스마다 차이가 많습니다.

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

이를 위해 쉘은 하위 쉘에서 명령을 분기하고 파이프 또는 소켓 쌍을 통해 출력을 읽습니다. 여기에서 많은 변화가 보입니다. 여기 50MiB 파일에서 bash가 ksh93보다 6배 느리지만 zsh보다 약간 빠른 것을 볼 수 있습니다. yes yash.

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

zshNUL 바이트를 이스케이프하려면 일부 사후 처리가 필요하며(다른 쉘은 NUL 바이트에서 중단됨) yash멀티바이트 문자를 구문 분석하여 더욱 강력한 처리를 수행합니다.

모든 쉘은 후행 개행 문자를 제거해야 하며 다소 효율적일 수 있습니다.

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

그런 다음 메모리에 이 큰 변수가 있으면 이에 대한 모든 작업에는 일반적으로 더 많은 메모리를 할당하고 데이터 전체를 처리하는 작업이 포함됩니다.

여기에서 변수의 내용을 echo.

운좋게 echo도 쉘에 내장되어 있습니다. 그렇지 않으면 실행이 실패할 수 있습니다.매개변수 목록이 너무 깁니다.실수. 그렇더라도 매개변수 목록 배열을 구축하려면 변수 내용을 복사해야 할 수도 있습니다.

명령 대체 접근 방식의 또 다른 주요 문제는 다음을 호출한다는 것입니다.분할+전역 연산자(변수를 인용하는 것을 잊어버렸습니다).

이렇게 하려면 쉘이 문자열을 문자열로 처리해야 합니다.수치(일부 쉘은 이 작업을 수행하지 않고 이 영역에 결함이 있지만) 따라서 UTF-8 로케일에서 이는 UTF-8 시퀀스를 구문 분석하고(아직 이전처럼 수행되지 않은 경우) yash문자열에서 문자를 찾는 것을 의미합니다. 공백, 탭 또는 줄 바꿈이 포함 $IFS되면 $IFS(기본적으로 포함됨) 알고리즘은 훨씬 더 복잡하고 비용이 많이 듭니다. 이 분할로 인해 생성된 단어는 할당 및 복사되어야 합니다.

글로브 부분은 더 비쌉니다. 이러한 단어 중 하나에 전역 문자( *, ?, [)가 포함되어 있으면 쉘은 일부 디렉토리의 내용을 읽고 비용이 많이 드는 패턴 일치를 수행해야 합니다( bash예: 구현이 매우 나쁩니다).

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

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

반면에 대부분의 셸에는 cat내장되어 있지 않으므로 프로세스를 분기하고 실행하는 것을 의미합니다(따라서 코드와 라이브러리 로드). 그러나 첫 번째 호출 후에는 코드와 입력 파일의 내용이 메모리에 캐시되어 있습니다. 반면에 중개인은 없을 것입니다. cat많은 양의 데이터를 한 번에 읽어서 별도의 처리 없이 바로 쓰는 방식으로, 대용량 메모리를 할당할 필요 없이 버퍼만 재사용하면 된다.

이는 또한 NUL 바이트를 차단하지 않고 후행 줄 바꿈을 자르지 않기 때문에 더 안정적이라는 것을 의미합니다. 변수를 인용하여 이를 피할 수 있지만 캐스트 시퀀스를 확장하지 않지만 분할+글로브를 수행하지 않습니다. , 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그러나 이는 루프에서 사용하는 것보다 결코 효율적이지 않습니다 (입력이 매우 작지 않은 한).

답변2

문제는 catand 가 echo아니라 잊혀진 인용 변수 입니다 $i.

Bourne과 같은 쉘 스크립트( 제외 zsh)에서 변수를 따옴표로 묶지 않으면 glob+split변수에 연산자가 사용됩니다.

$var

실제로는:

glob(split($var))

따라서 루프가 반복될 때마다 전체 내용 input(후행 줄 바꿈 제외)이 확장, 분할 및 와일드카드로 지정됩니다. 전체 프로세스에서는 쉘이 메모리를 할당하고 문자열을 계속해서 구문 분석해야 합니다. 그렇기 때문에 실적이 저조합니다.

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

를 사용하려면 cat셸은 각 루프 반복마다 프로세스를 생성하고 cat복사 I/O를 수행하기만 하면 됩니다. 시스템은 또한 파일 내용을 캐시하여 고양이 처리 속도를 높일 수도 있습니다.

답변3

당신이 전화하면

i=`cat input`

이를 통해 쉘 프로세스를 50MB에서 최대 200MB까지 늘릴 수 있습니다(내부 와이드 문자 구현에 따라 다름). 이로 인해 쉘 속도가 느려질 수 있지만 큰 문제는 아닙니다.

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

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

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

ksh93이 가장 빠른 이유는 ksh93이 mbtowc()libc를 사용하지 않고 자체 구현을 사용하기 때문인 것 같습니다.

참고: Stephane은 읽기 크기가 어느 정도 영향을 미친다고 잘못 믿었습니다. Bourne Shell을 컴파일하여 128바이트 대신 4096바이트 블록을 읽었고 두 경우 모두 동일한 성능을 얻었습니다.

답변4

이는 echo화면에 1개의 행을 배치하는 것을 의미합니다. 두 번째 예에서는 파일 내용을 변수에 넣은 다음 해당 변수를 인쇄합니다. 첫 번째에서는 콘텐츠를 즉시 화면에 표시합니다.

cat이 용도에 최적화되었습니다. echo아니요. 또한 환경 변수에 50Mb를 넣는 것도 좋은 생각이 아닙니다.

관련 정보