シミュレータ開発のプラクティス


数年ほど携わってきたシミュレータ開発から離れることになりました。
具体的にはC++/boostによるネットワークシミュレータの開発がメインでしたが、なるべく言語や分野に依らない形で、個人的に培ったシミュレータ開発のメモを残しておこうと思います。

入力

  • 諸々の自動化を見越して、GUIでしか入力できないパラメータなどは避けること。
  • 入力フォーマットはCSV・YAML・JSON・XMLなど、なるべく一般的なものを採用して、ファイルのパースといった処理はできるだけ専用ライブラリに任せること。
  • 入力パラメータは大量になりがちなので、なるべく各パラメータごとにデフォルト値を用意したり、同じ設定値を使いまわせるような記法を用意して、シナリオを作りやすくしたい。
  • 設定値の型変換や条件判定などを一括で行う関数を用意しておくと楽。たとえば下記のように。
const size_t param = convert<size_t>( // 設定値の型
  input, // 設定された文字列
  42, // デフォルト値
  [](size_t a){ return a >= 10; }, // 条件判定
  "This parameter must be 10 and more."); // エラーメッセージ

ログ

  • シミュレーションの詳細を追うためには、膨大なログを出力する必要がある。The Twelve-Factor App(日本語訳)にあるとおりログをイベントストリームとして扱うようにして、適宜フィルタかけながら注目する情報を追えるようにするのが望ましい。
  • ログレベルは、とくにこだわりが無ければBOOST_LOG_TRIVIALあたりの fatal / error / warning / info / debug / trace に倣う。
  • ロガーはシミュレーションの情報だけでなく、コードのファイル名・行番号・関数名やプロセスID・スレッドIDなども出力するようにしておくと、デバッグなど便利。

乱数

  • 乱数生成器は、とくに理由が無ければメルセンヌ・ツイスタを使う。規則性のでやすい線形合同法などは避けたい。
  • ノードごとに乱数生成器を持つなど、ノードを追加しても既存ノードには影響ないような乱数生成が望ましい。

物理計算

  • 様々な物理計算を扱っていると単位間違いのバグが発生しやすい。Boost.Unitsのように単位を表現できる型を使うのが望ましい。
  • たとえば重力や電波伝搬などノード間で発生する物理計算は、$O(n^2)$の計算オーダーになってしまうため、距離などの閾値による計算省略を行う。

状態遷移

  • 状態を持つ変数やクラスはなるべく減らしたい。ただしシミュレーションモデルそのものに含まれる状態遷移は避けようがない。
  • シミュレーションモデルの状態遷移自体がバグっていることもあるので、不安なところにはassertを仕込んでおく。

計算順序

  • 同時刻内の計算順序に起因するシミュレーションの不整合問題は、のちのち厄介なことになりやすい。まずは「毎時刻行う物理計算」「離散的に行うイベント計算」のようにカテゴリ分けしておいて、それらの順序が混ざらないようにすると良い。
  • ノード間やレイヤー間の情報伝達は、なるべく非同期メッセージパッシングで、次時刻以降に届くようにする。でないと、あとで並列化をしようとした時に、面倒なことになりやすい。

結合テスト

  • たとえば「アルゴリズムをAからBに変えると、指標αが良くなるはず」ことを実証するためにシミュレータ開発をしたとして、一回シミュレーションしただけでは、本当にそうだったのか偶然うまくいったのか判別がつかない。そのため乱数シードや各設定値を変えながら、何百回何千回とシミュレーションして、それらの平均値や中央値などを確認対象とすることが多い。これを人力で行うのはつらいので、シミュレータ自体にそういう機能を仕込んでおくか、別途シナリオ生成したり統計処理したりするスクリプトを用意する。
  • バタフライ効果よろしく、ちょっとした誤差がシミュレーションを大きく変えることはよくある。そのため、なるべくカバレッジの広いテストシナリオを作って、その重要なログと一緒にレポジトリに登録しておき、予期せぬシミュレーション結果の変化を引き起こしてしまった場合は、どの時刻にどの機能で変化が始まったかCIツールで検知できるようにするのが望ましい。
  • いかんせんシミュレーションはやってみて初めて分かることも多く、思い通りにいかなかった時の調査も時間が掛かりやすいので、工数はできるだけ大目に見積もっておく。

パフォーマンスチューニング

  • シナリオのスケールが求められる場合は、実行時間やメモリ使用量のプロファイルが取れるようにしておく。ボトルネックになるような機能を追加した場合には、CIツールで検知できるようにするのが望ましい。
  • パフォーマンスチューニングを詰めていくと、時間と空間のトレードオフに突き当たる。どちらを優先するか判断するためには、ターゲットとなる実行環境とシナリオ規模を明確にする必要がある。