|
|
連載第一回 「C/C++のビット操作」
|
|
|
H18/03/14
|
|
|
気がつくと20年以上プログラマをやっています。大手メーカーのオフコン用通信アプリを皮切りに、パソコン/コンシューマ機用ゲームソフトや大型機器制御ソフトなど、各種アセンブラとC言語をメインにプログラムしてきました。しかも、その大半についてメインプログラマという不幸(幸運?)。精神と肉体はかなりダメージを受けましたが、それなりにノウハウは身に付いているようです(当然?)。
現在は小型機器制御用ソフトを何本か手がけていますが、やっぱり問題に遭遇して頭を抱えたり、間抜けな勘違いをしたりしてしまいます。そんな中で思い浮かんだプログラミングに関するトピックをブログ的に書き散らしていこうと思います。正直言うと、こんな話をする相手があまりいないので、寂しさを紛らす意味が大きかったりするのですが・・・・。
対象は多少なりともプログラミングの経験のある方を想定していますが、初心者の方にもできるだけ楽しく読んでいただけるように工夫してみます。が、所詮はエッセイだとご理解ください。 |
|
|
|
|
|
さて、第一回のお題はというと、C/C++「ビット操作」です。
昨今はビット操作が必要な局面が本当に少なくなってきているように感じます。理由はメモリのビット単価が下がり、メモリを節約するという感覚そのものがなくなってきていることが大きいのでしょう。組込用の1チップCPUですらメガビット単位のメモリを搭載していたりするので無理もないのかもしれません。確かにこのような状態で、何も無理をしてビット単位の処理をしようとは思わないでしょう。真(TRUE)か偽(FALSE)かという状態を保存するために必要なメモリは当然1bitで済みますが、一般的な32ビットCPU用のC言語(以降C)では1ロングワード(32bit)を割り当てるのが当たり前です。31bitものメモリが全く無駄になっているのですが、高速処理のためにはこの方が都合がよいからです。今やビット操作は、入出力処理が必要な制御用アプリケーションやドライバーと呼ばれるOS用プログラムくらいでしか必要とされていないのかもしれません。
そんなときに敢えてビット操作を題材にするところが面白い(?)ところです。実は意外な使い道があったり、最先端のマルチメディア処理にも欠かせなかったりするので、そのあたりに話をつなげていこうと思います。 |
|
|
|
|
|
さて、C/C++でビット操作を行うにはビット演算子を用いるのが一般的です。言うまでもありませんが、AND(&)、OR(|)、XOR(^)、NOT(~)、左右ビットシフト(<<,>>)です。通常のビット演算はこれらでまず事足ります。32ビットCPU用のC(以降、特に説明のない限り、Cは32ビットCPU用とします)での操作例として、ロングワード中の最下位ビット(bit0)を読み書きするものを考えてみましょう。 |
|
|
1ビットを0にする : dwData &= 0xFFFFFFFE;
1ビットを1にする : dwData |= 0x00000001;
1ビットを読み出す : if ( dwData & 0x00000001 ) { 文1 } else { 文2 }
|
|
|
と、通常はこのようになります。このあたりはC初心者の方以外は解説の要らないところでしょう。
では、これ以外の方法を考えてみましょう。Cでビット演算子を使用しないでビット操作を実現するには、構造体のビットフィールドを利用するという手があります。Cの中級者以上であれば、構造体は間違いなくご存じでしょう。一連の変数をまとめて自由な名前を付けて操作できるようにするものです。一般的な使用例を以下に示します。
|
|
|
struct tagPerson {
int age; /* 年齢 */
int sex; /* 性別(0:男 1:女) */
float weight; /* 体重 */
float footsize; /* 靴のサイズ */
} Psn;
Psn.age = 13
Psn.sex = 1;
Psn.weight = 38.5;
Psn.footsize = 22.0;
|
|
|
このように、個人情報データに仮に"Psn"という名前をつけて、"."(ピリオド)でつないで中のメンバを指定することができます。
この例では4つのメンバが含まれており、各4バイトのサイズがあるので、全体で16バイトのサイズがあります。でも、よく見てみると、これらの情報を保存するのに16バイトも必要がないことがわかると思います。年齢は0~せいぜい127歳なので7ビット、性別は普通に考えて1ビット、体重は~255Kgとして8ビット、100g単位とすれば+4ビット、靴のサイズは5mm刻みで~40cmとしても12ビットで十分です。これをビットフィールドを使った構造体で定義すると |
|
|
struct tagPerson {
unsigned int age : 7;
unsigned int sex : 1;
unsigned int weight : 12;
unsigned int footsize : 6;
unsigned int footsize_dec : 1;
} Psnb;
|
|
|
と記述することができます。この構造体のサイズは27ビット。4バイトでおつりが来ます。メンバのsex(性別)は1ビットですが、このビットの読み書きは、 |
|
|
0にする : Psnb.sex = 0;
1にする : Psnb.sex = 1;
読み出す : if ( psnb.sex ) { 文1 } else { 文2 }
|
|
|
とするだけでOKです。もちろんこれは記述が簡素になるだけで、実際の動作が効率化するわけではありません。コンパイラがビット演算を使用して展開してくれるのです。他のメンバも同様で、2ビット以上のサイズのビットフィールドメンバは、シフト演算等が施されてから処理されます。つまり、データサイズ的には○ですが、プログラムサイズ&速度的には×となります。これはビット操作の宿命かもしれません。
ちなみに、サイズが1bitのメンバはint型ではなくunsigned int型で定義しておくようにしましょう。int型だと、0または1ではなく、0または-1が入ることになるからです。思わぬバグの原因になりかねないので要注意です。
下のアセンブルリストは最上行のCのソース1行をVisualC++.NET2003がコンパイルして出力したものです。nExCntメンバは、ビット構造体のbit8~bit15に割り当てられた1バイトサイズの領域です。そこへint型の自動変数nCountを代入しています。コンパイラは代入する値をテンポラリレジスタecxにロードし、FFHとANDをとって上位24bitを0クリアし、これを左に8bitシフトします。ANDをとるのはnCountが負の値や8bit以上の値(256以上)になっていたときに対処するためです。次に代入先8bit領域を含むロングワードをedxレジスタにロードし、FFFF00FFHとANDをとって代入先領域を0クリアします。これと先程のecxレジスタとORをとり、書き戻して処理は終了です。
いかにビット処理が面倒かがおわかりいただけたと思います。CPUがうまく投機実行(順序入れ替え実行)してくれないと並列処理もできず、クロックが大きく無駄になってしまいます。頻繁にアクセスする場合はできるだけ控えたい手法であるとも言えるでしょう。 |
|
|
inf.b.nExCnt = nCount; // 実行カウンタ値を更新
mov ecx, DWORD PTR _nCount$[ebp]
and ecx, 000000ffH
shl ecx, 8
mov edx, DWORD PTR _inf$[ebp]
and edx, ffff00ffH
or edx, ecx
mov DWORD PTR _inf$[ebp], edx
|
|
|
|
|
|
しかし、このビットフィールド構造体はこのままでは使いにくいことがままあります。Psnb構造体は4バイトに収まるサイズですから、ロングワードの変数にもコピーできるはずです。実際にこれができると、いろいろと都合がよいのです。例えばWin32APIのSet/GetWindowLong関数を使って、最大サイズの決まっているウィンドウの補足情報域を有効に使うことができます。(ヒープに必要メモリを確保して、そのポインタを補足情報域に入れておく方法が一般的ですが、エラートラップなどを考えるとかなり面倒です。)
サイズが小さくても構造体は構造体ですから、 |
|
|
CopyMemory ( &dwData, &Psnb, sizeof ( dwData ) ) ;
|
|
|
とするのがスジなのかもしれませんが、たかだかロングワードのブロック転送は精神的によくありません。そこで、キャストを使って、 |
|
|
dwData = *( ( DWORD * ) &Psnb ) ;
|
|
|
のようすれば、いちおうは目的を達成することができます。しかし、この方法では見た目があまり美しくありませんし、キャストという「危険」を冒さなければなりません。
そこで、そんな場合はちょっと面倒ですが共用体を利用する方法があります。 |
|
|
union tagDWORD {
long lDword;
struct tagPerson Psnb;
} Share;
|
|
|
と記述して、lDwordとPsnbが同じメモリエリアに重なるように定義します。こうしておけば、 |
|
|
dwData = Share.lDword;
|
|
|
とするだけで、dwDataにビットフィールド構造体Psnbをまるごと単純にコピーすることができます。ちなみに、Psnbのメンバにアクセスするときは、
|
|
|
Share.Psnb.age = 16;
|
|
|
のように記述します。
これは制御関連の仕事が多い私がよく利用する手段でもあります。ビットフィールド構造体を利用することによって、簡便に、かつ柔軟な対処がしやすくなるからです。特に制御する機器側のプログラムも同時に開発している場合、ハード的な変更があっても、メンバ名を変えることなく、ビットフィールドの割り当てを変更するだけで対処できることがよくあり、非常に効果的です。少なくともビット演算用の定数を#defineなどで定義するよりはスッキリと記述することができると思います。
|
|
|
|
|
|
第一回はこんなところで。次回もビット操作に関する話です。言ってしまえば、今回の続きですね。ただし、C/C++の限界を超えた部分の話になります。これだけ言うとなんとなく想像はつきますよね。ビット操作はやはりCPU命令が最も得意としているのですから。 |
|
|
|
|