Open2

自作Rustクイズ

lumaluma

疑問に思ったことをそのままクイズ形式にするスクラップを始めてみます。

そのクイズを作った直近(もしくは直近いくつか)のRustの仕様、ドキュメント、もしくは実装を参考にしています。

lumaluma

Q. Option<i8>のようなものを自分で実装しようと思います。以下の2つはメモリ的にどちらが有利でしょうか。

pub enum MyOption1 {
    None,
    Some(i8),
}
pub struct MyOption2 {
    pub is_some: bool,
    pub some: i8,
}
答え

message
書いたものの、実装が仕様であるというのを前面に出す形になってしまいました。

簡単に実行して確かめるなら以下を実行します。

use std::mem;

pub enum MyOption1 {
    None,
    Some(i8),
}

pub struct MyOption2 {
    pub is_some: bool,
    pub some: i8,
}

fn main() {
    println!("{}", mem::size_of::<MyOption1>());
    println!("{}", mem::size_of::<MyOption2>());
}

どちらも2と出力され、同等に2バイトで表現されていることがわかります。
よって、答えはどちらも同等ということになります。このことを確認していきます。

enum関連の用語

まずはenumの定義に登場するそれぞれの用語を確認します。

pub enum MyEnum1 {
    UnitItem,
    TupleItem(i8),
    StructItem { field: i8 },
}

pub enum MyEnum2 {
    UnitWithDiscriminant = 128,
}
  • アイテム: 各行のUnitItemTupleItem(i8)
    • ユニットアイテム: UnitItem,UnitWithDiscriminantなど。
    • 非ユニットアイテム
      • タプルアイテム: TupleItem(i8)など。フィールドとしてタプルを持つ。
      • 構造アイテム: StructItem { field: i8 }など。フィールドとして構造を持つ。
  • アイテムフィールド: 各アイテムと一緒に定義されるタプルやフィールドのこと
  • 判別子(discriminant): 各アイテムの番号付け。何も指定しないと0から順につけられる。
    • UnitWithDiscriminant = 128のように指定可能。省略するとそこからインクリメントされる。
    • その型はデフォルトでisize#[repr(u8)]などで判別子に用いられる型を指定可能。

「判別子」はdiscriminantの造語訳。他に良い訳があればご教示いただけると幸いです。

enumのメモリレイアウト

enumは最初の1octetでどのアイテムであるかを指定し、続くメモリのうち必要な分だけ利用して、フィールドの値を格納します。

enum general memory layout

当然ですが、最大で同時に一つまでのフィールドのみを考慮すれば良いので、フィールド部のメモリは、各フィールドのうち最大のサイズで確保されます。

たとえば、MyOption1のメモリレイアウトは以下になります。

enum MyOption1 memory layout

あえてstructで書くならば、以下のようになります。

struct EnumInStruct {
  discriminant: isize,
  field0: u8,
  field1: u8,
  field2: u8,
  // ...
}

このフィールド部を無理やりunsafeで操作すれば自分だけのenumがstructで表現できるでしょう。
これにより、結局MyOption1とMyOption2はほぼ似たような構造体だったことになります。structのメモリモデルは後述します。

判別子を確認する

まずはRustビルトインのOptionとMyOption1で判別子を見てみます。
std::mem::discriminantを利用します。

use std::mem;

pub enum MyOption1 {
    None, // discriminant = 0
    Some(i8), // discriminant = 1
}

fn main() {
    let some: Option<i8> = Some(0x42);
    let none: Option<i8> = None;

    println!("{:?}", mem::discriminant(&none));
    // > 0

    println!("{:?}", mem::discriminant(&some));
    // > 1

    println!("{:?}", mem::discriminant(&MyOption1::None));
    // > 0

    println!("{:?}", mem::discriminant(&MyOption1::Some(123)));
    // > 1
}

実際のメモリ上の値を確認する

enumのメモリレイアウトをダンプして実際に確かめましょう。なお、環境依存で結果が変わる可能性があります。
以下のようなmem_dump関数を実装してみます。非同期等は一切考慮していないので現場では使用しないでくださいね。

use std::mem;

fn mem_dump<T>(value: &T) -> String {
    let view = value as *const _ as *const u8;
    let size = mem::size_of::<T>();
    (0..size)
        .map(|i| {
            format!(
                "{:02x}{}",
                unsafe { *view.offset(i as isize) },
                if i + 1 == size { "" } else { " " }
            )
        })
        .collect::<Vec<String>>()
        .join("")
        .to_string()
}

fn main() {
    let some: Option<i8> = Some(0x42);
    let none: Option<i8> = None;

    println!("none: {}", mem_dump(&none));
    // > 00 00

    println!("some: {}", mem_dump(&some));
    // > 01 42

    println!("0x42i8: {}", mem_dump(&0x42i8));
    // > 42
}

16進表記で出力されます。メモリレイアウトと照らし合わせて、手前の2桁がdiscriminant、後続がfieldで、Someの場合はフィールド部にフィールド値がそのまま書かれているのがわかるかと思います。

mem_dumpに関する注意事項

i8以外の型で試してみることは有用ですが、バイトオーダーには気をつけてください。

// ...omitted...

fn main() {
    println!("12345u16: {}", mem_dump(&12345u16));
}

39 30と表示されればリトルエンディアンということになります。

> 3*(16**1) + 9*(16**0) + 3*(16**3) + 0*(16**2)
12345

message
未調査: 実際に環境依存で変わりうるのか否か(Rustがバイトオーダーを定義しているのか)、というところは未調査です。

structのメモリレイアウト

structのメモリレイアウトに関してはType Layout#representationsに詳しく書かれています。

一切の保証はないのですが、フィールドサイズが等しければCと同様にそのまま並べてくれます。構造体内のフィールドサイズがバラバラだと並び替えが起こります。
なお、boolが1bitではなく1byteなのは、Rustがメモリを扱う最小単位が1byteだからです。 (Primitive Data Layout)
その他、少しだけ以下のスクラップにまとめています。(中途半端なのでそのうち加筆するかも)

B: Type layout - The Rust Reference

リファレンス