自作Rustクイズ
疑問に思ったことをそのままクイズ形式にするスクラップを始めてみます。
そのクイズを作った直近(もしくは直近いくつか)のRustの仕様、ドキュメント、もしくは実装を参考にしています。
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,
}
- アイテム: 各行の
UnitItem
やTupleItem(i8)
- ユニットアイテム:
UnitItem
,UnitWithDiscriminant
など。 - 非ユニットアイテム
- タプルアイテム:
TupleItem(i8)
など。フィールドとしてタプルを持つ。 - 構造アイテム:
StructItem { field: i8 }
など。フィールドとして構造を持つ。
- タプルアイテム:
- ユニットアイテム:
- アイテムフィールド: 各アイテムと一緒に定義されるタプルやフィールドのこと
-
判別子(discriminant): 各アイテムの番号付け。何も指定しないと0から順につけられる。
-
UnitWithDiscriminant = 128
のように指定可能。省略するとそこからインクリメントされる。- 重複やオーバーフローはコンパイル時エラー
- 非ユニットアイテムと判別子指定は併用はできない ("Allow arbitrary enums to have explicit discriminants")
- その型はデフォルトで
isize
。#[repr(u8)]
などで判別子に用いられる型を指定可能。
-
「判別子」はdiscriminantの造語訳。他に良い訳があればご教示いただけると幸いです。
enumのメモリレイアウト
enumは最初の1octetでどのアイテムであるかを指定し、続くメモリのうち必要な分だけ利用して、フィールドの値を格納します。
当然ですが、最大で同時に一つまでのフィールドのみを考慮すれば良いので、フィールド部のメモリは、各フィールドのうち最大のサイズで確保されます。
たとえば、MyOption1
のメモリレイアウトは以下になります。
あえて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