タイトルの通りなんですが、RustのsliceやVecがしっくり来ていないので、メモリ上でどのように表現されているのかという観点で理解を深めようと思います。
環境
❯ cargo version
cargo 1.32.0
❯ rustc --version
rustc 1.33.0
また今回の環境ではusize
は8バイト長です。
println!("size of usize: {}", std::mem::size_of::<usize>()); // size of usize: 8
事前準備
メモリ上でどのように表現されているか確認するため、RustのSizedとfatポインタ - 簡潔なQのas_raw_bytes
を利用します。
引数x
を*const T
でキャストして、生ポインタからstd::mem::size_of_val(x)
で得たバイト長を読み出します。
fn as_raw_bytes<'a, T: ?Sized>(x: &'a T) -> &'a [u8] { unsafe { std::slice::from_raw_parts( x as *const T as *const u8, std::mem::size_of_val(x)) } }
結論
[T]
はスタック領域に連続して表現されるVec<T>
はスタック領域に「実データへのポインタ」「len
」「cap
」を持つ&str
はスタック領域に「実データへのポインタ」「len
」を持つ
具体例
[i32]
まずは[i32]
(i32
のslice)でメモリ上の内容を見てみましょう。
let a: [i32; 5] = [255, 256, 1023, 1024, 1025]; println!("{:x?}", as_raw_bytes(&a));
実行結果は以下のようになります。 読みやすいように改行などの整形を入れています。 以降でも同じように整形して読みやすくしています。
[ff, 0, 0, 0, 0, 1, 0, 0, ff, 3, 0, 0, 0, 4, 0, 0, 1, 4, 0, 0]
これだけだと分かりづらいので、as_raw_bytes
の返り値の型が[u8]
であることを考慮して0
を補完します。
[ff, 00, 00, 00, 00, 01, 00, 00, ff, 03, 00, 00, 00, 04, 00, 00, 01, 04, 00, 00]
リトルエンディアンで表現されていることに注意すれば、
0d255
= 0x000000ff
0d256
= 0x00000100
0d1023
= 0x000003ff
0d1024
= 0x00000400
0d1025
= 0x00000401
となることがわかると思います。 またヒープ領域へのポインタなどもなく、スタック領域に確保もされていることも確認できました。
Vec<i32>
次にVec<i32>
を見ていきます。
let a: Vec<i32> = vec![1, 2, 3, 4]; println!("{:x?}", as_raw_bytes(&a));
結果は以下のようになりました。
[d0, 2c, c0, d5, a8, 7f, 00, 00, 04, 00, 00, 00, 00, 00, 00, 00, 04, 00, 00, 00, 00, 00, 00, 00]
ここでVec<T>
の実装を見てみましょう。
vec.rs
の実装は以下のようになっています。
#[stable(feature = "rust1", since = "1.0.0")] pub struct Vec<T> { buf: RawVec<T>, len: usize, }
またRawVec
はraw_vec.rs
で実装されています。
#[allow(missing_debug_implementations)] pub struct RawVec<T, A: Alloc = Global> { ptr: Unique<T>, cap: usize, a: A, }
このようにVec<T>
はlen
とcap
を持ちます。
改めて結果を見てみると後半の4
から始まる8バイト2行はlen
とcap
であることがわかります。
// (再掲) [d0, 2c, c0, d5, a8, 7f, 00, 00, 04, 00, 00, 00, 00, 00, 00, 00, 04, 00, 00, 00, 00, 00, 00, 00]
最初の8バイトが実データへのポインタになっているのですが、本当にそうなっているか参照外しをして確認しましょう。
let a = vec![4, 1, 2, 3]; unsafe { let p = a.as_ptr(); println!("{:?}", p); // 0x7feba05027c0 println!("{:?}", *p); // 4 <- 先頭のデータ let data: &[u8] = std::slice::from_raw_parts(p, a.len()); println!("{:?}", data); // [4, 1, 2, 3] }
*p
やdata
の出力結果から、確かに実データへのポインタとなっていました。
文字列まわり
[i32]
とVec<i32>
を確認できたので、おまけとして文字列を扱う際に登場する以下3つのメモリ内容も確認してみます。
[char]
、Vec<char>
[&str]
、Vec<&str>
String
、Vec<String>
char
[char]
から見ていきましょう。
let a = ['a', 'b', 'c']; println!("{:x?}", as_raw_bytes(&a)); // [61, 0, 0, 0, 62, 0, 0, 0, 63, 0, 0, 0]
[i32]
と同じでスタック領域のみで表現されていることがわかります。
ちなみに、char - Rustにあるようにchar
は4バイト長です。
char
is always four bytes in size.
次にVec<char>
です。
let a = vec!['a', 'b', 'c']; println!("{:x?}", as_raw_bytes(&a)); // [d0, 2c, 40, d7, ad, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0] unsafe { let p = a.as_ptr(); println!("{:?}", p); // 0x7fadd7402cd0 println!("{:?}", *p); // 'a' let data = std::slice::from_raw_parts(p, a.len()); println!("{:?}", data); // ['a', 'b', 'c'] }
as_raw_bytes(&a)
の出力結果を整形します。
[d0, 2c, 40, d7, ad, 7f, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00]
len
とcap
が3
(うしろ2行)で、1行目はヒープにある実データへのポインタになっています。
実際、unsafe
の中で参照外しを行うと'a'
が得られるため、実データの先頭アドレスであったことがわかります。
[&str]
続けて[&str]
です。
let a: [&str; 3] = ["a", "b", "c"]; println!("{:x?}", as_raw_bytes(&a)); // [82, 59, 85, 4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 80, 59, 85, 4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 81, 59, 85, 4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
今までとは様子が違いそうです。 理解を深めるため、別の例をもうひとつ表示してみます。
let a: [&str; 3] = ["a", "ab", "abc"]; println!("{:x?}", as_raw_bytes(&a)); // [35, 77, 46, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 30, 77, 46, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 32, 77, 46, 1, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
上記2つの結果を見やすいように整形しました。
[82, 59, 85, 04, 01, 00, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, 80, 59, 85, 04, 01, 00, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, 81, 59, 85, 04, 01, 00, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00]
[35, 77, 46, 01, 01, 00, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, 30, 77, 46, 01, 01, 00, 00, 00, 02, 00, 00, 00, 00, 00, 00, 00, 32, 77, 46, 01, 01, 00, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00]
実は、str - Rustにあるように&str
は実データへのポインタとlen
で構成されます。
A &str is made up of two components: a pointer to some bytes, and a length.
今回の表示した対象は[&str]
(&str
のslice)です。[i32]
の例でみたようにsliceはスタック領域に連続してデータを格納します。
そして&str
は上記の通り、実データへのポインタとlen
で表現されます。
これらの組み合わせで、ポインタ+len
が3つ並ぶ結果となったのです。
念のため、3つの要素のうち最初の8バイトが実データへのポインタであることも確かめておきましょう。
let a = ["a", "ab", "abc"]; println!("{:x?}", as_raw_bytes(&a)); // [55, 8e, f9, 7, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 50, 8e, f9, 7, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 52, 8e, f9, 7, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0] unsafe { for i in 0..a.len() { println!("-------"); let p = a[i].as_ptr(); println!("{:?}", p); println!("{:?}", *p); let data = std::slice::from_raw_parts(p, a[i].len()); println!("{:?}", data); } // ------- // 0x107f98e55 // 97 // [97] // ------- // 0x107f98e50 // 97 // [97, 98] // ------- // 0x107f98e52 // 97 // [97, 98, 99] }
確かに、3つに分かれた各要素の先頭8バイトが実データへのポインタになっていました。
String
続いてString
です。
let a: [String; 3] = [ "a".to_string(), "ab".to_string(), "abc".to_string()]; println!("{:x?}", as_raw_bytes(&a)); // [90, 2c, c0, 4a, 9b, 7f, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, a0, 2c, c0, 4a, 9b, 7f, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, b0, 2c, c0, 4a, 9b, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
例によって整形します。
[90, 2c, c0, 4a, 9b, 7f, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, a0, 2c, c0, 4a, 9b, 7f, 00, 00, 02, 00, 00, 00, 00, 00, 00, 00, 02, 00, 00, 00, 00, 00, 00, 00, b0, 2c, c0, 4a, 9b, 7f, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00]
[String]
も[&str]
と同じようにスタック領域に3つのString
が並んでいます。
上述の通り&str
は実データへのポインタとlen
を持っていますが、String
はこれに加えてcap
を持ちます。
ちょうどVec
に対応しています。
そうとなれば実データを指しているのか確認したくなりますね。
let a: [String; 3] = [ "a".to_string(), "ab".to_string(), "abc".to_string()]; println!("{:x?}", as_raw_bytes(&a)); // [90, 2e, 40, 6f, 8c, 7f, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, a0, 2e, 40, 6f, 8c, 7f, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, b0, 2e, 40, 6f, 8c, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0] unsafe { for i in 0..a.len() { println!("-------"); let p = a[i].as_ptr(); println!("{:?}", p); println!("{:?}", *p); let data = std::slice::from_raw_parts(p, a[i].len()); println!("{:?}", data); } // ------- // 0x7f8c6f402e90 // 97 // [97] // ------- // 0x7f8c6f402ea0 // 97 // [97, 98] // ------- // 0x7f8c6f402eb0 // 97 // [97, 98, 99] }
確かに実データへのポインタとなっていました。
続けて、Vec<String>
です。
let a: Vec<String> = vec![ "a".to_string(), "ab".to_string(), "abc".to_string()]; println!("{:x?}", as_raw_bytes(&a)); // [a0, 2d, c0, d0, d5, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0] unsafe { let p = a.as_ptr(); // p: *const String println!("{:?}", p); // 0x7fd5d0c02da0 println!("{:?}", *p); // "a" let data = std::slice::from_raw_parts(p, a.len()); // data: &[String] println!("{:?}", data); // ["a", "ab", "abc"] for i in 0..a.len() { println!("-------"); let p = a[i].as_ptr(); // p: *const u8 println!("{:?}", p); println!("{:?}", *p); let data = std::slice::from_raw_parts(p, a[i].len()); // data: &[u8] println!("{:?}", data); } // ------- // 0x7fd5d0c02cd0 // 97 // [97] // ------- // 0x7fd5d0c02ce0 // 97 // [97, 98] // ------- // 0x7fd5d0c02cf0 // 97 // [97, 98, 99] }
一番最初のas_raw_bytes(&a)
の結果を整形します。
[a0, 2d, c0, d0, d5, 7f, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00, 03, 00, 00, 00, 00, 00, 00, 00]
a
はVec<String>
型のため、実データへのポインタ、len
、cap
を持つ結果となっていることがわかります。
a.as_ptr()
(0x00007fd5d0c02da0
)を参照外しすると"a"
が得られました。
a[0].as_ptr()
(0x00007fd5d0c02cd0
)は違うアドレスですが、同じ"a"
を参照しています。
図にすると以下のようになります。
References
- Rustのvtableの内部構造 - 簡潔なQ
- RustのSizedとfatポインタ - 簡潔なQ
- Function std::mem::size_of - rust-lang
- char - Rust
- str - Rust
- 生ポインタ
- プログラミングRust
- 作者: Jim Blandy,Jason Orendorff,中田秀基
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/08/10
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る