シェルスクリプトでの配列
Bashなどでは()
を使って
x=(a b c)
みたいにすると
$ echo ${x[0]}
a
$ echo ${x[1]}
b
$ echo ${x[2]}
c
と言った配列を作ることが出来ます。 ただし、Zshではデフォルトでは配列番号は1から始まります。
Bashの様に0からにするには
setopt ksharrays
と、ksharrays
オプションをセットしておく必要があります。
要素を加えるには
$ x=(${a[@]} d)
みたいに既存の要素+新しい物、みたいにして()
に入れる方法があります。
()
に入れた時の区切りは$IFS
の値で、この値で区切られたところが一つの要素になります。
(デフォルトでは空白、タブ、改行。)
また、各要素に直接入れることも出来て、
$ x[0]=1
の様にします。この時、別に0から埋まってなくても大丈夫で、
$ echo ${x[@]}
$ x[0]=0
$ x[5]=5
$ echo ${x[0]}
0
$ echo ${x[1]}
$ echo ${x[5]}
5
$ echo ${x[@]}
$ echo ${x[@]}
0 5
$ echo ${#x[@]}
2
みたいな感じで途中にも直接入れられます。
配列の初期化
何も入ってない配列を作りたければ
$ x=()
と空の()
を使えばOK。
これを10個の0を持つ配列にしたい時、
()
で追加していく方法だと
$ x=(); for i in $(seq 0 9);do x=(${x[@]} 0);done
みたいにします。
(勿論10個くらいなら直接(0 0 0 ...)
を代入しても良いしその方が下に書くように
速いですが数が変化するときやもっと大量の時を考えて。)
要素を直接入れてく方法だと
$ x=(); for i in $(seq 0 9);do x[$i]=0;done
とも出来ます。
前者の方がZshで1から番号が始まる場合や 途中から入れる場合に何も考えなくて楽なんです。
あと、上にも書いたように、シェルスクリプトの配列だと途中の番号が抜けてる場合もあるので、 最大数の後に追加しようとして、
$ x=()
$ x[0]=0
$ x[2]=2
$ x[${#x[@]}]=x
0 x
みたいに、要素数を${#x[@]}
で取ってきて入れようとすると
すでに入っているところに被ってしまう可能性があります。
ただし、大量の要素に対して行うとき、2つの方法はかなり速度が違います。
Mac OS X 10.11、Bash 4.3.42(1)-releaseでやってみると
$ time (x=(); for i in $(seq 0 999);do x=(${x[@]} 0);done ;echo ${#x[@]})
1000
real 0m1.427s
user 0m1.360s
sys 0m0.065s
$ time (x=(); for i in $(seq 0 999);do x[$i]=0;done ;echo ${#x[@]})
1000
real 0m0.012s
user 0m0.010s
sys 0m0.003s
こんな感じで1000個位の要素だと100倍違いますが、
100個位だと10倍位の差で、
逆に10000個にするとx[$i]=0
の方は0.1秒程で終わりますが、
x=(...)
の方は10秒経っても終わりません。
Zsh (5.2)でも
$ time (x=(); for i in $(seq 0 999);do x=(${x[@]} 0);done ;echo ${#x[@]})
1000
( x=() ; for i in $(seq 0 999); do; x=(${x[@]} 0) ; done; echo ${#x[@]}; ) 0.15s user 0.02s system 99% cpu 0.171 total
$ time (x=(); for i in $(seq 0 999);do x[$i]=0;done ;echo ${#x[@]})
zsh: x: assignment to invalid subscript range
( x=() ; for i in $(seq 0 999); do; x[$i]=0 ; done; echo ${#x[@]}; ) 0.00s user 0.00s system 96% cpu 0.007 total
な感じで100倍以上違っています。
ということで、シェルスクリプトで配列に値を代入するときは
()
を使って拡張していく方法は使わず
x[$i]
の様な直接代入を使って行くほうが圧倒的に高速化出来ます。
使い勝手は色々()
の方が良いんですが、
なるべくそれに頼らなくて良いように
(配列の穴を開けるようなことをしないとか、Bash/Zsh共通のスクリプトではsetopt ksharrays
をしておくとか)
して行くべきです。
ちなみに、すでにある配列を他の配列にコピーする場合は
$ y=(${x[@]})
の様に渡すのが圧倒的に速いです。 (ループを回して一つ一つ渡すのに比べて。)
感じとしては()
で拡張していく方法だと
毎回全ての要素を上書きしていくことをするので大変なのかな、と。
まあ考えれば当たり前ですが。
ループの方法による速度
最初この問題が起きた時に、原因はループの回し方だと思いました。
というのも、この前の変更で、
for((i=0; i<10000;i++));do
な方法から
for i in $(seq 0 999);do
みたいな方法へ変更したからです。
前者はBashでは動くんですがZshでは動きません。 (何故か動くと勘違いしてたっぽい。)
ただ、もしこれがネックだとBash専用でも良いから戻すかな、とか思ってたんですが そんなことはありませんでした。
{0..99999}
やwhile
を使った方法を含めてチェックしてみると(Bash 4.3.32)
$ time (x=();echo pre: ${#x[@]}, ${x[0]}, ${x[99999]};for i in $(seq 0 99999));do x[$i]=0;done;echo post: ${#x[@]}, ${x[0]}, ${x[99999]})
pre: 0, ,
post: 100000, 0, 0
real 0m0.843s
user 0m0.792s
sys 0m0.055s
$ time (x=();echo pre: ${#x[@]}, ${x[0]}, ${x[99999]};for((i=0; i<100000;i++));do x[$i]=0;done;echo post: ${#x[@]}, ${x[0]}, ${x[99999]})
pre: 0, ,
post: 100000, 0, 0
real 0m1.327s
user 0m1.243s
sys 0m0.080s
$ time (x=();echo pre: ${#x[@]}, ${x[0]}, ${x[99999]};i=0;for i in {0..99999};do x[$i]=0;done;echo post: ${#x[@]}, ${x[0]}, ${x[99999]})
pre: 0, ,
post: 100000, 0, 0
eal 0m0.882s user 0m0.821s sys 0m0.058s
$ time (x=();echo pre: ${#x[@]}, ${x[0]}, ${x[99999]};i=0;while [ $i -lt 100000 ];do x[$i]=0;((i++));done;echo post: ${#x[@]}, ${x[0]}, ${x[99999]})
pre: 0, ,
post: 100000, 0, 0
real 0m2.212s
user 0m2.087s
sys 0m0.116s
time (x=();echo pre: ${#x[@]}, ${x[0]}, ${x[99999]};i=0;while(($i < 100000));do x[$i]=0;((i++));done;echo post: ${#x[@]}, ${x[0]}, ${x[99999]})
pre: 0, ,
post: 100000, 0, 0
real 0m1.524s
user 0m1.425s
sys 0m0.091s
な感じでむしろseq
を使う方法が一番速いです。
{0..99999}
の方法はBash機能で完結出来て
同じくらい速いですが、
この方法では
中に変数を使えないので固定数のみ、という欠点があって、
seq
の方を使うほうが良いです。
2重括弧を使ったfor
とwhile
は同じくらい。
while
の方が別にi
をインクリメントしなくては行けない分遅い?
ちなみに、while
の2重括弧の方で、i
をインクリメントするのに
((i++))
の代わりにi=$(expr $i + 1)
とexpr
を使うと果てしなく遅くなります。
[]
で評価する方法は大分遅い結果となっています。
というわけで、このseq
にした変更はむしろスピードアップになっていたようです。