|ハイブリッドOS|File System|ARM|Android|Java|制御システム|オープンシステム

 

技術者コラム

 
フォーム
 
第4回目
2013-07-23

筆者:金田一

 

10年以上前の話になりますが、お付き合いください。

私が、大阪の事務所に出張していて、夜の10時に仕事を終えホテルに帰ろうとした時に

A君が「先輩助けてください」と泣きそうな顔で言ってきました。事情を聴くと...

 

・ 実績あるプログラムでお客さんの都合で仕様を変更したところバグが発生した

・ バグというのはプログラムのデータが破壊される

・ あるアクションをすると必ず現象が発生する

 

というものでした。A君が泣きそうだったのは、明日お客さんに納品するのに、バグの原因が分からないからなようです。プログラムはVCで開発したアプリケーションでした。あるアクションをすると現象が必ず発生するということで、私は「1時間半で原因を突き止めてやる」と宣言しました。

 

まずは、きっかけのアクションをしてもらい、現象を確認することから始めます。A君には、そのアクションとデータ破壊される領域との関係を聞きました。私が、VCを使ってソースデバッグしている間、A君は一生懸命プログラムのソースを調査していました。

私は、「いまさらソースを見ても、思い込みがあるから無駄」「こういう場合は、現象を把握することが問題解決への近道」と言いました。

 

データ破壊される領域を特定し、VCのハードブレーク機能を使い、破壊するタイミングを特定しました。破壊していたのは関数memcpyでした。関数memcpyを抜けた(関数memcpyを呼び出した)個所のソースを見て、A君は「あっ!」と大きな声で言いました。

 

聞くと、仕様の変更というのは、それまで512バイトであった領域を256バイトに変更するというものでした。先ほどのソースは、その領域を別の領域にまさにコピーする個所でした。なぜそのような事が起きたかを調べると、512バイトの領域を管理する構造体があり、その領域として

 

char area[512];

 

と記述されていました。

私はそれを見た時に、A君のお尻をキックしていました。「これはプロが作るプログラムではない!」

私は、A君にいろいろ説教しましたが「今回の君のいいところは、恥を忍んで先輩に素直に頼ったところだ」と慰めました。

ソースを修正し現象が発生しない事を確認すると共に、同じような個所がないかを確認させました。それが終わり、事務所を出たのが1時半でした(ちょいと宣言時間をオーバーしました)。

 

プログラムのソースに含まれる(仕様が分からない人に)意味不明な数値を“マジックナンバー”といいます。マジックナンバーを使うと、仕様が変わった場合の手間が多くかかってしまいます。

マジックナンバーにしないように、数値はdefineで定義するようにしましょう。

 
第3回目

筆者:金田一

 

3回目は、「ハードウェアの初期化」については話を進めてみたいと思います。

組み込みシステムではハードウェア制御を行う前に、初期化という処理を行います。

初期化処理を行うプログラムを作成する際にサンプル・プログラムがあれば比較的容易ですが、無い場合には、対象となるハードウェアの資料を熟読することになります。

 

初期化処理は、複数のレジスタに対して各種の設定を行います。

正確なデータをレジスタに設定するのは当然ですが、結構見落とされるのが、時間と初期値です。

レジスタに各種の設定を行う際に時間を置かないといけない場合があります。最近のCPU性能は向上していますし、CPUにキャッシュが搭載されている場合、単純なソフトウェアループでの遅延では必要な時間を確保していないときがあります。ハードウェアが時より正常に初期化されていないような動作をする現象が発生するような場合、この初期化処理でのレジスタへのアクセスの時間が確保されているかを確認してみてください。

 

また、ハードウェアの電源ON時に初期値というのがあります。電源ON時のハードウェアの初期化処理であればその初期値を利用することができます。それでも、一応プログラムでもその初期値を設定するようにしておいた方がいいでしょう。たとえば、ハードウェアに障害が発生し、システム稼働中にハードウェアをリセットするような場合に、その初期化処理を流用することができます。システム稼働中(ハードウェア動作後)は、前述の初期値は不定値となります。ハードウェアをリセットする処理で、初期化処理を動作させても正常に初期化できないような場合、その初期化処理が電源ON時の初期値を前提にしている事があります。無駄かもしれませんが、最初から初期化処理では電源ON時の初期値を設定するようにしておくと後々のトラブルを回避できることがあります。

 

『XXXX互換』と呼ばれるハードウェアを扱うことがあります。この場合、その「XXXX」を制御するプログラムがそのままでは動作しないことがあります。レジスタの配置が違うときもありますし、レジスタへのアクセス間の時間を確保しなければならない、レジスタに対する設定の順番が違うといった「クセ」があることもあります。

うまく動作しないという場合は、上記の事に注目してみてはどうでしょうか?

 
第2回目
2012-02-24

筆者:金田一

 

第一回目でデータのコピーについて解説をしました。今回はその続きになります。

前回、データのコピーをする際に、C言語では以下のように記載すると良いとしました。

*d++ = *s++;

*d++ = *s++;

*d++ = *s++;

*d++ = *s++;

C言語では、以下のようにコーディングすることができます。

d[0] = s[0];

d[1] = s[1];

d[2] = s[2];

d[3] = s[3];

C言語の処理としては同じですが、どちらが効率良い処理ができるかはCPU及びC言語のコンパイラに依存します。

 

