에코와 고양이의 실행 시간이 왜 다른가요?


15

질문에 대답 하면 또 다른 질문이
생겼습니다. 다음 스크립트는 같은 일을하고 두 번째 스크립트는 훨씬 빨라야한다고 생각했습니다. 첫 번째 스크립트는 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.


1
그리고 cat $(for i in $(seq 1 10); do echo "input"; done) >> output어때요? :)
netmonk

2
echo빠릅니다. 당신이 빠진 것은 변수를 사용할 때 따옴표를 쓰지 않음으로써 쉘이 너무 많은 일을하도록한다는 것입니다.
roaima

변수를 인용하는 것은 문제가되지 않습니다. 문제는 변수 i 자체 (즉, 입력과 출력 사이의 중간 단계로 사용)입니다.
Aleksander

`echo $ i`-이 작업을 수행하지 마십시오. printf를 사용하고 인수를 인용하십시오.
PSkocik

1
@PSkocik 내가 말하는 것은 당신이 printf '%s' "$i"아니라 원하는 것 echo $i입니다. @cuonglm은 그의 답변에서 에코의 문제 중 일부를 잘 설명합니다. echo가있는 경우 인용이 충분하지 않은 이유는 unix.stackexchange.com/questions/65803/…
PSkocik

답변:


24

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

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입력이 매우 작은 경우를 제외하고는 루프에서 사용하는 것보다 결코 효율적이지 않습니다.


긴 인수의 경우 메모리 부족을 가져올 수 있습니다 . 예/bin/echo $(perl -e 'print "A"x999999')
cuonglm

당신은 읽기 크기가 큰 영향을 미친다는 가정에 착각하므로 실제 이유를 이해하려면 내 대답을 읽으십시오.
schily

@schily, 128 바이트의 409600 읽기는 64k의 800 읽기보다 시간 (시스템 시간)이 더 걸립니다. dd bs=128 < input > /dev/null와 비교하십시오 dd bs=64 < input > /dev/null. 0.6s 중에서 파일을 읽으려면 bash가 필요하고 0.4는 read내 테스트 에서 해당 시스템 호출에 소비되는 반면 다른 쉘은 시간이 덜 걸립니다.
Stéphane Chazelas

글쎄, 당신은 실제 성능 분석을 실행하지 않은 것 같습니다. 읽기 호출의 영향 (다른 읽기 크기를 비교할 때)은 앞쪽에 있습니다. 함수 readwc()trim()Burne Shell 의 전체 시간의 1 %는 전체 시간의 30 %를 차지하지만에 대한 gprof주석이있는 libc가 없기 때문에 이것은 아마도 과소 평가되었을 것입니다 mbtowc().
schily

어느 것이 \x확장됩니까?
모하마드

11

문제에 대해하지 않습니다 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 프로세스를 더 빠르게 만들 수 있습니다.


2
@roaima : /*/*/*/*../../../../*/*/*/*/../../../../파일 내용에있을 수있는 무언가를 이미징하는 큰 이유가 될 수있는 glob 부분은 언급하지 않았습니다 . 세부 사항 을 지적하고 싶을 뿐입니다 .
cuonglm

감사합니다 그럼에도 불구하고, 인용되지 않은 변수를 사용하면 타이밍이 두 배가됩니다
roaima

1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
netmonk

인용으로 문제를 해결할 수없는 이유를 모르겠습니다. 자세한 설명이 필요합니다.
모하마드

1
@ mohammad.k : 내 대답에 썼을 때 변수를 인용하지 말고 glob+splitwhile 루프를 가속화하십시오. 또한 도움이되지 않는다고 언급했습니다. 대부분의 셸 echo동작이 POSIX 호환이 아니기 때문입니다. printf '%s' "$i"더 나은.
cuonglm

2

전화하면

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 바이트 청크로 읽었으며 두 경우 모두 동일한 성능을 얻었습니다.


i=`cat input`명령은 필드 분할 echo $i을 수행하지 않습니다. 소요 시간 i=`cat input`은와 비교할 때 무시할 만하지 만 단독으로는 echo $i비교할 수 없으며 cat input의 경우 작은 읽기 bash로 인해 차이가 가장 bash큽니다. 128에서 4096으로 변경해도의 성능에는 영향을 미치지 echo $i않지만 그 시점이 아닙니다.
Stéphane Chazelas

또한 echo $i입력 내용과 파일 시스템 (IFS 또는 glob 문자가 포함 된 경우)에 따라 성능 이 크게 달라질 수 있으므로 대답에서 쉘을 비교하지 않았습니다. 예를 들어 여기의 출력 yes | ghead -c50M에서 ksh93이 가장 느리지 만 on yes | ghead -c50M | paste -sd: -에서는 가장 빠릅니다.
Stéphane Chazelas

총 시간에 대해 이야기 할 때 전체 구현에 대해 이야기하고 있었으며 물론 필드 분할은 echo 명령으로 발생합니다. 이곳은 대부분의 시간이 소비되는 곳입니다.
schily

물론 성능은 내용 $ od에 의존한다는 것이 맞습니다.
schily

1

두 경우 모두 루프는 두 번만 실행됩니다 (단어에 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); do... done). 타이밍은 다음과 같습니다.

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

실제로 루프는 두 번이 아니라 10 번 실행됩니다.
fpmurphy

당신이 말한대로했지만 문제가 해결되지 않았습니다. cat보다 매우 빠릅니다 echo. 첫 번째 스크립트는 평균 3 초 동안 실행되지만 두 번째 스크립트는 평균 54 초 동안 실행됩니다.
모하마드

@ fpmurphy1 : 아니요. 내 코드를 시도했습니다. 루프는 10 회가 아닌 2 회만 실행됩니다.
모하마드

세 번째로 @ mohammad.k : 변수를 인용하면 문제가 해결됩니다.
roaima

@roaima :이 명령 tar cf - | dd bs=1M count=50은 무엇을합니까? 내부에 동일한 문자를 가진 일반 파일을 작성합니까? 그렇다면 입력 파일이 모든 종류의 문자와 공백으로 완전히 불규칙합니다. 그리고 다시, 나는 time당신이 사용한 대로 사용했으며 결과는 54 초 대 3 초였습니다.
모하마드

-1

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.


-2

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

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


궁금한. echo텍스트 작성에 최적화 되지 않은 이유는 무엇 입니까?
roaima

2
POSIX 표준에는 echo가 화면에 한 줄을 넣는다는 의미는 없습니다.
fpmurphy

-2

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

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

input -> output

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

input -> variable
variable -> output

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


stdin에서 복사하고 stdout에 쓰는 것 외에도 cat이 파일을 열어야한다고 언급해야한다고 생각합니다. 이것이 두 번째 스크립트의 우수성이지만 첫 번째 스크립트는 두 번째 스크립트보다 우수합니다.
모하마드

두 번째 스크립트에는 우수성이 없습니다. cat은 두 경우 모두 입력 파일을 열어야합니다. 첫 번째 경우에는 고양이의 표준 출력이 파일로 직접 이동합니다. 두 번째 경우 cat의 stdout은 먼저 변수로 이동 한 다음 변수를 출력 파일에 인쇄합니다.
Aleksander

@ mohammad.k, 두 번째 스크립트에는 "우수성" 이 강조되어 있지 않습니다.
와일드 카드
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.