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


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
効率が重要な懸念事項である場合、そもそもシェルスクリプトを使用しません。あなたのリストはどれだけ大きいのでしょうか?
バーマー


2
問題の実際のインスタンスについて統計を行わないと、何もわかりません。これには、「awkでのプログラミング」などとの比較が含まれます。統計が高すぎる場合は、効率を探すことはおそらく価値がありません。
デビッドトンホーファー

2
レヴィ、あなたの定義における「効率的な」方法とは何ですか?より高速な反復方法を見つけたいですか?
セルギーコロディアズニー

回答:


18

と非常に簡単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

さまざまなメソッド間でその出力を比較して、時間の面でどれが最も効率的かを確認できます。他のツールは、他の効率指標に使用できます。


1
良い点、@ MichaelHomer; 「どの方法が最も効率的であるかをどのように判断できますか」という質問に対処するための脇を追加しました。
DopeGhoti

2
@LeviUzodike echovs に関しては<<<、「同一」という言葉は強すぎます。あなたはそれstuff <<< "$list"がほぼ同じと言うことができますprintf "%s\n" "$list" | stuffechovs についてはprintfこの答えを
ご覧ください

5
@DopeGhoti実際にそうです。<<<最後に改行を追加します。これは$()、末尾から改行を削除する方法に似ています。これは、行が改行で終了しているためです。<<<式を行としてフィードするため、改行で終了する必要があります。"$()"行を取り、それらを引数として提供するため、終端の改行を削除して変換することは理にかなっています。
JoL

3
@LeviUzodike awkはあまり評価されていないツールです。一見複雑に見えるあらゆる種類の問題を簡単に解決できます。特に、sedのような複雑な正規表現を記述しようとする場合、代わりにawkで手続き的に記述することにより、多くの場合時間を節約できます。それを学ぶことは大きな利益をもたらすでしょう。
ジョー

1
@LeviUzodike:はいawk、起動する必要があるスタンドアロンのバイナリです。perlや特にPythonとは異なり、awkインタープリターはすぐに起動します(非常に多くのシステムコールを行う通常の動的リンカーオーバーヘッドはすべてそのままですが、awkはlibc / libmとlibdlのみを使用します。たとえばstrace、awkスタートアップのシステムコールをチェックアウトするために使用します) 。多くのシェル(bashなど)は非常に遅いため、1つのawkプロセスの起動は、リストのサイズが小さい場合でも、シェルに組み込まれたリスト内のトークンをループするよりも高速です。また、#!/usr/bin/awkスクリプトの代わりにスクリプトを作成できる場合もあります#!/bin/sh
ピーターコーデス

35
  • ソフトウェア最適化の最初のルール:しないでください

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

  • 二番目のルール:測定

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

    大きなプログラムでは、プロファイリングもここで行われます。最も遅い部分は、あなたがそう思っている部分ではないかもしれません。

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

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

    代わりに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...最速のは、私がテストしたシェルました
ilkkachu

1
@Joe、それは:) dashbusyboxあり、サポートしていません(( .. ))-私はそれが非標準の拡張機能だと思います。私が知る限り、または安全なものである限り、必須ではないこと++明示的に述べられています。i=$((i+1)): $(( i += 1))
イルカチュウ

1
「より多くの時間思考」:これは重要な要因を無視しています。実行頻度とユーザー数 プログラムが1秒を浪費する場合、プログラマが30分間それについて考えることで修正できますが、1回だけ実行するユーザーが1人しかいない場合は時間の無駄になる可能性があります。一方、100万人のユーザーがいる場合、それは100万秒、つまり11日間のユーザー時間です。コードが100万ユーザーの1分を無駄にした場合、それは約2年間のユーザー時間です。
agc

13

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

一般的な経験則として、純粋なシェルコードにループが含まれない限り、外部コマンドの呼び出しは、純粋なシェル構成で何かを行うよりもコストがかかります。一方、大きな文字列または大量の文字列を反復処理するシェルループは、専用ツールの1回の呼び出しよりも遅い可能性があります。たとえば、ループの呼び出し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を使用する場合、名前付き配列は取得しませんが、配列で位置パラメーターの1つを取得します。これはで設定できます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

一方で、大きな文字列または大量の文字列を反復処理するシェルループは、特殊な目的のツールを1回呼び出すよりも遅い可能性があります」が、そのツールにawkのようなループがある場合はどうでしょうか。@ikkachuはawkループの方が速いと言いましたが、反復するフィールドが1000個未満の場合、外部コマンドなのでawkを呼び出すコストはループの高速化の利点を上回りません(シェルで同じタスクを実行できると仮定すると)組み込みコマンドのみを使用したループ)?
レヴィウゾダイク

@LeviUzodike私の答えの最初の段落をもう一度読んでください。
ジル「SO-悪であるのをやめる」

また、置き換えることができshift && shift && shiftshift 3、あなたの第三の例では-あなたが使用しているシェルがそれをサポートしていない場合を除きます。
ジョー

2
@ジョー実はいや。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 最初の列をフィルタリングします(元のリストの3番目ごとの位置に対応)

私の意見では、わかりやすさを向上させます。人々はすでにこれらのツールが何をするのかを知っているので、左から右へ読み、何が起こるかについて推論するのは簡単です。このアプローチでは、ストライドの長さ(-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

申し訳ありませんが、以前は明確ではありませんでしたが、リストの長さを知らなくても、それらの位置で数字を取得できるようにしたかったのです。しかし、おかげで、私はそれを行うことができることを忘れていました。
レヴィ・ウゾダイケ

1

効率を上げたい場合は、シェルコマンドを使用しないでください。パイプ、リダイレクト、置換など、およびプログラムに自分自身を制限します。そのため、bash whileループは非効率的で非常に遅いため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


以前は明確ではありませんでしたが、リスト内の位置のみに基づいて値を抽出できるソリューションを探していました。必要な値を明確にするために、元のリストを作成しました。
レヴィウゾダイケ

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つまり POSIXsedおよび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.