• 初探: 通过pyo3用rust为python写扩展加速


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

    本文简单介绍使用rust为python计算性质的代码做一个优化,使用pyo3库为python写一个扩展供其调用,咱们下面开始,来看看具体的过程和效率的提升。(PS:本文只是抛砖引玉,初级教程)

    我的台式机环境:

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

    1. 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

    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 库:

    # (replace demo with the desired package name)
    $ mkdir demo
    $ cd demo
    $ pip install maturin
    

    然后初始化一个pyo3项目:

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

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

    .
    ├── 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 下写入:

    use pyo3::prelude::*;
    
    /// Caculate the integrate.
    #[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包

    $ maturin develop
    

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

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

    $ 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,下面是扩展的执行效果:

    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多进程很神奇,你写的不好的话,他比单进程下还要慢。

    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 替换为下面的多线程版本:

    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使用:

    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的学习曲线高,使用起来需要花费精力,但是还是可以慢慢尝试去使用它优化已有的项目性能,哪怕只是一个功能函数,熟能生巧,一切慢慢来。

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

  • 相关阅读:
    CPC客户端的安装教程
    面试经典150题——Day13
    golang工程——gRPC keep alive 配置
    微信小程序---页面事件
    手把手教程-自动抢单某知名外卖程序制作
    hack the box-blue team
    MyBatis-Plus快速入门
    在Spring Boot微服务使用RedisTemplate操作Redis集群
    Vue3.0-如此简单的setup
    安科瑞ADL400产品功能及参数说明,适用于5G基站计量使用
  • 原文地址:https://www.cnblogs.com/cpl9412290130/p/17965613