gccのbit field使用上の注意

gcc-4.3.4でbit fieldがどのように格納されるかを調べた。
bit fieldは規格上どのように格納されるかは規定されていない。私は、サイズが大きくならないように詰め込まれて行くものと理解していたが、gccで試したところパフォーマンスの観点からか、必ずしもそうはならないようだ。

処理系によってオーダーが変わるという注意は良く見掛けますが、どのようにパディングされるのか、また任意の配置にするにはどうしたら良いかというのを知らないと、嵌まることになります。

確認方法

シリアル通信関係のプログラムを考えているときに、stop/start bitを含め10bitでキャラクタを表現する方法を調べていたところ、構造体の定義の仕方によって挙動が異なることに気づきました。

テストプログラム

print関数はそれぞれの変数の値(decimal)と、供用体(binary)を表示します。
stop, start, cのそれぞれに1を立てて、どこが使われているか調べます。

#include 
#include 

union serial {
    unsigned short int all;
    struct {
        unsigned char start : 1;
        unsigned char c     : 8;
        unsigned char stop  : 1;
    } part;
} s;

void print(union serial *s){
    int i,*v;
    v=(int *)s;

    printf("all=%x stop=%x c=%x start=%x\n",
      s->all, s->part.stop, s->part.c, s->part.start);
    i=sizeof(*s)*8;
    while(i){
        printf("%d", 1&(*v>>--i));
        if(!(i%8))
            putchar(' ');
    }
    printf("\n");
}

int main(void){
    bzero(&s,sizeof(s));
    s.part.stop=1;
    print(&s);

    bzero(&s,sizeof(s));
    s.part.start=1;
    print(&s);

    bzero(&s,sizeof(s));
    s.part.c=255;
    print(&s);

    return(0);
}

実行結果

all=0 stop=1 c=0 start=0
00000000 00000001 00000000 00000000 
all=1 stop=0 c=0 start=1
00000000 00000000 00000000 00000001 
all=ff00 stop=0 c=ff start=0
00000000 00000000 11111111 00000000 

考察

供用体のサイズが4 bytes

詰めれば2 bytes、後述のchar型を意識しても3 bytesで収まるはずだが、供用体のサイズは4 bytesとなった。int型に昇格してしまった方が効率的と言う事だろうか?

part.cは2バイト目から配置

bit fieldは連続した領域には置かれておらず、それぞれの変数はそれぞれ異なるバイトオフセットに配置されてしまいました。恐らく、2番目の要素cが8bitの為、これはcharに納めておくほうが効率がよさそうだとコンパイラが判断したのだと思います。

供用体のサイズが4 bytes

改善策

場合によっては、前述のようになっていても問題は無いのですが、今回はs.allをbit処理することで転送に使い、キャラクタはs.part.cで取り出す。みたいな使い方をしたいので、連続領域に配置されていないと都合が悪い。なので、連続で配置されるよう対策を考えてみます。

bit fieldの定義をshort intに変える方法

つまり、次のように宣言を変更します。

union serial {
    unsigned short int all;
    struct {
        unsigned short start : 1;
        unsigned short c     : 8;
        unsigned short stop  : 1;
    } part;
} s;

こうすると、char境界を意識されないので、1 bytes毎にわけられません。
また、サイズも2 bytesで収まるようになりました。
実行結果

all=200 stop=1 c=0 start=0
00000010 00000000 
all=1 stop=0 c=0 start=1
00000000 00000001 
all=1fe stop=0 c=ff start=0
00000001 11111110 

packedアトリビュートを使う方法

gccには、構造体がパディングされないようにするための方法が用意されています。
これを使ってみます。

union serial {
    unsigned short int all;
    struct {
        unsigned char start : 1;
        unsigned char c     : 8;
        unsigned char stop  : 1;
    } __attribute__((packed)) part;
} s;

これはダメでした。
良く考えれば、bit fieldは元の型を任意のbit数で分割するので、byte境界を跨いで指定することは出来ない = 3組の異なるbit fieldを指定した。という解釈になるわけですね。

では、packed attributeの存在異義はどこににあるのかですが、次のようにint(32 bit)をbit fieldで分けたケースで2 bytesで収まるようになりました。

union serial {
    unsigned short int all;
    struct {
        unsigned int start : 1;
        unsigned int c     : 8;
        unsigned int stop  : 1;
    }  __attribute__((packed)) part;
} s;

あるいは、最初のケースで、3 bytesで済むはずのものが4 bytesになってしまうとき、次のようにすると3 bytesになることがわかりました。

union serial {
    unsigned short int all;
    struct {
        unsigned char start : 1;
        unsigned char c     : 8;
        unsigned char stop  : 1;
    } part;
} __attribute__((packed)) s;