まずはこのコードを見てもらいたい。
use std::num::NonZeroU8;
use std::mem::size_of;
fn print_size<T>(){
println!("{}", std::any::type_name::<T>());
println!("no: {}",size_of::<T>());
println!("opt: {}",size_of::<Option<T>>());
println!("opt2:{}",size_of::<Option<Option<T>>>());
}
fn main() {
print_size::<u8>();
print_size::<NonZeroU8>();
print_size::<String>();
}
この結果はどうなるだろうか?
Optionでデータが増えるのだからsizeが増えるように見えるが…。
正解は以下の様である。
u8
no: 1
opt: 2
opt2:2
core::num::nonzero::NonZeroU8
no: 1
opt: 1
opt2:2
alloc::string::String
no: 24
opt: 24
opt2:32
Optionが増えているのにsizeが変わっていないことがある!
これはRustのniche optimizationが働いているからである。
niche
nicheとはもともと「隙間」という意味がある。
Rustはその「隙間」を検知してその間に情報を埋めることができる。
例えばNonZeroU8
という型がある。
その名の通り、「ゼロにならないu8」な型である。
ゼロにならないのは(unsafeを使わない限り)保証されているので、この型には0
という隙間がある。
つまりNoneという情報をその0
に埋めてしまえばいいということだ。
メモリ上の表現として
- 0ならNoneとして扱う。
- それ以外ならSome(NonZeroU8)として扱う。
ということができるのである。
Stringの場合は内部にポインタを持っていて、これがnullポインタでないことが保証されている(unsafeで変なことしない限り)。
そういうことで
- ポインタがnullならNoneとして扱う。
- それ以外ならStringとして扱う。
というようなことをしてくれる。
またu8のときにoptとopt2でサイズが増えないのは、Optionの情報を1byteの領域でSome(Some)かSome(None)、Noneかのフラグを保存できるからである。
ZST、Empty Type
さてstd::convert::Infallible
という型でも上のprint_sizeを走らせてみると以下ようになる。
no: 0
opt: 0
opt2:1
なんとサイズは0になる。
バグに見えるかもしれないが、そうではない。
Rustは型の中身を見てその型の取りうる値が1通りしかない場合、実行時にはその情報がいらないとみて、コンパイル時に消してくれるのだ。これをZST(Zero-Sized-Type)と呼ぶ。
ZSTとして有名なものはunit()
だ。これは他の言語でいうvoidで、返り値がない型を表している。またZSTは自分で定義できる。struct Hoge;
だけでこのHogeはZSTとして扱われる。
ZSTはGenericsに入れてやることで、実行時には情報が残らない0コスト抽象化でよく使われるので覚えておくと高速化に役に立つかもしれない。
もとの結果に戻るとここのopt(つまりOption<Infalliable>
)でもサイズは0である。Option
はSome
とNone
の二通りであるから少なくともZSTでないと見えないか?
ここでInfalliableの型定義を見てみるとこうである。
pub enum Infallible {}
variantがないenumは作られない、なので実質never typeとして扱われる。このような型はEmpty Typeと呼ばれる。
Option<Infalliable>
ではSome(Infalliable)
になることはないとして、None
しかないからZSTとして扱われるようになるのである。
ちなみにこのInfalliable
は将来的にはnever type!
のaliasになってdeprecatedになっていく予定だが!
の安定化はまだみたいである。
またこのOption<Infalliable>
はTry
(?で返す記法)と関連があるので興味がある人はtry-trait-v2とか見てみると面白いかもしれない。
現状と展望
以上のようにRustは型から効率の良いメモリの配置にしてくれる。
ただ現状理論上Nicheとして最適化してくれるところでも効かない場合がある。
またMayBeUninit
などとの相性が悪いため仕様としてNicheが効かなくなっているところもある。
しっかりとNicheになってくれることが保証されているのはここのように書かれている場所のみである。
それから今、Nicheのしっかりと保証しようというrfcが上がっている。
またNicheなものに対してtransmuteしても動くはずであるが、これを安全に動かすためのtrait定義を作るというrfcもある。
そういった安全な最適化の進展が楽しみである。