运行 Python Rust 与 rust-cpython 的并行代码
Running Python code in parallel from Rust with rust-cpython
我正在尝试使用 Rust 加速数据管道。管道包含我不想修改的 Python 代码位,因此我尝试使用 rust-cpython 和多线程从 Rust 中按原样 运行 它们。
然而,性能不是我所期望的,它实际上与在单个线程中按顺序 运行ning python 代码位相同。
阅读文档,我了解到在调用以下内容时,您实际上得到了一个指向单个 Python 解释器的指针,即使您分别从多个线程 运行 它也只能创建一次.
let gil = Python::acquire_gil();
let py = gil.python();
如果是这样,则意味着 Python GIL 实际上也在阻止 Rust 中的所有并行执行。有没有办法解决这个问题?
这是我的测试代码:
use cpython::Python;
use std::thread;
use std::sync::mpsc;
use std::time::Instant;
#[test]
fn python_test_parallel() {
let start = Instant::now();
let (tx_output, rx_output) = mpsc::channel();
let tx_output_1 = mpsc::Sender::clone(&tx_output);
thread::spawn(move || {
let gil = Python::acquire_gil();
let py = gil.python();
let start_thread = Instant::now();
py.run("j=0\nfor i in range(10000000): j=j+i;", None, None).unwrap();
println!("{:27} : {:6.1} ms", "Run time thread 1, parallel", (Instant::now() - start_thread).as_secs_f64() * 1000f64);
tx_output_1.send(()).unwrap();
});
let tx_output_2 = mpsc::Sender::clone(&tx_output);
thread::spawn(move || {
let gil = Python::acquire_gil();
let py = gil.python();
let start_thread = Instant::now();
py.run("j=0\nfor i in range(10000000): j=j+i;", None, None).unwrap();
println!("{:27} : {:6.1} ms", "Run time thread 2, parallel", (Instant::now() - start_thread).as_secs_f64() * 1000f64);
tx_output_2.send(()).unwrap();
});
// Receivers to ensure all threads run
let _output_1 = rx_output.recv().unwrap();
let _output_2 = rx_output.recv().unwrap();
println!("{:37} : {:6.1} ms", "Total time, parallel", (Instant::now() - start).as_secs_f64() * 1000f64);
}
Python 的 CPython 实现不允许同时在多个线程中执行 Python bytecode。正如您自己注意到的,全局解释器锁 (GIL) 阻止了这种情况。
我们没有关于您的 Python 代码究竟在做什么的任何信息,因此我将提供一些一般提示,告诉您如何提高代码的性能。
如果您的代码是 I/O-bound,例如从网络上阅读,您通常会通过使用多线程获得不错的性能提升。阻塞 I/O 调用将在阻塞之前释放 GIL,因此其他线程可以在此期间执行。
一些库,例如NumPy,在不需要访问 Python 数据结构的长期 运行ning 库调用期间在内部释放 GIL。使用这些库,即使您只使用库编写纯 Python 代码,您也可以获得多线程、CPU 绑定代码的性能改进。
如果你的代码是 CPU-bound 并且大部分时间都在执行 Python 字节码,你可以经常使用 multipe processes 而不是线程来实现并行执行。 Python 标准库中的 multiprocessing
对此有所帮助。
如果您的代码是 CPU 绑定的,则大部分时间都在执行 Python 字节码 和 不能 运行 在并行进程中,因为它访问共享数据,你不能在多个线程中并行地 运行 它——GIL 阻止了这种情况。但是,即使没有 GIL,您也不能只 运行 并行顺序代码而不更改 any 语言。由于您可以并发访问某些数据,因此需要添加锁定并可能进行算法更改以防止数据竞争;如何执行此操作的详细信息取决于您的用例。 (如果你没有并发数据访问,你应该使用进程而不是线程——见上文。)
除了并行性之外,使用 Rust 加速 Python 代码的一个好方法是 profile 您的 Python 代码,找到 热点 花费了大部分时间,并且 重写 这些位作为您从 Python 代码调用的 Rust 函数。如果这不能给你足够的加速,你可以将这种方法与并行性结合起来——防止数据竞争在 Rust 中通常比在大多数其他语言中更容易实现。
我正在尝试使用 Rust 加速数据管道。管道包含我不想修改的 Python 代码位,因此我尝试使用 rust-cpython 和多线程从 Rust 中按原样 运行 它们。 然而,性能不是我所期望的,它实际上与在单个线程中按顺序 运行ning python 代码位相同。
阅读文档,我了解到在调用以下内容时,您实际上得到了一个指向单个 Python 解释器的指针,即使您分别从多个线程 运行 它也只能创建一次.
let gil = Python::acquire_gil();
let py = gil.python();
如果是这样,则意味着 Python GIL 实际上也在阻止 Rust 中的所有并行执行。有没有办法解决这个问题?
这是我的测试代码:
use cpython::Python;
use std::thread;
use std::sync::mpsc;
use std::time::Instant;
#[test]
fn python_test_parallel() {
let start = Instant::now();
let (tx_output, rx_output) = mpsc::channel();
let tx_output_1 = mpsc::Sender::clone(&tx_output);
thread::spawn(move || {
let gil = Python::acquire_gil();
let py = gil.python();
let start_thread = Instant::now();
py.run("j=0\nfor i in range(10000000): j=j+i;", None, None).unwrap();
println!("{:27} : {:6.1} ms", "Run time thread 1, parallel", (Instant::now() - start_thread).as_secs_f64() * 1000f64);
tx_output_1.send(()).unwrap();
});
let tx_output_2 = mpsc::Sender::clone(&tx_output);
thread::spawn(move || {
let gil = Python::acquire_gil();
let py = gil.python();
let start_thread = Instant::now();
py.run("j=0\nfor i in range(10000000): j=j+i;", None, None).unwrap();
println!("{:27} : {:6.1} ms", "Run time thread 2, parallel", (Instant::now() - start_thread).as_secs_f64() * 1000f64);
tx_output_2.send(()).unwrap();
});
// Receivers to ensure all threads run
let _output_1 = rx_output.recv().unwrap();
let _output_2 = rx_output.recv().unwrap();
println!("{:37} : {:6.1} ms", "Total time, parallel", (Instant::now() - start).as_secs_f64() * 1000f64);
}
Python 的 CPython 实现不允许同时在多个线程中执行 Python bytecode。正如您自己注意到的,全局解释器锁 (GIL) 阻止了这种情况。
我们没有关于您的 Python 代码究竟在做什么的任何信息,因此我将提供一些一般提示,告诉您如何提高代码的性能。
如果您的代码是 I/O-bound,例如从网络上阅读,您通常会通过使用多线程获得不错的性能提升。阻塞 I/O 调用将在阻塞之前释放 GIL,因此其他线程可以在此期间执行。
一些库,例如NumPy,在不需要访问 Python 数据结构的长期 运行ning 库调用期间在内部释放 GIL。使用这些库,即使您只使用库编写纯 Python 代码,您也可以获得多线程、CPU 绑定代码的性能改进。
如果你的代码是 CPU-bound 并且大部分时间都在执行 Python 字节码,你可以经常使用 multipe processes 而不是线程来实现并行执行。 Python 标准库中的
multiprocessing
对此有所帮助。如果您的代码是 CPU 绑定的,则大部分时间都在执行 Python 字节码 和 不能 运行 在并行进程中,因为它访问共享数据,你不能在多个线程中并行地 运行 它——GIL 阻止了这种情况。但是,即使没有 GIL,您也不能只 运行 并行顺序代码而不更改 any 语言。由于您可以并发访问某些数据,因此需要添加锁定并可能进行算法更改以防止数据竞争;如何执行此操作的详细信息取决于您的用例。 (如果你没有并发数据访问,你应该使用进程而不是线程——见上文。)
除了并行性之外,使用 Rust 加速 Python 代码的一个好方法是 profile 您的 Python 代码,找到 热点 花费了大部分时间,并且 重写 这些位作为您从 Python 代码调用的 Rust 函数。如果这不能给你足够的加速,你可以将这种方法与并行性结合起来——防止数据竞争在 Rust 中通常比在大多数其他语言中更容易实现。