日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747


作 者 | 吳強強(去鴻)

作者最近嘗試寫了一些Rust代碼,本文主要講述了對Rust的看法和Rust與C++的一些區別。

背景

S2在推進團隊代碼規范時,先后學習了盤古編程規范,CPP core guidelines,進而了解到clang-tidy,以及google Chrome 在安全方面的探索。

C++是一個威力非常強大的語言,但是能力越大,責任越大,它的內存安全性問題一直飽受詬病。NSA甚至明確提出,停止使用C++這種內存不安全的語言。

C++本身的確提出了一系列改進方案,但是遲遲不見落地。Bjarne對于NSA挑戰給出的方案也只能部分解決問題,并且看起來落地也是遙遙無期。

Rust作為一個新晉語言,是Mozilla應對內存安全性問題發明的新語言(之前它也是使用C++的),linux和Chrome都開始先后接納了它,Tikv與大部分區塊鏈項目,在第一天就選擇了它。

遇到若干次內存踩壞問題后,我有了了解Rust的沖動。

C++代碼中的風險

這張圖是Chrome團隊發布的Chrome的代碼被攻擊的Bug類型的分布,可以看到,內存安全性占了一半以上。

 

  •  

    Temporal safety: 簡單來說就是use after free

     

  •  

    Spatial safety: 簡單來說,就是out of bound訪問

     

  •  

    Logic error

     

  •  

    DCHECK: 簡單來說,就是debug assert的條件在release版本中被觸發了

     

 

 

其他不多展開。

Rust 初體驗

初體驗Rust,實際上更多的是感覺到它的一些小設計非常甜,它會讓我們的編程很舒服。

簡單來說,所有C++通過Best Practice/Effective C++/...等推行的寫法,Rust全部是編譯器強制的。

默認不可變

Rust中,所有變量是默認不可變的,可變需要額外的typing。這與C++是完全相反的。然而,這與C++ Core Guidelines中的推薦卻是一致的。

let x = 0;
x = 10; // error

let mut y = 0;
y = 10; //ok

禁止整數隱式轉換


fn foo(x: u32) {}

 

let x: i32 = 0;
foo(x); // error

 

Be explicit,這條軟件界的普遍規則,在C/C++中卻是完全不適用,真是反直覺。

簡化構造、復制與析構

C++中的Rule of 3 or 5 or 6可謂是大名鼎鼎,我們無數次需要寫以下代碼


class ABC
public:
virtual ~ABC();

 

ABC(const ABC&) = delete;
ABC(ABC&&) = delete;

ABC& operator=(const ABC&) = delete;
ABC& operator=(ABC&&) = delete;
};

 

明明是一件非常常規的東西,寫起來卻那么的復雜。

Rust非常簡單,所以對象默認只支持Destructive move(通過memcpy完成)。需要復制,要類顯式實現Clone trait,復制時寫.clone(), 對于trivial對象,期望能通過=來隱式copy,要顯式實現Copy,實現Copy時,不允許類再實現Drop(即析構函數)。


fn main()
// String類似std::string,只支持顯式clone,不支持隱式copy
let s: String = "str".to_string();

 

foo(s); // s will move
// cannot use s anymore

let y = "str".to_string();
foo(y.clone());

// use y is okay here
}

fn foo(s: String) {}

// can only be passed by move
struct Abc1
{
elems: Vec
}

// can use abc.clone() to explicit clone a new Abc
#[derive(Clone)]
struct Abc2
{
elems: Vec
}

// implement custom destructor for Abc
impl Drop for Abc2 {
// ...
}

// foo(xyz) will copy, 不能再定義Drop/析構函數,因為copy和drop是互斥的
#[dervie(Clone, Copy)]
struct Xyz
{
elems: i32
}

 

比起C++的move,以及其引入的use after move問題,還有各種Best Practice,Rust的做法實在是高明了不少。

媽媽再也不用擔心我會不小心copy一個有1千萬元素的vector了。review時也再也不用糾結,這里到底是用值還是const&了。

顯式參數傳遞

C++中函數參數傳遞的Best Practice能寫一堆,in/out/inout參數如何處理也沒有一個明確的規范。Rust則簡化了參數傳遞,并且將一切由隱式轉變為顯式。


let mut x = 10;

 

foo(x); // pass by move, x cannot be used after the call
foo(&x); // pass by immutable reference
foo(&mut x); // pass by mutable reference

 

統一的錯誤處理

錯誤處理一直是C++中一個非常分裂的地方,截止C++23,目前C++標準庫中,有以下用于錯誤處理的功能:

 

  •  

    errno

     

  •  

    std::exception

     

  •  

    std::error_code/std::error_condition

     

  •  

    std::expected

     

 

 

看,連標準庫自己都這樣。std::filesystem,所有接口都有至少兩個重載,一個拋異常,一直傳std::error_code。

Rust的方案與Herb提出的static異常類似,并且通過語法糖,讓錯誤處理非常容易。


enum MyError
NotFound,
DataCorrupt,
Forbidden,
Io(std::io::Error)

 

impl From for MyError {
fn from(e: io::Error) -> MyError {
MyError::Io(e)
}
}

pub type Result = result::Result;

fn main() -> Result<()>
{
let x: i32 = foo()?;
let y: i32 = bar(x)?;

foo(); // result is not handled, compile error

// use x and y
}

fn foo() -> Result
{
if (rand() > 0) {
Ok(1)
} else {
Err(MyError::Forbidden)
}
}

 

錯誤處理一律通過Result來完成,通過?,一鍵向上傳播錯誤(如同時支持自動從ErrorType1向ErrorType2轉換,前提是你實現了相關trait),沒有錯誤時,自動解包。當忘記處理處理Result時,編譯器會報錯。

內置格式化與lint

Rust的構建工具cargo,內置了cargo fmt和cargo clippy,一鍵格式化與lint,再也不用人工配置clang-format和clang-tidy了。

標準化的開發流程和包管理

Rust最為C++程序員所羨慕的地方是,它有官方包管理工具cargo。C++非官方包管理工具conan目前有1472個包,cargo的包管理平臺有106672個包。

cargo還原生支持了test與benchmark,一鍵運行


cargo test
cargo bench

 

cargo規定了目錄風格


benches // benchmark代碼go here
src
tests // ut go here

Rust在安全性的改進

上一節提的其實還是非致命的東西,Rust在內存安全方面的改進,才是讓它與眾不同的原因。

lifetime安全性

use-after-free是這個世界上最為著名的bug之一。解決它的方案一直以來都是依賴運行時檢查,兩個主要流派是GC與引用計數。而Rust在此之外引入了一種新的機制:Borrow Check。

Rust規定,所有對象都是有所有權的,賦值意味著所有權的轉讓。一旦所有權轉讓后,舊的對象將無法再被使用(destructive move)。Rust允許一個對象的所有權暫時被租用給其他引用。所有權可以租借給若干個不可變引用,或者一個獨占的可變引用。

舉個例子:


let s = vec![1,2,3]; // s owns the Vec
foo(s); // the ownership is passed to foo, s cannot be used anymore

 

let x = vec![1,2,3];
let a1 = &x[0];
let a2 = &x[0]; // a1/a2 are both immutable ref to x

x.resize(10, 0); // error: x is already borrowed by a1 and a2

println!("{a1} {a2}");
 

這種unique ownership + borrow check的機制,能夠有效的避免pointer/iterator invalidation bug以及aliasing所引發的性能問題。

在此之上,Rust引入了lifetime概念,即,每個變量有個lifetime,當多個變量間存在引用關系時,編譯器會檢查這些變量之間的lifetime關系,禁止一個非owning引用,在其原始對象lifetime結束之后再被訪問。


let s: &String;

 

{
let x = String::new("abc");
s = &x;
}

println!("s is {}", s); // error, lifetime(s) > lifetime(x)
 

這個例子比較簡單,再看一些復雜的。


// not valid rust, for exposition only
struct ABC
x: &String,

 