話は少しずれますが、ここでスタックの操作について考えてみてください。スタックにデータをPUSHする場合、一旦アドレスを減算してからデータを書き込みます。データをPOPする場合、データを読み出してからアドレスを加算します。

多くのCPUではデータを読み出してからアドレスを加算する命令(POP操作)はありますが、データを書き込んでからアドレスを加算する命令はありません。従って、上の方のコーディングであれば、式の右辺の処理(データの読み出し)で1命令、左辺の処理(データの書き込み)で2命令になるものが多いようです。ただし、データの書き込み後にアドレスを自動更新してくれる命令を持つCPUもあり、その場合左辺も1命令になります。

 

また、話はずれます。CPUの機械語には、アドレスにインデックスを加算してメモリにアクセスできる命令を持つものがあります(インデックスは0~15が多いです)。この場合、下の方のコーディングであれば、メモリの読み出しに1命令、書き込みに1命令となります。このような命令がないCPUの場合は、アドレスに値を加算してメモリにアクセスするため命令の数が増えます。

 

上記のように、CPUがどのような命令を持っているのか(アーキテクチャ)、命令を持っていてもコンパイラがそのような命令を使ってくれるかということで、同じC言語のコーディングでも機械語になった場合に異なる命令に変換されることになります。

プログラムの開発にどのコンパイラを使うのか、どんなCPU上で動作するのかということを考慮しないといけない場合があります。特に各種のCPUで動作することを目的とするプログラムの場合、CPUによってコーディングを変えた方が良いといった場合もあります。

 

非力なCPU性能、制限されたメモリ空間でのプログラミングを要求される純粋に組み込みシステムでは、機械語をイメージしてC言語のプログラミングをする必要も出てきます。

最近のCPUの性能は向上していますので、機械語レベルまで考えなくてもいいケースが多くなっています。ただ、CPUがどのような動作をするのかをイメージしてプログラミングすることでより良いプログラミングができるのでは

 
第1回目
2012-01-31

筆者:金田一


あるシステム構築でお客様から要望が出されました。
そのシステムは、「PCで動作するアプリケーションで、約1.5Gbpsでデータが入力されるので、そのデータを編集し、1Gbps以上のスピードでデータを出力する必要がある」というものでした。
入力されるデータは、256Kバイトの固定長ブロックに数バイトから数百Kバイトの可変長レコードがあります。レコードのヘッダにはデータ種別が4種類あり、種別毎にデータを並び変えて出力する必要があります。データをコピーして出力するデータのブロックを構築していきます。

システムの仕様に基づき、システムを構築して出力データのスピードを計測したところ200M~400Mbps程度の処理スピードでした。アプリケーションの処理を再検討し、データのアドレス情報を扱うようにして、できるだけデータのコピーを行わないようにしてみたところ、400M~600Mbps程度まで処理スピードは向上しましたが、お客様の要望に沿いません。
アプリケーションの各処理時間を計測したところ、データのコピーのオーバヘッドが一番大きいことが分かりました。データのコピーは、C言語の標準関数であるmemcpyを使っています。

そこで関数memcpyを自作することにしました。既存のmemcpyは1バイト単位で指定されるサイズ分をコピーしていました。そこで、1回のコピーを4バイトとすることでコピー回数を減らすようにしました。
更に、コピーサイズを16進数で考えて、ビット4が1のとき16バイト(4バイトのコピー4回)、ビット5が1のとき32バイト(4バイトのコピー8回)、ビット6が1のとき64バイト(4バイトのコピー16回)というように、まとめてコピーするようにしました。
まとめてコピーする場合は、

*d++ = *s++;
*d++ = *s++;
*d++ = *s++;
*d++ = *s++;

というように、列挙するようにしてループしないようにしました。このことでループによる終了判定処理を無くして処理時間を短くしました。

このように自作した関数memcpyにより、システムの出力は約1.3Gbpsになりました。このことで、お客様より絶賛をいただきました。
PCのCPUはINTEL社のx86互換のCPUなので、奇数アドレスからのワードアクセスが可能になっている点とキャッシュを搭載している点を考慮して、自作したmemcpyは比較的簡素になっています。
奇数アドレスからのワードアクセスができないRISC CPUの場合は、入力アドレスと出力アドレスの奇数/偶数のケースにより処理を分けないといけないなど、コピー処理は複雑になっていきます。しかし、標準関数memcpyが1バイト単位でコピーしている場合は、自作することも考えた方がいいでしょう。

<余談>
INTEL社のx86互換のCPUには、ストリング命令という命令があり、この命令によりデータのコピーや比較などを1命令で行うことができます。関数memcpyを自作しても1Gbpsに達しない場合はアセンブリ言語でコピー処理を記述するつもりでしたが、そこまでには至りませんでした。

 

連載記事のソースコード

連載記事のソースコード
 
・Haskellで問題を解く(Part1〜Part4) ソースコード
・Clojureで8クイーン問題にチャレンジ(Part1〜Part5) ソースコード
・OCamlでへびゲームを作る(Part1〜Part5) ソースコード
・Swiftでオセロを作る(Part1〜Part5) ソースコード
・Processingでシューティング(Part1〜Part4) ソースコード 
Haskellでテトリス(Part1〜Part9) ソースコード
・プチコン3号(BASIC)でさめがめ(Part1〜Part3) ソースコード
・Prologでさめがめを解く(Part1〜Part6) ソースコード