使用Rust加速Python


https://josephok.github.io/2019/03/30/Speed-up-Python-program-with-Rust/

Python具有開發快速的特點,但是在運行效率上比靜態編譯型語言慢不少,我們今天要介紹的Rust就是其中一種。

Rust是一種安全、並發、實用的編程語言,有着驚人的運行速度,能夠防止段錯誤,並保證線程安全,使每個人都能夠構建可靠、高效的軟件。

當我們的Python程序出現性能瓶頸時,可以從如下幾個方面優化:

  1. 優化算法,使用更高效率的算法來提升性能;
  2. 使用並發,如多線程程序;
  3. 使用編譯型語言編寫擴展;
  4. 優化網絡、磁盤、數據庫等。

性能優化是個大命題,我們需要從多個方面着手考慮,今天我介紹的是第3種方法,並且選擇Rust語言。我們將編寫一個so擴展供Python端調用。
這里不會講Rust的入門,具體規范可以看官方文檔或者中文文檔:https://rustlang-cn.org/

我們選擇Httpbin作為基准程序,進行修改然后對比效果。

1. 原始代碼

為了簡單,我稍微修改了view_get,使之只返回客戶端的請求方法。如下:

1
2
3
{
"method": "GET"
}

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# httpbin/core.py

@app.route("/get", methods=("GET",))
def view_get():
"""The request's query parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's query parameters.
"""

return jsonify(get_dict("method"))

 

2. 測試一下原始代碼性能:

使用wrk進行benchmark測試:

1
2
3
4
5
6
7
8
9
10
11
wrk http://127.0.0.1:5000/get -c 400 -t 10

Running 10s test @ http://127.0.0.1:5000/get
10 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 314.77ms 121.47ms 1.98s 95.24%
Req/Sec 54.09 37.46 200.00 64.29%
4355 requests in 10.04s, 1.00MB read
Socket errors: connect 0, read 0, write 0, timeout 9
Requests/sec: 433.98
Transfer/sec: 101.71KB

 

可以看出:平均時延為315ms左右,RPS為434左右。

3. 使用rust-cpython編寫擴展

首先新建一個lib類型的項目,比如名字為handle

1
cargo new handle --lib

 

這樣就在當前項目下新建了一個目錄:handle,我們需要編輯handle/Cargo.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
[package]
name = "handle"
version = "0.1.0"
authors = ["Joseph <josephok@qq.com>"]
edition = "2018"

[lib]
name = "handle"
crate-type = ["cdylib"]

[dependencies.cpython]
version = "0.2"
features = ["extension-module"]

然后是編輯handle/src/lib.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[macro_use]
extern crate cpython;

use cpython::ObjectProtocol;
use cpython::{PyObject, PyResult, Python};
use std::collections::HashMap;

fn ret_py_dict(py: Python, obj: PyObject) -> PyResult<HashMap<&'static str, String>> {
let mut response = HashMap::new();
response.insert("method", obj.getattr(py, "method")?.extract(py)?);

Ok(response)
}

py_module_initializer!(handle, init_handle, PyInit_handle, |py, m| {
m.add(py, "ret_py_dict", py_fn!(py, ret_py_dict(val: PyObject)))?;

Ok(())
});

這里的handle是模塊名,ret_py_dict就是模塊的方法,供Python調用,使用方法:

1
2
import handle
handle.ret_py_dict(...)

 

然后需要編譯此模塊,我們使用Makefile編寫編譯規則:

1
2
3
4
5
build:
# 編譯
cd handle && cargo build --release
# 將so文件拷貝到Python代碼目錄
cp handle/target/release/libhandle.so httpbin/handle.so

 

執行make命令編譯並將so文件拷貝到指定目錄。

4. Python端調用方法

編寫一個view_get1方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from . import handle

@app.route("/get1", methods=("GET",))
def view_get1():
"""The request's query parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's query parameters.
"""
resp = handle.ret_py_dict(request)
return jsonify(resp)

與我們的原始函數唯一區別是:jsonify函數的參數,即響應內容是由擴展模塊產生,類型都是dict

curl響應為:

1
2
3
4
5
curl localhost:5000/get1

{
"method": "GET"
}

 

結果一致。

5. 測試使用了擴展的性能

1
2
3
4
5
6
7
8
9
10
11
wrk http://127.0.0.1:5000/get1 -c 400 -t 10

Running 10s test @ http://127.0.0.1:5000/get1
10 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 247.73ms 101.96ms 1.91s 96.28%
Req/Sec 80.17 53.34 272.00 59.49%
5482 requests in 10.09s, 1.25MB read
Socket errors: connect 0, read 0, write 0, timeout 4
Requests/sec: 543.49
Transfer/sec: 127.38KB

可以看出:平均時延為248ms左右,RPS為544左右。
比原始版本:時延低70ms,RPS高110,效果比較明顯,這里僅僅改寫了獲取對象屬性的方式。

6. 總結

如果想繼續優化,可以考慮改寫jsonify函數。在這里我們提出了一個優化Python程序性能(Latency, RPS)的方案。前文也說到過,優化性能應該先從代碼結構、算法方面做優化,當語言成為瓶頸時,使用Rust編寫擴展實為一種好的方式。

參考:

https://developers.redhat.com/blog/2017/11/16/speed-python-using-rust/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM