rust让你的python飞起来

Note: 本文作为入门教程,抛砖引玉,帮你初步了解如何使用rust为python写扩展模块,涉及从头到尾的详细步骤,基于此,剩下的只有深入rust,才能做得更好。

众所周知,python性能比较差,尤其在计算密集型的任务当中,所以机器学习领域的算法开发,大多是将python做胶水来用,他们会在项目中写大量的C/C++代码然后编译为so动态文件供python加载使用。那么时至今日,对于不想学习c/c++的朋友们,rust可以是一个不错的替代品,它有着现代化语言的设计和并肩c/c++语言的运行效率。

本文简单介绍使用rust为python计算性质的代码做一个优化,使用pyo3库为python写一个扩展供其调用,咱们下面开始,来看看具体的过程和效率的提升。

我的台式机环境:

设备名称 DESKTOP 处理器 12th Gen Intel(R) Core(TM) i7-12700 2.10 GHz 机带 RAM 32.0 GB (31.8 GB 可用) 系统类型 64 位操作系统, 基于 x64 的处理器

1. python代码

首先给出python代码,这是一个求积分的公式:

python 复制代码
import time

def integrate_f(a, b, N):
    s = 0
    dx = (b - a) / N
    for i in range(N):
        s += 2.71828182846 ** (-((a + i * dx) ** 2))
    return s * dx


s = time.time()
print(integrate_f(1.0, 100.0, 200000000))
print("Elapsed: {} s".format(time.time() - s))

执行这段代码花费了: Elapsed: 32.59504199028015 s

2. rust

rust 复制代码
use std::time::Instant;

fn main() {
    let now = Instant::now();
    let result = integrate_f(1.0, 100.0, 200000000);
    println!("{}", result);

    println!("Elapsed: {:.2} s", now.elapsed().as_secs_f32())
}

fn integrate_f(a: f64, b: f64, n: i32) -> f64 {
    let mut s: f64 = 0.0;
    let dx: f64 = (b - a) / (n as f64);

    for i in 0..n {
        let mut _tmp: f64 = (a + i as f64 * dx).powf(2.0);
        s += (2.71828182846_f64).powf(-_tmp);
    }

    return s * dx;
}

执行这段代码花费了: Elapsed: 10.80 s

3. 通过pyo3写扩展

首先创建一个项目,并安装 maturin 库:

bash 复制代码
# (replace demo with the desired package name)
$ mkdir demo
$ cd demo
$ pip install maturin

然后初始化一个pyo3项目:

bash 复制代码
$ maturin init
✔ 🤷 What kind of bindings to use? · pyo3
  ✨ Done! New project created demo

整体项目结构如下: Cargo.toml中的一些字段含义:doc.rust-lang.org/cargo/refer...

text 复制代码
.
├── Cargo.toml	// rust包管理文件,会在[lib]中声明目标扩展包的名称
├── src			// rust源文件目录, 你的扩展文件就写在这里,这个目录是maturin初始化的时候自动创建
│   └── lib.rs	// 扩展文件
├── pyproject.toml	// python包管理文件,里面有python的包名字定义
├── .gitignore
├── Cargo.lock
└── demo	// 我们的目标模块名称,需手动创建
    ├── main.py	// 用来测试的文件
    └── demo.cp312-win_amd64.pyd	// 编译生成的动态链接库文件,供import给python使用

src/lib.rs 下写入:

rust 复制代码
use pyo3::prelude::*;

// Caculate the integrate. This function will be exposed to python.
#[pyfunction]
fn integrate_f(a: f64, b: f64, n: i32) -> f64 {
    let mut s: f64 = 0.0;
    let dx: f64 = (b - a) / (n as f64);

    for i in 0..n {
        let mut _tmp: f64 = (a + i as f64 * dx).powf(2.0);
        s += (2.71828182846_f64).powf(-_tmp);
    }

    return s * dx;
}

// A Python module implemented in Rust. The name of this function must match
// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
// import the module.
#[pymodule]
fn demo(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(integrate_f, m)?)?;
    Ok(())
}

然后我们通过两种途径来使用它:

3.1 将扩展安装为python包

bash 复制代码
$ maturin develop

这个命令会将rust代码转为python的包,并安装在当前python环境内。通过 pip list 就能看到。

3.2 编译成动态文件从python加载

bash 复制代码
$ maturin develop --skip-install

