Группа муравьев | Объект признаков или таблица виртуальных методов

задняя часть Rust

Группа муравьев | Объект признаков или таблица виртуальных методов

Автор: Ши Зеюй

Трейт-объекты — это реализация динамической диспетчеризации в Rust. В журнале Rust Magazine, опубликованном в апреле 2021 года, Цзякай Лю представил использование специального полиморфизма в Rust в статье «Анализ использования и реализации признаков», включая статическое и динамическое распределение, а также анализ признаков в объектах свойств. вопросы безопасности и причины объясняются подробно. Итак, является ли использование трейт-объектов концом динамической диспетчеризации в Rust? На самом деле, мы обнаружили, что во многих Rust-кодах вместо трейт-объектов используются примитивные виртуальные таблицы, в чем причина этого? В этой статье я кратко представлю трейт-объекты и виртуальные таблицы, а затем обсужу преимущества и недостатки ручного создания виртуальных таблиц вместо использования трейт-объектов в сочетании с несколькими репрезентативными фрагментами кода, выбранными автором.

Введение

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

image.png

Тем не менее, стандарт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_stdFutures, поэтому для достижения этой способности нам приходится использовать специальные приемы.

По этим причинам 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}

bytescrate предоставляет абстракцию для работы с байтами, включает в себя эффективную структуру байтового буфера и связанные с ней трейты. в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.
  • Или стандартный трейт-объект действительно не делает то, что вы хотите.

Ссылки по теме