attribute packedに関するコード生成

各 OSの IPヘッダ構造体を見ていると, attribute packedを使っているのと
使っていないのがあってなんでだ、って思って生成するコードを調べてみた。


attribute packedをつけているのは NetBSD, FreeBSD.
つけてないのが Linux, OpenBSDです.

サンプルコード

全部だと長いので頭のほうだけ取り出したものを例に示します。

struct packed_ip {
        unsigned char  ip_hl:4,
                       ip_v:4;
        unsigned char  ip_tos;
        unsigned short ip_len;
} __attribute__((__packed__));

struct not_packed_ip {
        unsigned char  ip_hl:4,
                       ip_v:4;
        unsigned char  ip_tos;
        unsigned short ip_len;
};


int get_ip_len_packed (struct packed_ip *a)
{
	return a->ip_len;
}

int get_ip_len_not_packed (struct not_packed_ip *a)
{
	return a->ip_len;
}


これをコンパイルします。わかりやすいように mipsのクロスコンパイラを
使いました。GCC version 4.6.1

   % mipsel-linux-elf-gcc -c -O2 align_attribute.c

アセンブル結果した結果は以下のようになりました。

00000000 <get_ip_len_packed>:
   0:   90820003        lbu     v0,3(a0)
   4:   90830002        lbu     v1,2(a0)
   8:   00021200        sll     v0,v0,0x8
   c:   03e00008        jr      ra
  10:   00431025        or      v0,v0,v1

00000014 <get_ip_len_not_packed>:
  14:   94820002        lhu     v0,2(a0)
  18:   03e00008        jr      ra
  1c:   00000000        nop

ともに unsigned short型のメンバ変数を読み出しているだけ
なのですが、packedの方は 4命令使っています。2バイトのデータを
1バイトずつ読み出し、シフトし、orを取っています。一方 packed
しない方は half word load一回だけです。


参考までに armでの結果も示します。

00000000 <get_ip_len_packed>:
   0:   e5d03002        ldrb    r3, [r0, #2]
   4:   e5d00003        ldrb    r0, [r0, #3]
   8:   e1830400        orr     r0, r3, r0, lsl #8
   c:   e12fff1e        bx      lr

00000010 <get_ip_len_not_packed>:
  10:   e1d000b2        ldrh    r0, [r0, #2]
  14:   e12fff1e        bx      lr

armでも同様です。orrの列は orと shiftを同時にやっているっぽい.

考察

このような命令が生成される具体的な理由はわからないですが、
packedをつけた場合は, データ構造が整列されていないものと想定して
コンパイラはコード生成を行っているものと考えられます。
もしデータが整列していない場合, (shortが奇数バイトに配置されている等)
half word loadを行うと alignment例外が発生して, ユーザランドなら
終了, カーネルなら最悪 panicという自体になります。ですが, 上記の
ように 1バイトずつ読めば alignment例外が発生することはありません.
そのあたりのことを考慮しているのだと考えられます。


ネットワークプロトコルのようなぎゅうぎゅうに詰まったデータだと
packedを付けないといけないのかなと思ってしまうのですが、それで
結構性能やコードサイズを犠牲にしているのではないかと思いました。
OpenBSDLinuxがつけていなくていないのは, そのあたりのことも
いくらか考慮されているのかもしれません。(属性をサポートしていない
コンパイラを考慮しているだけかもしれないですが・・・)

packedをつけた場合のコード効率の改善

FreeBSDでは packedをつけたものだと効率が悪いということで、
packedに加えて aligned attributeも付けられています。その場合の
例を示します。

struct packed_aligned_ip {
        unsigned char  ip_hl:4,
                       ip_v:4;
        unsigned char  ip_tos;
        unsigned short ip_len;
} __attribute__((__packed__)) __attribute__((aligned(4)));

int get_ip_len_packed_aligned (struct packed_aligned_ip *a)
{
	return a->ip_len;
}

これを MIPSコンパイラコンパイルした結果は

00000020 <get_ip_len_packed_aligned>:
  20:   94820002        lhu     v0,2(a0)
  24:   03e00008        jr      ra
  28:   00000000        nop

ARMの場合は

00000018 <get_ip_len_packed_aligned>:
  18:   e1d000b2        ldrh    r0, [r0, #2]
  1c:   e12fff1e        bx      lr

このようになり, packedをつけていない場合とどうようの
コードになります。アラインメントされているいう情報から
このようなコードが生成されたのでしょう.


packedをつけたデータが必ず nバイトのアラインメントにある
場合であれば, aligned属性も合わせてつけた方がよいかと思い
ます。FreeBSDについてはつけるべきでないと考えているの
ですが、確証が得られていないのでそれはもう少し調べてから
書くことにします。

まとめ

attribute packedをつけたデータ構造について生成されるコード
について述べました。