スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

時間処理の落とし穴 - 時間精度とtick

「旦那様、旦那様、起きて下さいませ。睡眠薬を飲む時間です」

マルチメディア関連のソフトウェアやデバイスドライバ、組み込み系のようなリアルタイム処理が必要なプログラム等を書いていると、タイミング制御や経過時間の取得等、時間に関する処理が必要になってきます。時間処理については、正しく理解していないと陥りがちな落とし穴がいくつかありますので、今回はそのあたりについて書いておこうと思います。


sleep系関数の精度と挙動

一定時間ごとの周期処理や、リソース競合時の遅延リトライ。そんな時に用いられるのがsleep系関数(sleep/usleep/nanosleep)です。一定時間呼び出しスレッドを停止させる関数(システムコール)であり、sleepなら秒、usleepならマイクロ秒(1/1000000秒)、nanosleepならナノ秒精度(1/1000000000秒)の指定が可能になっています。
しかし、実際この精度で、次に起床されるタイミングを制御できるわけではありません。実際、x86系のLinuxの実装では10ms以下の精度の指定は、10ms精度に丸めらてしまいます(※1)。これは、sleepによって、待ちキューに入ったスレッド(プロセス)を起床させるタイミングが、tick単位であるためです。tickは、上位アプリケーションの開発者にはあまり馴染みがないかもしれませんが、OSが周期的な処理間隔であり、x86 Linuxの場合、これが10msになのです。
大抵のOSでは、tick単位で、タイマ割り込みが発生し、このタイマ割り込みのタイミングで、時間指定でsleepしていたスレッド(プロセス)が起床されるという実装になっています。このため、tick以下のsleep時間を指定しても起こされるタイミングがないのです。

こう書くと、何でtickはそんなに大きな(※2)数字になっているんだという疑問もでそうなのですが、これには理由があります。このtickを小さくすればするほど、定期的なタイマ割り込みの頻度があがるため、システム全体の負荷に影響が出てしまいます。それをおしてでもということであれば、カーネルソース内のtick定義を変更すれば、sleep精度を改善することは可能です。但し、非常の根本にかかわるところなので、変更には十分な注意が必要です。

以上は、POSIXのsleep関数の問題ですが、ITRON系のwai_tsk()システムコールではもともとsleep時間をtick単位で指定するような仕様であるため、このような勘違いは起こりにくい仕様になっています。こういうところに、もともと組み込み等のリアルタイム処理向けの仕様として作られたか、汎用システムの仕様として作られたかという点に起因する思想の違いが見てとれます。

※1 Linuxのnanosleepでは、リアルタイムスレッドで2ms以下のsleep指定をするとbusy waitによるsleepを行います。よって、2ms以下の精度のsleepも可能にはなっていますが、busy waitのため、他のスレッドは動作できません。
※2 10msというと十分短いと感じる人もいるでしょうが、リアルタイム処理系にとっては、非常に長い時間です


時間情報取得関数の精度

sleep以外の時間処理としては、時刻や経過時間等の時間情報を取得するというケースがあります。リアルタイム系の処理では、定期的なポーリングで経過時間を取得しつつ処理を行ったりする時等に用いられます。リアルタイム処理以外でも、処理の実行時間等のプロファイル計測に用いられることが多いでしょう。
さて、この時間情報取得関数、POSIX準拠のC言語ライブラリだけでもいくつか種類があります。時間情報を正しく取得するためには、これらの違いとメリット・デメリットを正しく理解しておく必要があります。

◎clock()
clock()はANSI Cで定義されており、その返り値の型はclock_tです。clock_tの単位はCLOCKS_PER_SECtとして定義されており、POSIXではこれ(CLOCKS_PER_SEC)が1000000(1us)である亊を要求しています。しかし、この精度があるというわけではなく、実際大抵はtick精度しかでません。なお、clockは実行中のプロセスの処理経過時間を返すため、APIのプロファイル等には便利です。
◎gettimeofday()
時間情報取得の定番です。現在の時刻情報が取得できます。これも単位はusecだが、こちらはtick以下の精度もでる。但し、実行中以外のスレッドも含む全システムでの経過時間を返すため、プロファイリングで用いる時には注意が必要です。また、現在時刻を取得するというものであるため、バックグラウンドでntp等が動いていると、時刻情報に補正が入ったために経過時間計測が狂うことがあるのでこれも注意が必要です。
◎times()
clock()同様にスレッドの実行時間をtick精度で取得できます。tick精度の情報であれば十分です。
◎getrusage
POSIX系ならこれば一番無難でしょう。gettimeofday同様精度が高く、times、clockのようにプロセス単位の経過時間も取得できます。


