背景
- 之前都是在看C/Cpp,也在努力的学这两个语言。但是感觉太难了。语法太多了(主要还是人笨,学不过来)。
- 一直也在看rust,感觉rust写起来和python差不多优雅,而且写法和c++又是有点类似。
- 然后最近空闲时间就把《Rust程序设计语言》和《通过例子学Rust》两本书看完了。学到了不少新内容。
- 最近也发现一些新的包,比如一个叫
polar
包的,提供类pandas的功能,但是比pandas更快,这个包的底层是使用rust写的,并且python也能使用。那我就在想:“我能不能学习一下,看看怎么让python用rust编译的东西,或者说,如果使用rust加速python。” 这篇文章主要就是介绍一个简单的rust加速python的案例。(主要是翻译,原文已放在文末的参考链接中)。
步骤
创建一个rust包
cargo new pyext-myrustlib
然后使用vscode进入这个文件夹下,打开src
文件夹,创建一个新文件:lib.rs
。
编辑Cargo.toml
- 依赖的是
rust-cpython
。当前版本是0.7(2022年4月)。 - 输出的是一个
dylib
。这个可以让python直接import。
[package]
name = "pyext-myrustlib"
version = "0.1.0"
edition = "2021"
[lib]
name="myrustlib"
crate-type=["dylib"]
[dependencies.cpython]
version="0.7"
features=["extension-module"]
编写src/lib.rs
- 从
cpython
导入宏。 - 调用
Python
,PyResult
- 构建一个
count_doubles
函数。
- 3.1 这个函数第一个参数是
Python
,是对python解释器的引用,可以让rust使用python的GIL。 - 3.2 函数的第二个参数是val,是一个字符串的引用。
- 3.3 返回的对象是
PyResult
。即使到时候有异常,也可以让这个函数报错。 - 3.4 更多的细节,其实可以看看rust官网上两本书。
- 使用宏
py_module_initializer!
给这个lib注册一个新的属性。比如这里就是给count_doubles
函数添加了一个函数文档。
#[macro_use]
extern crate cpython;
use cpython::{Python, PyResult};
fn count_doubles(_py:Python, val:&str) -> PyResult<u64>{
let mut total = 0u64;
// there is an imporved version later on this post
for (c1, c2) in val.chars().zip(val.chars().skip(1)) {
if c1 == c2 {
total += 1;
}
}
Ok(total)
}
py_module_initializer!(libmyrustlib, initlibmyrustlib, PyInit_myrustlib, |py, m | {
m.add(py, "__doc__", "This module is implemented in Rust")?;
m.add(py, "count_doubles", py_fn!(py, count_doubles(val: &str)))?;
Ok(())
});
build
- build一下
cargo build --release
- 看输出的结果
build后,会产生一个文件:
ls -la target/release/libmyrustlib*
看到上面的结果,就知道,这个时候已经生成了so
和d
格式的文件。
接下来,把这个so
结尾的文件复制到一个新的文件夹中。
我就是把这个so
结尾的文件放在了mypythoncode
文件夹中。
运行python
我在mypythoncode
文件夹,还创建了doubles.py
文件,用来做评测。这个文件的代码如下:
import re
import string
import random
import libmyrustlib # <-- 这里就是我们要import的rust的打包后的文件 (libmyrustlib.so)
def count_doubles(val):
"""Count repeated pair of chars ins a string"""
total = 0
for c1, c2 in zip(val, val[1:]):
if c1 == c2:
total += 1
return total
double_re = re.compile(r'(?=(.)\1)')
def count_doubles_regex(val):
return len(double_re.findall(val))
val = ''.join(random.choice(string.ascii_letters) for i in range(1000000))
def test_pure_python(benchmark):
benchmark(count_doubles, val)
def test_regex(benchmark):
benchmark(count_doubles_regex, val)
def test_rust(benchmark): # <-- 测试rust打包的函数的速度
benchmark(libmyrustlib.count_doubles, val)
接下来我们比较各个函数的运行效率。
如果没有安装pytest
。需要先安装一下,不然python程序跑不起来:pip install pytest-benchmark
.
benchmark
在mypythoncode
文件夹下,运行:
pytest doubles.py
在mean这一列,会发现rust写的函数运行时间
是python写的函数运行时间
的1/25倍左右。差距非常大。
注意事项:⚠️
- 本文不是一个比较性能的文章。因此上面的运行效率参考可能没什么价值,实际上,如果使用numpy计算的话,时间应该是rust版本的2倍。而不是25倍。
- 大家最好看一下原作者的内容【在参考链接[1] 中】. 原作者中一下代码和依赖的版本和现在不一样了,如果要说看代码部分,看我这个部分就行了。
- 目前还在学习rust中,只是把《Rust程序设计语言》和《通过例子学Rust》两本书看完了,代码都敲了一遍。说实话,还是非常喜欢这个语言的。感觉大部分代码写法和python差不多。后面应该还是会继续深入学习。
- 未来会继续把时间投资在
python
和rust
上。学习算法、学习科学计算。