Группа муравьев | Объект признаков или таблица виртуальных методов
Автор: Ши Зеюй
Трейт-объекты — это реализация динамической диспетчеризации в Rust. В журнале Rust Magazine, опубликованном в апреле 2021 года, Цзякай Лю представил использование специального полиморфизма в Rust в статье «Анализ использования и реализации признаков», включая статическое и динамическое распределение, а также анализ признаков в объектах свойств. вопросы безопасности и причины объясняются подробно. Итак, является ли использование трейт-объектов концом динамической диспетчеризации в Rust? На самом деле, мы обнаружили, что во многих Rust-кодах вместо трейт-объектов используются примитивные виртуальные таблицы, в чем причина этого? В этой статье я кратко представлю трейт-объекты и виртуальные таблицы, а затем обсужу преимущества и недостатки ручного создания виртуальных таблиц вместо использования трейт-объектов в сочетании с несколькими репрезентативными фрагментами кода, выбранными автором.
Введение
Есть два способа реализации полиморфизма с помощью трейтов в Rust: статическая диспетчеризация или динамическая диспетчеризация. Статическое распределение использует привязку типажа или черту типаж для достижения мономорфизма времени компиляции, создавая соответствующие структуры или функции на основе параметров типа. Динамическое распределение реализуется с помощью трейт-объектов, а поскольку трейт-объекты представляют собой типы с динамическим размером, размер типа нельзя определить во время компиляции, поэтому для управления трейт-объектом обычно используется ссылка или указатель на трейт-объект. Ссылка или указатель на трейт-объект — это, по сути, толстый указатель, который содержит указатель на объект с определенным стертым типом и таблицу виртуальных функций. Следовательно, каждый раз, когда вызывается метод типаж-объекта, толстый указатель необходимо разыменовывать, поэтому некоторые представления считают, что динамическое распределение дороже статического распределения, в то время как противоположное мнение состоит в том, что использование статического распределения приведет к увеличению времени компиляции и раздутые бинарные файлы после компиляции.И такие проблемы, как увеличение вероятности аннулирования кеша, так что это вопрос мнения, какой метод использовать.
Тем не менее, стандартTrait
Структуры также имеют некоторые недостатки, например, некоторые трейты нельзя использовать в качестве трейт-объектов из-за требований безопасности объекта. Поэтому, когда мы используем или читаем некоторые крейты Rust, мы обнаружим, что эти библиотеки реализуют свою собственную структуру трейт-объектов, такую как стандартная библиотекаRawWaker
структура,tokio
изRawTask
структура,bytes
изBytes
структура,anyhow
изErrorImpl
Структуры и распределитель памяти при стирании типа1обсуждение. В следующем содержании я провожу поверхностный анализ некоторых из этих реализаций в сочетании с некоторыми существующими обсуждениями.2, попробуйте подытожить, что у них общего. Уровень автора ограничен, если есть ошибки или неточности, просьба указывать.
Examples
std
серединаRawWaker
{#RawWaker}
Ядром асинхронного программирования на Rust являются Executor и Reactor, из которых Reactor в основном состоит изWaker
структура. Просмотр обнаружения исходного кодаWaker
Структура просто обертываетRawWaker
структура. а такжеRawWaker
Структура очень похожа на толстый указатель на трейт-объект, содержащий указатель на любой тип данных.data
с настройкой этогоWaker
таблица поведения указателя виртуальной функцииvtable
. при звонкеWaker
При использовании связанного метода будет фактически вызвана соответствующая функция в виртуальной таблице, иdata
Передайте в качестве первого параметра функции.
/// A `RawWaker` allows the implementor of a task executor to create a [`Waker`]
/// which provides customized wakeup behavior.
///
/// [vtable]: https://en.wikipedia.org/wiki/Virtual_method_table
///
/// It consists of a data pointer and a [virtual function pointer table (vtable)][vtable]
/// that customizes the behavior of the `RawWaker`.
#[derive(PartialEq, Debug)]
#[stable(feature = "futures_api", since = "1.36.0")]
pub struct RawWaker {
/// A data pointer, which can be used to store arbitrary data as required
/// by the executor. This could be e.g. a type-erased pointer to an `Arc`
/// that is associated with the task.
/// The value of this field gets passed to all functions that are part of
/// the vtable as the first parameter.
data: *const (),
/// Virtual function pointer table that customizes the behavior of this waker.
vtable: &'static RawWakerVTable,
}
#[stable(feature = "futures_api", since = "1.36.0")]
#[derive(PartialEq, Copy, Clone, Debug)]
pub struct RawWakerVTable {
clone: unsafe fn(*const ()) -> RawWaker,
wake: unsafe fn(*const ()),
wake_by_ref: unsafe fn(*const ()),
drop: unsafe fn(*const ()),
}
При изучении этой части кода у меня возник вопрос, а почему бы не использовать тот, что предоставлен RustTrait
так какWaker
абстракцию, но и вручную реализовать сложные, опасные и подверженные ошибкам, подобные толстым указателям типаж-объектов.RawWaker
. Чтобы решить эту проблему, я попытался использоватьTrait
имитироватьRawWaker
функция.
pub trait RawWaker: Send + Sync {
fn clone(&self) -> Box<dyn RawWaker>;
fn wake(&self);
fn wake_by_ref(&self);
}
impl Clone for Box<dyn RawWaker> {
fn clone(&self) -> Self {
RawWaker::clone(&**self)
}
}
pub struct Waker {
waker: Box<dyn RawWaker>,
}
По виртуальной таблицеRawWakerVTable
метод, мы можем написать простойRawWaker
черта. Здесь есть несколько проблем, прежде всего,RawWaker
требования для достиженияClone
, причина этого в Saoirse Shipwreckt3сообщение в блоге имеет краткое содержание:
Когда источник события регистрирует
future
будет ждать события, когда он должен хранитьWaker
, так что вы можете вызвать его позжеwake
метод. Чтобы ввести параллелизм, очень важно иметь возможность ожидать несколько событий одновременно, поэтомуWaker
не может принадлежать только одному источнику событий, поэтомуWaker
Типы должны быть клонируемыми.
Однако,Clone
Черта сама по себе не объектно-безопасна, потому что онаSized
подходит суперчерта. То есть, если мы используемpub trait RawWaker: Clone
написано, тоtrait
не будет доступен как трейт-объект. Итак, используяtrait
смоделированныйRawWaker
, я возвращаюсь к следующемуBox<dyn RawWaker>
ДостигнутоClone
, и передал конкретные детали вRawWaker::clone
внутри, чтобы каждый звонокclone
будут создавать новый трейт-объект, и эти трейт-объекты будут использовать одни и те же данные.
Во-вторых, чтобы иметь возможность использовать в многопоточной среде, мне требуетсяRawWaker
СуперчертаSend + Sync
, чтобы мы могли поделиться им между несколькими потоками или отправить его в поток.
Наконец, чтобы использовать типаж-объект через указатель, нам нужно сделать это, упаковав объект в кучу. Итак, какой умный указатель мы должны выбрать? В приведенном выше коде я использовалBox
В качестве конкретного типа указателя не используйтеArc
Причина в том, что совместное использование общих данных в будках должно зависеть от конкретной реализации. НапримерRawWaker
Реализация может использовать подсчет ссылок для отслеживания объектов в другой куче, или все они указывают на статический глобальный объект, например:
use std::sync::Arc;
#[derive(Debug, Default)]
struct RcWakerInner {}
#[derive(Debug, Default)]
pub struct RcWaker {
inner: Arc<RcWakerInner>,
}
impl RawWaker for RcWaker {
fn clone(&self) -> Box<dyn RawWaker> {
Box::new(RcWaker {
inner: self.inner.clone(),
})
}
fn wake(&self) {
todo!()
}
fn wake_by_ref(&self) {
todo!()
}
}
static GLOBAL_RAW_WAKER: StaticWakerInner = StaticWakerInner {};
#[derive(Debug, Default)]
struct StaticWakerInner {}
#[derive(Debug)]
pub struct StaticWaker {
global_raw_waker: &'static StaticWakerInner,
}
impl Default for StaticWaker {
fn default() -> Self {
Self {
global_raw_waker: &GLOBAL_RAW_WAKER,
}
}
}
impl RawWaker for StaticWaker {
fn clone(&self) -> Box<dyn RawWaker> {
Box::new(StaticWaker {
global_raw_waker: self.global_raw_waker,
})
}
fn wake(&self) {
todo!()
}
fn wake_by_ref(&self) {
todo!()
}
}
Далее мы будем использовать стандартную библиотекуRawWaker
Можно найти некоторые сравнения с приведенными выше реализациями:
- Поскольку мы нашли
Box<dyn RawWaker>
Во-первых, он обернут слоем указателей для реализации типаж-объектов, а конкретныеRawWaker
Реализации также могут использовать указатели для совместного использования одного и того же объекта. Это не только займет дополнительное место в памяти кучи, что приведет к появлению множества мелких объектов, но и приведет к дополнительным затратам времени из-за разыменования многоуровневых указателей. - стандартная библиотека
Waker
родыcore::task
модуль, покаBox
а такжеArc
и другие сооружения расположены вalloc
модуль, ониstd
подмножество . в обычномstd
приложение, мы действительно можем использоватьArc
и другие умные указатели. Но Руст не хочетno_std
Futures, поэтому для достижения этой способности нам приходится использовать специальные приемы.
По этим причинам Rust предпочитает использовать указатели данных и виртуальные таблицы для достижения высокопроизводительного динамического распределения.std::task::RawWaker
серединаdata
Указатели не только обеспечивают возможность стирания типов, но и реализуют совместное использование объектов, что является очень гибким.
tokio
а такжеasync-task
серединаRawTask
{#RawTask}
существуетtokio
а такжеasync-task
в коде будетRawTask
так какTask
Конкретная реализация конструкции. с только что упомянутымRawWaker
сходство,RawTask
Также предоставляет функциональность, аналогичную трейт-объектам через vtables, однако они имеют разные варианты размещения памяти. Ниже сtokio::runtime::raw::RawTask
Например.
#[repr(C)]
pub(crate) struct Header {
pub(super) state: State,
pub(super) owned: UnsafeCell<linked_list::Pointers<Header>>,
pub(super) queue_next: UnsafeCell<Option<NonNull<Header>>>,
/// Table of function pointers for executing actions on the task.
pub(super) vtable: &'static Vtable,
pub(super) owner_id: UnsafeCell<u64>,
#[cfg(all(tokio_unstable, feature = "tracing"))]
pub(super) id: Option<tracing::Id>,
}
/// Raw task handle
pub(super) struct RawTask {
ptr: NonNull<Header>,
}
через сRawWaker
Сравнивая со структурой , находим, чтоRawTask
Часть vtable перемещена в указатель данныхptr
Внутри. Польза от этого очевидна, т.RawTask
Структура памяти более компактна, и ей нужно занимать размер всего лишь одного указателя.
Таким образом, настраивая такие структуры, как толстые указатели типаж-объектов, мы можем управлять расположением памяти и делать указатели тоньше или толще (например,async_task::raw::RawTask
).
bytes
серединаBytes
{#Bytes}
bytes
crate предоставляет абстракцию для работы с байтами, включает в себя эффективную структуру байтового буфера и связанные с ней трейты. вbytes::bytes::Bytes
является эффективным контейнером для хранения и манипулирования смежными фрагментами памяти, позволяя использовать несколькоBytes
Объекты указывают на одну и ту же базовую память.Bytes
Структура действует как функция интерфейса и сама занимает четыреusize
Размер , в основном, содержит встроенные трейт-объекты.
pub struct Bytes {
ptr: *const u8,
len: usize,
// inlined "trait object"
data: AtomicPtr<()>,
vtable: &'static Vtable,
}
Его виртуальная таблица в основномclone
метод, который позволяетBytes
Конкретная реализация для определения конкретного клона или стратегии совместного использования.Bytes
В документации приведены два примера
- за
Bytes
ссылка на постоянную память (например, черезBytes::from_static()
создать) реализация,clone
Внедрение будет бесперспективным. - за
Bytes
Указывает на общее хранилище с подсчетом ссылок (например,Arc<[u8]>
) будут совместно использоваться путем увеличения счетчика ссылок.
можно увидеть, сstd::task::RawWaker
сходство,Bytes
Нужно использоватьclone
метода, а конкретная реализация полностью передается исполнителю. Если вы выберетеTrait
Интерфейсный метод, поскольку общедоступная часть данных уже представлена в виде указателя, приведет к дополнительным затратам на выделение памяти и разыменование.Заинтересованные студенты могут попробовать его использовать.Trait
Способ реализации этих двух примеров, конечный эффект такой же, как и выше.RawWaker
аналогичный. А после встраивания трейт-объекта весь дизайн стал очень элегантным,data
часть указывает на разделяемую память,vtable
определяет, как действоватьclone
, а остальные поля используются как эксклюзивные данные.
Суммировать
Rust предоставляет безопасные абстракции, чтобы избежать проблем с безопасностью или ошибок. Например, мы используемRC
Вместо прямого управления счетчиками ссылок используйтеBox
вместоmalloc/free
Непосредственно управляет распределением памяти. такой же,dyn Trait
Скрывает сложную и опасную реализацию виртуальной таблицы, предоставляя нам простое и безопасное динамическое распределение. Мы видим, что приведенный выше код для ручной реализации виртуальных таблиц заполнен большим количествомunsafe
, небольшая небрежность приведет к ошибкам. Если ваш проект не может использовать стандартdyn Trait
структуру для выражения, то вам следует сначала попытаться провести рефакторинг вашей программы и сослаться на следующие причины, чтобы решить, использовать ли пользовательскую виртуальную таблицу.
- Вы хотите реализовать полиморфизм для класса объектов-указателей и не можете допустить снижения производительности, вызванного несколькими уровнями разыменования указателя, см.RawWakerа такжеBytes.
- Вы хотите настроить структуру памяти, например, компактную структуру памяти, подобную vtable в C++ (указатели vtable расположены внутри объектов), см.RawTask.
- Ваш ящик должен быть в
no_std
Используйте динамическое распространение в среде, см.RawWaker. - Или стандартный трейт-объект действительно не делает то, что вы хотите.