我們的第三個(gè)項(xiàng)目,我們要選擇展示那些能展示 Rust 最大優(yōu)點(diǎn)的點(diǎn):大量運(yùn)行時(shí)的減少。
隨著我們組織的發(fā)展,其越來(lái)越依賴其他的一些編程語(yǔ)言。不同的編程語(yǔ)言有不同的優(yōu)點(diǎn)和缺點(diǎn),通曉數(shù)種語(yǔ)言的堆棧允許你使用一個(gè)特定的語(yǔ)言,在其的優(yōu)勢(shì)方面,而在其弱勢(shì)的方面,你可以使用另一種語(yǔ)言。
許多程語(yǔ)言一個(gè)共同薄弱的地方就是程序的運(yùn)行時(shí)性能。通常情況下,使用了一種運(yùn)行比較慢的語(yǔ)言,但是如果它同時(shí)能提升程序員的工作效率也是值得的。為了幫助緩解這個(gè)問(wèn)題,他們提供了一個(gè)方法,系統(tǒng)中的一部分用 C 來(lái)寫,然后再調(diào)用 C 代碼,那么這一部分好像就是用高級(jí)語(yǔ)言編寫的似得。這被稱作“外部程序接口”,一般縮寫成“FFI”。
Rust 在兩個(gè)方面上支持 FFI:它可以容易的調(diào)用 C 代碼,但至關(guān)重要的是,它也可以像容易調(diào)用 C 代碼那樣被調(diào)用。當(dāng)你需要一些額外的一些其他功能時(shí),Rust 的無(wú)垃圾收集器和較的低運(yùn)行時(shí)需求,這兩點(diǎn)使得 Rust 成為一個(gè)嵌入到其他語(yǔ)言中的很好的方案。
在本教程中,我們有一整章來(lái)講述 FFI 和它的細(xì)節(jié),但是在本章中,我們將用三個(gè)例子來(lái)展示 FFI 的特定用例,它們分別是在 Ruby,Python 和 JavaScript 中。
這里我們有很多不同的項(xiàng)目可供選擇,但我們要選擇一個(gè)能展示 Rust 比其他許多語(yǔ)言有明顯優(yōu)勢(shì)的例子:數(shù)值計(jì)算和線程。
許多語(yǔ)言為了一致性,將數(shù)字存放在堆上,而不是在堆棧上。尤其是在專注于面向?qū)ο缶幊毯褪褂美占恼Z(yǔ)言上,默認(rèn)的分配模式是堆分配。有時(shí)候優(yōu)化會(huì)將特定的數(shù)字分配給堆棧,但它不是依靠?jī)?yōu)化器來(lái)完成的這項(xiàng)工作。同時(shí),我們可能希望確保我們使用的總是原始的數(shù)字類型而不是某種形式的對(duì)象類型。
第二問(wèn)題,許多語(yǔ)言有一個(gè)“全局解釋器鎖”,這在許多情況下限制了并發(fā)。這是以安全的名義來(lái)進(jìn)行的,本來(lái)這是一個(gè)好意,但是它限制了同一時(shí)間可以完成的工作量,這就非常不好了。
為了強(qiáng)調(diào)這兩個(gè)方面,我們要?jiǎng)?chuàng)建一個(gè)能使用到這兩方面的一個(gè)小項(xiàng)目。因?yàn)槭纠闹攸c(diǎn)是將 Rust 嵌入到其他語(yǔ)言,而不是這個(gè)問(wèn)題的本身,所以我們只用一個(gè)玩具例子:
開(kāi)十個(gè)線程。每個(gè)線程內(nèi)部實(shí)現(xiàn)從一數(shù)到五百萬(wàn)。數(shù)完后,十個(gè)線程結(jié)束,并打印出“done!”。
這里我基于我計(jì)算機(jī)的能力選擇了五百萬(wàn)。下面是一個(gè)用 Ruby 寫的例子的代碼:
threads = []
10.times do
threads << Thread.new do
count = 0
5_000_000.times do
count += 1
end
end
end
threads.each {|t| t.join }
puts "done!"
嘗試運(yùn)行這個(gè)例子,并選擇一個(gè)數(shù)字運(yùn)行幾秒鐘?;谀愕碾娔X硬件,你可能要增大或減小這個(gè)數(shù)字。
在我的系統(tǒng)中,運(yùn)行這個(gè)程序需要 2.156 秒。如果我用某種類似 top 的進(jìn)程監(jiān)控工具,我可以看到它在我的機(jī)器上只使用一個(gè) CPU 核。這是由于 GIL 在起作用。
雖然這確實(shí)是一個(gè)人工合成的程序,但是你也可以想象許多與現(xiàn)實(shí)世界相似的問(wèn)題。就我們的目的而言,運(yùn)行一些繁忙線程就代表了某種并行,昂貴的計(jì)算問(wèn)題。
讓我們用 Rust 重寫這個(gè)問(wèn)題。首先,讓我們用 Cargo 創(chuàng)建一個(gè)新項(xiàng)目:
$ cargo new embed
$ cd embed
這個(gè)程序用 Rust 寫起來(lái)很簡(jiǎn)單:
use std::thread;
fn process() {
let handles: Vec<_> = (0..10).map(|_| {
thread::spawn(|| {
let mut _x = 0;
for _ in (0..5_000_001) {
_x += 1
}
})
}).collect();
for h in handles {
h.join().ok().expect("Could not join a thread!");
}
}
這個(gè)程序中的一些內(nèi)容與從先前的例子看起來(lái)很相似。我們開(kāi)啟了十個(gè)線程,將它們收集成一個(gè) handles 向量。在每個(gè)線程中,我們都循環(huán)五百萬(wàn)次,每次給 _x
加一。這里為什么使用下劃線?嗯,如果我們刪除了它并編譯:
$ cargo build
Compiling embed v0.1.0 (file:///home/steve/src/embed)
src/lib.rs:3:1: 16:2 warning: function is never used: `process`, #[warn(dead_code)] on by default
src/lib.rs:3 fn process() {
src/lib.rs:4 let handles: Vec<_> = (0..10).map(|_| {
src/lib.rs:5 thread::spawn(|| {
src/lib.rs:6 let mut x = 0;
src/lib.rs:7 for _ in (0..5_000_001) {
src/lib.rs:8 x += 1
...
src/lib.rs:6:17: 6:22 warning: variable `x` is assigned to, but never used, #[warn(unused_variables)] on by default
src/lib.rs:6 let mut x = 0;
^~~~~
第一個(gè)警告是因?yàn)槲覀冋跇?gòu)建一個(gè)庫(kù)。如果我們有一個(gè)關(guān)于此函數(shù)的測(cè)試,那么這個(gè)警告將會(huì)消失。但是現(xiàn)在,這個(gè)函數(shù)從未被調(diào)用過(guò)。
第二個(gè)警告與 x ,_x
有關(guān)。因?yàn)槲覀儗?duì) x 沒(méi)有做過(guò)任何操作,所以我們得到了一個(gè)警告。在我們的例子中,這是完全沒(méi)有問(wèn)題的,因?yàn)槲覀兙褪窍肜速M(fèi) CPU 周期。為 x 添加下劃線前綴就會(huì)消除這個(gè)警告。
最后,我們連接了每個(gè)線程。
然而現(xiàn)在這是一個(gè) Rust 庫(kù),它還沒(méi)有公開(kāi)任何從 C 中可調(diào)用的代碼。如果現(xiàn)在我們?cè)噲D將其鏈接到另一種語(yǔ)言,那么它是不能使用的。我們只需要做兩個(gè)小改變就能解決這個(gè)問(wèn)題。第一個(gè)就是修改我們代碼的開(kāi)始部分:
#[no_mangle]
pub extern fn process() {
我們必須添加一個(gè)新的屬性,no_mangle。當(dāng)你創(chuàng)建一個(gè) Rust 庫(kù)時(shí),在編譯輸出階段會(huì)改變函數(shù)的名稱。這個(gè)的原因超出了本教程的范圍。為了讓其他語(yǔ)言知道如何調(diào)用函數(shù),我們不需要改變函數(shù)的名稱。這個(gè)屬性就是將這個(gè)改變功能關(guān)掉。
另一個(gè)變化就是 pub extern。pub 意味著在這個(gè)模塊以外這個(gè)函數(shù)應(yīng)該是可調(diào)用的。extern 表示它應(yīng)該能夠從 C 中被調(diào)用。就這樣了!沒(méi)有很多變化了。
我們需要做的第二件事就是是改變 Cargo.toml 的設(shè)置。添加如下的內(nèi)容到底部:
[lib]
name = "embed"
crate-type = ["dylib"]
這會(huì)告訴 Rust,我們想將我們的庫(kù)編譯成標(biāo)準(zhǔn)動(dòng)態(tài)庫(kù)。默認(rèn)情況下,Rust 會(huì)編譯成一個(gè) ‘rlib’,這是一個(gè) Rust 獨(dú)有的格式。
現(xiàn)在讓我們來(lái)構(gòu)建項(xiàng)目:
$ cargo build --release
Compiling embed v0.1.0 (file:///home/steve/src/embed)
我們選擇最優(yōu)化的構(gòu)建方式 cargo build --release
。因?yàn)槲覀兿M绦蚰苓\(yùn)行的盡可能快!你可以從 target/release
中找到庫(kù)文件的輸出:
$ ls target/release/
build deps examples libembed.so native
這里的 libembed.so 是我們的‘共享對(duì)象’庫(kù)。我們可以像用 C 寫的對(duì)象庫(kù)一樣使用這個(gè)文件!說(shuō)句題外話,根據(jù)平臺(tái)的不同,這個(gè)文件可能是 embed.dll 或是 libembed.dylib。
現(xiàn)在我們已經(jīng)構(gòu)建了我們的 Rust 庫(kù),讓我們?cè)?Ruby 使用它吧。
在我們的項(xiàng)目中打開(kāi) embed.rb 文件,并且按如下所做:
require 'ffi'
module Hello
extend FFI::Library
ffi_lib 'target/release/libembed.so'
attach_function :process, [], :void
end
Hello.process
puts "done!”
在我們可以運(yùn)行這個(gè)程序之前,我們要先安裝 ffi gem:
$ gem install ffi # this may need sudo
Fetching: ffi-1.9.8.gem (100%)
Building native extensions. This could take a while...
Successfully installed ffi-1.9.8
Parsing documentation for ffi-1.9.8
Installing ri documentation for ffi-1.9.8
Done installing documentation for ffi after 0 seconds
1 gem installed
最終,我們可以試著運(yùn)行一下:
$ ruby embed.rb
done!
$
哇,好快!在我的系統(tǒng)上,這花費(fèi)了 0.086 秒的時(shí)間,而不是純 Ruby 版本花費(fèi)的兩秒鐘。讓我們?cè)敿?xì)講一下這段 Ruby 代碼:
require 'ffi
首先我們需要 ffi gem。這能讓我們像連接 C 庫(kù)一樣與 Rust 庫(kù)連接。
module Hello
extend FFI::Library
ffi_lib 'target/release/libembed.so'
ffi gem 的作者推薦使用一個(gè)模塊來(lái)圈定我們將要從共享庫(kù)中導(dǎo)入的方法的作用域。在模塊里面,我們 extend 了必要的 FFI::Library 庫(kù)模塊,然后調(diào)用 ffi_lib
來(lái)加載共享對(duì)象庫(kù)。我們只是給它傳遞了我們庫(kù)文件的存儲(chǔ)路徑,正如我們之前看到的,這個(gè)路徑是 target/release/libembed.so
。
attach_function :process, [], :void
attach_function
方法是由 FFI gem 提供的。它是用來(lái)連接 Rust 與 Ruby 中同名函數(shù) process() 的。因?yàn)?process() 不需要任何參數(shù),所以第二個(gè)參數(shù)是一個(gè)空數(shù)組。因?yàn)樗环祷厝魏螙|西,所以我們傳遞 :void
作為最后的一個(gè)參數(shù)。
Hello.process
這才是對(duì) Rust 的實(shí)際調(diào)用。我們的 module 和 attach_function
的調(diào)用結(jié)合才成就了它。它看起來(lái)像是一個(gè) Ruby 函數(shù),但實(shí)際上是 Rust 的!
puts "done!"
最后,根據(jù)我們之前的項(xiàng)目的需求,我們打印出 done!
就是它了!正如我們所看到的,兩種語(yǔ)言之間的橋接是真的很容易,并且提升了很多性能。
接下來(lái),讓我們來(lái)試試 Python 吧!
在目錄中創(chuàng)建一個(gè) embed.py 文件,并將如下內(nèi)容輸入:
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.process()
print("done!")
這個(gè)更簡(jiǎn)單!我們用 ctypes 模塊中的 cdll。稍后對(duì) LoadLibrary 的一個(gè)快速調(diào)用后,我們就可以調(diào)用 process()了。
在我的系統(tǒng)中,這花費(fèi)了 0.017 秒??彀?!
Node 不是一門語(yǔ)言,但它目前是服務(wù)器端 JavaScript 的主要實(shí)現(xiàn)。
為了用 Node 實(shí)現(xiàn) FFI,我們首先需要安裝庫(kù):
$ npm install ffi
安裝完成后,我們就可以使用它了:
var ffi = require('ffi');
var lib = ffi.Library('target/release/libembed', {
'process': [ 'void', [] ]
});
lib.process();
console.log("done!");
與 Python 的例子相比,它看起來(lái)更像 Ruby 的例子。我們用 ffi 模塊獲得 ffi.Library(),它負(fù)責(zé)加載共享對(duì)象。我們要解釋下函數(shù)的返回類型和參數(shù)類型,返回的是‘空’,參數(shù)是一個(gè)空數(shù)組來(lái)表示的。從此之后,我們就可以調(diào)用它并打印結(jié)果。
在我的系統(tǒng)中,程序運(yùn)行花費(fèi)了 0.092 秒。
正如你可以看到的,這些基本操作是非常簡(jiǎn)單的。當(dāng)然,這里我們還有很多可以做的。更多的細(xì)節(jié)請(qǐng)查看 FFI 章節(jié)。
更多建議: