Chienomi

Zshでの連続的呼び出しに環境変数を使ってみる (Zshのガベージコレクション)

プログラミング

  • TOP
  • Old Archives
  • Zshでの連続的呼び出しに環境変数を使ってみる (Zshのガベージコレクション)

Zshは極めて強力なシェルだが、大規模プログラミングにおいて使われることは少ない。 だが、Orbital designにおいてはシェルスクリプトは強力な武器だ。できれば積極的に活用していきたい。 そうなると、Zshにおいて明らかでない、あるいは公知ではないことと付き合っていかなければならない。

単純に言えば今回は

integer count=1
while (( count++ < 1000000 ))
do
  invoke_worker "$unit_dir" $count
done

のような処理を行うスクリプトを、invoke_workerがファイルを読み込むのではなく、事前に読み込んでおくことでワーカーの処理時間を短縮したいということだ。

integer count=1
export config="$(<"$unit_dir"/config)"
while (( count++ < 1000000 ))
do
  export data_db="$(<$unit_dir/$count.db)"
  wait
  invoke_worker &
done
wait

のようにしたいということだ。

だが、問題は「これがメモリを食いつぶさないか」ということである。

意図と効果

普通のスクリプトの場合、この方法はほとんど効果をもたらさない。もしかしたらマイナスになるかもしれない。

そもそもこれらの情報が「単純にファイルとして読み込むだけ」の設計になっているところも工夫のなせる技なのだが、必ずしもそのようにできるわけではない。 特にクラスタ仕様になっているとファイルアクセスが遅いかもしれないし、その違いを吸収するためにさらに別の方法をとるかもしれないのだ。

integer count=1
export config="$(readconfig "$unit")"
while (( count++ < 1000000 ))
do
  export data_db="$(readdata "$unit" "$count")"
  wait
  invoke_worker &
done
wait

ワーカーは起動数も多く、当然ながら処理としては重い。 逐次実行されるワーカー部分であまりその処理を担わせたくない。 特に設定のほうは起動される全てのワーカーにとって共通である。 これを環境変数にするメリットは大きい。

Linuxのfork(2)はコピーオンライトだ。 configは文字列であり、起動されるワーカーではその文字列をパースすることになるから、この$config環境変数が変更されることはない。 つまり、この環境変数は実体はひとつであり、常に同一の実体が参照される。コピーされることもないので速度が上がる。ディスクIOとメモリへのコピーが2回目以降省略できるのだ。

ただし、環境変数で渡す文字列は「システムコールがCライブラリである」ためにC文字列(の配列)であり、\000が含まれていると文字列がそこで終わってしまう。 私は勘違いと慎重さを欠いたことから赤っ恥をやらかしてしまった。

データのほうは、ほぼ単純に、ワーカー実行中に時間がかかる(かもしれない)データの取得を裏でやっておきたい、ということである。 これも環境変数にしてしまえばワーカーはリード時間をとられない。所要時間の総量は変わらないが、裏でやることで逐次実行部分に含めることを避けるということである。

問題と検証

だが、これは

export data_db="$(readdata "$unit" "$count")"

というのが100万回実行されるということだ。 しかも、これはループのたびに異なる値になる。

もしZshにガベージコレクタがなければ100万件のデータを抱えてしまうことになり、これはメモリ消費量的にまずい。

というわけで、「Zshは変数を上書きしたらメモリを開放してくれる(ガベージコレクションしてくれる)のか?」という疑問を解き明かす必要ができた。

この調査は割と簡単で

(while sleep 2; do export data_db=$(dd if=/dev/urandom bs=100M count=1); cat >| /dev/null <<< "$data_db" &; done)

とし、

while sleep 5; do awk '/^Rss/{sum += $2}END{print sum}' < /proc/nnnnn/smaps; done

のようにしてメモリ消費量を監視すれば良い。

ちなみに、100Mを指定しているが、/dev/urandomは一度に読めるのは32MiBまでである。

80340
215640
80332
80332
80348
43344
80340
80340
80332
80332
215636
80340
80332

だいたい2倍程度と見ていいと思うのだが、ときどき3.5倍程度まで増加する。 ただ、だいたい2倍ということはかなり迅速にメモリを回収してくれている印象だ。

しかし、実際のワーカーはこんなに長時間実行はしないので、実際のワーカーの実行時間に近い感じにしてみよう。

108932
80340
195300
80340
215628
80340
211472
111616
80332
178644
43344
215644

回収しきれないタイミングが増えてメモリ使用量にばらつきは生じているが、回収ペースを上回ってメモリ消費量が果てしなく増えていくようなことはないようだ。

結論

  • Zshにはガベージコレクタがある
  • Zshでパラメータを上書きすると、ローカル変数か環境変数かによらず迅速にメモリは解放される
  • IOコストが高いなどで逐次的に実行するコストを削減したい場合、コントローラで環境変数にする作戦はなかなか有効

追加の注意点

フルスケールするように設計されているワーカーの場合、CPUコア数を使い切るだけワーカーを起動していると、結果的に裏で色々するためのコンテキストスイッチコストが高くついて全体では遅くなる可能性もある。 というか、ワーカーの実行がそれほど時間のかからないものであるならばwaitの時間は短くなり、waitの時間が短くなるほどスケール阻害要素としては大きくなる。