web-dev-qa-db-ja.com

配管、シフト、またはパラメータ拡張はより効率的ですか?

スペースで区切られた単語のリストで、一定の数の値が互いに離れている特定の値を反復処理する最も効率的な方法を見つけようとしています(配列を使用したくありません)。例えば、

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

したがって、リストを繰り返し処理して、1、5、6、9、15にのみアクセスできるようにしたいと考えています。

EDIT:リストから取得しようとしている値は、リストの他の部分とは形式が異なります。それらを特別なものにしているのは、リスト内でのそれらの位置のみです(この場合、位置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

方法パイプ処理でこれが最悪のオプションになると確信していますが、好奇心から、セットを使用しない方法を見つけようとしました。

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

それで、最も効率的なものは何ですか、またはより簡単な方法がありませんか?

26
Levi Uzodike

awkを使用すると、非常に簡単です。これにより、任意の長さの入力に対して4つおきのフィールドの値が得られます。

$ 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

さまざまなメソッド間でその出力を比較して、時間に関して最も効率的な方法を確認できます。他のツールを他の効率性メトリックに使用できます。

18
DopeGhoti
  • ソフトウェア最適化の最初のルール:禁止

    プログラムの速度が問題であることがわかるまで、それがどれほど速いかを考える必要はありません。リストがその長さか、約100〜1000アイテムの長さである場合、どれだけ時間がかかるかさえ気付かないでしょう。最適化について考えるのに、違いよりも多くの時間を費やす可能性があります。

  • 2番目のルール:Measure

    それが確実に調べる方法であり、システムに答えを与える方法です。特にシェルでは非常に多くあり、それらはすべて同じではありません。 1つのシェルに対する答えは、あなたには当てはまらないかもしれません。

    大規模なプログラムでは、プロファイリングもここに適用されます。最も遅い部分は、あなたが思っているものではないかもしれません。

  • 第三に、シェルスクリプト最適化の最初のルール:シェルを使用しないでください

    ええ、本当に。多くのシェルは高速に作られていません(外部プログラムの起動はそうである必要がないため)。また、毎回ソースコードの行を再度解析することさえあります。

    代わりにawkやPerlなどを使用してください。私が行った些細なマイクロベンチマークでは、awkは、単純なループ(I/Oなし)の実行において、一般的なシェルより数十倍高速でした。

    ただし、シェルを使用する場合は、外部コマンドの代わりにシェルの組み込み関数を使用してください。ここでは、exprを使用しています。これは、システムで見つけたシェルには組み込まれていませんが、標準の算術展開で置き換えることができます。例えば。 iをインクリメントするには、i=$((i+1))の代わりにi=$(expr $i + 1)を使用します。最後の例でのcutの使用は、標準のパラメーター展開で置き換えることもできます。

    参照: シェルループを使用してテキストを処理するのが悪い習慣と見なされるのはなぜですか?

手順#1および#2が質問に適用されます。

35
ilkkachu

この回答では、ベンチマークではなく、一般的なアドバイスのみを行います。ベンチマークは、パフォーマンスに関する質問に確実に回答する唯一の方法です。しかし、操作しているどのくらいデータであり、どのくらいの頻度この操作を実行しているのかを述べていないため、有用なベンチマークを行う方法はありません。多くの場合、10個のアイテムの効率と1000000個のアイテムの効率は同じではありません。

一般的な経験則として、純粋なシェルコードにループが含まれていない限り、外部コマンドの呼び出しは、純粋なシェル構成で何かを行うよりもコストがかかります。一方、大きな文字列または大量の文字列を反復するシェルループは、専用ツールを1回呼び出すよりも遅くなる可能性があります。たとえば、実際にcutを呼び出すループは著しく遅くなる可能性がありますが、cutを1回呼び出すだけですべてを実行する方法を見つけると、同じことを行うよりも高速になる可能性があります。シェルでの文字列操作に関すること。

カットオフポイントはシステム間で大きく異なる可能性があることに注意してください。これは、カーネル、カーネルのスケジューラの構成方法、外部実行可能ファイルを含むファイルシステム、現時点での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

システムがbashではなくshとしてダッシュまたはkshを持っている場合、shを使用すると、スクリプトがより高速になる可能性があります。 shを使用する場合、名前付き配列は取得されませんが、配列はsetで設定できる位置パラメーターの1つを取得します。実行時までわからない位置の要素にアクセスするには、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は素晴らしい選択ですif Awkスクリプト内ですべての処理を実行できます。それ以外の場合は、Awk出力を他のユーティリティにパイプして、awkのパフォーマンス向上を破壊するだけです。

bash配列の繰り返し処理も素晴らしいです。リスト全体を配列内に収めることができる場合(最新のシェルではこれがおそらく保証されます)and配列の構文を気にしません体操。

ただし、パイプラインアプローチ:

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

どこ:

  • xargsは、空白で区切られたリストを3つのバッチにグループ化し、各改行を区切ります
  • while readそのリストを使用して、各グループの最初の列を出力します
  • grep最初の列をフィルタリングします(元のリストの3番目ごとの位置に対応)

私の意見では、理解しやすさが向上します。人々はこれらのツールの機能をすでに知っているので、左から右に読みやすく、何が起こるかについての理由がわかります。このアプローチでは、歩幅(-n3)とフィルターパターン(9)なので、変動させるのは簡単です:

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

「効率」について質問するときは、必ず「トータルライフタイム効率」を考えてください。この計算には、コードを機能させ続けるためのメンテナーの努力が含まれています。

3
bishop

たぶんこれ?

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

効率を高めたい場合は、シェルコマンドを使用しないでください。パイプ、リダイレクト、置換など、およびプログラムに制限します。これがxargsおよびparallelユーティリティが存在する理由です。bashwhileループは非効率的で非常に遅いためです。 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を使用すると、おそらく多少速くなるはずです。

1
KamilCuk
  1. [〜#〜] gnu [〜#〜]sedおよび[〜#〜] posix [〜#〜]シェルスクリプトを使用:

    echo $(printf '%s\n' $list | sed -n '1~3p')
    
  2. またはbashの-​​パラメータ置換を使用:

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
    
  3. 非-[〜#〜] gnu [〜#〜]ie[〜#〜] posix [〜#〜]sed、およびbash

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

    または、より移植性の高い[〜#〜] posix [〜#〜]sedとシェルスクリプトの両方を使用:

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

これらのいずれかの出力:

1 5 6 9 15
1
agc

私の意見では、最も明確な解決策(そしておそらく最もパフォーマンスも高い)は、RSおよびORS awk変数を使用することです。

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