--skip-install 命令会产生一个 pyd 文件而不是将其安装为python的包 - demo.cp312-win_amd64.pyd 文件在当前目录下,然后python可以直接导入使用。

另外还有一个指令替换 --skip-install--release 会生成一个 xxxx.whl 文件,也就是Python pip安装的包源文件。

首先我们在rust项目下,与 Cargo.toml 同级目录下,创建一个 demo 目录,然后我们写一个python文件 demo/main.py,下面是扩展的执行效果:

python 复制代码
import time

import demo

s = time.time()
print(demo.integrate_f(1.0, 100.0, 200000000))
print("Elapsed: {} s".format(time.time() - s))

花费时间为:Elapsed: 10.908721685409546 s

可以看到python的执行时间是rust和rust扩展的3倍时长,单进程看着好像不太大是吧,下面还有并行版本。

4. 并行加速

4.1 python多进程效果

Python多进程很神奇,你写的不好的话,他比单进程下还要慢。

python 复制代码
import math
import os
import time
from functools import partial
from multiprocessing import Pool


def sum_s(i: int, dx: float, a: int):
    return math.e ** (-((a + i * dx) ** 2))


def integrate_f_parallel(a, b, N):
    s: float = 0.0
    dx = (b - a) / N

    sum_s_patrial = partial(sum_s, dx=dx, a=a)

    with Pool(processes=os.cpu_count()) as pool:
        tasks = pool.map_async(sum_s_patrial, range(N), chunksize=20000)

        for t in tasks.get():
            s += t

    return s * dx


if __name__ == "__main__":
    s = time.time()
    print(integrate_f_parallel(1.0, 100.0, 200000000))
    print("Elapsed: {} s".format(time.time() - s))

花费时间: Elapsed: 18.86696743965149 s,比单进程下时间少了不到一半。

4.2 rust多线程加速给python使用

如果我们使用rust的并行库,将rust进一步加速,速度效果更明显: 将上面的 integrate_f 替换为下面的多线程版本:

rust 复制代码
use pyo3::prelude::*;
use rayon::prelude::*;

#[pyfunction]
fn integrate_f_parallel(a: f64, b: f64, n: i32) -> f64 {
    let dx: f64 = (b - a) / (n as f64);

    let s: f64 = (0..n)
        .into_par_iter()
        .map(|i| {
            let x = a + i as f64 * dx;
            (2.71828182846_f64).powf(-(x.powf(2.0)))
        })
        .sum();

    return s * dx;
}

/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn demo(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(integrate_f_parallel, m)?)?;
    Ok(())
}

执行上一个标题3.2的步骤,然后在引入python使用:

python 复制代码
import time

import demo

s = time.time()
print(demo.integrate_f_parallel(1.0, 100.0, 200000000))
print("Elapsed: {} s".format(time.time() - s))

花费时间为:Elapsed: 0.9684994220733643 s。这比原先的单线程rust版本又快了10倍。但是差不多是python并行版本的18倍左右,是python单进程版本的32倍左右。如果我们将一些关键的性能通过rust重写,可以节省的时间成本是十分可观的。

总体来看,整体的使用过程相当简洁方便,难点就是rust的学习曲线高,使用起来需要花费精力,但是还是可以慢慢尝试去使用它优化已有的项目性能,哪怕只是一个功能函数,熟能生巧,一切慢慢来。

以上数据比较仅供参考,不同机器可能差异也不同。

相关推荐
寻月隐君24 分钟前
想用 Rust 开发游戏?这份超详细的入门教程请收好!
后端·rust·github
Otaku love travel34 分钟前
实施运维文档
运维·windows·python
测试老哥1 小时前
软件测试之单元测试
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
presenttttt1 小时前
用Python和OpenCV从零搭建一个完整的双目视觉系统(六 最终篇)
开发语言·python·opencv·计算机视觉
测试19983 小时前
软件测试之压力测试总结
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·压力测试
LuckyLay3 小时前
使用 Docker 搭建 Rust Web 应用开发环境——AI教你学Docker
前端·docker·rust
李昊哲小课3 小时前
销售数据可视化分析项目
python·信息可视化·数据分析·matplotlib·数据可视化·seaborn
烛阴3 小时前
带参数的Python装饰器原来这么简单,5分钟彻底掌握!
前端·python
SoniaChen334 小时前
Rust基础-part3-函数
开发语言·后端·rust
全干engineer4 小时前
Flask 入门教程:用 Python 快速搭建你的第一个 Web 应用
后端·python·flask·web