【関連リンク】
Manpage of sleep/usleep/nanosleep
Manpage of gettimeofday/clock
C言語: 実行時間測定の方法
Linuxによる一定周期実行


【関連書籍】
リアルタイム組込みOS基礎講座 Qing Li
RTLinuxテキストブック―ハードリアルタイム機能を使いこなす Matt Sherer FSMLabs
リアルタイムJavaプログラミング ピーター・C・ディブル
詳解Linuxカーネル 第2版 ダニエル・P・ボベット 他
Linuxデバイスドライバ 第2版 Alessandro Rubini 他
組み込みLINUXシステム構築 カリム・ヤフマー

スポンサーサイト

Unicodeの落し穴 - 文字コードと文字列処理

宇宙の境界条件は、それが境界を持たないということだ。
スティーヴン・W・ホーキング 『ホーキング、宇宙を語る
Unicodeといえば、文字コードの一種。文字コードをあまり意識しない方でもほとんどの方は御存じでしょう。しかし、Unicodeというか文字コードに関して、いくつかありがちな誤解があり、それが原因でバグにはまることもあります。ということで、今回はUnicodeと文字コードの話です。

■文字コードと符号化方式

4798100307文字コードといった時によく、混同されるのが文字集合(character set)と符号化方式(encoding type)。文字集合はその名のとおり、文字の集合を定義したもので、日本語であれば JIS X 0208(第一、第ニ水準)をはじめ、JIS X 0212(補助漢字) 、 JIS X 0213(第三、第四水準)等があります。 それに対して、符号化方式とは、文字集合に含まれる各文字をどのようなコードに割り当てるのかというもの。 1つの文字集合JIS X 0208に対しても、ISO-2022-JPShift-JISEUC-JP等があります。この辺の話は、textファイルやソースコードの編集時や、Webページの閲覧時に問題になることが多いので馴染み深いですよね。
そして、問題のUnicodeですが、これは多言語を同時に扱うことを目的として開発された文字コード体系です。符号化方式は可変長の8ビットコードであるUTF-8、16ビットもしくは32ビットでエンコードするUTF-16等があります。

さて、ここまでで何が問題なのかというと、Unicodeを符号化方式を表すものと勘違いしている人がいることです。UTF-8とUTF-16はともに良く使用される符号化方式ですが、「Unicode文字列を処理する」といった場合に、どちらか一方でエンコードされていることを期待しているソースコードを書いてしまい、それがバグの原因になることがあります。

■ワイド文字列・マルチバイト文字列の文字列処理

プログラミングにおいて、文字コードが直接影響するのは文字列処理です。C言語でいれば、strcpy, strcmp, printf等など。C言語における文字列は、「NULL終端文字('\0')までのバイト列(char配列)」というのは、これは、ASCIIを始めとする1バイト文字列での話。ワイド文字列では、これが「ワイドNULL文字('\0\0' 2バイトの0)までのワイド文字列」となります。
これはつまり、ワイド文字列に対してstrcpy等の従来の文字列処理のライブラリが使えないということを意味しています。内部で、strcpy等を利用しているライブラリも同様です。ワイド文字列では、終端以外でも0が入ることがあるので、意図しないところで終端扱いされ、途中までしか文字列がコピーされないといったようなことが起こります。

Unicodeに関しては、UTF-8でエンコードされている間はNULL終端文字を期待する従来の文字列処理関数が使用できますが、これがUTF-16だとそうはいきません。よって、Unicode対応をUTF-8対応と思っていると、strcpyを使っているようなコードでも問題ないと判断してしまいそうですが、実はUTF-16への対応が必要だったという場合にバグを引き起こします。

■資産の流用と文字コード

Unicode自体はまだ発展途上とも言える技術ですが、徐々に使用範囲が拡がっています。よって、今まではUnicodeを扱わなかったようなライブラリでも、Unicode文字列を扱わなければならないケースがあるでしょう。その際には、文字コードにおける符号化方式の違い、ワイド文字列に対する互換性問題に対して、正しい認識を持って問題にあたることが必要でしょう。

【関連書籍】
Unicode標準入門 トニー・グラハム 乾和志
Cプログラミングの落とし穴 A.コーニグ
文字コード超研究 深沢 千尋
プログラミング言語C ANSI規格準拠 B.W.カーニハン D.M.リッチー
図解入門 よくわかる最新ファイル形式と文字コードの基本と仕組み―データ形式、ファイル構造、文字コード基礎講座

【関連リンク】
Unicode (Wilipedia)
文字コード (Wikipedia)
Manpage of PRINTF

ビットシフトの落とし穴 - 算術シフトと論理シフト

「不定だ・・・。犀川創平君。君の方程式の解は、今や不定だ」
C言語には、ビットシフト演算子というものがあります。左シフト演算子(<<)と右シフト演算子(>>)です。同じビット演算でも、ビット単位の論理和(|)や論理積(&)、NOT(~)等はの方は、フラグ型の変数の処理で使われる事が多い気がしますが、ビットシフトの方は使用されるケースはあまりないかもしれません。

さて、このビットシフト演算子で時々問題になるのが、符号ビットが立っている時の右シフト演算です。見逃されがちなポイントは、
  • 型によって挙動(算術シフトか論理シフトか)がかわることがある
  • C言語の規格として、算術シフトか論理シフトかは不定
  • Nbitの算術シフトと2のN乗での除算は等価ではない
といったところにあります。
算術シフト(shift arithmetic)と論理シフト(shift logical:又は0充填シフト)という言葉をご存知ない方のためにちょっと説明を書いておくと、シフトによって空いたビット部分を符号ビットを同じもので詰めるのが算術シフト、0で詰めるのが論理シフトです。そのため、それぞれ、符号充填シフト、0充填シフトと呼ばれることもあります。詳しくは、Wikipedia(ビット演算)等を参照してください。

型によって挙動がかわることがある

具体的な例をあげて説明します。次のコードを実行するとどうなるでしょうか。
#include <stdio.h>
int
main()
{
  long sval = 0xffffffff;
  unsigned long uval = 0xffffffff;

  sval >>= 8;
  uval >>= 8;

  printf("%#010x %#010x\n", (unsigned int)sval, (unsigned int)uval);

  return 0;
}

「0x00ffffff 0x00ffffff」という結果を予想される方もいるでしょう。しかし、実際gccでこのソースをコンパイルして実行すると、結果は「0xffffffff 0x00ffffff」となります。同じビットパターンを8bit右シフトしただけなのですが、型が符号付整数(signed)か符号なし整数(unsigned)かによって結果が異なっています。実は、gccではsignedなら算術シフト、unsignedなら論理シフトとなる実装になっているのです。実際、上記コードをコンパイルしたものをobjdumpで逆アセンブルした結果も乗せておきます。

  sval >>= 4;
 80483a6:       8d 45 f8                lea    0xfffffff8(%ebp),%eax
 80483a9:       c1 38 04                sarl   $0x4,(%eax)
  uval >>= 4;
 80483ac:       8d 45 fc                lea    0xfffffffc(%ebp),%eax
 80483af:       c1 28 04                shrl   $0x4,(%eax)

signedの場合は算術右シフト(sarl)命令、unsignedの場合は、論理右シフト命令(shrl)になっているのが確認できます。

このような実装のため、何気なく論理シフトを期待して右シフト処理を行っていると、たまたま変数の型がsignedの場合、思わぬバグの原因になることがあります。

C言語の規格として、算術シフトか論理シフトかは不定

先程の例は、gccでコンパイルした時の話でした。なぜ、わざわざコンパイラを明記したかというと、先ほどの挙動(signedなら算術シフト、unsignedなら論理シフト)は、gccの仕様であって、C言語の仕様ではないのです。残念なことに、C言語の規格としては、右シフト時に算術シフトになるか論理シフトになるかは規定していません。つまり、処理系依存の動作になっています。よって、gccでコンパイルしている限りは、先程の挙動は保障されますが、他の処理系(例えば、全て論理シフトになっている処理系)との互換性は保障されません。大抵の処理系ではgccと同じような実装になっているため、C言語の解説サイトでも、C言語の仕様として書かれているのを見かけることがあります。しかし、保障はないので移植性が必要な場合は注意する必要があります。ちなみに、VC++も、gccと同じ実装です。

使用している処理系の右シフト動作がgccやVC++の動作に依存している場合、次のようなstatic_assertマクロでチェックを入れておくことができます。
#define STATIC_ASSERT(expr) { \
    char __STATIC_ASSERTION[(expr) ? 1 : -1]; \
    (void)__STATIC_ASSERTION; \
  }

