Rust內存示例深入講解
內存
大多數應用程序,不論服務器、區(qū)塊鏈、人工智能、游戲還是其他領域。都需要數據。因此理解內存的復雜性相當重要。
不同類型的內存往往依賴于幾個因素:
- 數據大小
- 所有權
- 生命周期
- 可變性
- 持久性
這些因素綜合起來,將幫助你做出明確的決定。
其中三個主要的內存區(qū)域是棧、堆和靜態(tài)。你可以將數據放置在這些位置中的任何一個。有時,Rust會提供一些指引,比如將向量的元素放置在堆上。 然而主要還是你自己決定數據的位置。
應該注意的是,Rust沒有正式的內存管理模型(一套定義良好的規(guī)則和機制,用于自動管理內存的分配,使用和釋放從而確保內存高效、安全地使用)。但是Rust的特性(例如默認不可變性、智能指針、所有權和生命周期)形成了一種非正式的內存管理模型。
棧
每個線程都擁有一個棧,它是一種專用內存。
- 當線程調用一個函數時,棧會增長。
- 當線程從函數返回時,棧會縮小。
每個函數都有一個棧幀,它為函數保留內存。棧幀中的內存用于局部變量、參數、返回值和系統(tǒng)數據。這些數據會被系統(tǒng)自動釋放。
棧的實現是一個先進后出(LIFO)隊列,類似于一碟盤子,新的盤子總放置在頂部,并按順序從頂部移除。這種方式意味著數據高效地存儲在連續(xù)的內存中。棧具有可預測的行為,因此系統(tǒng)能有效地管理棧。
對于Rust,除主線程(main線程)外,默認棧大小是2K字節(jié),當生成一個線程時, - 你可以使用
Builder類型和stack_size函數明確設置最小棧的大小。 - 或者使用
RUST_MIN_STACK環(huán)境變量更改默認棧大小。
然而上面兩種方法都沒有設置棧大小上限,因為棧是可增長的,會在需要時擴展,直到達到可用內存容量的限制。
let語句可用于當前棧幀內的內存中創(chuàng)建一個本地變量。
fn main() {
let a = 1;
let b = 2;
let c = do_something();
println!("{:p} + {}(i32) = {:p} + {}(i32) {:p}",&a, &b-&a, &b, &c-&b, &c);
}
fn do_something()->i32 {
3
}
可以看到,本地變量a、b和c在棧上占據連續(xù)的內存位置
即使在函數內部,數據也可以被添加和從棧中移除。
fn main() {
let a = 1;
let b = 2;
{
let c = 3;
}
println!("{} + {} = {}", a, b, c); //報錯,因為c已經被移除了
}
UnSized(?Sized)類型不能放在棧上。
fn do_something(a: Copy) {
}
因為Copy trait是一個UnSized的。編譯器阻止了不符合條件的參數被放置到棧上。
解決方法是結合dyn(dyn Copy)或impl(impl Dopy)關鍵字,這些關鍵字用具體類型替換了trait,它們是定長的。
注意事項
- 棧可能會消耗大量內存,因此在棧上放置大量對象時要小心。
- 另一個問題是遞歸函數,不經意的無限遞歸會迅速耗盡可用內存。
- 一些數據類型(比如向量和字符串)是智能指針,當使用let語句聲明時,這些類型的值會在堆上分配。指向該值的指針存放在棧上
fn main() {
let vp = vec![1,2,3,4];
println!("{:?}",vp);
}
上述例子中,vp是一個變量,代表胖指針,被放在棧上,而值[1,2,3,4]被放置在堆上
靜態(tài)值
靜態(tài)值在應用程序的生命周期內是持久的。這是通過將靜態(tài)值存儲在二進制文件本身來實現的。這種方式使得這些值始終可用。這也意味著大量的靜態(tài)值會導致二進制文件膨脹,這可能會影響性能。此外,為了保證靜態(tài)安全,靜態(tài)值很少是可變的。
可以使用static關鍵字聲明靜態(tài)綁定。按照規(guī)范,靜態(tài)值的名稱應該全大寫。此外,靜態(tài)值的類型不可被推斷,必須顯式聲明類型。
fn main() {
static PI: f64 = 3.14;
let r = 4.0;
println!("面積: {}",PI*r*r);
}
與棧變量相比,靜態(tài)值的地址明確顯示出它們位于內存中不同的區(qū)域。
fn main() {
static A: i32 = 10;
static B: i32 = 20;
let a = 10;
let b = 20;
println!("Global: ptr_A {:p} ptr_B {:p}", &A, &B);
println!("Stack: prt_a {:p} ptr_b {:p}", &a, &b);
}
堆
堆是運行時可供應用程序使用的進程內存。這通常是應用程序最大的可用內存池,是放置大型對象的地方。在運行時,應用程序會根據需要在堆上分配內存。這通常被稱為動態(tài)內存分配。當不需要時,堆內存可被釋放,返回可用池中。
堆內存取自應用程序的虛擬內存。一個進程與設備上其他正在運行的進程共享物理內存。因此一個應用程序并不擁有計算機上的所有內存。相反,應用程序被分配了一個虛擬地址空間(虛擬內存),然后操作系統(tǒng)將其映射到物理內存。
在申請對內存時,操作系統(tǒng)必須首先找到足夠的連續(xù)內存以滿足要求,然后在該位置分配內存,并返回一個指向該地址的指針。地位和分配內存的過程可能會比較耗時。
- 此外,堆可能會因為一系列不同大小的數據分配操作而變得碎片化。即使有足夠的內可用存,但不是單一位置的整塊內存,也可能導致內存分配失敗。一些操作系統(tǒng)提供了系統(tǒng)API來對堆進行整理,以緩解該問題。
與棧不同,堆是進程內所有線程都可以訪問的共享內存。因此堆上的數據可能不是內存安全的。但是我們可以用RwLock這樣的類型來管理共享內存。
在Rust中,Box類型用于在堆上分配內存。當Box被釋放時,通常在當前塊的末尾,釋放這個堆內存。然而,如果Box的值一直沒得到釋放,就會導致內存泄漏?;蛘撸梢允褂胐rop關鍵字顯式釋放Box及相關內存。
pub struct Box<T, A = Global>(_, _)
where A: Allocator, T: ?Sized;
Box是泛型結構體,它的類型參數是T,T是動態(tài)分配的類型(?Sized),類型參數A是對內存分配器的引用,Global是默認的分配器件,用于在堆上分配內存。如果需要,你可以使用自定義分配器。
可以使用new構造函數創(chuàng)建一個Box
fn new(x: T)->Box<T, Global>
Box::new函數用于在堆上創(chuàng)建一個值,并返回一個Box值,而不是指向堆內存的原始指針。
要訪問堆上的Box值,需要對Box進行解引用。然而這種解引用也不是必須的,有時會發(fā)生自動解引用。例如println!宏。
fn main() {
let boxa = Box::new(10);
let stackb=*boxa+1;
println!("{} {}", boxa, stackb);
}
示例
fn main() {
let boxa = Box::new(1);
let boxb = Box::new(2);
let c = 1;
let d = 2;
println!("boxa:{:p} boxb:{:p} &c{:p} &d:{:p}",&boxa, &boxb, &c, &d);
let rawa = Box::into_raw(boxa);
let rawb = Box::into_raw(boxb);
println!("rawa:{:p} rawb{:p} &c:{:p} &d:{:p}", rawa, rawb, &c, &d);
let boxc;
let boxd;
unsafe {
boxc = Box::from_raw(rawa);
boxd = Box::from_raw(rawb);
}
println!("boxc value:{}", *boxc);
println!("boxd value:{}", *boxd);
}
- Box本身位于棧上,即使引用了堆上的數據。
- into_raw函數用于獲取Box值的原始指針(rawa和rawb)。原始指針直接指向堆內存,并且是unsafe的。當它被釋放時,堆內存不會移除。
- Box值與局部變量c和d位于內存的不同區(qū)域。
- 你還可以使用from_raw函數將原始指針重新放回Box中,此后,Box將恢復對堆上數據項的責任。必須將from_raw標記為unsafe來調用。
你也可以將棧上的值移動到堆,其結果取決于值移動是復制語義還是移動語義。
例如,將String變?yōu)?code>Box<String>變量時,所有權被轉移到堆上,也就是說String智能指針本身被移動到堆上。
fn main() {
let a = 10;
let mut boxa = Box::new(a);
*boxa += 1;
println!("{} {}", a, *boxa);
}
內部可變性
內部可變性用一個場景來描述是:
你管理你個大型連鎖超市內的一家雜貨店。在結賬時,顧客購物車里的商品會被合計并記錄到收據上。收據上的商店Id和交易Id是固定的,而總金額字段是可變的。
struct Transaction {
storeid: i8,
txid: i32,
mut total: f64 //錯誤的,結構體某單個字段無法聲明為可變的
}
結構體單個字段無法聲明為可變,可變性是在結構體級別上聲明的。
但是這會導致不恰當的更改
#[derive(Debug)]
struct Transaction {
storeid: i8,
txid: i32,
total: f64,
}
fn main() {
let mut tx = Transaction {storeid: 0, txid: 0, total: 64.0};
tx.storeid=101; // oops
println!("{:?}", tx);
}
解決辦法是內部可變性。
支持內部可變性的類型是一種內部值的包裝器,包裝器呈現了一種不可變的外觀,同時間接允許對其內部值的修改。
Cell
Cell是一種支持內部可變性的類型,它的類型參數是T,其中T描述了內部值,Cell位于std::cell模塊中
Cell可以保證不變性,而內部值可以使用方法修改
- Cell::get 返回內部值的副本
- Cell::set 修改內部值
fn get(&self)->T fn set(&self, val: T)
可以使用Cell::new函數創(chuàng)建一個Cell
fn new(value: T)->Cell<T>
修改最開始的例子
use std::cell::Cell;
fn main() {
let cell = Cell::new(0);
let data = cell.get();
cell.set(1);
println!("cell:{} data:{}", cell.get(), data);
}
use std::cell::Cell;
#[derive(Debug)]
struct Transaction {
storeid: i8,
txid: i32,
total: Cell<f64>,
}
fn main() {
let item_prices = [11.21, 25.45, 30.5];
let tx = Transaction {
storeid: 100,
txid: 203,
total: Cell::new(0.0),
};
for prices in item_prices {
let total = tx.total.get()+prices;
tx.total.set(total);
}
println!("{:?}",tx);
}
Cell還有一個好處,比如下面的代碼,Rust不允許存在多個可變借用
fn main() {
let mut a = 1;
let ref1 = &a;
let ref2 = &a;
let mut ref3 = &mut a;
let mut ref4 = &mut a;
*ref3=2;
println!("{ref3}");
}
如果用Cell
use std::cell::Cell;
fn main() {
let a = 10;
let cell = Cell::new(a);
let cell1 = &cell;
let cell2 = &cell;
cell1.set(11);
cell2.set(12);
println!("{}",cell.get());
}
我們可以通過多個不同的引用修改內部值。
還有一些Cell的函數
- replace: 用新值替換內部值,然后返回被替換的舊值
- swap: 交換兩個Cell的內部值
- take: 獲取內部值并將其內部替換為默認值
RefCell
RefCell也位于std::cell模塊中,與Cell不同的是,RefCell只提供對內部值的引用,而不是副本。
- RefCell::borrow 獲取不可變借用
- RefCell::borrow_mut 獲取可變借用
fn borrow(&self)->Ref<'_, T> fn borrow_mut(&self)->RefMut<'_, T>
可以用new函數創(chuàng)建一個ReCell
fn new(value: T)->RefCell<T>
示例
use std::cell::RefCell;
fn main() {
let ref_cell = RefCell::new(0);
*ref_cell.borrow_mut() += 10;
println!("*ref_cell: {}",ref_cell.borrow());
}
對于RefCell,可變性規(guī)則(不允許存在多個可變借用)完全適用。然而這些規(guī)則是在運行時而不是編譯時強制執(zhí)行的。因此要格外小心不要違反這些規(guī)則,會出現panic
use std::cell::RefCell;
fn main() {
let refcell = RefCell::new(0);
let mut ref1 = refcell.borrow_mut();
let mut ref2 = refcell.borrow_mut();
*ref1 = 10;
println!("{}",ref1);
}
示例二
```rust
use std::cell::RefCell;
fn main() {
let refcell = RefCell::new(0);
let ref1 = refcell.borrow();
let mut ref2 = refcell.borrow_mut();
println!("{}",ref1);
}
try_borrow函數是borrow函數的一個替代方案。該函數返回一個Result類型,當已存在一個可變引用時,它不會引發(fā)panic.而是直接返回Result類型的Err。如果成功則返回Ok(reference)
use std::cell::RefCell;
fn main() {
let refcell = RefCell::new(0);
let ref1 = refcell.borrow();
let ret = refcell.try_borrow_mut();
match ret {
Ok(value) => println!("Interior value: {}", value),
Err(_) => println!("不可再聲明可變借用"),
}
}
還是修改最開始的例子
use std::cell::RefCell;
#[derive(Debug)]
struct Transaction {
storeid: i8,
txid: i32,
total: RefCell<f64>,
}
fn main() {
let item_prices = [11.5, 20.5, 30.0, 40.3];
let tx = Transaction{storeid: 100, txid: 203, total: RefCell::new(0.0)};
for prices in item_prices {
*tx.total.borrow_mut() += prices
}
println!("{:#?}", tx)
}
RefCell還有其他有用的方法
- replace: 用另一個值替換內部值,并返回當前值
- swap: 交換兩個RefCell的內部值
OnceCell
與Cell和RefCell類似,區(qū)別是OnceCell只能修改一次內部值。如果再次修改會發(fā)生錯誤。可以用new函數創(chuàng)建一個OnceCell.
- set用于初始化內部值
- get返回內部值,可以根據需要多次獲取內部值
fn new()->OnceCell<T> fn set(&selfm value, T)->Result<(), T> fn get(&self)->Option<&T>
示例
use std::cell::OnceCell;
fn main() {
let once = OnceCell::new();
let mut result = Ok(());
for i in 1..=3 {
result = once.set(i);
match result {
Ok(_) => println!("Updated"),
Err(_) => println!("Not undated")
}
}
println!("{:?}", result);
}
OnceCell其他有用的函數
- get_mut 獲取內部值的可變引用
- get_or_init 獲取內部值,如果未初始化,則使用閉包初始化它
- take 獲取內部值然后將內部設置為默認值
總結
到此這篇關于Rust內存的文章就介紹到這了,更多相關Rust內存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

