カテゴリー
【Jqコマンド応用編】xargsをwhile/forループの代わりとして使う場合の勘所
※ 当ページには【広告/PR】を含む場合があります。
2021/05/19
2022/09/30

一般的にシェルスクリプトは外部コマンドを組み合わせて実装されるプログラミングスタイルをとることが多いため、処理パフォーマンスを気にせす思いのままにプログラムを組んでいくと、最終的に仕上がったものは非常に処理が重く使いものにならないことがあります。
そんなときには、
今回はそんなリファクタリングの代名詞的な、
『処理のパイプ化』
Jqは重いコマンド
JqはシェルコマンドでJSONを扱うには非常に強力で便利過ぎるユーティリティなのですが、だからといってやたらめったらスクリプト中に呼び出してしまうと、過ぎたるは及ばざるが如し、完成したプログラムのレスポンスの遅さにびっくりするときがあります...
ということでJqは良く
先に結論を挙げておくと、
1. リファクタリングの観点から、Jqコマンドを含む重めの外部コマンドのループ処理は、
可能な限りパイプライン化したほうがパフォーマンスは向上する。
2. ただし、そもそもjqコマンドをループさせる手法がパフォーマンス的によろしくない。
大幅な高速化を求めるのではあれば、JqではなくAwkやCなどの言語で作成した
コマンドツールなどでスクリプトそのものを見直す。
というお話を以降で具体例で確認していきます。
jqでのループ処理
jqでの配列の要素を編集するのにもっともお手軽なものはwhileかforを使ったスクリプトだと思います。
処理速度の計測用にまずは以下のようなJSON形式のデータセットを用意しておきます。
employee=$(cat << EOF
[
{"社員":"山下モゲ雄","部署":"営業部","事業所":"本社","勤続":"3年"},
{"社員":"島田フガ子","部署":"経理部","事業所":"名古屋支部","勤続":"15年"},
{"社員":"岡田ピポ太","部署":"製造部","事業所":"山口工場","勤続":"8年"},
{"社員":"沢口モフ代","部署":"人事部","事業所":"本社","勤続":"4年"},
{"社員":"銭形ガメ吉","部署":"海外部","事業所":"メキシコ支部","勤続":"11年"},
{"社員":"上岡ムメ美","部署":"営業部","事業所":"本社","勤続":"23年"},
{"社員":"京谷マハ次","部署":"製造部","事業所":"山口工場","勤続":"3年"},
{"社員":"園田フマ由","部署":"人事部","事業所":"本社","勤続":"17年"},
{"社員":"田川ポゥ子","部署":"製造部","事業所":"ベトナム工場","勤続":"12年"},
{"社員":"満田クタ郎","部署":"営業部","事業所":"本社","勤続":"2年"},
{"社員":"島寺ルン大","部署":"営業部","事業所":"本社","勤続":"18年"},
{"社員":"香下ウル蔵","部署":"製造部","事業所":"山口工場","勤続":"5年"},
{"社員":"蒲田ウオ奈","部署":"海外部","事業所":"メキシコ支部","勤続":"9年"},
{"社員":"郷田ポポ生","部署":"営業部","事業所":"名古屋支部","勤続":"4年"},
{"社員":"梅岡ボル伍","部署":"経理部","事業所":"本社","勤続":"25年"},
{"社員":"亀川ヲル士","部署":"製造部","事業所":"山口工場","勤続":"14年"}
]
EOF
)
まずはjqをループ処理に使ってしまうある意味最悪のケースについて考えてみます。
#!/bin/bash
employee=$(cat << EOF
[
{"社員":"山下モゲ雄","部署":"営業部","事業所":"本社","勤続":"3年"},
#中略...上記で説明したデータセット
{"社員":"山下モゲ雄","部署":"営業部","事業所":"埼玉支部","勤続":"3年"},
]
EOF
)
employee_alt='[]'
for person in $(echo "$employee" | jq -c '.[]'); do
person=$(
echo $person | jq -c --arg division "営業部" --arg branch "埼玉支店" '
(.["部署"] |= $division) | (.["事業所"] |= $branch)
')
employee_alt=$(echo "${employee_alt}" | jq -s -c --argjson person "$person" '.[] + [ $person ]')
done
echo "${employee_alt}" | jq -c '.'
このスクリプトのパフォーマンスを計測してみると手元のパソコン上では以下のようになります。
$ hyperfine 'bash 1.sh'
Benchmark #1: bash 1.sh
Time (mean ± σ): 1.084 s ± 0.014 s [User: 1.044 s, System: 0.054 s]
Range (min … max): 1.068 s … 1.117 s 10 runs
たったJSON要素16項目をさばくのに平均で1084msかかっているようです。
正直これでは100項目もあるJSON要素で処理させたら一体何分かかるのか...
xargsで処理パイプ化
先ほどの例ではjqを使ってJSON形式の配列の要素を評価するために、forループで処理を回してきたわけですが、一般的にループ内での外部コマンド呼び出しは処理負荷が大きいことが分かります。
そこでループ処理の代わりに、
参照先の記事の方にxargsの基本的な用法が解説してあるので、パイプ化の理屈の方は詳しくはそちらを確認していただくことにして、要は、
$ [CMD1] | xargs -n1 bash -c '[CMD2 $0]'
というような感じで配列のmapメソッドのように扱えるのがミソになります。
早速例として以下のスクリプトを評価してみます。
#!/bin/bash
employee=$(cat << EOF
[
{"社員":"山下モゲ雄","部署":"営業部","事業所":"本社","勤続":"3年"},
#中略...上記で説明したデータセット
{"社員":"山下モゲ雄","部署":"営業部","事業所":"埼玉支部","勤続":"3年"},
]
EOF
)
employee=$(
echo "$employee" | jq -c '.[]' | sed -e 's/"/\\"/g' | xargs -n1 bash -c '
echo "$0" | jq -c "(.[\"部署\"] |= \"営業部\") | (.[\"事業所\"] |= \"埼玉支店\")"
')
echo "["${employee//$'\n'/,}"]" | jq -c '.'
このスクリプトの性質上、
bash -c
"
sed -e 's/"/\\"/g'
ベンチマークを行うと、
$ hyperfine 'bash 2.sh'
Benchmark #1: bash 2.sh
Time (mean ± σ): 589.0 ms ± 9.8 ms [User: 561.7 ms, System: 35.4 ms]
Range (min … max): 578.2 ms … 614.0 ms 10 runs
半減とは行かないまでも、割と処理がスリムになりました。
このようにxargsを使うケースだと、
bash -c '[CMD2 $0]'
そこで別に処理をさせるために外部で定義した関数を呼び出せるようにするには、
export -f
以下のように少しだけ先程のスクリプトを修正してみます。
#!/bin/bash
employee=$(cat << EOF
[
{"社員":"山下モゲ雄","部署":"営業部","事業所":"本社","勤続":"3年"},
#中略...上記で説明したデータセット
{"社員":"山下モゲ雄","部署":"営業部","事業所":"埼玉支部","勤続":"3年"},
]
EOF
)
function mod() {
local division='営業部'
local branch='埼玉支店'
echo "$1" | jq -c '. | (.["部署"] |= "'$division'") | (.["事業所"] |= "'$branch'")'
}
export -f mod
employee=$(
echo "$employee" | jq -c '.[]' | sed -e 's/"/\\"/g' | xargs -n1 bash -c '
mod "$0"
')
echo "["${employee//$'\n'/,}"]" | jq -c '.'
このスクリプトの処理速度も計測すると、
$ hyperfine 'bash 3.sh'
Benchmark #1: bash 3.sh
Time (mean ± σ): 591.0 ms ± 7.1 ms [User: 563.1 ms, System: 36.3 ms]
Range (min … max): 579.8 ms … 603.0 ms 10 runs
となり、外部定義の関数を利用してもパフォーマンスにはほぼ影響が出ないことが分かります。
Jqネイティブのmap関数
今回の例のようにJSON配列を使って複雑な処理をさせるのでなければ、そもそもmap関数で事足りる場合もあります。
#!/bin/bash
employee=$(cat << EOF
[
{"社員":"山下モゲ雄","部署":"営業部","事業所":"本社","勤続":"3年"},
#中略...上記で説明したデータセット
{"社員":"亀川ヲル士","部署":"製造部","事業所":"山口工場","勤続":"14年"}
]
EOF
)
echo "$employee" | jq -c --arg division "営業部" --arg branch "埼玉支店" 'map((.["部署"] |= $division) | (.["事業所"] |= $branch))'
これを計測してみると、以下のようにメチャ速になります。
hyperfine 'bash 4.sh'
Benchmark #1: bash 4.sh
Time (mean ± σ): 34.3 ms ± 3.3 ms [User: 33.0 ms, System: 1.8 ms]
Range (min … max): 29.5 ms … 47.5 ms 62 runs
簡単なJSON配列の操作であれば、ビルドインのmap関数を使ったほうが良いでしょう。
番外編〜Awkでも同等の操作で比較
そもそもjqが便利なのは他のデータフォーマットからJSON形式への変換であって、内部要素のごとの細かな計算処理を作り込むには不向きな面があります。
jqを用いたプログラムの処理速度が重くなって、期待通りに動かないのあれば、いっそのことJSON形式でのデータ構造を見直し、繰り返し処理などにjqを使わないのも手です。
その点、Awkは素朴なテキスト形式のデータ構造しか扱えませんが、内部で高度な計算処理を高速で行ってくれますので、今回の場合のリファクタリングにはうってつけです。
入力データはAwkで読み込ませる前提の以下のようなCsv形式で与えておきます。
employee=$(cat <<EOF
山下モゲ雄,営業部,本社,3年
島田フガ子,経理部,名古屋支部,15年
岡田ピポ太,製造部,山口工場,8年
沢口モフ代,人事部,本社,4年
銭形ガメ吉,海外部,メキシコ支部,11年
上岡ムメ美,営業部,本社,23年
京谷マハ次,製造部,山口工場,3年
園田フマ由,人事部,本社,17年
田川ポゥ子,製造部,ベトナム工場,12年
満田クタ郎,営業部,本社,2年
島寺ルン大,営業部,本社,18年
香下ウル蔵,製造部,山口工場,5年
蒲田ウオ奈,海外部,メキシコ支部,9年
郷田ポポ生,営業部,名古屋支部,4年
梅岡ボル伍,経理部,本社,25年
亀川ヲル士,製造部,山口工場,14年
EOF
)
なお、
処理時間を計測するスクリプトは以下の通りに修正します。
#!/bin/bash
employee=$(cat <<EOF
山下モゲ雄,営業部,本社,3年
#中略...上記で説明したデータ
亀川ヲル士,製造部,山口工場,14年
EOF
)
echo "$employee" | awk -F"," '
BEGIN{OFS=","}
{print $1,"営業部","埼玉支部",$4}
' | jq -c -s -R '
[split("\n")[] | select(length > 0) | split(",")]
| map({"社員":.[0],"部署":.[1],"事業所":.[2],"勤続":.[3]})
'
これを実行するとループ内やパイプラインでjqを使うよりも以下のように圧倒的に早く処理できることが分かります。
$ hyperfine 'bash 5.sh'
Benchmark #1: bash 5.sh
Time (mean ± σ): 34.3 ms ± 2.9 ms [User: 33.5 ms, System: 2.0 ms]
Range (min … max): 30.2 ms … 44.7 ms 65 runs
まとめ
では今回の考察で得られたエッセンスの繰り返しになりますが、
1. リファクタリングの観点から、Jqコマンドを含む重めの外部コマンドのループ処理は、
可能な限りパイプライン化したほうがパフォーマンスは向上する。
2. ただし、そもそもjqコマンドをループさせる手法がパフォーマンス的によろしくない。
大幅な高速化を求めるのではあれば、JqではなくAwkやCなどの言語で作成した
コマンドツールなどでスクリプトそのものを見直す。
以上です。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー