$ cat hoge.R #!/usr/bin/R n1 <- as.numeric(commandArgs()[4]) n2 <- as.numeric(commandArgs()[5]) for (i in n1:n2) { x <- seq(-4, 4, len = 101) y <- sin(x - i) png(sprintf("%04d.png", i)) plot(x, y) } $ cat hoge.R.sh #!/bin/bash for n1 in `seq 199 10 300` do n2=`expr $i + 1` xvfb-run R --vanilla --args $n1 $n2 < hoge.R done $ nohup nice -n 19 sh hoge.R.sh > nohup.out &
このようにして、可能な限り機能を分解しそれぞれの機能をつなぎ合わせることで大きなシステムを作ることは、エラーが起きても継続的に計算をさせたい場合にもうれしい。もちろん、戻り値チェックしてエラー判定するのが最良の手段だが、お手軽にできてエラーがあっても続けて計算を継続できるという点ではこちらのほうが優れている。また、どのような場合にエラーが起こるかわからない場合についてもこの方法は有効である。メモリが足りないだのだれかが勝手にプロセスを殺しただのと、トラップ可能なエラーとそうでないエラーがあるわけで、「とにかくエラーが起きたらおちるわけだから落ちること前提でコード書きましょう」というアプローチも悪くは無いと思う。また、とにかく早く計算をスタートさせねばならないけれど、最終的には堅牢なシステムにしたいという場合に、スタートアップとしてこのような手法をとることは計算機の死に時間を減らすためにも間違いではないと思う。
例えば、上に挙げたスクリプトにおいて、引数が 353 354 の場合に、何らかの原因で R のスクリプトがうまく走らなかったとする。この場合には R は「おちる」わけだが、R がおちたところでシェルスクリプトは落ちない。だから、其の次の 355 356 の引数について R を走らせることが出来る。もし、R 内部の for ループの範囲を広げて 1:1000 とかにした場合に 353:354 でおちると計算全体が止まり、355:1000 までの計算は再開されるまで走らないことになってしまう。このようにして、「依存関係の無い計算については粒度を細かくすることで計算機の死に時間を減らすことが出来る」と思う。
今回は粒度を 2 にしているので 2 個のパラメータのどちらかが失敗するとシェルスクリプトに戻るようになっているが、この粒度を 1 個にしてしまえばよりよくなるわけだ。ただし、R スクリプトの最初で巨大なファイルをメモリに読み込んだりすると、其の読み込みに長い時間がかかることがある。読み込み以降の計算時間と読み込み時間についてよく考えないと、粒度を細かくしたら実際に結果を返すまでの計算時間がすごく長くなってしまったということにもなりかねない。
粒度 1 の場合の読み込み時間と計算時間の比率が 9:1 で結果が返るまでの総時間が t 秒の場合に 1000 パラメータについて計算することを考えてみる。粒度 1 で 1000 パラメータについて計算すると 1000 * 0.9 * t + 1000 * 0.1 * t = 1000 * t 秒かかるが、粒度 100 で計算すると 100 * 0.9 * t + 1000 * 0.1 * t = 190 * t 秒、となる。安全な粒度 1 を選択するか、粒度 1000 で賭けに出るか、粒度 100 ぐらいでお茶を濁すか、これらは計算している人が適切に選択しなければいけない点だと思う。
粒度 | 読み込み時間 | 計算時間 | 総時間 |
1 | 1000 * 0.9 * t | 1000 * 0.1 * t | 1000 * t |
10 | 100 * 0.9 * t | 1000 * 0.1 * t | 190 * t |
100 | 10 * 0.9 * t | 1000 * 0.1 * t | 109 * t |
1000 | 1 * 0.9 * t | 1000 * 0.1 * t | 100.9 * t |