fn foo(x: String)
{
let z = ABC { x: &x };

consume_string(x); // not compile, x is borrowed by z
drop(z); // call destructor explicitly

consume_string(x); // ok

// won't compile, bind a temp to z.x
let z = ABC { x: &String::new("abc") };
// use z

// Box::new == make_unique
// won't compile, the box object is destroyed soon
let z = ABC{ x: &*Box::new(String::new("abc") };
// use z
}

 

再看一個更加復雜的,涉及到多線程的。


void foo(ThreadPool* thread_pool)
Latch latch{2};

 

thread_pool->spawn([&latch] {
// ...
latch.wait(); // dangle pointer訪問
});

// forget latch.wait();
}

 

這是一個非常典型的lifetime錯誤,C++可能要到運行時才會發現問題,但是對于Rust,類似代碼的編譯是不通過的。因為latch是個棧變量,其lifetime非常短,而跨線程傳遞引用時,這個引用實際上會可能在任意時間被調用,其lifetime是整個進程生命周期,rust中為此lifetime起了一個專門的名字,叫'static。正如cpp core guidelines所說:CP.24: Think of a thread as a global container ,never save a pointer in a global container。

在Rust中,rust編譯器會強制你使用引用計數,來顯示說明共享需求(em...發現這句話問題的,已經是Rust高手了)。


fn foo(thread_pool: &mut ThreadPool)
let latch = Arc::new(Latch::new(2));
let latch_copy = Arc::clone(&latch);

 

thread_pool.spawn(move || {
// the ownership of latch_copy is moved in
latch_copy.wait();
});

latch.wait();
}

 

再看一個具體一些例子,假設你在寫一個文件reader,每次返回一行。為了降低開銷,我們期望返回的這一行,直接引用parser內部所維護的buffer,從而避免copy。


FileLineReader reader(path);

 

std::string_view line = reader.NextLine();
std::string_view line2 = reader.NextLine();

// ops
std::cout << line;

 

再看看Rust


let reader = FileReader::next(path);
let line = reader.next_line();

 

// won't compile, reader is borrowed to line, cannot mutate it now
let line2 = reader.next_line();

println!("{line}");

// &[u8] is std::span
fn foo() -> &[u8] {
let reader = FileReader::next(path);
let line = reader.next_line();

// won't compile, lifetime(line) > lifetime(reader)
return line;
}

總結來說,Rust定義了一套規則,按照此規則進行編碼,絕對不會有lifetime的問題。當Rust編譯器無法推導某個寫法的正確性時,它會強制你使用引用計數來解決問題。

邊界安全性

Buffer overflow以及out of bound訪問也是一類非常重要的問題,這類問題相對好解,給標準庫實現加下bound check就好了。Rust標準庫會進行bound check。

這方面,C++稍微努力下,還是能避免的。

啥?bound check性能差。Em...看看Chrome發布的漏洞報告吧,人呢,還是不要太自信得好。畢竟,Bjarne都開始妥協了。Herb的slide中有對out of bound問題一些數字的說明。

類型安全性

Rust默認強制變量初始化,并且禁止隱式類型轉換。


let i: i32;

 

if rand() < 10 {
i = 10;
}

println!("i is {}", i); // do not compile: i is not always initialized

Rust 的多線程安全性

如果說lifetime + ownership模型是Rust的安全核心的話,Rust的多線程安全性就是在此基礎上結出的果實。Rust的多線程安全性,完全是通過庫機制來實現的。

首先介紹兩個基礎概念:

 

  •  

    Send: 一個類型是Send,表明,此類型的對象的所有權,可以跨線程傳遞。當一個新類型的所有成員都是Send時,這個類型也是Send的。幾乎所有內置類型和標準庫類型都是Send的,Rc(類似local shared_ptr)除外,因為內部用的是普通int來計數。

     

  •  

    Sync: 一個類型是Sync,表明,此類型允許多個線程共享(Rust中,共享一定意味著不可變引用,即通過其不可變引用進行并發訪問)。

     

 

 

