iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0

接下來我們要進入 Rust 的重頭戲了,處理記憶體

在這之前,我們先來認識 Stack 跟 Heap 這兩個資料結構

我們將分成記憶體處理方式 / 儲存方式 / 分配記憶體的彈性限制 以及運作模式來解釋這兩個差別

Stack

在程式開發中,常會用來存取區域變數

Stack 負責儲存固定長度的資料格式(即是在執行期也不能改其長度),以連續,且 array 的形式來存放資料

會存放在 Stack 中的資料為此變數的 指標 容量 長度,這些資料基本上都是固定不會變動的

指標(pointer) 會指向該變數的 heap 位置
容量 表示 heap 位置中可以容納多少個元素
長度 目前變數中有多少元素

我們用 Vec 來看一下:

Vec 在產生一個產生一個實體時,會以 stack 方式分配一個固定位置給 x 這個變數,並存放上述的 指標 容量 長度

一開始會給 Vec 固定的記憶體大小,這個記憶體大小怎麼算?

上述說的三個資料,一個資料佔據 8 bytes,三個就會是 24 byte

為什麼一個資料佔據的是 8 bytes ?
因為在 64 位元的系統上,一個欄位就是 64 位元,而 64 位元就是 8 bytes

fn main() {
    let x = vec!['A', 'B', 'C'];

    // std::mem::size_of_val 會回傳佔據多少 bytes
    // & 回傳 x 變數,但不是變數的值
    let size = std::mem::size_of_val(&x);
    
    // capacity() 回傳可容納的元素數量
    let cap = x.capacity();

    println!("{:?}", x);
    println!("容量: {}", cap);
    println!("佔據的記憶體大小(stack): {} byte", size);
}

結果:
['A', 'B', 'C']
容量: 3
佔據的記憶體大小(stack): 24 byte

Stack 是以 FILO 概念來針對記憶體做儲存,

FILO (First In Last Out)是什麼呢?

就像我們在裝箱行李一樣,放越裡面的東西,到飯店後越後面拿出來。

也因為 Stack 負責處理固定長度的資料格式,在處理記憶體上的效率會比 Heap 更好一點

但也因為沒辦法彈性的調整存取空間,也常常會導致 stack over flow 的情況產生

我們可以使用 Push Pop 來針對資料作增加及移除的動作

順帶一提, Stack 的儲存空間取決於機器的記憶體空間

Heap

Heap 跟 Stack 會有很大的差異,在程式開發中,我們通常會拿來存取全域變數

儲存的資料格式並沒有限定長度(即是在執行期可以改其長度),
將會以 array 以及 trees 的方式來做存取,存放的地方採隨機制,
哪裡有空位並且已經釋出就可以進行存取

因為 Heap 並沒有限制存取的資料長度,
所以在執行的效能會比 Stack 差,且他並不像 Stack 一樣,
針對變數的生命週期需要去做手動的設定,
一旦忘記就有可能會造成記憶體的浪費

其實 Stack 跟 Heap 會同時使用到

我們來看一下 Vec 型態的資料

我們以剛剛的範例來看,上面有提到 Stack 會存放變數所指向的 heap 位置以及長度等固定資料,那所以 heap 存了什麼東西?

fn main() {
    let mut x = vec!['A', 'B', 'C'];

    let address = &x as *const Vec<char>;
    let size = std::mem::size_of_val(&x);
    let cap = x.capacity();

    println!("{:?}", x);
    println!("容量: {}", cap);
    println!("佔據的記憶體大小: {} byte", size);
}

結果分別是:
['A', 'B', 'C']
容量: 3
佔據的記憶體大小: 24 byte

我們可以用以下的方法來看

fn main() {
    let x = vec!['A', 'B', 'C'];

    let address = &x as *const Vec<char>;
    let ptr = x.as_ptr();

    println!("{:?}", x);
    println!("查看 Vec 的前三個元素的資料內容:");

    unsafe {
        for i in 0..3 {
            println!("位置 {:?} 的資料內容: {}", ptr.offset(i), *ptr.offset(i));
        }
    }
}

結果:
查看 Vec 的前三個元素的資料內容:
位置 0x600003a5c040 的資料內容: A
位置 0x600003a5c044 的資料內容: B
位置 0x600003a5c048 的資料內容: C

如果我們的 x 是可以更動的,這時候會長怎樣呢?

x 更改前後的元素數量不一樣,所以我們把 unsafe 裡面的 for 改成大一點的數字來方便觀察

可以看到,在更改前的 x 為 ['A', 'B', 'C']
Stack 位置存放在 0x16ef76450
元素存放在 heap 的 0x600001860040 0x600001860044 0x600001860048

當我們 push 一個元素進去後,Stack 的位置還是一樣的

不過 heap 的位置不一樣了,主要是有做任何更動時,會把他視為一個新的東西,並且另外去找記憶體存放

fn main() {
    let mut x = vec!['A', 'B', 'C'];

    // &x 是將這個變數指向 x ,並且轉變為指標,且不可變
    let address = &x as *const Vec<char>;
    
    // 回傳 x 的 heap 位址
    let ptr = x.as_ptr();

    println!("{:?}", x);
    println!("x 的記憶體位置在:{:?}", address);
    println!("查看 Vec 的前三個元素的資料內容:");

    // 我們需要去看記憶體的內容,在 Rust 中算是危險的動作,因此需要加上 unsafe,並且要小心動作
    unsafe {
        for i in 0..5 {
            // 變數前面加上 * 指的是要去看這變數的 value
            println!("位置 {:?} 的資料內容: {}", ptr.offset(i), *ptr.offset(i));
        }
    };

    x.push('D');
    println!("{:?}", x);
    println!("x 的記憶體位置在:{:?}", address);
    let new_ptr = x.as_ptr();
    unsafe {
        for i in 0..5 {
            println!(
                "位置 {:?} 的資料內容: {}",
                new_ptr.offset(i),
                *new_ptr.offset(i)
            );
        }
    }
}

結果:
['A', 'B', 'C']
x 的記憶體位置在:0x16ef76450
查看 Vec 的前三個元素的資料內容:
位置 0x600001860040 的資料內容: A
位置 0x600001860044 的資料內容: B
位置 0x600001860048 的資料內容: C
位置 0x60000186004c 的資料內容: 
位置 0x600001860050 的資料內容: 
['A', 'B', 'C', 'D']
x 的記憶體位置在:0x16ef76450
位置 0x600001a651a0 的資料內容: A
位置 0x600001a651a4 的資料內容: B
位置 0x600001a651a8 的資料內容: C
位置 0x600001a651ac 的資料內容: D
位置 0x600001a651b0 的資料內容: 

Rust 新手上路,如有錯誤歡迎指正 /images/emoticon/emoticon41.gif


上一篇
Day 07 - 標准函式庫型別
下一篇
Day 09 Cargo 與 Crate
系列文
成為程式界的 F1 賽車手,用 30 天認識 Rust 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

我要留言

立即登入留言