배관, 변속 또는 파라미터 확장이 더 효율적입니까?


26

공백으로 구분 된 단어 목록에서 일정한 수의 값을 가진 특정 값을 반복하는 가장 효율적인 방법을 찾으려고합니다 (배열을 사용하고 싶지 않습니다). 예를 들어

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"

그래서 목록을 반복하고 1, 5, 6, 9 및 15에만 액세스 할 수 있기를 원합니다.

편집 : 목록에서 가져 오려고하는 값이 나머지 목록과 형식이 다를 필요가 없다는 것을 분명히해야했습니다. 그것들을 특별하게 만드는 것은 전적으로 목록에서 그들의 위치입니다 (이 경우 위치 1,4,7 ...). 그래서 목록은 가능1 2 3 5 9 8 6 90 84 9 3 2 15 75 55하지만 여전히 같은 숫자를 원할 것입니다. 또한 목록의 길이를 모른다는 가정하에 할 수 있기를 원합니다.

지금까지 내가 생각한 방법은 다음과 같습니다.

방법 1

set $list
found=false
find=9
count=1
while [ $count -lt $# ]; do
    if [ "${@:count:1}" -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
done

방법 2

set list
found=false
find=9
while [ $# ne 0 ]; do
    if [ $1 -eq $find ]; then
    found=true
    break
    fi
    shift 3
done

방법 3 파이핑은 이것이 최악의 옵션이 될 것이라고 확신하지만 호기심에서 set을 사용하지 않는 방법을 찾으려고했습니다.

found=false
find=9
count=1
num=`echo $list | cut -d ' ' -f$count`
while [ -n "$num" ]; do
    if [ $num -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
    num=`echo $list | cut -d ' ' -f$count`
done

그렇다면 가장 효율적인 방법은 무엇입니까, 아니면 더 간단한 방법이 누락 되었습니까?


10
효율성이 중요한 관심사라면 우선 쉘 스크립트를 사용하지 않을 것입니다. 당신의 목록이 얼마나 큰 차이가 있습니까?
Barmar


2
문제의 실제 인스턴스에 대한 통계를 수행하지 않으면 아무것도 알 수 없습니다. 여기에는 "awk에서의 프로그래밍"등과 비교하는 것이 포함됩니다. 통계가 너무 비싸면 효율성을 찾는 것이 그만한 가치가 없을 것입니다.
David Tonhofer

2
레위, 정의에서 "효율적인"방법은 정확히 무엇입니까? 더 빠른 반복 방법을 찾고 싶습니까?
Sergiy Kolodyazhnyy

답변:


18

로 매우 간단합니다 awk. 그러면 길이를 입력 할 때마다 네 번째 필드마다 값을 얻게됩니다.

$ awk -F' ' '{for( i=1;i<=NF;i+=3) { printf( "%s%s", $i, OFS ) }; printf( "\n" ) }' <<< $list
1 5 6 9 15

이것은 (레코드의 필드 수) awk와 같은 내장 변수를 활용 하고 필드를 반복하여 NF몇 개의 간단한 for루프를 수행 하여 얼마나 많은 필드가 있는지 미리 알 필요없이 원하는 필드를 제공합니다.

또는 실제로 예제에 지정된 특정 필드를 원할 경우 :

$ awk -F' ' '{ print $1, $4, $7, $10, $13 }' <<< $list
1 5 6 9 15

효율성에 대한 질문에서 가장 간단한 방법은이 방법이나 다른 방법을 테스트 time하고 시간이 얼마나 걸리는지 보여주는 것입니다. 또한 strace시스템 호출의 흐름을 확인하는 도구를 사용할 수도 있습니다 . 사용법은 time다음과 같습니다.

$ time ./script.sh

real    0m0.025s
user    0m0.004s
sys     0m0.008s

다양한 방법들 사이의 출력을 비교하여 시간 측면에서 가장 효율적인 방법을 확인할 수 있습니다. 다른 도구는 다른 효율성 메트릭에 사용될 수 있습니다.


1
좋은 지적, @MichaelHomer; 나는 "어떻게 가장 효율적인 방법을 결정할 수 있는가"라는 문제를 해결하기 위해 추가했다 .
DopeGhoti

2
@LeviUzodike echovs <<<와 관련하여 "동일한"단어는 너무 강력합니다. 당신은 그것 stuff <<< "$list"과 거의 동일 하다고 말할 수 printf "%s\n" "$list" | stuff있습니다. echovs printf와 관련 하여이 답변으로 안내합니다.
JoL

5
@DopeGhoti 실제로 그렇습니다. <<<끝에 줄 바꿈을 추가합니다. 이것은 $()줄 바꿈을 끝에서 제거하는 방법과 유사합니다 . 줄 바꿈으로 줄이 끝나기 때문입니다. <<<식을 줄로 공급하므로 개행으로 끝나야합니다. "$()"행을 사용하여 인수로 제공하므로 종료 줄 바꿈을 제거하여 변환하는 것이 좋습니다.
JoL

3
@LeviUzodike awk는 많은 평가를받지 못한 도구입니다. 복잡한 것처럼 보이는 모든 종류의 문제를 쉽게 해결할 수 있습니다. 특히 sed와 같은 복잡한 정규 표현식을 작성하려고 할 때 종종 awk로 절차 적으로 작성하여 시간을 절약 할 수 있습니다. 그것을 배우면 큰 배당금이 지급 될 것입니다.

1
@LeviUzodike : 예 awk는 시작 해야하는 독립형 바이너리입니다. Perl 또는 특히 Python과 달리 awk 인터프리터는 빠르게 시작됩니다 (여전히 시스템 호출을 거의하지 않는 일반적인 동적 링커 오버 헤드가 많지만 awk는 libc / libm 및 libdl 만 사용합니다 (예 : straceawk 시작의 시스템 호출을 확인하는 데 사용 )). . bash와 같은 많은 셸은 매우 느리므로 하나의 awk 프로세스를 실행하면 작은 목록 크기에도 셸이 내장 된 목록에서 토큰을 반복하는 것보다 빠를 수 있습니다. 그리고 때때로 당신은 쓸 수있는 #!/usr/bin/awk스크립트 대신#!/bin/sh스크립트를.
Peter Cordes

35
  • 소프트웨어 최적화의 첫 번째 규칙 : 하지 마십시오 .

    프로그램의 속도가 문제가된다는 것을 알기 전까지는 얼마나 빠를 지 생각할 필요가 없습니다. 당신의 목록이 그 길이이거나 ~ 100-1000 항목 정도라면, 얼마나 오래 걸 렸는지조차 알지 못할 것입니다. 차이에 대한 것보다 최적화에 대해 생각하는 데 더 많은 시간을 할애 할 수 있습니다.

  • 두 번째 규칙 : 측정 .

    이것이 확실한 방법이며 시스템에 대한 답변을 제공하는 방법입니다. 특히 껍질에는 너무 많으며 모두 동일하지는 않습니다. 하나의 쉘에 대한 답변이 귀하에게 적용되지 않을 수도 있습니다.

    더 큰 프로그램에서는 프로파일 링도 여기로갑니다. 가장 느린 부분은 당신이 생각하는 부분이 아닐 수도 있습니다.

  • 셋째, 쉘 스크립트 최적화의 첫 번째 규칙 : 쉘을 사용하지 마십시오 .

    그래 진짜. 많은 쉘은 외부 프로그램을 시작할 필요가 없기 때문에 빠르지 않으며 매번 소스 코드의 라인을 다시 구문 분석 할 수도 있습니다.

    대신 awk 또는 Perl과 같은 것을 사용하십시오. 내가 한 사소한 마이크로 벤치 마크 awk에서 (I / O없이) 간단한 루프를 실행하는 데 일반적인 쉘보다 수십 배 빠릅니다.

    그러나 쉘을 사용하는 경우 외부 명령 대신 쉘의 내장 함수를 사용하십시오. 여기서는 expr시스템에서 찾은 쉘에 내장되어 있지 않지만 표준 산술 확장으로 대체 할 수있는 것을 사용하고 있습니다. 예 를 들어 증가 i=$((i+1))대신 . 의 사용 마지막 예는 표준 매개 변수 확장과 교체 할 수 있습니다.i=$(expr $i + 1)icut

    참조 : 나쁜 사례로 간주 처리 텍스트 쉘 루프를 사용하는 이유는 무엇입니까?

질문 # 1과 # 2가 적용되어야합니다.


12
# 0, 확장 인용문 :-)
Kusalananda

8
그것은이지 않는다 awk루프는 반드시 어떤 좋든 나쁘 든 쉘 루프보다. 쉘은 명령실행 하고 입력 및 출력을 프로세스로 보내고 처리 하는 데 능숙 하며 솔직히 다른 모든 것에서는 어수선합니다. 같은 툴 awk은 텍스트 데이터를 처리 하는 데 환상적입니다 . 왜냐하면 쉘과 툴 awk은 (각각) 처음부터 만들어 졌기 때문입니다 .
DopeGhoti

2
@DopeGhoti, 쉘은 객관적으로 느리게 보입니다. 아주 간단한 루프는 25 배 느린> 것 같다 동안 dash보다 gawk하고 dash있었다 가장 빠른 I 시험 쉘 ...
ilkkachu

1
@ 조, 그것은 :) dash이며 busybox지원하지 않습니다 (( .. ))-비표준 확장이라고 생각합니다. ++또한 명시 적으로 언급 이 필요하지, 지금까지 내가 말할 수로 i=$((i+1))또는 : $(( i += 1))안전한 것들이다.
ilkkachu

1
다시 "더 많은 시간 생각" 이 중요한 요소를 무시한다. 얼마나 자주 실행되며 얼마나 많은 사용자에게 적용됩니까? 프로그램이 1 초를 낭비하고 30 분 동안 프로그램에 대해 고칠 수있는 경우 한 번만 실행하는 사용자가 한 명인 경우 시간 낭비 일 수 있습니다. 반면에 백만 명의 사용자가 있다면 백만 초 또는 11 일의 사용자 시간입니다. 코드가 1 백만 분의 사용자에게 낭비된다면 약 2 년 의 사용자 시간입니다.
agc

13

나는이 답변에서 벤치 마크가 아닌 일반적인 조언 만 제공 할 것입니다. 벤치 마크는 성능에 관한 질문에 확실하게 대답 할 수있는 유일한 방법입니다. 그러나 얼마나 많은 데이터를 조작하고 얼마나 자주이 작업을 수행 하는지 는 말하지 않기 때문에 유용한 벤치 마크를 수행 할 방법이 없습니다. 10 개 품목에 대해 더 효율적인 것과 1000000 개 품목에 대해 더 효율적인 것은 종종 동일하지 않습니다.

일반적으로, 순수 쉘 코드에 루프가 포함되어 있지 않으면 외부 명령을 호출하는 것이 순수 쉘 구성으로 수행하는 것보다 비용이 많이 듭니다. 반면에 큰 문자열이나 많은 양의 문자열을 반복하는 쉘 루프는 특수 목적 도구를 한 번 호출하는 것보다 느릴 수 있습니다. 예를 들어 루프 호출 cut은 실제로 눈에 띄게 느릴 수 있지만 단일 cut호출로 전체 작업을 수행하는 방법을 찾으면 셸에서 문자열 조작으로 동일한 작업을 수행하는 것보다 빠릅니다.

컷오프 지점은 시스템마다 크게 다를 수 있습니다. 커널, 커널 스케줄러 구성 방법, 외부 실행 파일이 포함 된 파일 시스템, 현재 CPU 대 메모리 압력, 기타 여러 요인에 따라 달라질 수 있습니다.

expr성능이 전혀 염려 되지 않으면 산술 수행을 위해 전화하지 마십시오 . 실제로, expr산술을 전혀 수행 하지 마십시오 . 쉘에는 내장 산술 기능이있어 호출보다 명확하고 빠릅니다 expr.

sh에는 존재하지 않는 bash 구문을 사용하고 있기 때문에 bash를 사용하는 것 같습니다. 그렇다면 왜 지구상에서 배열을 사용하지 않습니까? 어레이는 가장 자연스러운 솔루션이며 가장 빠를 수도 있습니다. 배열 인덱스는 0에서 시작합니다.

list=(1 2 3 5 9 8 6 90 84 9 3 2 15 75 55)
for ((count = 0; count += 3; count < ${#list[@]})); do
  echo "${list[$count]}"
done

시스템에 shbash가 아닌 dash 또는 ksh가있는 경우 sh를 사용하면 스크립트 속도가 더 빠를 수 있습니다 . sh를 사용하면 명명 된 배열은 얻지 않지만 배열을 사용하여 위치 매개 변수 중 하나를 얻습니다 set. 런타임까지 알려지지 않은 위치에있는 요소에 액세스하려면 사용해야합니다 eval(물건을 올바르게 인용하십시오!).

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
count=1
while [ $count -le $# ]; do
  eval "value=\${$count}"
  echo "$value"
  count=$((count+1))
done

배열에 한 번만 액세스하고 왼쪽에서 오른쪽으로 이동하는 경우 (일부 값 건너 뛰기) shift가변 인덱스 대신 사용할 수 있습니다 .

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
while [ $# -ge 1 ]; do
  echo "$1"
  shift && shift && shift
done

더 빠른 방법은 쉘과 요소 수에 따라 다릅니다.

또 다른 가능성은 문자열 처리를 사용하는 것입니다. 위치 매개 변수를 사용하지 않는 이점이 있으므로 다른 용도로 사용할 수 있습니다. 많은 양의 데이터는 속도가 느려지지만 적은 양의 데이터에는 눈에 띄는 차이가 없을 것입니다.

# List elements must be separated by a single space (not arbitrary whitespace)
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
while [ -n "$list" ]; do
  echo "${list% *}"
  case "$list" in *\ *\ *\ *) :;; *) break;; esac
  list="${list#* * * }"
done

" 반면에, 큰 문자열이나 많은 양의 문자열을 반복하는 쉘 루프는 특수 목적 도구를 한 번만 호출하는 것보다 느릴 수 있습니다. 그러나 그 도구가 awk처럼 루프를 가지고 있다면 어떨까요? @ikkachu는 awk 루프가 더 빠르지 만 1000 미만의 필드를 반복하면 더 빠른 루프의 이점이 외부 명령이기 때문에 awk를 호출하는 비용보다 중요하지 않다고 말합니다 (쉘에서 동일한 작업을 수행 할 수 있다고 가정) 내장 명령 만 사용하여 루프)?
Levi Uzodike

@LeviUzodike 내 답변의 첫 번째 단락을 다시 읽으십시오.
Gilles 'SO- 악마 그만'

또한 대체 할 수 shift && shift && shiftshift 3세 번째 예에 - 사용중인 쉘을 지원하지 않습니다 않는.

2
@Joe 사실은 없습니다. shift 3남은 인수가 너무 적 으면 실패합니다. 당신은 같은 것을 필요 했어if [ $# -gt 3 ]; then shift 3; else set --; fi
질 'SO-정지 존재 악마'

3

awkAwk 스크립트 내에서 모든 처리를 수행 할 수 있다면 훌륭한 선택 입니다. 그렇지 않으면 Awk 출력을 다른 유틸리티로 파이핑하여의 성능 향상을 파괴합니다 awk.

bash당신이 (현대 쉘에 대한 아마 보장) 배열 내부의 전체 목록을 들어갈 수있는 경우 배열을 통해 반복, 또한 훌륭한입니다 그리고 당신이 배열 구문 체조 상관 없어.

그러나 파이프 라인 접근 방식은 다음과 같습니다.

xargs -n3 <<< "$list" | while read -ra a; do echo $a; done | grep 9

어디에:

  • xargs 공백으로 구분 된 목록을 3 개의 묶음으로 묶습니다.
  • while read 해당 목록을 사용하고 각 그룹의 첫 번째 열을 출력합니다.
  • grep 첫 번째 열을 필터링합니다 (원래 목록의 세 번째 위치마다 해당)

내 의견으로는 이해를 향상시킵니다. 사람들은 이러한 도구의 기능을 이미 알고 있으므로 왼쪽에서 오른쪽으로 쉽게 읽을 수 있으며 어떤 일이 발생할지 추론 할 수 있습니다. 이 방법은 보폭 ( -n3)과 필터 패턴 ( 9) 도 명확하게 문서화 하므로 쉽게 변형 할 수 있습니다.

count=3
find=9
xargs -n "$count" <<< "$list" | while read -ra a; do echo $a; done | grep "$find"

"효율성"에 대한 질문을 할 때는 "총 수명 효율성"에 대해 생각해야합니다. 이 계산에는 코드를 유지하기위한 관리자의 노력이 포함되며, 미트 백은 전체 작업에서 가장 효율적인 기계입니다.


2

아마도 이것?

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15

전에는 명확하지 않았지만 목록의 길이를 모르면서 해당 위치에서 숫자를 얻을 수 있기를 원했습니다. 하지만 고마워, 나는 그 컷 할 수 잊어 버렸습니다.
Levi Uzodike

1

효율적이려면 쉘 명령을 사용하지 마십시오. 파이프, 방향 전환, 대체 등 및 프로그램으로 자신을 제한하십시오. 이유 xargsparallel존재 유틸리티 - 배쉬 때문에 루프가 매우 느린 비효율적이고있다. bash 루프는 마지막 해결 방법으로 만 사용하십시오.

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"
if 
    <<<"$list" tr -d -s '[0-9 ]' | 
    tr -s ' ' | tr ' ' '\n' | 
    grep -q -x '9'
then
    found=true
else 
    found=false
fi
echo ${found} 

그러나 당신은 아마 약간 더 빠를 것 awk입니다.


이전에 명확하지 않았지만 목록의 위치만을 기준으로 값을 추출 할 수있는 솔루션을 찾고있었습니다. 나는 내가 원했던 가치를 분명히하기를 원했기 때문에 원래 목록을 만들었습니다.
Levi Uzodike

1

내 생각에 가장 명확한 해결책은 아마도 가장 성능이 좋은 방법은 RS 및 ORS awk 변수를 사용하는 것입니다.

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"

1
  1. 사용 GNU sedPOSIX 쉘 스크립트를 :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. 또는 bash매개 변수 대체로 :

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
  3. GNU ( 예 : POSIX ) sed, 및 bash:

    sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g' <<< "$list"

    또는 POSIX sed 및 셸 스크립트 를 모두 사용하여 이식성이 뛰어납니다 .

    echo "$list" | sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g'

다음 중 하나의 출력 :

1 5 6 9 15
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.