/* 符号付き整数の右シフトが算術シフトかどうか */
#define SHIFT_LEFT_SINGNED_USES_SAL \
  (((signed int)0xffffffff >> 1) == 0xffffffff)

/* 符号無し整数の右シフトが論理シフトかどうか */
#define SHIFT_LEFT_UNSIGNED_USES_SHL \
  (((unsigned int)0xffffffff >> 1) == 0x7fffffff)

ソースのどこかで、
STATIC_ASSERT(SHIFT_LEFT_SINGNED_USES_SAL)
のように書いておけば、処理系の動作が期待値と異なる場合にコンパイルエラーになります。
勿論最初から依存しないように書けばいいのですが、条件分岐等を無駄に入れたくない場合には、処理系依存を承知の上でコードを書いて、上記のようなチェックコードを埋めておくというのもひとつの手です。

ちなみに、JavaではCにおける>>演算子に加えて>>>演算子が追加されているので、問題は緩和されていますね。

Nbitの算術シフトと2のN乗での除算は等価ではない

では、gccとVC++は、型によって算術シフトと論理シフトを使い分けているのでしょうか。何もそんなややこしい仕様にしなくてもいいのにと思われる方もいるでしょう。
signedが算術シフトになっている理由とは、その名が示しているように、右シフト演算を整数演算(除算相当)になるようにするためです。具体的には、負数が2の補数で現れている場合、負数に右論理シフトを行うと、値の符号が変化してしまいますが、算術シフトにするとつじつまがあいます。右シフトが2の累乗での除算(例. x >> 1 == x / 2)、左シフトが2の累乗倍(例. x << 1 == x * 2)というルールが負数にも「ほぼ」当てはまるようになります。
ここで、「ほぼ」と書いたのがポイントで、実際は除算の方は「x >> 1 == x / 2」とはいかないことがあります。
C99以前のC言語の場合、負数の整数除算(/演算)は、切捨てになるか切り上げになるかが処理系依存でした。C99では、正負にかかわらず切り捨て(負数であれば、0方向への切り上げ)という規定が入りました。これによって、整数除算に関する移植性の問題はなくなりました。
2の補数で表現された負数の算術右シフトは、切り下げ(-1.5なら-2)です。よって、C99に従った整数除算と、算術右シフトは、切り上げ/切捨てが異なってしまいます。具体的は、「-3 / 2 == -1」「-3 >> 1 == -2」となります。C99以前のCの場合、整数除算の方が処理系依存ですので、一致することもあるかもしれないし、一致しないこともあるかもしれません。(私自身も「ハッカーのたのしみ―本物のプログラマはいかにして問題を解くか」を読んでいて、初めて認識しました :-P)

何かと落とし穴の多い右シフト。使うときは十分注意しましょう。

(追記)
最初この記事を書いた時には、C99以前のC言語では負数の整数除算の扱いが不定ということを知りませんでした(konumaさんに指摘いただきました)。記事を書く前に、gccで動作&アセンブラ確認もしたのですが、それでは処理系依存か否かという点はわかるはずもないですね。今回の教訓です。一度、こういった処理系依存の話についてもまとめてみたいと思います。

4434046683 【関連記事】
コンパイル時の静的チェック (STATIC_ASSERT)
charの落とし穴 - 暗黙の型変換と符号拡張

【関連リンク】
ビット演算 - Wikipedia
シフト演算子 (Microsoft)
シフト演算子 (二流プログラマの三流な日常)
プログラミング言語Cの新機能 (C99に関する詳しい解説)

【関連書籍】
ハッカーのたのしみ―本物のプログラマはいかにして問題を解くか Jr,ヘンリー.S.ウォーレン
Cプログラミングの落とし穴 A.コーニグ
GCC GNU C Compiler―Manual & Reference 遠藤 俊徳
プログラミング言語C ANSI規格準拠 B.W.カーニハン D.M.リッチー

bool型の落とし穴

奇蹟は教理の真偽を見わけ、教理は奇蹟の真偽を見わける。
パスカル
C/C++でbool型(ブール型/boolean型)を使用する時のお話です。