Send/Sync是兩個標準庫的Trait,標準庫在定義它們時,為已有類型提供了對應實現或者禁止了對應實現。

通過Send/Sync與ownership模型,Rust讓Data race完全無法出現。

簡單來說:

 

  •  

    lifetime機制要求:一個對象要跨線程傳遞時,必須使用Arc(Arc for atomic reference counted)來封裝(Rc不行,因為它被特別標注為了!Send,即不可跨線程傳遞)

     

  •  

    ownership+borrow機制要求:Rc/Arc包裝的對象,只允許解引用為不可變引用,而多線程訪問一個不可變對象,是天生保證安全的。

     

  •  

    內部可變性用于解決共享寫問題:Rust默認情況下,共享一定意味著不可變,只有獨占,才允許變。如果同時需要共享和可變,需要使用額外的機制,Rust官方稱之為內部可變性,實際上叫共享可變性可能更容易理解,它是一種提供安全變更共享對象的機制。如果需要多線程去變更同一個共享對象,必須使用額外的同步原語(RefCell/Mutex/RwLock),來獲得內部/共享可變性,這些原語會保證只有一個寫者。RefCell是與Rc一起使用的,用于單線程環境下的共享訪問。RefCell被特別標注為了!Sync,意味著,如果它和Arc一起使用,Arc就不是Send了,從而Arc>無法跨線程。

     

 

 

看個例子:假設我實現了一個Counter對象,希望多個線程同時使用。為了解決所有權問題,需要使用Arc,來傳遞此共享對象。但是,以下代碼是編譯不通過的。


struct Counter
counter: i32

 

fn main()
{
let counter = Arc::new(Counter{counter: 0});
let c = Arc::clone(&counter);
thread::spawn(move || {
c.counter += 1;
});

c.counter += 1;
}

 

因為,Arc會共享一個對象,為了保證borrow機制,訪問Arc內部對象時,都只能獲得不可變引用(borrow機制規定,要么一個可變引用,要么若干個不可變引用)。Arc的這條規則防止了data race的出現。

為了解決這個問題,Rust引入了內部可變性這個概念。簡單來說,就是一個wrApper,wrapper可以獲得一個內部對象的可變引用,但是wrapper會進行borrow check,保證只有一個可變引用,或者若干個不可變引用。

單線程下,這個wrapper是RefCell,多線程下,是Mutex/RwLock等。當然,如果你嘗試寫這樣的代碼,也是編譯不通過的


fn main()
let counter = Arc::new(RefCell::new(Counter{counter: 0}));
let c = Arc::clone(&counter);
thread::spawn(move || {
c.get_mut().counter += 1;

 

c.get_mut().counter += 1;
}

 

為啥?因為RefCell不是Sync,即不允許多線程訪問。Arc只在內部類型為Sync時,才為Send。即,Arc>不是Send,無法跨線程傳遞。

Mutex是Send的,因此,可以這么寫:


struct Counter
counter: i32

 

fn main()
{
let counter = Arc::new(Mutex::new(Counter{counter: 0}));
let c = Arc::clone(&counter);
thread::spawn(move || {
let mut x = c.lock().unwrap();

x.counter += 1;
});
}

Rust 的性能

作為C++的挑戰者,更多的人會關注Rust的性能到底怎么樣。Rust的官方哲學是zero cost principle,是不是和C++的zero overhead原則很像。當然,這個名字起的實際上沒有C++好,畢竟,做事情就是有cost的,怎么可能是zero cost呢。

Rust添加了bound check,可能會比C++弱一點點,但也有限。同時Rust支持unsafe模式,完全跳過bound check。這讓Rust的上限可以和C++持平。

另外,Rust禁止了很多conditionally正確的用法,也會有一定性能損失,比如跨線程必須shared_ptr(額外的動態分配)。


// for demo purpose

 

fn foo(tasks: Vec)
{
let latch = Arc::new(Latch::new(tasks.len() + 1));

for task in tasks {
let latch = Arc::clone(&latch);
thread_pool.submit(move || {
task.run();
latch.wait();
});
}

latch.wait();
}

 

這里,latch必須用Arc(即shared_ptr)。

在某些場景下,Rust會比C++還快。優化圣經有言,阻礙編譯器優化的兩大天敵:

 

  •  

    函數調用

     

  •  

    指針別名

     

 

 

C++和Rust都可以通過inline來消除函數調用引起的開銷。但是C++面對指針別名時,基本上是無能為力的。C++對于指針別名的優化依賴strict aliasing rule,不過這個rule出了名的惡心,Linus也罵過幾次。Linux代碼中,會使用-fno-strict-aliasing來禁止這條規則的。

不過,C好歹有__restricted__來救救命,C++程序員就只有god knows了。

而Rust通過所有權和借用機制,是能保證沒有aliasing現象的。考慮下面這段代碼


int foo(const int* x, int* y)
*y = *x + 1;

 

return *x;
}

 

