Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

效能

為了達到最佳效能,了解 PyO3 API 的一些技巧與陷阱很有幫助。

extractcast

使用 PyO3 實作的 Python 風格 API 通常是多型的,也就是接受 &Bound<'_, PyAny> 並嘗試轉換為多個更具體的型別以套用指定操作。這經常導致連串的 extract 呼叫,例如:

#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::{exceptions::PyTypeError, types::PyList};

fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult<Bound<'py, PyAny>> {
    todo!()
}

fn frobnicate_vec<'py>(vec: Vec<Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyAny>> {
    todo!()
}

#[pyfunction]
fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
    if let Ok(list) = value.extract::<Bound<'_, PyList>>() {
        frobnicate_list(&list)
    } else if let Ok(vec) = value.extract::<Vec<Bound<'_, PyAny>>>() {
        frobnicate_vec(vec)
    } else {
        Err(PyTypeError::new_err("Cannot frobnicate that type."))
    }
}

這並不理想,因為 FromPyObject<T> 特徵要求 extract 回傳 Result<T, PyErr>。對於 PyList 這類原生型別,在忽略錯誤值時使用 castextract 內部會呼叫它)會更快。這可避免將 PyDowncastError 轉為 PyErr 的昂貴成本,以滿足 FromPyObject 合約,例如:

#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::{exceptions::PyTypeError, types::PyList};
fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult<Bound<'py, PyAny>> { todo!() }
fn frobnicate_vec<'py>(vec: Vec<Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyAny>> { todo!() }

#[pyfunction]
fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
    // 使用 `cast` 取代 `extract`,因為將 `PyDowncastError` 轉為 `PyErr` 成本很高。
    if let Ok(list) = value.cast::<PyList>() {
        frobnicate_list(list)
    } else if let Ok(vec) = value.extract::<Vec<Bound<'_, PyAny>>>() {
        frobnicate_vec(vec)
    } else {
        Err(PyTypeError::new_err("Cannot frobnicate that type."))
    }
}

存取 Bound 代表可存取 Python token

當我們已附加到直譯器時,呼叫 Python::attach 幾乎是 no-op,但檢查這件事仍有成本。若無法存取既有的 Python token(例如在實作既有特徵時),但已持有 Python 綁定的參照,就可利用 Bound::py 以零成本取得 Python token,避免該成本。

例如,與其寫成

#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::types::PyList;

struct Foo(Py<PyList>);

struct FooBound<'py>(Bound<'py, PyList>);

impl PartialEq<Foo> for FooBound<'_> {
    fn eq(&self, other: &Foo) -> bool {
        Python::attach(|py| {
            let len = other.0.bind(py).len();
            self.0.len() == len
        })
    }
}

請改用較有效率的

#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::types::PyList;
struct Foo(Py<PyList>);
struct FooBound<'py>(Bound<'py, PyList>);

impl PartialEq<Foo> for FooBound<'_> {
    fn eq(&self, other: &Foo) -> bool {
        // 存取 `&Bound<'py, PyAny>` 代表可存取 `Python<'py>`。
        let py = self.0.py();
        let len = other.0.bind(py).len();
        self.0.len() == len
    }
}

呼叫 Python 可呼叫物件(__call__

CPython 支援多種呼叫協定:tp_callvectorcallvectorcall 是更高效的協定,可加速呼叫。PyO3 會在可能時嘗試使用 vectorcall 呼叫慣例來達到最高效能,否則退回 tp_call。這是透過(內部的)PyCallArgs 特徵實作的,它定義 Rust 型別如何作為 Python call 參數使用。此特徵目前為以下項目實作:

  • Rust tuple,其中每個成員都實作 IntoPyObject
  • Bound<'_, PyTuple>
  • Py<PyTuple>

Rust tuple 可使用 vectorcall,而 Bound<'_, PyTuple>Py<PyTuple> 只能使用 tp_call。為求最佳效能,請優先使用 Rust tuple 作為參數。

長時間的純 Rust 工作請從直譯器分離

當執行不需要與 Python 直譯器互動的 Rust 程式碼時,請使用 Python::detach,讓 Python 直譯器在不等待目前執行緒的情況下繼續執行。

在啟用 GIL 的建置中,這對最佳效能至關重要,因為同一時間只能有單一執行緒附加。

在自由執行緒建置中,這仍是最佳實務,因為存在多個「stop the world」事件(如垃圾回收),會迫使所有附加到 Python 直譯器的執行緒等待。

一般而言,附加與分離 Python 直譯器所需時間不到 1 毫秒,因此預期耗時數毫秒以上的工作,多半可受益於先從直譯器分離。

停用全域參考池

PyO3 使用全域可變狀態來追蹤在未附加到直譯器時呼叫 impl<T> Drop for Py<T> 所隱含的延後參考計數更新。當 PyO3 程式碼下一次附加到直譯器時,需要進行同步以取得並套用這些參考計數更新,成本不低,且可能成為跨越 Python-Rust 邊界的重要開銷。

可透過設定 pyo3_disable_reference_pool 條件編譯旗標來避免此功能。這會完全移除全域參考池與相關成本。然而,它_不會_移除 Py<T>Drop 實作,該實作對於與不以 PyO3 為前提撰寫的既有 Rust 程式碼互通是必要的。為了與更廣泛的 Rust 生態系相容,我們保留該實作,但在未附加到直譯器時呼叫 Drop 會中止。若另外啟用 pyo3_leak_on_drop_without_reference_pool,則在未附加到 Python 時被釋放的物件會改為洩漏,這永遠是安全的,但長期可能造成資源耗盡等不良影響。

使用此設定時務必留意這項限制,特別是在把 Python 程式碼嵌入 Rust 應用程式時,因為很容易在未先重新附加的情況下,意外釋放由 Python::attach 回傳的 Py<T>(或包含它的型別,如 PyErrPyBackedStrPyBackedBytes)。例如,下列程式碼

use pyo3::prelude::*;
use pyo3::types::PyList;
let numbers: Py<PyList> = Python::attach(|py| PyList::empty(py).unbind());

Python::attach(|py| {
    numbers.bind(py).append(23).unwrap();
});

Python::attach(|py| {
    numbers.bind(py).append(42).unwrap();
});

若清單未明確透過下列方式處理,將會中止

use pyo3::prelude::*;
use pyo3::types::PyList;
let numbers: Py<PyList> = Python::attach(|py| PyList::empty(py).unbind());

Python::attach(|py| {
    numbers.bind(py).append(23).unwrap();
});

Python::attach(|py| {
    numbers.bind(py).append(42).unwrap();
});

Python::attach(move |py| {
    drop(numbers);
});