C言語は言語レベルでbool型(ブール型)をもっていません。しかし、プログラミング上、bool型を定義した方が可読性が良くなることもありますので、独自にtypedefでbool型を定義して使用されている方も多いと思います。C99(1999年に制定されたC言語の第2版)ではstdbool.hが導入され、その中で_Boolが定義されていますが、これが実際に使用されているのはあまり見かけません(そもそもC99仕様をフルに活用したコードを見かけることは少ないのですが)。 今回はこのように独自定義されたbool型、及び、処理系によって用意されたbool型を用いる時の注意点について説明します。

TRUE,FALSEの値

bool型を独自定義する場合、通常TRUEとFALSEもあわせて定義します。この時問題となるのが、それらにどのような値を持たせるかという点です。大抵はTRUEが1、FALSEが0となるようにマクロ定義する、あるいはenum型でbool型を定義するというケースがほとんどでしょう。==演算子や!=演算子が返す値にあわせれば自然とそうなります。こうすることで次のような書き方が可能になります。
if (is_valid(&pos)) {
  /*必要な処理*/
}
bool型を返す関数をそのまま条件式として使用できます(※1)。しかし、TRUE,FALSEの値はこれ以外の値で定義されていることもあり得ます。

例えば、POSIX系の関数(open/read/write等)は成功時に0または正数を返し、エラーで-1を返すものがあります。strcmp()やmemcmp()は、内容が等しいときに0を返します。そう考えるとTRUEが0というのもありかなという人がいても不思議ではありません。

個人的には最初に書いたTRUEが1、FALSEが0が良いと思いますが、他人のソースの場合は、念のため確認した方が賢明です。 なお、関連リンクにもありますが、マクロ定義がかえって話を複雑にしては本末転倒ですので、TRUE,FALSEは使わず、1や0の直値を使うというのも一つの手です。

BOOL型のサイズ

BOOL型のサイズ(sizeof(BOOL))も悩みの種です。C99で導入された_Boolでも、「型_Boolとして宣言されたオブジェクトは, 値0及び1を格納するのに十分な大きさをもつ」とされているだけで、実際にどのようなサイズになるかは処理系依存となっています。BOOLの二値を表現するには1byteでも十分なのですが、処理効率を重視してintを同じサイズ(4byte)で定義されていることもあります。これは、C99の_Boolだけに言える事ではなく、C++のbool等でも言えることです。例えば、Visual C++では、5.0以下ではintと同じ(4byte)ですが、最近は1byteです。

そのため、これらのbool型を用いたソースを移植する際には、次のような影響が出ることがあります。
  • 構造体のアライメントがかわる。
    場合によっては、移植後にアライメントエラーを起こす可能性もある
  • BOOL型変数が多く使われている場合、使用メモリサイズに影響する。
    1byteと4byteでは4倍!
そのため、_Boolやbool等の処理系依存の型をそのまま用いている場合は、移植の際にはbool型のサイズを確認した方が良いでしょう。

以上は、処理系によって用意されたBOOLについても注意ですが、独自定義したBOOLについてもサイズに関しては注意が必要です。例えば次のソースを見て下さい。
void TOM_check_flag(TOM_BOOL* p_flag);
void BOB_check_flag(BOB_BOOL* p_flag);
void JON_check_flag(JON_BOOL* p_flag);