rust版本


fn foo(x: &i32, y: &mut i32) -> i32
*y = *x + 1;

 

*x
}

 

對應的匯編如下:


# c++
__Z3fooPKiPi:
ldr w8, [x0]
add w8, w8, #1
str w8, [x1]
ldr w0, [x0]
ret

 

# rust
__ZN2rs3foo17h5a23c46033085ca0E:
ldr w0, [x0]
add w8, w0, #1
str w8, [x1]
ret

 

看出差別沒?

感想

最近嘗試寫了一些Rust代碼,發覺體驗真的不錯。按照Rust社區的說法,使用Rust后,可以無畏地編程,再也不用擔心內存錯誤與Data Race。

然而,對于大量使用C++實現的產品來說,C++是負債,更是資產。已經存在的C++生態很難向Rust進行遷移,Chrome也只是允許在三方庫中使用Rust代碼。

網上對于Rust與C++的爭論也是十分激烈。從我的角度來說,

 

  •  

    C++的安全性演進是趨勢,但是未來很不明朗:C++在全世界有數十億行的存量代碼,期望C++在維持兼容性的基礎上,提升內存安全性,這是一個幾乎不可能的任務。clang-format與clang-tidy,都提供了line-filter,來對特定的行進行檢查,避免出現改動一個老文件,需要把整個文件都重新format或者修改掉所有lint失敗的情況。大概也是基于此,Bjarne才會一直嘗試通過靜態分析+局部檢查來提升C++的內存安全性。

     

  •  

    Rust有利于大團隊協作:Rust代碼只要能編譯通過,并且沒有使用unsafe特性,那么是能夠保證絕對不會有內存安全性或者線程安全性問題的。這大大降低了編寫復雜代碼的心智負擔。然而,Rust的安全性是以犧牲語言表達力而獲得的,這對于團隊合作與代碼可讀性,可能是一件好事。至于其他,在沒有足夠的Rust實踐經驗前,我也無法作出更進一步的判斷。

     

 

 

Relax and enjoy coding!

參考資料:

1.Cpp core guidelines: https://isocpp.Github.io/CppCoreGuidelines/CppCoreGuidelines

2.Chrome在安全方面的探索:https://docs.google.com/document/d/e/2PACX-1vRZr-HJcYmf2Y76DhewaiJOhRNpjGHCxliAQTBhFxzv1QTae9o8mhBmDl32CRIuaWZLt5kVeH9e9jXv/pub

3.NSA對C++的批評:https://www.theregister.com/2022/11/11/nsa_urges_orgs_to_use/

4.C++ Lifetime Profile: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#SS-lifetime

5.C++ Safety Profile: https://open-std.org/JTC1/SC22/WG21/docs/papers/2023/p2816r0.pdf?file=p2816r0.pdf

6.Herb CppCon2022 Slide: https://github.com/CppCon/CppCon2022/blob/main/Presentations/CppCon-2022-Sutter.pdf

7.Rust所有權模型理解:https://limpet.NET/mbrubeck/2019/02/07/rust-a-unique-perspective.html

8.Rust無畏地并發編程:https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html

分享到:
標簽:Rust
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定