※この読み物はあんまり出来が良くないので最後の「●おわりに」だけ読んでください。 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * - * - /* * サイクルを意識しよう な お話 * by uratan! 2013.1.21 */ ●処理の基本の流れ 自動処理の基本としてよく使われる流れに以下のものがあります。 ---------- ( はじめ ) ---------- | +----------------+ | 初期状態の設定 | +----------------+ | 繰り返し(ループ) |<----------+ | | +--------+ | | 処理 | | +--------+ | | | -------- | < まだ? >------+ -------- | ---------- ( おわり ) ---------- 少ない記述で対象を大規模に操作するためにループはよく使われます。 このため、|処理| では「i番目を処理」とかの抽象的(?)な形に書きます。 繰り返しのたびに i を変えてループさせれば、同一の記述で必要な量の 処理を任意に行えるわけです、こりゃ便利だ。 ここで i は、ループに入る前に初期値を与える必要があるため、 ループ外の |初期状態の設定| で初期化しておく必要があります。 |初期状態の設定| をするということは、それ以前にどんな状態にあっても ある確定した所望な状態にするため、に行います。 ループを処理するにあたり必要な条件を整える、という意味では、 メモリの確保とか特定のハードウェアを初期化するとかもあるかも しれません。これを忘れるとか、ループ内に書くべきものとループ外に 書くべきものを間違える、とかがバグの王道パターンであります。 以上、ちょちょいとコーディングして、走らせて見て、所望の結果が 得られれば「完成!」という場合が多いですが、もう少し掘り下げて みましょう。 ●基本の流れのスケーラビリティと(おわり) この (はじめ)→(おわり) の流れは、さまざまなスケールで様々な処理に 当てはめることができます。 例えば、(はじめ) として「電源が入った」を当てはめれば、システムの 起動からの処理に当てはめることが出来ますし、memset() 関数などの 小さな関数の内部処理の流れにもバッチリはまります。 (はじめ) が「電源が入った」、であれば、(おわり) は「電源が切れた」 でしょうから (おわり) 以降を吟味する必要はないのですが、例えば memset() 関数ではどうでしょう。 memset() 関数としての処理は (おわり) ますが、システムは動作を続けて いる、そして memset() 関数が再び呼び出されるかもしれない。 (おわり) って、終わりじゃない! かもしれない…。 ●基本の流れをサイクルで見る 以上を織り込んでみると、意識するべき基本の流れは以下のようになります。 ---------- 再呼び出し(サイクル) ( はじめ ) <------------------+ ---------- | | | +----------------+ | | 初期状態の設定 | | +----------------+ | | 繰り返し(ループ) | |<----------+ | | | | +--------+ | . | 処理 | | . +--------+ | . | | . -------- | . < まだ? >------+ . -------- | | | +----------------+ | | 終了状態の設定 | | +----------------+ | | | ---------- | ( おわり ) -------------------+ ---------- 再呼び出しによって処理が再び (はじめ) られる(かもしれない)、 こういう繰り返しを「サイクル」と呼ぶことにします。 サイクルによって再び処理が (はじめ) られるかもしれない、わけ ですが、それは上位の階層の処理次第なので、それがあるかどうか、 (おわり) から 再呼び出し までは他所でどんな処理が行われるか、は この流れ図では規定できません。 そういうわけでサイクルにおいては、|終了状態の設定| を意識する 必要がでてきます。 ループに対して初期状態の設定を行うと同様に、終了状態も重要では ないか、要は「やりっ放しじゃ いかんのじゃないの?」ってことです。 ●終了状態 |終了状態の設定| とは、何をなんのためにどう設定するべきなの でしょうか? そもそもこれってホントに必要なの? |終了状態の設定| の基本は「呼び出し前の状態に戻す」です。 立つ鳥後を濁さず、開けた戸は閉める、ですね。 イメージが memset() 関数だとピンとこないかもしれませんが、 コンパイラレベルでは、確保したローカル変数の解放とか、呼び出される 前の状態に戻す、という処理がちゃんと挿入されています。 複数の関数セットでサイクルを構成するものもあります。例えばファイル 操作の open()/read()/close() ですね。 (ここいらへんから、スケールの当て方がくるくる変わり出します (例えば open()=初期状態の設定+open処理+終了状態の設定、と見たり、 (open()=初期状態の設定+read()=処理+close()=終了状態の設定、と見たり… これらはファイル操作に必要な一連の管理メモリを持ち、任意の関数が 任意の順番に呼ばれてサイクルを形成しても良いように後処理されて いるはずです。 (それは明示的に return の直前ではないだろうし、状態変数の管理と (いう見方をされて終了状態とはみなされていないかもしれませんが。 サイクルの概念を得たあなたは当然 open()→open() や close()→read() の サイクルも意識せざるをえませんね、なんかバグ防止に役立ちそうでしょ? ●設定タイミング サイクルを切り出してみると、|終了状態の設定|→|初期状態の設定| と 処理はつながりますので、どちらかを省略することは不可能ではないです。 もちろんそれには、再呼び出しまでに状態がかわらない、という条件が必要 ですが。 最もやってはいけないのは、複数の関数でサイクルを構成する場合に ある関数は「頭設定」、ある関数は「お尻設定」と混在することです。 “頭設定な関数→お尻設定な関数”とサイクルした場合に、設定されない 状態が生まれます。 まあ普通は、頭設定が一等最初の初期化も兼ねますし、設定の記述場所から 利用場所まで流れに沿ってかつ距離が近いので自然で無難ではあります。 (どこで誰がなにやってるかわからないコードで、さっきの (お尻設定値がそのまま残っていることは期待するのはアホでしょ? 例えば malloc()/free() の組み合わせに置いては、メモリをゼロクリア するのは malloc() の役割に(たぶん)なってますね。これ free()されたら メモリクリアしておく、でもいいのですが、前設定が採用されているわけです。 | | [2013.5.25] | これ思いっきり勘違いしてますね。malloc() はゼロクリアなんて | しなかったですね?、あれ? あれあれ??、もうダメポ… メモリであれば、CPU/プログラムが何かしなければ前の状態がそのまま 残るので、前設定で問題がない場合が多いですが、管理対象が(レジスタ 経由での)ハードウェアだったりすると、「使い終わったらオフにする」が 素直なものも多いのでお尻設定も無意識のうちによく使われます。 ●サイクルの応用 サイクルの概念は、プログラム構築面でも上記のように結構いいツボで あると思うのですが、そのほかにも応用できます。 例えば処理時間に関して。 最初の図での (はじめ)〜(終わり) を測定して処理時間とすることが多い ですが、|終了状態の設定| の存在が見えてしまった以上これを無視する わけにはいきません。 そのため、時間測定対象の処理を最短サイクルで繰り返させて、(はじめ) 〜(はじめ)の時間を測定すべき、な場合も多々あります。 例えばネットワークパケットの処理速度など、(はじめ)〜(終わり) の 単発の「1packet 当たりの処理時間」がいくら速くても、サイクル途中に アホな処理が入って繰り返しにおける「1秒当たりの処理 packet数」が ヘロヘロ、なんてことありそうでしょ。 ●そんなの関係ねえ!? 「つーか、やりたい処理の前に必要な設定がされてなければ動かなくて バグとわかるんだから どこで設定するかなんて気にする必要ないじゃん」 そうでしょうか? もう一歩踏み込んで繰り返しでの動作を意識する、そのために しかるべきニュートラル状態・アイドル状態を定義して、使用後はその 状態にしておくことを常に意識する、という考え、だめでしょうか。 サイクルの話からずれますが、例えば windows の CreateWindowEx() では 先頭引き数でオプションを指定しますが、オプションをなにも指定しない時 だけ直接 0 を渡すのヘンだと思いませんか? +------------------------------ | ... | hWnd = CreateWindowEx(WS_EX_TOPMOST, ...); | ... +------------------------------ | ... | hWnd = CreateWindowEx( 0, ...); | ... +------------------------------ ここは是非とも「オプション何も無しよ」というマクロ定義が ちゃんとあるべきだと思いませんか? 時間軸で変化する状態においても、「それ以外なら」とか「ホッタラカシ」 ではなく、(次のサイクルのための)「これが NULL状態」を意識したほうが いいと思いませんか? ●まあそうはいっても… 実際には我々は、数多ある様々な状態(メモリ・レジスタ)の一つ一つを 個別に認識し、これはホッタラカシでOK、これはこういう理由で後始末必要、 とかちゃんと区別しています。 (あんまり終了状態とは意識してないことが多いですが) 例えば昨今の電力消費にうるさいシステムでは、必要ない機能・使い終わった 機能はスイッチオフにしなければなりませんが、これも無意識に「これとこれは 終了状態の管理が必要!」と区別しますよね。 が、あまりにもその対象が個別かつ多いので、時々勘違いして管理が漏れたり するんですね。なので意識の持ち位置のデフォルトを、「これで終わりでない」 ゆえ次のために「後始末あり」にしましょう、という提起なのです。 (ちなみに、スイッチオフのままで動かない、のはすぐ気づきますが、 (使わないのにスイッチオンのまま、というのは漏れることが多いです。 (windows で「ここをクリックしたらフリーズ」なバグはよく見つかりますが、 (「1週間放っておいたらヘン」はなかなか見つけられません。 (これらはテストもしにくいので、論理的に攻める必要があります。 ●おわりに なんかよくわかんない文章になっちゃいましたね。 いや、何が言いたいかというと、LPC1830 の boot-ROM の SPIFI-flash 初期化不良問題ですよ。 ようやっと SPI-flash の S25FL032P のデータシートを読みまして、 S25FL032P にはリセット信号入力がない、にもかかわらず、「高速 読み出しモード」という内部状態を持のです。これは与えられた データをコマンドと解釈せず、頭からいきなりアドレスと解釈します。 それで SPIFI-インターフェースは、通常使用時 常に高速読み出しモードに 置いてコマンド無しでメモリアクセスしているわけですが、ダメboot-ROM は 自分が初期化に行く時にはコマンドを受け付けるモードにあることを期待 して書かれていた、よう。 でハードウェアリセットサイクルにおいて、高速読み出しモードにいる SPI-flash に boot-ROM がコマンドを投げてしまって……と。確かにこれは かなり難しいレベルのバグで気づかなくても まぁ許せないこともない。 基本はそのようなモードが存在するということを認識している(知らないと 初期化すらできませんから)なんですが、ハードウェアリセットも電源オン・ オフに内包された一つのサイクルだというイメージがあれば(実際 POR と リセット入力は動作が違うし)気づくヒントに少しはなったはずなのにねぇ、 と思ったので、いい機会なので私の思う「サイクル」について書いてみました。 (それ以前に初期化関数が別パラメータでまた呼ばれる、とか (普通な使い方だろ、とも言えるけどね…。(→サイクル問題) (spifi_init() に対して spifi_close() も要求されていれば、とも また S25FL032P のデータシートでは、この高禄読み出しモードからの抜け方の 記述がまたよくわかりません。「これが最後だから」という指定で抜けるのは わかるとして、「未定義コマンドを送ったり中途ハンパなアクセスをしても 抜けるから」とも書いてあるんですが、これ仮でいいから中途ハンパなアクセス 方法を“抜けるコマンド”として独立に定義してあれば決して忘れないのに…、 とも思ったのでちょっと味付けしてみました。(→NULL 状態定義問題) (mov a,a を nop と定義するようなイメージ (ちなみに もし定義済みコマンドを送るとそれは有効なのか、も (今ひとつ はっきりしない… というところで、今ひとつ収まりが悪いけど… おしまい