Rustの構造体メモリレイアウト

Rustの構造体のメモリレイアウトについてのメモ。

Rustで次のような構造体を定義したときに、構造体のメモリレイアウトはどうなるか?

struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

検証時のRustのバージョンは次の通り。

stable-x86_64-unknown-linux-gnu
rustc 1.24.1 (d3ae9a9e0 2018-02-27)

TL;DR

先に結論を書く。
アトリビュート指定によって構造体のメモリレイアウトとサイズは以下のように変化する。

デフォルト

f:id:ryochack:20180323172547p:plain 構造体サイズ20Byte

repr(C)アトリビュート指定

f:id:ryochack:20180323172534p:plain 構造体サイズ24Byte

repr(packed)アトリビュート指定

f:id:ryochack:20180323172539p:plain 構造体サイズ17Byte

以下に確認の過程を残しておく。

C言語的にメモリレイアウトを予想

まずは、C言語的にRustの構造体でのメモリレイアウトを予想してみる。
各フィールドの宣言順の通りにメモリ上にデータが配置され、アライメント制約を守るために適宜パディングが入るとすると、次のようなメモリ配置になり、構造体サイズは24Byteとなるはず。

f:id:ryochack:20180323172534p:plain

確認方法

次のようなコードを書いて確認した。

use std::mem;

struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

fn main() {
    let mem = Layout {
        b1: 0,
        b2: 0,
        b3: 0,
        s1: 0,
        s2: 0,
        s3: 0,
        w1: 0,
        w2: 0,
    };

    let mem_ptr: *const Layout = &mem;
    let base = mem_ptr as usize;
    let ptr_b1: *const u8 = &mem.b1;
    let ptr_b2: *const u8 = &mem.b2;
    let ptr_b3: *const u8 = &mem.b3;
    let ptr_s1: *const u16 = &mem.s1;
    let ptr_s2: *const u16 = &mem.s2;
    let ptr_s3: *const u16 = &mem.s3;
    let ptr_w1: *const u32 = &mem.w1;
    let ptr_w2: *const u32 = &mem.w2;

    println!("Memory size = {}", mem::size_of::<Layout>());
    println!("base = 0x{:x}", base);
    println!("offset ptr_w1 = {}", ptr_w1 as usize - base);
    println!("offset ptr_w2 = {}", ptr_w2 as usize - base);
    println!("offset ptr_s1 = {}", ptr_s1 as usize - base);
    println!("offset ptr_s2 = {}", ptr_s2 as usize - base);
    println!("offset ptr_s3 = {}", ptr_s3 as usize - base);
    println!("offset ptr_b1 = {}", ptr_b1 as usize - base);
    println!("offset ptr_b2 = {}", ptr_b2 as usize - base);
    println!("offset ptr_b3 = {}", ptr_b3 as usize - base);
}

デフォルト

前述のコードを使って確認したところ、以下のような構造体のメモリレイアウトになっているのがわかった。

f:id:ryochack:20180323172547p:plain

Rustではメモリが最も節約されるように、構造体の各フィールドは各型サイズ降順に並び替えられるようだ。
そのおかげで、最初に24Byteと予想した構造体のサイズも20Byteに抑えられている。

repr(C)

前述のようなメモリレイアウトの最適化は、通常のアプリケーションを作る上ではありがたい。
しかし、組み込みソフトのドライバ開発でペリフェラルレジスタマップを構造体で定義したい場合などには、構造体のフィールドが勝手に並び替えられてしまうのは具合がよくない。
そこでRustには、構造体のメモリレイアウトの最適化を抑制する #[repr(C)] アトリビュートが用意されている。
構造体定義の頭に #[repr(C)] アトリビュートを追加してみよう。

#[repr(C)]
struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

#[repr(C)] アトリビュートを指定後のメモリレイアウトは次のようになった。 これは最初に予想したC言語的なメモリレイアウトと一致する。

f:id:ryochack:20180323172534p:plain

よって、構造体のメモリレイアウト最適化を抑制したい時には #[repr(C)] アトリビュートを指定すればいい。

repr(packed)

Rustには、#[repr(C)] アトリビュートの他にも、#[repr(packed)] アトリビュートが用意されている。
#[repr(packed)] アトリビュートは構造体のフィールド間のパディングを無くすアトリビュート指定となる。(gccなどが提供する pragma pack(1) 指定と同じ効果が得られる)

#[repr(packed)]
struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

#[repr(packed)] アトリビュートを指定後のメモリレイアウトは次のようになった。

f:id:ryochack:20180323172539p:plain

構造体のデータ途中のパディング、及び最後のパディングが削除されているため、構造体のサイズも最小の17Byteとなる。 ただし、このメモリレイアウトはアライメント制約を無視したものとなっているため、通信のペイロードサイズに制約がありとにかくデータを詰めて送りたい、などの場合にのみ使うのが良さそう。

※補足として、#[repr(C)] アトリビュート#[repr(packed)] アトリビュートの両方を同時に指定してもこのメモリレイアウトになる。

参考