check_all_flags(
{
  MY_BOOL flag[3];
  
  TOM_check_flag(&flag[0]);
  BOB_check_flag(&flag[1]);
  JON_check_flag(&flag[2]);

  /* flagを見る処理 */

}
少々不自然で、あまりいい例ではありませんが(^^;)。 このソースでは、TOM,BOB,JONのcheck_flag関数で何かのflag値を取得しています。このコードの問題点は、MY_BOOL型と、TOM_BOOL,BOB_BOOL,JON_BOOLがすべて同じサイズであるという前提で書かれています。しかし、MY_BOOLが1byte、その他のXXX_BOOLが4byteの場合どうなるでしょうか?XXX_check_flagの方では、引数のポインタを4byte変数へのポインタとして扱いますので、思わぬメモリ破壊をおこす可能性があります。
なお、このソースの場合は、キャストが入っていないのでコンパイル時にwarningが出て気がつくとは思いますが、十分注意が必要です。

二重定義の注意

BOOL型は独自定義されることが多いので、BOOLやbool、boolean等のありきたりな名前で独自定義して使っていると、外部ヘッダをインクルードした時に二重宣言エラー(previous declaration of ...)が出ることがあります。 この辺りは、コーディング規約で、型名の頭にプレフィックスをつける等の対策がなされていれば発生しません。起こっても置換すれば済む話ではありますが、気をつけましょう。 BOOL以外でも、INTやCHAR,BYTE等もよく被る気がします。

Javaのbooleanのように最初から言語機能として組み込まれ、独立した型として強く型付けされていればもっと使いやすいのですが。ないものねだりをしてもしようがないので、boolは注意して使いましょう。

(※1)比較式を省略することについては賛否両論あるかと思いますが、私は分かりやすいと思っているので、bool型を返す関数ではこのような書き方をしてます。

【関連書籍】
改訂版 組込みソフトウェア開発向けコーディング作法ガイド[C言語] (SEC BOOKS)
Cプログラミングの落とし穴 A.コーニグ
GCC GNU C Compiler―Manual & Reference 遠藤 俊徳
プログラミング言語C ANSI規格準拠 B.W.カーニハン D.M.リッチー

【関連リンク】
C99あれこれ ~_Bool編~ (闘わないプログラマ)
直接数値を書いても悪くはない場合(フィンローダのあっぱれご意見番)

volatileで最適化を抑制する

プログラムは思った通りに動かない、書いた通りに動く。
(プログラミングの格言)

C言語やC++,Javaにはvolatileという修飾子があります。組み込み系ソフトウェアやマルチスレッドのアプリケーションを書いている方にとっては、なじみ深い存在ですが、そうでない方にはあまり縁がないのかもしれません。しかし、volatileの使い方や存在意義を知らないままコーディングを行うと、思わぬバグを引き起こす場合があります。今回は、そのvolatileキーワードについて簡単に説明したいと思います。

volatileは初期のCであるK&Rには含まれていませんでしたが、ANSI C(C89)以降のC標準規格にはconstと一緒に含まれるようになりました(constとvolatileをあわせてcv修飾子と呼ぶこともあります)。一般的なCなら必ず備えている修飾子です。

volatile修飾子の意味ですが、「プログラミング言語C ANSI規格準拠」によると、
volatileの目的は,黙っていると処理系で行われる最適化を抑止することにある.例えば,メモリ・マップ方式の入出力をもつマシンでは,ステータス・レジスタに対するポインタは,ポインタによる見かけ上,冗長な参照をコンパイラが除去するのを防ぐのに,volatileへのポインタと宣言することが可能である.
となっています。 なぜ最適化を抑止する必要があるのか?例をあげて説明します。

条件分削除の抑制

次のコードを見てください。変数の値が変化するまで、変数をポーリングし続けています。
extern int event_flag

void poll_event()
{
  while (event_flag == 0) {
    /* 処理。但しevent_flagは操作しない */
    ....
  }
  ....
}
whileの続行条件としてグローバル変数event_flagの値を参照していますが、ループ内部でevent_flagの値を一切処理していません。よって、単純に見ると、このwhileの条件は毎回評価する必要なく、次のような最適化が可能となります。
void poll_event()
{
  if (event_flag == 0) {
    while (1) {
      /* 処理。但しevent_flagは操作しない */
    ....
    }
  }
  ....
}
不要な条件評価を削除するのは最適化の基本であり、実際、大抵のコンパイラでは上記相当のコードが生成されます。
このような最適化は処理速度の向上にとって大変有難いのですが、上記の関数は、本当にこのような最適化をしても良いのでしょうか?
シングルスレッドモデルの場合は、特に問題はありません。問題は、この変数が他のスレッドや、ハードウェアによって書き換えられる可能性がある場合です。つまり、上記のループはその関数をコールしているスレッド自身ではなく、割り込み処理や他のスレッドからの操作されることによって変化することを期待していたという場合です。
この場合は、上記のような最適化がなされた場合、途中で他スレッド、あるいは割り込み処理が値を変更したとしても、最適化のためにループから抜けることがなくなってしまいます。コンパイラは、現在の関数コンテクスト以外からその変数を操作することはないという前提にたって最適化を行っているため、このようなことが起こります。

そこで、このような複数スレッドからアクセスされる変数に対する最適化を抑制するために、volatile修飾子を使用します。具体的には、次の例のように、変数宣言にvolatile指定を追加します。
extern volatile int event_flag
これで、event_flagに対する最適化は抑制され、先ほどのような意図しない最適化も防ぐことが出来ます。

処理手順の保存

条件分岐の最適化以外にも、処理手順の最適化によって意図しない動きになることもあります。次の例を見てください。
extern int* p_regster1;
extern int* p_regster2;

void set_regester2(int val)
{
  /*必ず次の手順でレジスタ設定する必要がある*/
  *p_register1 = 1;
  *p_register2 = 0;
  *p_register2 = val;
  *p_register1 = 0;
}
register2の値を設定する前に、regster1を一旦1にしており、register2設定後今度は0を設定しています。組み込みソフトウェアなどでは、HWの仕様上このような一見(Cコード上)無駄な手順が必要なことがあります。 これも一般的なコードとして見れば冗長なので、volatile指定無しの場合、最適化される可能性があります。例えば、次のようになります。
void set_regester2(int val)
{
  *p_register2 = val;
  *p_register1 = 0;
}
このような最適化は、単一のスレッドからしか呼ばれない関数でも起こりえます。回避策は、先ほどと同様、変数のvolatile指定をつけることです。
extern int* p_regster1;
extern int* p_regster2;

4434046683 このようにvolatile指定は、組み込み系やマルチスレッドプログラミングで重要な役割を果たしており、その動作を理解することは重要です。

近年のコンパイラの最適化能力はすばらしいので、アセンブラを強く意識したコードを書く必要はなくなってきました。しかし、最適化による影響を正しく理解していないと、上記のように予期せぬ不具合に見舞われることがあります。仮想関数、ガベッジコレクション等、言語レベルでの機能も高級化してきていますが、その根底にある動作をある程度把握していないと、最適なコードは書けません。

最適化に関しては、デバッグ時にデバッガが表示する変数の値に関する注意などもあるのですが、それはまた別の機会に。

【関連リンク】
「組み込み」ならではの基礎知識 (組み込みネット)
法大奥山研究室:C言語 volatile
特集:スレッドの落とし穴 (ITmedia)

【関連書籍】
実践マルチスレッドプログラミング Steve Kleiman
Pthreadsプログラミング ブラッド・ニコルス 他
CプログラミングFAQ―Cプログラミングのよく尋ねられる質問 Steve Summit 北野欽一
Cプログラミングの落とし穴 Andrew Koenig 中村明
C/C++による組み込みシステムプログラミング
組み込みLinux入門―開発環境/デバイスドライバ/ミドルウェア/他OSからの移行
ITRONプログラミング入門―組み込みOSのデファクト・スタンダード プログラミング詳細とサービス・コール徹底解説

計算途中の桁あふれ

最近はHDD容量やメモリ量も大きくなって、32bitで不足する値というものが増えてきました。
32bitで表せるのは2^32(2の32乗)=4294967296(4G)です。
そのため、例えば4G以上のファイルサイズは、32bitではbyte単位で表しきれません。
実際、多くのシステムでは、ファイルシステムやファイルにアクセスする関数で使用する型が、符号付型(signed long)であるために、2G以上のファイルを扱う際にはいろいろと問題が発生します。
この件については、また別で扱うとして、今回は桁あふれのお話です。

先ほどのファイルの話では、2GB以下の場合32bitでも問題ないと書きました。そのため、2GB以下のファイルを扱うプログラムの場合は、この32bit境界を意識しなくても良いかというとそういうわけにはいきません。

例えば、次の例があったとします。

int
calc_ratio(int size1, int size2)
{
assert(size1 < 2 * 1024 * 1024 * 1024);
assert(size2 < 2 * 1024 * 1024 * 1024);

return size1 * 100 / size2;
}

size1のsize2に対する割合(%:百分率)を計算する関数です。
size1, size2とも実用上2G以下しか使わないというassertを入れているので、このシステムではsizeには2G以下しか指定されないのでしょう。

よって、この式は問題ないのでしょうか?

実際は、問題があります。
それは、size1が2G近い値の場合です。
その場合、size1 * 100は32bitでは表しきれない値となってしまい、桁あふれを起こします。その桁あふれを起こした計算結果をsize2で割るので、その結果も不正なものになってしまいます。
対策としては、size1や定数100を64bitでキャストするなどして、計算途中での桁あふれを防ぐことです。

非常に初歩的なバグですが、このようなバグの問題は、意外に後になって発見されることが多いように思います。なぜなら、size1が大きな値にならない限り問題が表面化しないからです。
境界値テストを重要性を物語ってもいます。

本のおすすめ

4873115655

4274065979

4822236862

4274068579

4822255131

B00SIM19YS


プロフィール

  • Author:proger
  • 組み込み関係で仕事してます

ブログ内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。