Выполнение Python кода параллельно с Rust с rust-cpython - PullRequest
1 голос
/ 10 февраля 2020

Я пытаюсь ускорить конвейер данных, используя Rust. Конвейер содержит биты кода Python, которые я не хочу изменять, поэтому я пытаюсь запустить их как есть из Rust, используя rust- cpython и несколько потоков. Тем не менее, производительность не та, которую я ожидал, на самом деле это то же самое, что последовательный запуск 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);
}

1 Ответ

2 голосов
/ 11 февраля 2020

Реализация C *1048*Python не позволяет выполнять Python байт-код в нескольких потоках одновременно. Как вы сами отметили, глобальная блокировка интерпретатора (GIL) предотвращает это.

У нас нет никакой информации о том, что именно делает ваш код Python, поэтому я дам несколько общих советов, как можно улучшить производительность вашего кода.

  • Если ваш код привязан к вводу / выводу, например, при чтении из сети, вы, как правило, получите хорошие улучшения производительности от используя несколько потоков. Блокировка вызовов ввода / вывода освобождает GIL перед блокировкой, поэтому другие потоки могут выполняться в течение этого времени.

  • Некоторые библиотеки, например NumPy, внутренне освобождают GIL во время длительной работы библиотеки вызовы, которым не нужен доступ к Python структурам данных. Эти библиотеки позволяют повысить производительность многопоточного кода с привязкой к ЦП, даже если вы используете только чистый код Python с использованием библиотеки.

  • Если ваш код - ЦП Связанный и тратящий большую часть своего времени на выполнение Python байт-кода, вы часто можете использовать несколько процессов вместо потоков для достижения параллельного выполнения. multiprocessing в стандартной библиотеке Python помогает в этом.

  • Если ваш код привязан к процессору, большую часть своего времени тратит на выполнение Python Байт-код и не может быть запущен в параллельных процессах, потому что он обращается к совместно используемым данным, вы не можете запустить его в нескольких потоках параллельно - GIL предотвращает это. Однако даже без GIL вы не можете просто запустить последовательный код параллельно без изменений на любом языке. Поскольку у вас есть одновременный доступ к некоторым данным, вам нужно добавить блокировку и, возможно, внести изменения в алгоритмы c, чтобы предотвратить скачки данных; детали того, как это сделать, зависят от вашего варианта использования. (И если у вас нет одновременного доступа к данным, вы должны использовать процессы вместо потоков - см. Выше.)

Помимо параллелизма, хороший способ ускорить работу up Python код с Rust для профиля вашего Python кода, найдите горячих точек , где проводится большая часть времени, и переписайте эти биты как функции Rust, которые вы вызываете из кода Python. Если это не дает вам достаточного ускорения, вы можете объединить этот подход с параллелизмом - предотвращение гонок данных в Rust обычно проще, чем в большинстве других языков.

...