Rust: Свойства распределителя и std :: heap

Созданный на 8 апр. 2016  ·  412Комментарии  ·  Источник: rust-lang/rust

📢 Для этой функции есть специальная рабочая группа , пожалуйста, направляйте комментарии и проблемы в репозиторий рабочей группы .

Исходное сообщение:


Предложение FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336957415
Флажки FCP: https://github.com/rust-lang/rust/issues/32838#issuecomment -336980230


Проблема отслеживания для rust-lang / rfcs # 1398 и модуля std::heap .

  • [x] посадка struct Layout , trait Allocator и реализации по умолчанию в ящик alloc (https://github.com/rust-lang/rust/pull/42313)
  • [x] решает, где должны располагаться детали (например, импы по умолчанию зависят от alloc crate, но Layout / Allocator _ может быть в libcore ...) (https://github.com/rust-lang/rust/pull/42313)
  • [] fixme из исходного кода: аудит реализаций по умолчанию (в Layout предмет ошибок переполнения (возможно переключение на overflowing_add и overflowing_mul при необходимости).
  • [x] решает, следует ли заменить realloc_in_place на grow_in_place и shrink_in_place ( комментарий ) (https://github.com/rust-lang/rust/pull/42313)
  • [] просмотреть аргументы в пользу / против связанного типа ошибки (см. подпоток здесь )
  • [] определить, какие требования предъявляются к выравниванию, предоставленному для fn dealloc . (См. Обсуждение распределителя rfc и глобального распределителя rfc и признака Alloc PR .)

    • Требуется ли освободить точные align которые вы выделяете? Были высказаны опасения, что распределители, такие как jemalloc, этого не требуют, и трудно представить себе распределитель, который действительно этого требует. ( подробное обсуждение ). @ruuda и @rkruppe похоже, что у них пока больше всего мыслей по этому поводу.

  • [] вместо AllocErr должно быть Error ? ( комментарий )
  • [x] Требуется ли освобождение с точным размером, который вы выделяете? В случае с бизнесом usable_size мы можем разрешить, например, что вы, если вы выделяете с помощью (size, align) , должны освобождать размер где-то в диапазоне size...usable_size(size, align) . Похоже, что jemalloc полностью согласен с этим (не требует, чтобы вы освобождались с помощью точного size вы выделяете), и это также позволило бы Vec естественным образом использовать избыточную емкость jemalloc дает его, когда выполняет распределение. (хотя на самом деле это тоже несколько ортогонально этому решению, мы просто расширяем возможности Vec ). Пока что у @Gankro есть большинство мыслей по этому поводу. ( @alexcrichton считает, что это было решено в https://github.com/rust-lang/rust/pull/42313 из-за определения «подходит»)
  • [] аналогично предыдущему вопросу: требуется ли освобождение с точным выравниванием, с которым вы выделили? (См. Комментарий от 5 июня 2017 г. )
  • [x] OSX / alloc_system содержит ошибки при огромных выравниваниях (например, при выравнивании 1 << 32 ) https://github.com/rust-lang/rust/issues/30170 # 43217
  • [] должен ли Layout предоставлять метод fn stride(&self) ? (См. Также https://github.com/rust-lang/rfcs/issues/1397, https://github.com/rust-lang/rust/issues/17027)
  • [x] Allocator::owns как метод? https://github.com/rust-lang/rust/issues/44302

Состояние std::heap после https://github.com/rust-lang/rust/pull/42313 :

pub struct Layout { /* ... */ }

impl Layout {
    pub fn new<T>() -> Self;
    pub fn for_value<T: ?Sized>(t: &T) -> Self;
    pub fn array<T>(n: usize) -> Option<Self>;
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
    pub fn align_to(&self, align: usize) -> Self;
    pub fn padding_needed_for(&self, align: usize) -> usize;
    pub fn repeat(&self, n: usize) -> Option<(Self, usize)>;
    pub fn extend(&self, next: Self) -> Option<(Self, usize)>;
    pub fn repeat_packed(&self, n: usize) -> Option<Self>;
    pub fn extend_packed(&self, next: Self) -> Option<(Self, usize)>;
}

pub enum AllocErr {
    Exhausted { request: Layout },
    Unsupported { details: &'static str },
}

impl AllocErr {
    pub fn invalid_input(details: &'static str) -> Self;
    pub fn is_memory_exhausted(&self) -> bool;
    pub fn is_request_unsupported(&self) -> bool;
    pub fn description(&self) -> &str;
}

pub struct CannotReallocInPlace;

pub struct Excess(pub *mut u8, pub usize);

pub unsafe trait Alloc {
    // required
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);

    // provided
    fn oom(&mut self, _: AllocErr) -> !;
    fn usable_size(&self, layout: &Layout) -> (usize, usize);
    unsafe fn realloc(&mut self,
                      ptr: *mut u8,
                      layout: Layout,
                      new_layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn alloc_excess(&mut self, layout: Layout) -> Result<Excess, AllocErr>;
    unsafe fn realloc_excess(&mut self,
                             ptr: *mut u8,
                             layout: Layout,
                             new_layout: Layout) -> Result<Excess, AllocErr>;
    unsafe fn grow_in_place(&mut self,
                            ptr: *mut u8,
                            layout: Layout,
                            new_layout: Layout) -> Result<(), CannotReallocInPlace>;
    unsafe fn shrink_in_place(&mut self,
                              ptr: *mut u8,
                              layout: Layout,
                              new_layout: Layout) -> Result<(), CannotReallocInPlace>;

    // convenience
    fn alloc_one<T>(&mut self) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn dealloc_one<T>(&mut self, ptr: Unique<T>)
        where Self: Sized;
    fn alloc_array<T>(&mut self, n: usize) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn realloc_array<T>(&mut self,
                               ptr: Unique<T>,
                               n_old: usize,
                               n_new: usize) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn dealloc_array<T>(&mut self, ptr: Unique<T>, n: usize) -> Result<(), AllocErr>
        where Self: Sized;
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}
B-RFC-approved B-unstable C-tracking-issue Libs-Tracked T-lang T-libs disposition-merge finished-final-comment-period

Самый полезный комментарий

@alexcrichton Решение о переходе с -> Result<*mut u8, AllocErr> на -> *mut void может стать значительным сюрпризом для людей, которые следили за первоначальной разработкой RFC распределителя.

Я не согласен с точками вы делаете , но , тем не менее , казалось , как значительное число людей было бы готовы жить с «тяжелой weightness» в Result за увеличения вероятности пропуска нуль- проверьте возвращаемое значение.

  • Я игнорирую проблемы эффективности времени выполнения, налагаемые ABI, потому что я, как и @alexcrichton , предполагаю, что мы могли бы справиться с ними каким-либо образом с помощью уловок компилятора.

Есть ли какой-то способ добиться большей осведомленности об этом позднем изменении?

Один способ (из головы): изменить подпись сейчас, в PR самостоятельно, в главной ветке, пока Allocator все еще нестабильно. А потом посмотрите, кто жалуется на пиар (а кто празднует!).

  • Это слишком жестко? Похоже, что это по определению менее тяжеловесно, чем совмещать такое изменение со стабилизацией ...

Все 412 Комментарий

К сожалению, я не уделил достаточно внимания, чтобы упомянуть об этом в обсуждении RFC, но я думаю, что realloc_in_place следует заменить двумя функциями: grow_in_place и shrink_in_place для двоих. причины:

  • Я не могу придумать ни одного варианта использования (кроме реализации realloc или realloc_in_place ), где неизвестно, увеличивается или уменьшается размер распределения. Использование более специализированных методов немного проясняет, что происходит.
  • Пути кода для увеличения и уменьшения распределений обычно радикально различаются - увеличение включает в себя проверку того, свободны ли соседние блоки памяти и их захват, в то время как сжатие включает вырезание субблоков надлежащего размера и их освобождение. Хотя стоимость ветки внутри realloc_in_place довольно мала, использование grow и shrink лучше захватывает отдельные задачи, которые должен выполнять распределитель.

Обратите внимание, что они могут быть добавлены с обратной совместимостью рядом с realloc_in_place , но это ограничит, какие функции будут по умолчанию реализованы с точки зрения других.

Для единообразия, realloc , вероятно, также следует разделить на grow и split , но это единственное преимущество наличия перегружаемой функции realloc о которой я знаю - иметь возможность использовать параметр переназначения mmap , который не имеет такого различия.

Кроме того, я думаю, что реализации по умолчанию realloc и realloc_in_place должны быть немного скорректированы - вместо проверки usable_size , realloc следует сначала попытаться realloc_in_place . В свою очередь, realloc_in_place по умолчанию должен проверять допустимый размер и возвращать успех в случае небольшого изменения вместо универсального возврата отказа.

Это упрощает создание высокопроизводительной реализации realloc : все, что требуется, - это улучшение realloc_in_place . Однако производительность по умолчанию realloc не пострадает, поскольку проверка по usable_size все еще выполняется.

Другая проблема: в документе для fn realloc_in_place сказано, что если он возвращает Ok, то можно быть уверенным, что ptr теперь "подходит" new_layout .

Для меня это означает, что он должен проверить, соответствует ли выравнивание данного адреса любому ограничению, подразумеваемому new_layout .

Однако я не думаю, что спецификация базовой функции fn reallocate_inplace подразумевает, что _it_ будет выполнять любую такую ​​проверку.

  • Более того, кажется разумным, что любой клиент, ныряющий в использование fn realloc_in_place , сам будет обеспечивать работу выравнивания (на практике я подозреваю, что это означает, что одинаковое выравнивание требуется везде для данного варианта использования ...)

Итак, должна ли реализация fn realloc_in_place действительно быть обременена проверкой того, что выравнивание заданного ptr совместимо с выравниванием new_layout ? Вероятно, лучше _ в этом случае_ (этого единственного метода) вернуть это требование вызывающей стороне ...

@gereeter вы делаете хорошие

(на данный момент я жду поддержки #[may_dangle] чтобы поехать на поезде в канал beta чтобы затем я мог использовать его для коллекций std как часть интеграции распределителя)

Я новичок в Rust, так что простите меня, если это обсуждалось в другом месте.

Есть ли какие-нибудь мысли о том, как поддерживать объектно-ориентированные распределители? Некоторые распределители, такие как распределители блоков и журналов , привязаны к определенному типу и выполняют работу по созданию новых объектов, кэшированию созданных объектов, которые были «освобождены» (а не их фактическому удалению), возвращению уже созданных кэшированных объектов и удаление объектов перед освобождением базовой памяти нижележащему распределителю при необходимости.

В настоящее время это предложение не включает ничего похожего на ObjectAllocator<T> , но было бы очень полезно. В частности, я работаю над реализацией слоя кэширования объектов распределителя журналов (ссылка выше), и хотя я могу сделать это только обернуть Allocator и выполнить работу по созданию и удалению объектов в кешировании сам по себе, было бы здорово, если бы я мог также обернуть другие распределители объектов (например, блок-распределитель) и действительно быть общим слоем кэширования.

Каким образом тип или характеристика распределителя объектов вписываются в это предложение? Будет ли это оставлено для будущего RFC? Что-то другое?

Я не думаю, что это еще обсуждалось.

Вы можете написать свой собственный ObjectAllocator<T> , а затем сделать impl<T: Allocator, U> ObjectAllocator<U> for T { .. } , чтобы каждый регулярный распределитель мог служить объектно-зависимым распределителем для всех объектов.

Дальнейшая работа будет заключаться в изменении коллекций для использования вашего признака для их узлов, а не напрямую простых старых (общих) распределителей.

@pnkfelix

(на данный момент я жду поддержки # [may_dangle], чтобы поехать на поезде в бета-канал, чтобы затем я мог использовать его для коллекций std как часть интеграции распределителя)

Я думаю, это случилось?

@ Ericson2314 Да, написание моего собственного - определенно вариант для экспериментальных целей, но я думаю, что было бы намного больше пользы от стандартизации с точки зрения совместимости (например, я планирую также реализовать slab-распределитель, но это было бы хорошо, если бы сторонний пользователь моего кода мог использовать чей-нибудь _else_ slab-распределитель с моим слоем кэширования журнала). Мой вопрос просто в том, стоит ли обсуждать черту ObjectAllocator<T> или что-то подобное. Хотя кажется, что может быть лучше для другого RFC? Я не очень хорошо знаком с руководящими принципами того, сколько должно быть в одном RFC и когда что-то принадлежит отдельным RFC ...

@joshlf

Каким образом тип или характеристика распределителя объектов вписываются в это предложение? Будет ли это оставлено для будущего RFC? Что-то другое?

Да, это был бы еще один RFC.

Я не очень хорошо знаком с руководящими принципами того, сколько должно быть в одном RFC и когда что-то принадлежит отдельным RFC ...

это зависит от объема самого RFC, который определяется тем, кто его пишет, а затем каждый дает обратную связь.

Но на самом деле, поскольку это проблема отслеживания для этого уже принятого RFC, размышления о расширениях и изменениях дизайна не совсем для этого потока; вам следует открыть новый в репозитории RFC.

@joshlf А, я думал, что ObjectAllocator<T> - это черта характера. Я имел в виду прототип признака, а не конкретный распределитель. Да, эта черта заслуживает отдельного RFC, как говорит @steveklabnik .


@steveklabnik да теперь обсуждение было бы лучше в другом месте. Но @joshlf также поднимал проблему, чтобы не выявить ранее непредвиденный изъян в принятой, но не реализованной конструкции API. В этом смысле он соответствует более ранним сообщениям в этой теме.

@ Ericson2314 Ага, я думал, ты это имел в виду. Думаю, мы на одной странице :)

@steveklabnik Звучит хорошо; Я попробую свою собственную реализацию и отправлю RFC, если это окажется хорошей идеей.

@joshlf У меня нет причин, по которым пользовательские распределители должны входить в компилятор или стандартную библиотеку. Как только этот RFC появится, вы можете легко опубликовать свой собственный ящик, который выполняет произвольное распределение (даже полноценный распределитель, такой как jemalloc, может быть настроен на заказ!).

@alexreg Речь идет не о конкретном настраиваемом распределителе, а, скорее, о характеристике, которая определяет тип всех распределителей, параметрических для определенного типа. Так же, как RFC 1398 определяет признак ( Allocator ), который является типом любого низкоуровневого распределителя, я спрашиваю о признаке ( ObjectAllocator<T> ), который является типом любого распределителя, который может выделять / освобождать и создавать / удалять объекты типа T .

@alexreg См. мое раннее замечание об использовании коллекций стандартных библиотек с пользовательскими распределителями для конкретных объектов.

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

4 января 2017 года в 21:59 Джошуа Либов-Физер [email protected] написал:

@alexreg https://github.com/alexreg Речь идет не о конкретном настраиваемом распределителе, а скорее о характеристике, которая определяет тип всех распределителей, которые являются параметрическими для определенного типа. Так же, как RFC 1398 определяет признак (Распределитель), который является типом любого низкоуровневого распределителя, я спрашиваю о признаке (ObjectAllocator), который является типом любого распределителя, который может выделять / освобождать и создавать / удалять объекты типа T.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499064 или отключите поток https://github.com/notifications/unsubscribe-auth/ AAEF3IhyyPhFgu1EGHr_GM_Evsr0SRzIks5rPBZGgaJpZM4IDYUN .

Я думаю, вы захотите использовать коллекции стандартной библиотеки (любое значение, выделенное в куче) с произвольным настраиваемым распределителем; т.е. не ограничиваясь конкретными объектами.

4 января 2017 года в 22:01 Джон Эриксон [email protected] написал:

@alexreg https://github.com/alexreg См. мой ранний замечание об использовании коллекций стандартных библиотек с настраиваемыми распределителями для конкретных объектов.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499628 или отключите поток https://github.com/notifications/unsubscribe-auth/ AAEF3CrjYIXqcv8Aqvb4VTyPcajJozICks5rPBbOgaJpZM4IDYUN .

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

Да, но вы, вероятно, захотите использовать некоторые стандартные функции библиотеки (например, предложенные @ Ericson2314 ).

Я думаю, вы захотите использовать коллекции стандартной библиотеки (любое значение, выделенное в куче) с произвольным настраиваемым распределителем; т.е. не ограничиваясь конкретными объектами.

В идеале вам нужно и то, и другое - принять любой тип распределителя. Использование объектно-ориентированного кэширования дает очень значительные преимущества; например, и распределение блоков, и кэширование журналов дают очень значительные преимущества в производительности - если вам интересно, посмотрите документы, на которые я ссылался выше.

Но признак распределителя объектов может быть просто вычитанием общего признака распределителя. Насколько я понимаю, это так просто. Конечно, определенные типы распределителей могут быть более эффективными, чем распределители общего назначения, но ни компилятор, ни стандарт на самом деле не должны (или должны) знать об этом.

4 января 2017 года в 22:13 Джошуа Либов-Физер [email protected] написал:

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

Да, но вы, вероятно, хотите, чтобы на нее полагались некоторые стандартные функции библиотеки (например, предложенные @ Ericson2314 https://github.com/Ericson2314 ).

Я думаю, вы захотите использовать коллекции стандартной библиотеки (любое значение, выделенное в куче) с произвольным настраиваемым распределителем; т.е. не ограничиваясь конкретными объектами.

В идеале вам нужно и то, и другое - принять любой тип распределителя. Использование объектно-ориентированного кэширования дает очень значительные преимущества; например, и распределение блоков, и кэширование журналов дают очень значительные преимущества в производительности - если вам интересно, посмотрите документы, на которые я ссылался выше.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270502231 или отключите поток https://github.com/notifications/unsubscribe-auth/ AAEF3L9F9r_0T5evOtt7Es92vw6gBxR9ks5rPBl9gaJpZM4IDYUN .

Но признак распределителя объектов может быть просто вычитанием общего признака распределителя. Насколько я понимаю, это так просто. Конечно, определенные типы распределителей могут быть более эффективными, чем распределители общего назначения, но ни компилятор, ни стандарт на самом деле не должны (или должны) знать об этом.

Ах, проблема в том, что семантика другая. Allocator выделяет и освобождает необработанные байтовые капли. ObjectAllocator<T> , с другой стороны, будет выделять уже построенные объекты, а также отвечать за удаление этих объектов (включая возможность кэширования построенных объектов, которые могут быть переданы позже при создании вновь выделенного объекта. , что дорого). Признак будет выглядеть примерно так:

trait ObjectAllocator<T> {
    fn alloc() -> T;
    fn free(t T);
}

Это несовместимо с Allocator , методы которого работают с необработанными указателями и не имеют понятия типа. Кроме того, при использовании Allocator s вызывающая сторона несет ответственность за drop объект, освобождаемый первым. Это действительно важно - знание типа T позволяет ObjectAllocator<T> делать такие вещи, как вызов T drop метода free(t) перемещает t в free , вызывающий _cannot_ drop t первым - это ответственность ObjectAllocator<T> . По сути, эти две черты несовместимы друг с другом.

Ага, понятно. Я думал, что это предложение уже включало нечто подобное, то есть распределитель «более высокого уровня» на уровне байтов. В таком случае, совершенно справедливое предложение!

4 января 2017 года в 22:29 Джошуа Либов-Физер [email protected] написал:

Но признак распределителя объектов может быть просто вычитанием общего признака распределителя. Насколько я понимаю, это так просто. Конечно, определенные типы распределителей могут быть более эффективными, чем распределители общего назначения, но ни компилятор, ни стандарт на самом деле не должны (или должны) знать об этом.

Ах, проблема в том, что семантика другая. Распределитель выделяет и освобождает необработанные байтовые капли. ObjectAllocator, с другой стороны, будет выделять уже созданные объекты, а также отвечать за удаление этих объектов (включая возможность кэширования созданных объектов, которые могут быть переданы позже при создании вновь выделенного объекта, что дорого). Признак будет выглядеть примерно так:

трейт ObjectAllocator{
fn alloc () -> T;
fn free (t T);
}
Это несовместимо с Allocator, методы которого работают с необработанными указателями и не имеют понятия типа. Кроме того, в случае с Allocators ответственность за удаление освобождаемого объекта лежит на вызывающей стороне. Это действительно важно - знание типа T позволяет ObjectAllocatorчтобы делать такие вещи, как вызов метода drop T, и поскольку free (t) перемещает t в free, вызывающий не может сначала отбросить t - вместо этого это ObjectAllocatorответственность. По сути, эти две черты несовместимы друг с другом.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270505704 или отключите поток https://github.com/notifications/unsubscribe-auth/ AAEF3GViJBefuk8IWgPauPyL5tV78Fn5ks5rPB08gaJpZM4IDYUN .

@alexreg Ах да, я тоже на это надеялся :) Ну что

Да, начните этот RFC, я уверен, что он получит широкую поддержку! И спасибо за разъяснения (я вообще не в курсе деталей этого RFC).

5 января 2017 года в 00:53 Джошуа Либоу-Физер [email protected] написал:

@alexreg https://github.com/alexreg Ах да, я тоже на это надеялся :) Ну что ж, придется дождаться следующего RFC.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270531535 или отключите поток https://github.com/notifications/unsubscribe-auth/ AAEF3MQQeXhTliU5CBsoheBFL26Ee9WUks5rPD8RgaJpZM4IDYUN .

Было бы полезно установить ящик для тестирования пользовательских распределителей.

Простите меня, если я упускаю что-то очевидное, но есть ли причина, по которой признак Layout описанный в этом RFC, не реализует Copy а также Clone , поскольку это просто POD?

Я не могу ни о чем думать.

Извините, что поднял этот вопрос так поздно, но ...

Может быть, стоит добавить поддержку dealloc -подобной функции, которая не является методом, а скорее функцией? Идея состоит в том, чтобы использовать выравнивание, чтобы иметь возможность делать вывод по указателю, где в памяти находится его родительский распределитель, и, таким образом, иметь возможность освободить без необходимости явной ссылки на распределитель.

Это может быть большой победой для структур данных, использующих настраиваемые распределители. Это позволило бы им не хранить ссылку на сам распределитель, а, скорее, нужно было бы параметрически только для _type_ распределителя, чтобы иметь возможность вызывать правильную функцию dealloc . Например, если Box в конечном итоге будет изменен для поддержки настраиваемых распределителей, тогда он сможет сохранить только одно слово (просто указатель), в отличие от необходимости расширяться до двух слов для хранения ссылки. к распределителю.

В связи с этим, также может быть полезно поддерживать функцию alloc без метода, чтобы разрешить использование глобальных распределителей. Это хорошо сочетается с функцией dealloc без метода - для глобальных распределителей не было бы необходимости делать какой-либо вывод указателя на распределитель, поскольку был бы только один статический экземпляр распределитель для всей программы.

@joshlf Текущий дизайн позволяет вам получить это, просто имея ваш распределитель (нулевого размера) типа единицы, то есть struct MyAlloc; , для которого вы затем реализуете черту Allocator .
Хранение ссылок или вообще ничего, всегда менее распространено, чем сохранение распределителя по значению.

Я мог видеть, что это верно для типа, встроенного напрямую, но как насчет того, чтобы структура данных вместо этого решила сохранить ссылку? Занимает ли ссылка на тип нулевого размера нулевое пространство? То есть, если у меня есть:

struct Foo()

struct Blah{
    foo: &Foo,
}

У Blah нулевой размер?

На самом деле, даже возможно, вы не хотите, чтобы ваш распределитель имел нулевой размер. Например, у вас может быть распределитель с ненулевым размером, который вы выделяете _from_, но который имеет возможность освобождать объекты, не зная об исходном распределителе. Это все равно было бы полезно для того, чтобы Box брал только слово. У вас будет что-то вроде Box::new_from_allocator которое должно будет принимать распределитель в качестве аргумента - и это может быть распределитель ненулевого размера, - но если распределитель поддерживает освобождение без ссылки на исходный распределитель, возвращается Box<T> может избежать сохранения ссылки на распределитель, которая была передана в исходном вызове Box::new_from_allocator .

Например, у вас может быть распределитель с ненулевым размером, из которого вы выделяете, но который имеет возможность освобождать объекты, не зная об исходном распределителе.

Я припоминаю, что давным-давно предлагал выделить отдельные черты распределителя и освобождения памяти (с ассоциированными типами, соединяющими их) в основном по этой причине.

Разрешить / следует ли компилятору оптимизировать выделение памяти с помощью этих распределителей?

Разрешить / следует ли компилятору оптимизировать выделение памяти с помощью этих распределителей?

@Zoxc Что ты имеешь в виду?

Я припоминаю, что давным-давно предлагал выделить отдельные черты распределителя и освобождения памяти (с ассоциированными типами, соединяющими их) в основном по этой причине.

Для потомков позвольте мне прояснить это утверждение (я говорил об этом с @ Ericson2314 в автономном режиме): идея заключается в том, что Box может быть параметрическим только на деаллокаторе. Итак, у вас может быть следующая реализация:

trait Allocator {
    type D: Deallocator;

    fn get_deallocator(&self) -> Self::D;
}

trait Deallocator {}

struct Box<T, D: Deallocator> {
    ptr: *mut T,
    d: D,
}

impl<T, D: Deallocator> Box<T, D> {
    fn new_from_allocator<A: Allocator>(x: T, a: A) -> Box<T, A::D> {
        ...
        Box {
            ptr: ptr,
            d: a.get_deallocator()
        }
    }
}

Таким образом, при вызове new_from_allocator , если A::D является типом нулевого размера, тогда поле d в Box<T, A::D> занимает нулевой размер, и поэтому Размер результирующего Box<T, A::D> - это одно слово.

Есть ли сроки, когда это произойдет? Я работаю над некоторыми вещами, связанными с распределителями, и было бы неплохо, если бы я мог их использовать.

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

Хорошо, мы недавно встретились, чтобы оценить состояние распределителей, и я думаю, что для этого есть хорошие новости! Похоже, что поддержка этих API в libstd еще не реализована, но все по-прежнему довольны их появлением в любое время!

Одна вещь, которую мы обсуждали, заключается в том, что изменение всех типов libstd может быть немного преждевременным из-за возможных проблем с логическим выводом, но, несмотря на это, кажется хорошей идеей использовать черту Allocator и Layout type в предлагаемом модуле std::heap для экспериментов в других частях экосистемы!

@joshlf, если вы хотите здесь помочь, я думаю, это будет более чем приветствоваться! Первая часть, вероятно, будет переносить базовый тип / черту из этого RFC в стандартную библиотеку, а затем мы можем начать экспериментировать и экспериментировать с коллекциями в libstd.

@alexcrichton Думаю, твоя ссылка не работает? Он указывает сюда.

Одна вещь, которую мы обсуждали, заключается в том, что изменение всех типов libstd может быть немного преждевременным из-за возможных проблем с логическим выводом

Добавление трейта - хороший первый шаг, но без рефакторинга существующих API для его использования они не будут широко использоваться. В https://github.com/rust-lang/rust/issues/27336#issuecomment -300721558 я предлагаю немедленно провести рефакторинг ящиков за фасадом, но добавить обертки newtype в std . Это раздражает, но позволяет нам добиться прогресса.

@alexcrichton Каков будет процесс здесь заставили меня поверить в то, что между чертами распределителя и объектом будет почти идеальная симметрия. свойства распределителя. Например, у вас будет что-то вроде (я изменил Address на *mut u8 для симметрии с *mut T с ObjectAllocator<T> ; мы, вероятно, получим Address<T> или что-то в этом роде):

unsafe trait Allocator {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);
}
unsafe trait ObjectAllocator<T> {
    unsafe fn alloc(&mut self) -> Result<*mut T, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut T);
}

Таким образом, я думаю, что эксперименты с распределителями памяти и объектами одновременно могут быть полезны. Я не уверен, что это подходящее место для этого, или должен быть другой RFC или, по крайней мере, отдельный PR.

О, я хотел указать здесь ссылку, в которой также есть информация о глобальном распределителе. @joshlf , ты об этом думаешь?

Похоже , @alexcrichton хочет PR , который обеспечивает Allocator черты и Layout типа, даже если его не интегрирован в любую коллекцию в libstd .

Если я правильно понимаю, то могу поставить за это пиар. Я не сделал этого, потому что продолжаю пытаться получить хотя бы интеграцию с прототипами RawVec и Vec . (На данный момент у меня RawVec готово, но Vec немного сложнее из-за множества других построенных на нем структур, таких как Drain и IntoIter т.д ...)

на самом деле, похоже, что моя текущая ветка действительно может быть построена (и один тест на интеграцию с RawVec пройден), поэтому я пошел дальше и опубликовал его: # 42313

@hawkw спросил:

Простите меня, если я упускаю что-то очевидное, но есть ли причина для свойства Layout, описанного в этом RFC, не реализовывать Copy, а также Clone, поскольку это просто POD?

Причина, по которой я сделал Layout только реализацией Clone а не Copy заключается в том, что я хотел оставить открытой возможность добавления дополнительной структуры к типу Layout . В частности, меня все еще интересует попытка Layout отслеживать любую структуру типов, используемую для ее создания (например, 16-массив из struct { x: u8, y: [char; 215] } ), чтобы у распределителей была возможность предоставление инструментальных процедур, которые сообщают, из каких типов состоит их текущее содержимое.

Это почти наверняка должно быть необязательной функцией, т.е. похоже, что волна категорически против принуждения разработчиков к использованию обогащенных типами конструкторов Layout . поэтому любой инструментарий этой формы должен включать что-то вроде категории «неизвестные блоки памяти» для обработки выделений, не имеющих информации о типе.

Но, тем не менее, подобные функции были основной причиной, по которой я не решил заставить Layout реализовать Copy ; Я в основном полагал, что реализация Copy будет преждевременным ограничением для самого Layout .

@alexcrichton @pnkfelix

Похоже, что у @pnkfelix этот вопрос покрыт, и этот PR

В настоящее время подпись для Allocator::oom :

    fn oom(&mut self, _: AllocErr) -> ! {
        unsafe { ::core::intrinsics::abort() }
    }

Однако мое внимание было обращено на то, что Gecko, по крайней мере, любит знать размер выделения в OOM. Мы можем пожелать учесть это при стабилизации, чтобы, возможно, добавить контекст вроде Option<Layout> объясняющий, почему происходит OOM.

@alexcrichton
Может быть, стоит иметь несколько вариантов oom_xxx или перечисление разных типов аргументов? Есть несколько разных сигнатур для методов, которые могут не работать (например, alloc принимает макет, realloc принимает указатель, исходный макет, новый макет и т. Д.), И может быть случаями, когда oom -подобный метод захочет узнать обо всех из них.

@joshlf это правда, да, но я не уверен, что это полезно. Я не хотел бы просто добавлять функции, потому что мы можем, они должны и дальше иметь хорошую мотивацию.


Точкой стабилизации здесь также является «определение требований к выравниванию, обеспечиваемому fn dealloc », а текущая реализация dealloc в Windows использует align чтобы определить, как правильно бесплатный. @ruuda может вас заинтересовать этот факт.

Точкой стабилизации здесь также является «определение требований к выравниванию, обеспечиваемому fn dealloc », а текущая реализация dealloc в Windows использует align чтобы определить, как правильно бесплатный.

Да, думаю, именно так я изначально столкнулся с этим; из-за этого моя программа вылетела в Windows. Поскольку HeapAlloc дает никаких гарантий выравнивания, allocate выделяет более крупную область и сохраняет исходный указатель в заголовке, но в качестве оптимизации этого избегают, если требования выравнивания все равно будут выполнены. Интересно, есть ли способ преобразовать HeapAlloc в распределитель с учетом выравнивания, который не требует выравнивания бесплатно, без потери этой оптимизации.

@ruuda

Поскольку HeapAlloc дает никаких гарантий выравнивания

Он обеспечивает минимальную гарантию выравнивания в 8 байтов для 32-битных или 16 байтов для 64-битных, он просто не обеспечивает никакого способа гарантировать выравнивание выше этого.

_aligned_malloc предоставляемый CRT в Windows, может обеспечивать выделения с более высоким выравниванием, но, в частности, он должен сочетаться с _aligned_free , использование free является незаконным. Так что, если вы не знаете, было ли выделено выделение через malloc или _aligned_malloc то вы попали в ту же загадку, что и alloc_system в Windows, если вы этого не сделаете. Не знаю выравнивания для deallocate . CRT не предоставляет стандартную функцию aligned_alloc которую можно использовать в паре с free , поэтому даже Microsoft не смогла решить эту проблему. (Хотя это функция C11 и Microsoft не поддерживает C11 , так что это слабый аргумент.)

Обратите внимание, что deallocate заботится только о выравнивании, чтобы знать, превышено ли оно, само фактическое значение не имеет значения. Если вам нужен deallocate который действительно не зависит от выравнивания, вы можете просто рассматривать все выделения как завышенные, но вы потратите много памяти на небольшие выделения.

@alexcrichton написал :

В настоящее время подпись для Allocator::oom :

    fn oom(&mut self, _: AllocErr) -> ! {
        unsafe { ::core::intrinsics::abort() }
    }

Однако мое внимание было обращено на то, что Gecko, по крайней мере, любит знать размер выделения в OOM. Мы можем пожелать учесть это при стабилизации, чтобы, возможно, добавить контекст вроде Option<Layout> объясняющий, почему происходит OOM.

AllocErr уже содержит Layout в варианте AllocErr::Exhausted . Мы могли бы просто добавить Layout к варианту AllocErr::Unsupported , что, на мой взгляд, было бы самым простым с точки зрения ожиданий клиентов. (У него есть недостаток в том, что он глупо увеличивает сторону самого перечисления AllocErr , но, возможно, нам не стоит об этом беспокоиться ...)

О, я подозреваю, что это все, что нужно, спасибо за исправление @pnkfelix!

Я собираюсь начать перепрофилировать эту проблему для проблемы отслеживания для std::heap в целом, как это будет после того, как https://github.com/rust-lang/rust/pull/42727 приземлится. Я закрою несколько других связанных вопросов в пользу этого.

Есть ли проблема с отслеживанием конвертации коллекций? Теперь, когда PR объединены, я хотел бы

  • Обсудите связанный тип ошибки
  • Обсудите преобразование коллекций для использования любого локального распределителя (особенно с использованием связанного типа ошибки)

Я открыл https://github.com/rust-lang/rust/issues/42774, чтобы отслеживать интеграцию Alloc в коллекции std. При историческом обсуждении в команде libs это, вероятно, будет на отдельном пути стабилизации, чем начальный проход модуля std::heap .

При рассмотрении вопросов, связанных с распределителем, я также наткнулся на https://github.com/rust-lang/rust/issues/30170, который некоторое время назад был @pnkfelix . Похоже, что в системном распределителе OSX есть ошибки с высокими выравниваниями, и при запуске этой программы с jemalloc происходит сбой во время освобождения, по крайней мере, в Linux. Стоит задуматься при стабилизации!

Я открыл # 42794 как место, чтобы обсудить конкретный вопрос о том, должны ли выделения нулевого размера соответствовать запрашиваемому выравниванию.

(подождите, выделения нулевого размера недопустимы в пользовательских распределителях!)

Поскольку в Nightly больше нет функции alloc::heap::allocate и друзей, я обновил Servo, чтобы использовать этот новый API. Это часть разницы:

-use alloc::heap;
+use alloc::allocator::{Alloc, Layout};
+use alloc::heap::Heap;
-        let ptr = heap::allocate(req_size as usize, FT_ALIGNMENT) as *mut c_void;
+        let layout = Layout::from_size_align(req_size as usize, FT_ALIGNMENT).unwrap();
+        let ptr = Heap.alloc(layout).unwrap() as *mut c_void;

Я чувствую, что эргономика не на высоте. Мы перешли от импорта одного элемента к импорту трех из двух разных модулей.

  • Имеет ли смысл иметь удобный метод для allocator.alloc(Layout::from_size_align(…)) ?
  • Имеет ли смысл сделать <Heap as Alloc>::_ методы доступными как бесплатные функции или внутренние методы? (Чтобы импортировать на один элемент меньше, используйте свойство Alloc .)

В качестве альтернативы, может ли черта Alloc быть в прелюдии или это слишком ниша для варианта использования?

@SimonSapin ИМО, нет особого смысла в оптимизации эргономики такого низкоуровневого API.

@SimonSapin

Я чувствую, что эргономика не на высоте. Мы перешли от импорта одного элемента к импорту трех из двух разных модулей.

То же самое и с моей кодовой базой - сейчас она довольно неуклюжая.

Имеет ли смысл иметь удобный метод для allocator.alloc(Layout::from_size_align(…))?

Вы имеете в виду признак Alloc или только Heap ? Здесь следует учитывать, что теперь существует третье условие ошибки: Layout::from_size_align возвращает Option , поэтому он может вернуть None в дополнение к обычным ошибкам, которые вы можете получить при распределении .

В качестве альтернативы, может ли черта Alloc быть в прелюдии или это слишком ниша для варианта использования?

ИМО, нет особого смысла в оптимизации эргономики такого низкоуровневого API.

Я согласен с тем, что это, вероятно, слишком низкоуровневая программа для прелюдии, но я все же думаю, что есть смысл в оптимизации эргономики (по крайней мере, эгоистично - это был действительно досадный рефакторинг 😝).

@SimonSapin ты std все три типа доступны в модуле std::heap (они должны быть в одном модуле). Также вы раньше не сталкивались с переполнением размеров? Или типы нулевого размера?

ты раньше не занимался OOM?

Когда она существовала, функция alloc::heap::allocate возвращала указатель без Result и не оставляла выбора при обработке OOM. Я думаю, это прервало процесс. Теперь я добавил .unwrap() чтобы вызвать панику.

они должны быть в одном модуле

Теперь я вижу, что heap.rs содержит pub use allocator::*; . Но когда я щелкнул Alloc в импланте, указанном на странице rustdoc, для Heap меня отправили на alloc::allocator::Alloc .

В остальном я не разбирался. Я переношу на новый компилятор большую кучу кода, написанного много лет назад. Я думаю, что это обратные вызовы для FreeType, библиотеки C.

Когда он существовал, функция alloc :: heap :: allocate возвращала указатель без результата и не оставляла выбора при обработке OOM.

Это давало вам выбор. Возвращенный указатель мог быть нулевым указателем, который указывал бы, что распределителю кучи не удалось выделить. Вот почему я так рад, что он переключился на Result чтобы люди не забывали заниматься этим делом.

Ну, может, FreeType завершил проверку на null, я не знаю. В любом случае, да, возвращать Result - это хорошо.

Учитывая # 30170 и # 43097, я испытываю искушение решить проблему OS X с смехотворно большими выравниваниями, просто указав, что пользователи не могут запрашивать выравнивания> = 1 << 32 .

Один очень простой способ добиться этого: изменить интерфейс Layout так, чтобы align обозначался u32 вместо usize .

@alexcrichton у вас есть мысли по этому

@pnkfelix Layout::from_size_align все равно будет принимать usize и возвращать ошибку при переполнении u32 , верно?

@SimonSapin, по какой причине он должен продолжать принимать usize align, если статическим предварительным условием является то, что передавать значение> = 1 << 32 небезопасно?

и если ответ - «хорошо, некоторые распределители могут поддерживать выравнивание> = 1 << 32 », то мы вернулись к статус-кво, и вы можете проигнорировать мое предложение. Суть моего предложения - это, по сути, «+1» к таким комментариям, как этот.

Поскольку std::mem::align_of возвращает usize

@SimonSapin ах, старый добрый стабильный API ... вздох.

Ограничение @pnkfelix до 1 << 32 мне кажется разумным!

@rfcbot fcp слияние

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

pub struct Layout { /* ... */ }

extern {
    pub type void;
}

impl Layout {
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
}

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> *mut void;
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> *mut void;
    unsafe fn dealloc(&mut self, ptr: *mut void, layout: Layout);

    // all other methods are default and unstable
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}

Оригинальное предложение

pub struct Layout { /* ... */ }

impl Layout {
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
}

// renamed from AllocErr today
pub struct Error {
    // ...
}

impl Error {
    pub fn oom() -> Self;
}

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, Error>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);

    // all other methods are default and unstable
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}

В частности:

  • На данный момент стабилизируются только методы alloc , alloc_zeroed и dealloc для трейта Alloc . Я думаю, что это решает самую насущную проблему, которая у нас сегодня есть, - определение настраиваемого глобального распределителя.
  • Удалите тип Error в пользу использования простых указателей.
  • Измените тип u8 в интерфейсе на void
  • Урезанная версия типа Layout .

Все еще остаются нерешенными вопросы, например, что делать с dealloc и выравниванием (точное выравнивание? Подходит? Неуверенно?), Но я надеюсь, что мы сможем решить их во время FCP, поскольку это, вероятно, не будет API- нарушение изменения.

+1 к стабилизации!

Переименовывает AllocErr в Error и делает интерфейс более консервативным.

Исключает ли это возможность для распределителей указывать Unsupported ? Рискуя повторить то, о чем я много говорил, я думаю, что # 44557 все еще остается проблемой.

Layout

Похоже, вы удалили некоторые методы из Layout . Вы имели в виду, что те, которые вы пропустили, были удалены или просто оставлены как нестабильные?

impl Error {
    pub fn oom() -> Self;
}

Это конструктор того, что сегодня AllocErr::Exhausted ? Если да, то не должен ли он иметь параметр Layout ?

Член команды @alexcrichton предложил объединить это. Следующий шаг - проверка остальными отмеченными командами:

  • [x] @BurntSushi
  • [x] @Kimundi
  • [x] @alexcrichton
  • [x] @aturon
  • [x] @cramertj
  • [x] @dtolnay
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @sfackler
  • [x] @withoutboats

Проблемы:

Как только эти рецензенты достигнут консенсуса, наступит последний период комментариев. Если вы заметили серьезную проблему, которая не поднималась ни на одном этапе этого процесса, сообщите об этом!

См. Этот документ для получения информации о том, какие команды могут дать мне члены команды с тегами.

Я очень рад, что смогу стабилизировать некоторые из этих работ!

Один вопрос: в вышеупомянутой ветке @joshlf и @ Ericson2314 подняли интересный вопрос о возможности разделения черт Alloc и Dealloc для оптимизации для случаев, когда alloc требует некоторых данных, но dealloc требует дополнительной информации, поэтому тип Dealloc может иметь нулевой размер.

Был ли этот вопрос решен? Каковы недостатки разделения двух черт?

@joshlf

Исключает ли это возможность для распределителей указывать "Неподдерживаемый"?

Да и нет, это означало бы , что такая операция не поддерживается стабильной ржавчины сразу, но мы могли бы продолжать поддерживать его в неустойчивом Rust.

Вы имели в виду, что те, которые вы пропустили, были удалены или просто оставлены как нестабильные?

Конечно! Опять же, хотя я просто предлагаю стабильную поверхность API, мы можем оставить все другие методы нестабильными. Со временем мы сможем стабилизировать большую часть функциональности. Я думаю, что лучше начать как можно более консервативно.


@SimonSapin

Это конструктор того, что сегодня AllocErr :: Exhausted? Если да, то не должен ли он иметь параметр Layout?

Ага хороший момент! Я вроде как хотел оставить возможность сделать Error типом нулевого размера, если он нам действительно нужен, но мы, конечно, можем сохранить нестабильность методов компоновки и при необходимости стабилизировать их. Или вы думаете, что сохраняющий макет Error должен быть стабилизирован на первом проходе?


@cramertj

Я лично еще не видел такого вопроса / беспокойства (думаю, я его пропустил!), Но лично я не считаю, что это того стоит. Две черты в целом вдвое превышают шаблон, так как теперь каждый должен будет вводить, например, Alloc + Dealloc в коллекциях. Я ожидал, что такое специализированное использование не приведет к информированию об интерфейсе, которым в конечном итоге будут пользоваться все другие пользователи.

@cramertj @alexcrichton

Я лично еще не видел такого вопроса / беспокойства (думаю, я его пропустил!), Но лично я не считаю, что это того стоит.

В целом согласен, что не стоит за одним вопиющим исключением: Box . Box<T, A: Alloc> , учитывая текущее определение Alloc , должен состоять как минимум из двух слов (указатель, который у него уже есть, и ссылка на Alloc как минимум ), кроме случая глобальных синглтонов (которые могут быть реализованы как ZST). Меня беспокоит двукратный (или более) взрыв в пространстве, необходимом для хранения такого общего и фундаментального типа.

@alexcrichton

так как теперь каждый должен будет вводить Alloc + Dealloc в коллекциях, например

Мы могли бы добавить что-то вроде этого:

trait Allocator: Alloc + Dealloc {}
impl<T> Allocator for T where T: Alloc + Dealloc {}

2-кратное (или более) раздутие в пространстве, необходимом для хранения такого общего и фундаментального типа

Только когда вы используете настраиваемый распределитель, не глобальный для процесса. std::heap::Heap (по умолчанию) имеет нулевой размер.

Или вы думаете, что ошибка сохранения макета должна быть стабилизирована на первом проходе?

@alexcrichton Я действительно не понимаю, почему этот предлагаемый первый проход вообще существует. Едва ли больше, чем уже можно было сделать, злоупотребляя Vec , и этого недостаточно, например, для использования https://crates.io/crates/jemallocator.

Что еще нужно решить, чтобы все это стабилизировать?

Только когда вы используете настраиваемый распределитель, не глобальный для процесса. std :: heap :: Heap (по умолчанию) имеет нулевой размер.

Это похоже на основной вариант использования параметрических распределителей, не так ли? Представьте себе следующее простое определение дерева:

struct Node<T, A: Alloc> {
    t: T,
    left: Option<Box<Node<T, A>>>,
    right: Option<Box<Node<T, A>>>,
}

Дерево, построенное из однословных Alloc будет иметь размер ~ 1,7x для всей структуры данных по сравнению с ZST Alloc . Мне это кажется довольно плохим, и такого рода приложения как раз и заключают в себе смысл того, чтобы Alloc было чертой.

@cramertj

Мы могли бы добавить что-то вроде этого:

У нас также будут настоящие псевдонимы трейтов :) https://github.com/rust-lang/rust/issues/41517

@glaebhoerl Да, но до стабилизации еще далеко, так как реализации еще нет. Если мы отключим ручные импликации Allocator я думаю, мы сможем переключиться на псевдонимы признаков с обратной совместимостью, когда они появятся;)

@joshlf

Меня беспокоит двукратный (или более) взрыв в пространстве, необходимом для хранения такого общего и фундаментального типа.

Я полагаю, что все реализации сегодня представляют собой просто тип нулевого размера или большой указатель, верно? Разве не возможна оптимизация, при которой некоторые типы указателей могут иметь нулевой размер? (или что-то вроде того?)


@cramertj

Мы могли бы добавить что-то вроде этого:

Конечно! Затем мы уменьшили одну черту до трех . В прошлом у нас никогда не было большого опыта с такими качествами. Например, Box<Both> не преобразуется в Box<OnlyOneTrait> . Я уверен, что мы могли бы подождать, пока языковые функции все это сгладят, но похоже, что до них в лучшем случае еще далеко.


@SimonSapin

Что еще нужно решить, чтобы все это стабилизировать?

Я не знаю. Я хотел начать с самого малого, чтобы не было споров.

Я полагаю, что все реализации сегодня представляют собой просто тип нулевого размера или большой указатель, верно? Разве не возможна оптимизация, при которой некоторые типы указателей могут иметь нулевой размер? (или что-то вроде того?)

Да, идея заключается в том, что, имея указатель на объект, выделенный вашим типом распределителя, вы можете выяснить, из какого экземпляра он пришел (например, используя встроенные метаданные). Таким образом, единственная информация, которую вам нужно освободить, - это информация о типе, а не информация о времени выполнения.

Чтобы вернуться к выравниванию при освобождении, я вижу два пути вперед:

  • Стабилизируйте как предложено (с выравниванием при освобождении). Отказ от владения выделенной вручную памятью будет невозможен, если не указан Layout . В частности, невозможно построить контейнер Vec или Box или String или другой контейнер std с более строгим выравниванием, чем требуется (например, потому что вы не 'не хочу, чтобы элемент в штучной упаковке занимал строку кэша), не разбирая и не освобождая его вручную позже (что не всегда возможно). Другой пример невозможного - заполнить Vec с помощью операций simd и затем передать его.

  • Не требовать выравнивания при освобождении и удалить оптимизацию малого выделения из HeapAlloc alloc_system Windows. Всегда храните центровку. @alexcrichton , когда вы фиксировали этот код, вы помните, почему он вообще был туда помещен? Есть ли у нас доказательства того, что он экономит значительный объем памяти для реальных приложений? (С помощью микробенчмарков можно получить результаты в любом случае в зависимости от размера выделения - если только HeapAlloc все равно не округлял размеры.)

В любом случае это очень трудный компромисс; Влияние на память и производительность будет во многом зависеть от типа приложения, и то, для чего нужно оптимизировать, также зависит от конкретного приложения.

Я думаю, что мы действительно можем быть Just Fine (TM). Цитата из Alloc docs :

Некоторые методы требуют, чтобы макет соответствовал блоку памяти.
То, что макет "подходит" к блоку памяти, означает (или
эквивалентно, для блока памяти, "подходящего" к макету) заключается в том, что
должны выполняться следующие два условия:

  1. Начальный адрес блока должен быть выровнен по layout.align() .

  2. Размер блока должен находиться в диапазоне [use_min, use_max] , где:

    • use_min - это self.usable_size(layout).0 , а

    • use_max - это емкость, которая была (или должна была быть)
      возвращается, когда (если) блок был выделен через вызов
      alloc_excess или realloc_excess .

Обратите внимание, что:

  • размер макета, который в последний раз использовался для выделения блока
    гарантированно находится в диапазоне [use_min, use_max] , а

  • нижнюю границу use_max можно безопасно аппроксимировать вызовом
    usable_size .

  • если макет k подходит для блока памяти (обозначается ptr )
    в настоящее время распределяется через распределитель a , тогда это законно
    используйте этот макет, чтобы освободить его, т.е. a.dealloc(ptr, k); .

Обратите внимание на последний пункт. Если я выделяю макет с выравниванием a , тогда мне должно быть разрешено освободить место с выравниванием b < a потому что объект, который выровнен по a , также выровнен по b , и, следовательно, макет с выравниванием b соответствует объекту, выделенному макетом с выравниванием a (и того же размера).

Это означает, что вы должны иметь возможность выделить с выравниванием, которое больше минимального выравнивания, требуемого для определенного типа, а затем позволить некоторому другому коду освободиться с минимальным выравниванием, и он должен работать.

Разве не возможна оптимизация, при которой некоторые типы указателей могут иметь нулевой размер? (или что-то вроде того?)

Недавно для этого был разработан RFC, и кажется маловероятным, что это можно сделать из-за проблем совместимости: https://github.com/rust-lang/rfcs/pull/2040

Например, Box<Both> не преобразуется в Box<OnlyOneTrait> . Я уверен, что мы могли бы подождать, пока языковые функции все это сгладят, но похоже, что до них в лучшем случае еще далеко.

Черта объект на приведение к базовому типу другой стороны, кажется, неоспоримому желательно, и в основном вопрос усилий / пропускной способности / силы воли, чтобы заставить его реализовать. Недавно была ветка: https://internals.rust-lang.org/t/trait-upcasting/5970

@ruuda Я был тем, кто написал эту реализацию alloc_system изначально. alexcrichton просто переместил его во время больших рефакторинговых операций распределителя <time period> .

Текущая реализация требует, чтобы вы освободили с тем же указанным выравниванием, с которым вы выделили данный блок памяти. Независимо от того, что может утверждать документация, это текущая реальность, которую каждый должен соблюдать, пока не будет изменен alloc_system в Windows.

Выделения в Windows всегда кратны MEMORY_ALLOCATION_ALIGNMENT (хотя они запоминают размер, который вы выделили для байта). MEMORY_ALLOCATION_ALIGNMENT - это 8 на 32-битной и 16 на 64-битной. Для типов с избыточным выравниванием, поскольку выравнивание больше, чем MEMORY_ALLOCATION_ALIGNMENT , накладные расходы, вызванные alloc_system , постоянно равны указанной величине выравнивания, поэтому выделение с выравниванием 64 байтов будет иметь 64 байта накладных расходов.

Если бы мы решили распространить этот трюк с избыточным распределением на все выделения (что избавило бы от необходимости освобождать выделение с тем же выравниванием, которое вы указали при распределении), тогда дополнительные выделения будут иметь накладные расходы. Распределения, выравнивания которых идентичны MEMORY_ALLOCATION_ALIGNMENT будут нести постоянные накладные расходы в MEMORY_ALLOCATION_ALIGNMENT байтов. Распределения, выравнивание которых меньше MEMORY_ALLOCATION_ALIGNMENT будут иметь накладные расходы в MEMORY_ALLOCATION_ALIGNMENT байтов примерно в половине случаев. Если размер выделения, округленный до MEMORY_ALLOCATION_ALIGNMENT , больше или равен размеру выделения плюс размер указателя, то накладных расходов нет, в противном случае они есть. Учитывая, что 99,99% распределений не будут чрезмерно распределены, вы действительно хотите понести такие накладные расходы на все эти распределения?

@ruuda

Я лично считаю, что реализация alloc_system сегодня в Windows является большим преимуществом, чем возможность передать право собственности на выделение другому контейнеру, например Vec . AFAIK, хотя нет данных для измерения влияния постоянного заполнения с выравниванием и отсутствия необходимости выравнивания при освобождении.

@joshlf

Я думаю, что этот комментарий неверен, поскольку alloc_system в Windows полагается на то же выравнивание, которое передается при освобождении, что и при распределении.

Учитывая, что 99,99% распределений не будут чрезмерно распределены, вы действительно хотите понести такие накладные расходы на все эти распределения?

От приложения зависит, значительны ли накладные расходы и нужно ли оптимизировать память или производительность. Я подозреваю, что для большинства приложений это нормально, но небольшое меньшинство глубоко заботится о памяти, и они действительно не могут позволить себе эти лишние байты. А другому небольшому меньшинству нужен контроль над выравниванием, и он им действительно нужен.

@alexcrichton

Я думаю, что этот комментарий неверен, поскольку alloc_system в Windows полагается на то же выравнивание, которое передается при освобождении, что и при распределении.

Разве это не означает, что alloc_system в Windows на самом деле не реализует должным образом трейт Alloc (и, таким образом, возможно, нам следует изменить требования трейта Alloc )?


@ retep998

Если я правильно читаю ваш комментарий, разве эти накладные расходы на выравнивание не присутствуют для всех распределений, независимо от того, нужно ли нам иметь возможность освободить место с другим выравниванием? То есть, если я выделяю 64 байта с 64-байтовым выравниванием, а также освобождаю с 64-байтовым выравниванием, описанные вами накладные расходы все еще присутствуют. Таким образом, это не столько возможность освобождения памяти при различных выравниваниях, сколько возможность запрашивать выравнивания, превышающие нормальные.

@joshlf Накладные расходы, вызванные alloc_system настоящее время связаны с запросом большего, чем обычно, выравнивания. Если ваше выравнивание меньше или равно MEMORY_ALLOCATION_ALIGNMENT , то накладные расходы, вызванные alloc_system .

Однако, если мы изменим реализацию, чтобы разрешить освобождение с различными выравниваниями, то накладные расходы будут применяться почти ко всем выделениям, независимо от выравнивания.

Ах я вижу; имеет смысл.

В чем смысл реализации Alloc и для кучи, и для & Heap? В каких случаях пользователь мог бы использовать один из этих имплицитов вместо другого?

Это первая стандартная библиотека API, в которой *mut u8 будет означать "указатель на что угодно"? Есть String :: from_raw_parts, но он действительно означает указатель на байты. Я не поклонник *mut u8 означающего "указатель на что угодно" - даже C лучше. Какие еще варианты? Может быть, указатель на непрозрачный тип был бы более значимым.

@rfcbot проблема * mut u8

@dtolnay Alloc for Heap - это своего рода "стандарт", а Alloc for &Heap - это как Write for &T где для свойства требуется &mut self а для реализации этого не требуется. В частности, это означает, что такие типы, как Heap и System являются потокобезопасными и не нуждаются в синхронизации при выделении.

Однако, что еще более важно, использование #[global_allocator] требует, чтобы статический объект, к которому он прикреплен, имеющий тип T , имел Alloc for &T . (все глобальные распределители должны быть потокобезопасными)

За *mut u8 я думаю, что *mut () может быть интересным, но лично я не чувствую себя слишком обязанным "делать это правильно" как таковое.

Основное преимущество *mut u8 том, что .offset очень удобно использовать с байтовыми смещениями.

Для *mut u8 я думаю, что *mut () может быть интересным, но я лично не чувствую себя слишком обязанным "делать это правильно" как таковое.

Если мы используем *mut u8 в стабильном интерфейсе, разве мы не блокируемся? Другими словами, как только мы это стабилизируем, у нас не будет шанса «исправить это» в будущем.

Кроме того, *mut () кажется мне немного опасным на случай, если мы когда-нибудь сделаем оптимизацию, подобную RFC 2040, в будущем.

Основное преимущество *mut u8 в том, что очень удобно использовать .offset с байтовыми смещениями.

Верно, но вы могли бы легко сделать let ptr = (foo as *mut u8) а затем продолжить свой веселый путь. Это не похоже на достаточную мотивацию, чтобы придерживаться *mut u8 в API, если есть убедительные альтернативы (которые, честно говоря, я не уверен).

Кроме того, * mut () кажется мне немного опасным на случай, если мы когда-нибудь сделаем оптимизацию, подобную RFC 2040, в будущем.

Эта оптимизация, вероятно, уже никогда не произойдет - она ​​сломает слишком много существующего кода. Даже если бы это было так, оно было бы применено к &() и &mut () , а не к *mut () .

Если бы RFC 1861 был близок к реализации / стабилизации, я бы предложил использовать его:

extern { pub type void; }

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut void, Error>;
    unsafe fn dealloc(&mut self, ptr: *mut void, layout: Layout);
    // ...
}

Возможно, это слишком далеко, правда?

@joshlf Я думал, что видел открытый пиар о них, остальное неизвестное - DynSized .

Будет ли это работать для объектов типа struct hack? Скажем, у меня есть Node<T> который выглядит так:

struct Node<T> {
   size: u32,
   data: T,
   // followed by `size` bytes
}

и тип значения:

struct V {
  a: u32,
  b: bool,
}

Теперь я хочу выделить Node<V> строкой размера 7 за одно выделение. В идеале я хочу выделить размер 16, выровнять 4 и уместить все в нем: 4 для u32 , 5 для V и 7 для байтов строки. Это работает, потому что последний член V имеет выравнивание 1, а байты строки также имеют выравнивание 1.

Обратите внимание, что это не разрешено в C / C ++, если типы составлены, как указано выше, поскольку запись в упакованное хранилище является неопределенным поведением. Я думаю, что это дыра в стандарте C / C ++, которую, к сожалению, исправить нельзя. Я могу объяснить, почему это не работает, но вместо этого остановимся на Rust. Это может сработать? :-)

Что касается размера и выравнивания самой структуры Node<V> , вы в значительной степени зависите от прихоти компилятора Rust. Это UB (неопределенное поведение) для выделения с любым размером или выравниванием, меньшим, чем требуется Rust, поскольку Rust может производить оптимизацию на основе предположения, что любой объект Node<V> - в стеке, в куче, за ссылкой и т. Д. - имеет размер и выравнивание, соответствующие ожидаемым во время компиляции.

На практике похоже, что ответ, к сожалению, отрицательный: я запустил эту программу и обнаружил, что, по крайней мере, на Rust Playground, Node<V> имеет размер 12 и выравнивание 4, что означает, что любые объекты после Node<V> должно быть смещено не менее чем на 12 байтов. Похоже, что смещение поля data.b в Node<V> составляет 8 байтов, подразумевая, что байты 9-11 являются завершающим заполнением. К сожалению, даже несмотря на то, что эти байты заполнения в некотором смысле «не используются», компилятор по-прежнему рассматривает их как часть Node<V> и оставляет за собой право делать с ними все, что угодно (что наиболее важно, включая запись к ним, когда вы назначаете Node<V> , подразумевая, что если вы попытаетесь скрыть там дополнительные данные, они могут быть перезаписаны).

(обратите внимание, кстати: вы не можете рассматривать тип как упакованный, который компилятор Rust не считает упакованным. Однако вы _ можете_ сообщить компилятору Rust, что что-то упаковано, что изменит макет типа (удалив отступы), используя repr(packed) )

Однако, что касается размещения одного объекта за другим без того, чтобы они оба были частью одного и того же типа Rust, я почти на 100% уверен, что это действительно так - в конце концов, это то, что делает Vec . Вы можете использовать методы типа Layout чтобы динамически вычислить, сколько места требуется для общего распределения:

let node_layout = Layout::new::<Node<V>>();
// NOTE: This is only valid if the node_layout.align() is at least as large as mem::align_of_val("a")!
// NOTE: I'm assuming that the alignment of all strings is the same (since str is unsized, you can't do mem::align_of::<str>())
let padding = node_layout.padding_needed_for(mem::align_of_val("a"));
let total_size = node_layout.size() + padding + 7;
let total_layout = Layout::from_size_align(total_size, node_layout.align()).unwrap();

Хотелось бы, чтобы это сработало?

#[repr(C)]
struct Node<T> {
   size: u32,
   data: T,
   bytes: [u8; 0],
}

… Затем выделите больший размер и используйте slice::from_raw_parts_mut(node.bytes.as_mut_ptr(), size) ?

Спасибо @joshlf за подробный ответ! TL; DR для моего варианта использования заключается в том, что я могу получить Node<V> размером 16, но только если V равно repr(packed) . В остальном лучшее, что я могу сделать, это размер 19 (12 + 7).

@SimonSapin не уверен; Я попробую.

Есть на самом деле не догнали эту тему, но я категорически против стабилизации еще ничего. Мы еще не добились прогресса в решении сложных задач:

  1. Аллокатор-полиморфные коллекции

    • даже не раздутый ящик!

  2. Ошибочные коллекции

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

Re: комментарий @ Ericson2314

Я думаю, что актуальный вопрос, связанный с конфликтом между этой точкой зрения и желанием @alexcrichton что-то стабилизировать, звучит так: насколько мы получаем пользу от стабилизации минимального интерфейса? В частности, очень немногие потребители будут вызывать методы Alloc напрямую (даже большинство коллекций, вероятно, будут использовать Box или какой-либо другой аналогичный контейнер), поэтому реальный вопрос заключается в следующем: что делает стабилизация покупки для пользователей, которые будут не вызывать методы Alloc напрямую? Честно говоря, единственный серьезный вариант использования, о котором я могу думать, это то, что он прокладывает путь для распределителей-полиморфных коллекций (которые, вероятно, будут использоваться гораздо более широким кругом пользователей), но похоже, что это заблокировано на # 27336, что далеко от решается. Может быть, есть и другие важные варианты использования, которые мне не хватает, но, основываясь на этом быстром анализе, я склонен отказываться от стабилизации, поскольку она дает лишь незначительные выгоды за счет того, что мы замыкаемся в дизайне, который мы позже можем найти неоптимальным. .

@joshlf позволяет людям определять и использовать свои собственные глобальные распределители.

Хммм хороший момент. возможно ли стабилизировать указание глобального распределителя без стабилизации Alloc ? То есть код, реализующий Alloc , должен быть нестабильным, но он, вероятно, будет инкапсулирован в его собственный ящик, а механизм пометки этого распределителя как глобального распределителя сам будет стабильным. Или я неправильно понимаю, как взаимодействуют стабильный / нестабильный и стабильный компилятор / ночной компилятор?

Ах, @joshlf, помни, что # 27336 - это отвлечение, согласно https://github.com/rust-lang/rust/issues/42774#issuecomment -317279035. Я почти уверен, что мы столкнемся с другими проблемами - проблемами с признаками в том виде, в каком они существуют, поэтому я хочу работать над этим сейчас. Гораздо легче обсуждать эти проблемы, когда они появятся на всеобщее обозрение, чем обсуждать предсказанное будущее после # 27336.

@joshlf Но вы не можете скомпилировать ящик, определяющий глобальный распределитель, с помощью стабильного компилятора.

@sfackler Ах да, вот это недоразумение, которого я боялся: P

Я считаю, что имя Excess(ptr, usize) немного сбивает с толку, потому что usize - это не excess по размеру запрошенного распределения (как в выделенном дополнительном размере), а total размер выделения.

IMO Total , Real , Usable или любое другое имя, которое означает, что размер - это общий размер или реальный размер выделения, лучше, чем "избыток", который я считаю вводящие в заблуждение. То же самое относится к методам _excess .

Я согласен с @gnzlbg выше, я думаю, что простой (ptr, usize) кортеж подойдет.

Обратите внимание, что Excess не предлагается для стабилизации на первом проходе, однако

Разместил эту ветку для обсуждения на Reddit, что вызывает у некоторых обеспокоенных людей: https://www.reddit.com/r/rust/comments/78dabn/custom_allocators_are_on_the_verge_of_being/

После дальнейшего обсуждения с @ rust-lang / libs сегодня я хотел бы внести несколько изменений в предложение по стабилизации, которые можно резюмировать следующим образом:

  • Добавьте alloc_zeroed в набор стабилизированных методов, иначе имеющий ту же сигнатуру, что и alloc .
  • Измените *mut u8 на *mut void в API, используя поддержку extern { type void; } , решив проблему @dtolnay и предоставив возможность для объединения c_void в экосистеме.
  • Измените тип возврата alloc на *mut void , удалив Result и Error

Возможно, наиболее спорным является последний пункт, поэтому я хочу остановиться и на нем. Это стало результатом обсуждения с командой libs сегодня и, в частности, вращалось вокруг того, как (а) интерфейс на основе Result имеет менее эффективный ABI, чем интерфейс, возвращающий указатель, и (б) сегодня почти нет "производственных" распределителей. предоставить возможность узнать что-либо большее, чем «это просто OOM'd». Для повышения производительности мы можем в основном скрыть это с помощью встраивания и тому подобного, но остается, что Error - это дополнительная полезная нагрузка, которую трудно удалить на самых нижних уровнях.

Идея возврата полезной нагрузки ошибок заключается в том, что распределители могут обеспечить интроспекцию, зависящую от реализации, чтобы узнать, почему выделение не удалось, а в противном случае почти всем потребителям нужно только знать, было ли выделение успешным или неудачным. Вдобавок предполагается, что это будет очень низкоуровневый API, который на самом деле не так часто вызывается (вместо этого следует вызывать типизированные API, которые красиво оборачивают вещи). В этом смысле не имеет первостепенного значения, чтобы у нас был наиболее удобный и эргономичный API для этого местоположения, а, скорее, важнее включить варианты использования без потери производительности.

Основное преимущество *mut u8 том, что .offset очень удобно использовать с байтовыми смещениями.

На собрании библиотек мы также предложили impl *mut void { fn offset } который не конфликтует с существующим offset определенным для T: Sized . Также может быть byte_offset .

+1 за использование *mut void и byte_offset . Будет ли проблема со стабилизацией функции внешних типов, или мы можем обойти эту проблему, потому что нестабильно только определение (а liballoc может делать нестабильные вещи внутри), а не использование (например, let a: *mut void = ... isn не нестабильно)?

Да, нам не нужно блокировать стабилизацию внешнего типа. Даже если поддержка внешнего типа будет удалена, void мы определяем для этого, всегда может быть наихудшим случаем магического типа.

Было ли обсуждение на собрании библиотек о том, должны ли Alloc и Dealloc быть отдельными чертами?

Мы не касались этого конкретно, но в целом чувствовали, что не следует отклоняться от предшествующего уровня техники, если у нас нет особо веских причин для этого. В частности, концепция распределителя C ++ не имеет подобного разделения.

Я не уверен, что в данном случае это подходящее сравнение. В C ++ все явно освобождается, поэтому нет эквивалента Box который должен хранить копию (или ссылку) своего собственного распределителя. Это то, что вызывает у нас большой взрыв.

@joshlf unique_ptr - эквивалент Box , vector - эквивалент Vec , unordered_map - эквивалент HashMap и т. д.

@cramertj Ах, интересно, я смотрел только на типы коллекций. Похоже, тогда это можно было бы сделать. Мы всегда можем добавить его позже с помощью blanket impls, но, вероятно, было бы проще этого избежать.

На самом деле подход blanket impl может быть чище:

pub trait Dealloc {
    fn dealloc(&self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

На одну черту меньше, о которой нужно беспокоиться в большинстве случаев использования.

  • Измените тип возвращаемого значения alloc на * mut void, удалив Результат и Ошибка

Возможно, наиболее спорным является последний пункт, поэтому я хочу остановиться и на нем. Это стало результатом сегодняшнего обсуждения с командой libs и, в частности, вращалось вокруг того, как (а) интерфейс на основе результатов имеет менее эффективный ABI, чем интерфейс, возвращающий указатель, и (б) сегодня почти нет «производственных» распределителей, обеспечивающих возможность обучения ничего больше, чем «это просто OOM'd». Для повышения производительности мы можем в основном обрисовать это с помощью встраивания и тому подобного, но остается, что ошибка - это дополнительная полезная нагрузка, которую трудно удалить на самых нижних уровнях.

Я обеспокоен тем, что это упростит использование возвращаемого указателя без проверки на null. Кажется, что накладные расходы также можно было бы удалить без добавления этого риска, вернув Result<NonZeroPtr<void>, AllocErr> и сделав AllocErr нулевого размера?

( NonZeroPtr - это объединенные ptr::Shared и ptr::Unique как предложено в https://github.com/rust-lang/rust/issues/27730#issuecomment-316236397.)

@SimonSapin, что-то вроде Result<NonZeroPtr<void>, AllocErr> требует для стабилизации трех типов, все из которых являются совершенно новыми, а некоторые из которых исторически довольно сильно томились для стабилизации. Что-то вроде void даже не требуется и, на мой взгляд, полезно иметь.

Я согласен, что его «легко использовать без проверки на null», но это, опять же, очень низкоуровневый API, который не предназначен для интенсивного использования, поэтому я не думаю, что мы должны оптимизировать для эргономики вызывающего абонента.

Люди также могут создавать абстракции более высокого уровня, такие как alloc_one поверх нижнего уровня alloc которые могут иметь более сложные типы возврата, такие как Result<NonZeroPtr<void>, AllocErr> .

Я согласен с тем, что AllocErr бесполезен на практике, но как насчет Option<NonZeroPtr<void>> ? API, которые невозможно случайно неправильно использовать без накладных расходов, - это одна из вещей, которая отличает Rust от C, и возвращение к нулевым указателям в стиле C кажется мне шагом назад. Сказать, что это «очень низкоуровневый API, который не предназначен для интенсивного использования», - все равно что сказать, что мы не должны заботиться о безопасности памяти на необычных архитектурах микроконтроллеров, потому что они очень низкоуровневые и мало используются.

Каждое взаимодействие с распределителем включает небезопасный код, независимо от типа возвращаемого значения этой функции. API распределения низкого уровня могут использоваться неправильно, независимо от того, является ли тип возвращаемого значения Option<NonZeroPtr<void>> или *mut void .

Alloc::alloc в частности, API низкого уровня, который не предназначен для интенсивного использования. Такие методы, как Alloc::alloc_one<T> или Alloc::alloc_array<T> - это альтернативы, которые будут более интенсивно использоваться и имеют "более приятный" тип возвращаемого значения.

AllocError отслеживанием состояния не стоит, но тип нулевого размера, который реализует Error и имеет Display allocation failure , неплохо иметь. Если мы пойдем по маршруту NonZeroPtr<void> , я вижу, что Result<NonZeroPtr<void>, AllocError> предпочтительнее, чем Option<NonZeroPtr<void>> .

Почему порыв стабилизировать :( !! Result<NonZeroPtr<void>, AllocErr> , бесспорно , лучше для клиентов использования. Говоря это «очень низкого уровня API» , что не должно быть приятно просто удручающе неамбициозны. Код на всех уровнях должны быть максимально безопасный и удобный в обслуживании; тем более непонятный код, который не редактируется постоянно (и, следовательно, не сохраняется в кратковременной памяти людей)!

Более того, если нам нужно иметь написанные пользователем коллекции с полиморфным распределением памяти, на что я, конечно, надеюсь, это открытый объем довольно сложного кода, напрямую использующий распределители.

При повторном освобождении мы почти наверняка хотим ссылаться / клонировать аллоактор только один раз для каждой древовидной коллекции. Это означает передачу распределителя в каждый уничтожаемый пользовательский блок-распределитель. Но это открытый вопрос, как лучше всего это сделать в Rust без линейных типов. Вопреки моему предыдущему комментарию, я был бы в порядке с некоторым небезопасным кодом в реализациях коллекций для этого, потому что идеальный вариант использования изменяет реализацию Box , а не реализацию свойств разделенного распределителя и освободителя. Т.е. мы можем добиться стабильного прогресса без ограничения линейности.

@sfackler Я думаю, нам нужны некоторые связанные типы, связывающие освобождающее устройство с распределителем; может быть невозможно их модернизировать.

@ Ericson2314 Существует «спешка» к стабилизации, потому что люди хотят использовать распределители для реальных вещей в реальном мире. Это не научный проект.

Для чего будет использоваться этот связанный тип?

Люди @sfackler все еще могут прикреплять ночные / и люди, которым навсегда задуматься , если мы не хотим разделить экосистему с помощью новой версии 2.0.

Связанные типы будут связывать освобождающее устройство с распределителем. Каждый должен знать о другом, чтобы это работало. [По-прежнему существует проблема использования неправильного (де) распределителя правильного типа, но я согласен с тем, что никто удаленно не предложил решения для этого.]

Если люди могут просто прикреплять ночные часы, зачем нам вообще стабильные сборки? Набор людей, которые напрямую взаимодействуют с API распределителя, намного меньше людей, которые хотят воспользоваться преимуществами этих API, например, заменив глобальный распределитель.

Можете ли вы написать код, который показывает, почему освободителю нужно знать тип связанного с ним распределителя? Почему API распределителя C ++ не нуждается в подобном отображении?

Если люди могут просто прикреплять ночные часы, зачем нам вообще стабильные сборки?

Для обозначения языковой стабильности. Код, который вы пишете против этой версии, никогда не сломается. На более новом компиляторе. Вы прикрепляете каждую ночь, когда вам нужно что-то настолько плохое, что не стоит ждать последней итерации функции, качество которой будет признано достойным этой гарантии.

Набор людей, которые напрямую взаимодействуют с API распределителя, намного меньше людей, которые хотят воспользоваться преимуществами этих API, например, заменив глобальный распределитель.

Ага! Это было бы для перемещения jemalloc из дерева и т. Д.? Никто не предлагал стабилизировать ужасные хаки, которые позволяют выбрать глобальный распределитель, только статику кучи? Или я неправильно прочитал предложение?

Ужасные хаки, позволяющие выбрать глобальный распределитель, предлагается стабилизировать, что составляет половину того, что позволяет нам переместить jemalloc из дерева. Эта проблема - вторая половина.

#[global_allocator] стабилизация атрибутов: https://github.com/rust-lang/rust/issues/27389#issuecomment -336955367

Yikes

@ Ericson2314 Как вы думаете, какой не ужасный способ выбрать глобальный распределитель?

(Ответ на https://github.com/rust-lang/rust/issues/27389#issuecomment-342285805)

Предложение было изменено на использование * mut void.

@rfcbot разрешен * mut u8

@rfcbot просмотрел

После некоторого обсуждения IRC я ​​одобряю это при том понимании, что мы _не_ намерены стабилизировать Box generic на Alloc , а вместо этого на некотором трейте Dealloc с соответствующий бланкет impl, как предлагает @sfackler здесь . Пожалуйста, дайте мне знать, если я неправильно понял намерение.

@cramertj Чтобы прояснить, можно добавить это одеяло постфактум и не нарушить определение Alloc которое мы здесь стабилизируем?

@joshlf да, это будет выглядеть так: https://github.com/rust-lang/rust/issues/32838#issuecomment -340959804

Как мы укажем Dealloc для данного Alloc ? Я бы вообразил что-то подобное?

pub unsafe trait Alloc {
    type Dealloc: Dealloc = Self;
    ...
}

Думаю, это ставит нас на тернистую территорию WRT https://github.com/rust-lang/rust/issues/29661.

Да, я не думаю, что есть способ сделать добавление Dealloc обратно совместимым с существующими определениями Alloc (у которых нет этого связанного типа) без использования значения по умолчанию.

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

Но с этим можно справиться в будущем, добавив этот материал к отдельному субтреку Alloc я думаю.

@sfackler, я не уверен, что понимаю. Можете ли вы написать подпись Box::new под своим дизайном?

Это игнорирует синтаксис размещения и все такое, но вы могли бы сделать это одним из способов:

pub struct Box<T, D>(NonZeroPtr<T>, D);

impl<T, D> Box<T, D>
where
    D: Dealloc
{
    fn new<A>(alloc: A, value: T) -> Box<T, D>
    where
        A: Alloc<Dealloc = D>
    {
        let ptr = alloc.alloc_one().unwrap_or_else(|_| alloc.oom());
        ptr::write(&value, ptr);
        let deallocator = alloc.deallocator();
        Box(ptr, deallocator)
    }
}

Примечательно, что нам нужно действительно иметь возможность создавать экземпляр освобождающего средства, а не просто знать его тип. Вы также можете параметризовать Box A::Dealloc вместо Alloc и вместо этого сохранить A::Dealloc , что может помочь с выводом типа. Мы можем сделать эту работу после этой стабилизации, переместив Dealloc и deallocator в отдельную черту:

pub trait SplitAlloc: Alloc {
    type Dealloc;

    fn deallocator(&self) -> Self::Dealloc;
}

Но как бы выглядело значение Drop ?

impl<T, D> Drop for Box<T, D>
where
    D: Dealloc
{
    fn drop(&mut self) {
        unsafe {
            ptr::drop_in_place(self.0);
            self.1.dealloc_one(self.0);
        }
    }
}

Но если предположить, что мы сначала стабилизируем Alloc , тогда не все Alloc будут реализовывать Dealloc , верно? И я думал, что до специализации по имплементации еще далеко? Другими словами, теоретически вы бы хотели сделать что-то вроде следующего, но я не думаю, что это еще работает?

impl<T, D> Drop for Box<T, D> where D: Dealloc { ... }
impl<T, A> Drop for Box<T, A> where A: Alloc { ... }

Во всяком случае, у нас был бы

default impl<T> SplitAlloc for T
where
    T: Alloc { ... }

Но я не думаю, что это действительно необходимо. Сценарии использования настраиваемых распределителей и глобальных распределителей достаточно различны, поэтому я бы не предположил, что между ними будет много совпадений.

Полагаю, это может сработать. Тем не менее, мне кажется намного чище сразу иметь Dealloc чтобы у нас был более простой интерфейс. Я предполагаю, что у нас может быть довольно простой, бесспорный интерфейс, который не потребует изменения существующего кода, который уже реализует Alloc :

unsafe trait Dealloc {
    fn dealloc(&mut self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

unsafe trait Alloc {
    type Dealloc: Dealloc = &mut Self;
    fn deallocator(&mut self) -> Self::Dealloc { self }
    ...
}

Я хоть с ошибками ассоциированного типа по умолчанию?

Dealloc который является изменяемой ссылкой на распределитель, кажется не таким уж полезным - вы можете выделять только одну вещь за раз, верно?

Я хоть с ошибками ассоциированного типа по умолчанию?

О, я полагаю, что значения по умолчанию для ассоциированного типа находятся достаточно далеко, и мы не можем полагаться на них.

Тем не менее, у нас может быть и попроще:

unsafe trait Dealloc {
    fn dealloc(&mut self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

unsafe trait Alloc {
    type Dealloc: Dealloc;
    fn deallocator(&mut self) -> Self::Dealloc;
    ...
}

и просто потребуйте от разработчика написать немного шаблона.

Dealloc который является изменяемой ссылкой на распределитель, кажется не таким уж полезным - вы можете выделять только одну вещь за раз, верно?

Да, хорошее замечание. В любом случае, вероятно, спорный вопрос, учитывая ваш другой комментарий.

Следует ли deallocator брать self , &self или &mut self ?

Вероятно, &mut self в соответствии с другими методами.

Существуют ли какие-либо распределители, которые предпочли бы принимать себя по значению, чтобы им не приходилось, например, клонировать состояние?

Проблема с взятием self по значению заключается в том, что это не позволяет получить Dealloc и затем продолжить выделение.

Я думаю о гипотетическом распределителе с одним выстрелом, хотя я не знаю, насколько это реально.

Такой распределитель может существовать, но выбор self по значению потребует, чтобы _все_ распределители работали таким образом, и не позволит любым распределителям, разрешающим выделение после вызова deallocator .

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

Как вы думаете, https://github.com/rust-lang/rust/issues/27336 или пункты, обсуждаемые в https://github.com/rust-lang/rust/issues/32838#issuecomment -339066870, позволят нам продвигаться по коллекциям?

Меня беспокоит влияние подхода псевдонима типа на читаемость документации. (Очень подробный) способ разрешить прогресс - это обернуть типы:

pub struct Vec<T>(alloc::Vec<T, Heap>);

impl<T> Vec<T> {
    // forwarding impls for everything
}

Я знаю, что это неприятно, но похоже, что изменения, которые мы здесь обсуждаем, достаточно велики, поэтому, если мы решим продолжить с чертами разделения выделения / освобождения, мы должны сначала попробовать их в std, а затем повторно FCP.

Каковы сроки ожидания реализации этого материала?

Метод grow_in_place не возвращает никакой избыточной емкости. В настоящее время он вызывает usable_size с макетом, расширяет выделение до _ по крайней мере_, подходящего для этого макета, но если распределение расширяется за пределы этого макета, пользователи не имеют возможности узнать.

Мне сложно понять преимущество методов alloc и realloc перед alloc_excess и realloc_excess .

Распределителю необходимо найти подходящий блок памяти для выполнения распределения: для этого необходимо знать размер блока памяти. Возвращает ли распределитель затем указатель или кортеж «указатель и размер блока памяти» не влияет на измеримые различия в производительности.

Таким образом, alloc и realloc просто увеличивают поверхность API и, кажется, поощряют написание менее производительного кода. Зачем они вообще есть в API? В чем их преимущество?


РЕДАКТИРОВАТЬ: или другими словами: все потенциально выделяющие функции в API должны возвращать Excess , что в основном устраняет необходимость во всех методах _excess .

Избыток интересен только для случаев использования, в которых используются массивы, которые могут расти. Это бесполезно и бесполезно, например, для Box или BTreeMap . Вычисление избыточной мощности может потребовать определенных затрат, и, безусловно, существует более сложный API, поэтому мне не кажется, что код, который не заботится о избыточной емкости, должен быть вынужден платить за нее.

Вычисление превышения может потребовать некоторых затрат.

Вы можете привести пример? Я не знаю и не могу представить себе распределитель, который может выделять память, но не знает, сколько памяти он на самом деле выделяет (что и есть Excess : реальный объем выделенной памяти; мы должны переименуйте его).

Единственный обычно используемый Alloc ator, где это может быть немного спорным, - это POSIX malloc , который, хотя он всегда вычисляет Excess внутри себя, не раскрывает его как часть своего C API. Однако возвращение запрошенного размера в виде Excess в порядке, переносимо, просто, совсем не требует затрат, и это то, что все, кто использует POSIX malloc в любом случае уже предполагают.

jemalloc и в основном любые другие Alloc ator там предоставляют API, который возвращает Excess без каких-либо затрат, поэтому для этих распределителей возврат Excess равен нулю. стоимость тоже.

Вычисление избыточной мощности может потребовать определенных затрат, и, безусловно, существует более сложный API, поэтому мне не кажется, что код, который не заботится о избыточной емкости, должен быть вынужден платить за нее.

Прямо сейчас все уже расплачиваются за свойство распределителя, имеющее два API для выделения памяти. И хотя можно построить API без Excess без Excess -full, обратное неверно. Поэтому мне интересно, почему это не делается так:

  • Alloc методы признаков всегда возвращают Excess
  • добавить трейт ExcessLessAlloc который просто отбрасывает Excess из Alloc методов для всех пользователей, которые 1) достаточно внимательны, чтобы использовать Alloc но 2) не заботиться о реальном объеме памяти, выделяемой в данный момент (для меня это ниша, но я все еще думаю, что такой API - это хорошо)
  • если однажды кто-то обнаружит способ реализовать Alloc ators с быстрыми путями для Excess -less методов, мы всегда сможем предоставить для этого индивидуальную реализацию ExcessLessAlloc .

FWIW Я снова попал в эту ветку, потому что не могу реализовать то, что хочу, поверх Alloc . Я уже упоминал, что в нем не хватает grow_in_place_excess , но я снова застрял, потому что в нем также отсутствует alloc_zeroed_excess (и кто знает, что еще).

Мне было бы удобнее, если бы стабилизация здесь была сосредоточена на стабилизации Excess -full API в первую очередь. Даже если его API-интерфейс не является самым эргономичным для всех видов использования, такой API-интерфейс, по крайней мере, разрешит все виды использования, что является необходимым условием, чтобы показать, что дизайн не имеет недостатков.

Вы можете привести пример? Я не знаю и не могу представить себе распределитель, который может выделять память, но не знает, сколько памяти он на самом деле выделяет (что и есть Excess : реальный объем выделенной памяти; мы должны переименуйте его).

Большинство распределителей сегодня используют классы размера, где каждый класс размера выделяет только объекты определенного фиксированного размера, а запросы на выделение, которые не соответствуют определенному классу размера, округляются до класса наименьшего размера, в который они помещаются. В этой схеме обычно используются такие вещи, как массив объектов класса размера, а затем выполнение classes[size / SIZE_QUANTUM].alloc() . В этом мире для выяснения того, какой класс размера используется, требуются дополнительные инструкции: например, let excess = classes[size / SIZE_QUANTUM].size . Это может быть немного, но производительность высокопроизводительных распределителей (таких как jemalloc) измеряется в единичных тактовых циклах, поэтому это может представлять значимые накладные расходы, особенно если этот размер в конечном итоге проходит через цепочку возвращаемых функций.

Вы можете привести пример?

По крайней мере, переходя от вашего PR к alloc_jemalloc, alloc_excess явно выполняет больше кода, чем alloc : https://github.com/rust-lang/rust/pull/45514/files.

В этой схеме обычно используются такие вещи, как массив объектов класса размера, а затем выполнение классов [size / SIZE_QUANTUM] .alloc (). В этом мире для выяснения того, какой класс размера используется, требуются дополнительные инструкции: например, let Extra = classes [size / SIZE_QUANTUM] .size

Итак, позвольте мне посмотреть, правильно ли я следую:

// This happens in both cases:
let size_class = classes[size / SIZE_QUANTUM];
let ptr = size_class.alloc(); 
// This would happen only if you need to return the size:
let size = size_class.size;
return (ptr, size);

Это оно?


По крайней мере, если ваш PR переходит в alloc_jemalloc, alloc_excess явно выполняет больше кода, чем alloc

Этот PR был исправлением ошибки (а не исправлением perf), есть много ошибок с текущим состоянием нашего слоя jemalloc с точки зрения производительности, но поскольку этот PR он, по крайней мере, возвращает то, что должен:

  • nallocx - это функция const в смысле GCC, то есть настоящая чистая функция. Это означает, что у него нет побочных эффектов, его результаты зависят только от его аргументов, он не имеет доступа к глобальному состоянию, его аргументы не являются указателями (поэтому функция не может получить доступ к глобальному состоянию, выбрасывая их), а для программ C / C ++ LLVM может использовать это информация для отмены вызова, если результат не используется. AFAIK Rust в настоящее время не может помечать функции FFI C как const fn или аналогичные. Так что это первое, что можно исправить, и это сделает realloc_excess нулевой стоимостью для тех, кто не использует излишки, если встраивание и оптимизация работают правильно.
  • nallocx всегда вычисляется для выровненных распределений внутри mallocx , то есть весь код уже вычисляет его, но mallocx отбрасывает свой результат, поэтому здесь мы фактически вычисляем его дважды , а в некоторых случаях nallocx почти так же дорого, как mallocx ... У меня есть форк jemallocator, в ветвях которого есть некоторые тесты для подобных вещей, но это должно быть исправлено с помощью jemalloc, предоставляя API, который не выбрасывает это. Однако это исправление затрагивает только тех, кто в настоящее время использует Excess .
  • а затем проблема в том, что мы вычисляем флаги выравнивания дважды, но это то, что LLVM может оптимизировать на нашей стороне (и это легко исправить).

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


РЕДАКТИРОВАТЬ: @sfackler Мне удалось освободить для этого немного времени сегодня, и я смог сделать alloc_excess "свободным" по отношению к alloc в медленном пути jemallocs и иметь только накладные расходы ~ 1 нс в быстрый путь джемаллоков. Я не особо подробно рассматривал быстрый путь, но, возможно, можно было бы улучшить его дальше. Подробности здесь: https://github.com/jemalloc/jemalloc/issues/1074#issuecomment -345040339

Это оно?

Да.

Так что это первое, что можно исправить, и это сделает realloc_excess нулевой стоимостью для тех, кто не использует излишки, пока встраивание и оптимизация работают правильно.

При использовании в качестве глобального распределителя ничего из этого нельзя встроить.

Даже если его API-интерфейс не является самым эргономичным для всех видов использования, такой API-интерфейс, по крайней мере, разрешит все виды использования, что является необходимым условием, чтобы показать, что дизайн не имеет недостатков.

На Github практически отсутствует код, вызывающий alloc_excess . Если это такая критически важная функция, почему ее никто никогда не использовал? API распределения C ++ не предоставляют доступа к избыточной емкости. Кажется невероятно простым добавить / стабилизировать эти функции в будущем обратно совместимым способом, если есть реальные конкретные доказательства того, что они улучшают производительность, и кто-то действительно заботится о том, чтобы их использовать.

При использовании в качестве глобального распределителя ничего из этого нельзя встроить.

Тогда это проблема, которую мы должны попытаться решить, по крайней мере, для сборок LTO, потому что глобальные распределители, такие как jemalloc полагаются на это: nallocx - это то, как это _в дизайне_, и первая рекомендация Разработчики jemalloc убедили нас в том, что производительность alloc_excess заключается в том, что эти вызовы должны быть встроены, и мы должны правильно распространять атрибуты C, чтобы компилятор удалял вызовы nallocx с сайтов вызовов, которые не используйте Excess , как это делают компиляторы C и C ++.

Даже если мы не сможем этого сделать, Excess API все равно можно сделать с нулевыми затратами, исправив jemalloc API (у меня есть первоначальная реализация такого патча в моем rust-lang / вилка jemalloc). Мы могли бы либо поддерживать этот API самостоятельно, либо попытаться вывести его вверх по течению, но для этого мы должны убедительно объяснить, почему другие языки могут выполнять такую ​​оптимизацию, а Rust - нет. Или у нас должен быть другой аргумент, например, этот новый API значительно быстрее, чем mallocx + nallocx для тех пользователей, которым нужен Excess .

Если это такая критически важная функция, почему ее никто никогда не использовал?

Это хороший вопрос. std::Vec - это дочерний элемент для использования Excess API, но в настоящее время он его не использует, и все мои предыдущие комментарии, в которых говорится, что "то и другое отсутствуют в Excess API ", когда я пытался заставить Vec использовать его. API Excess :

Я не могу понять, почему никто не использует этот API. Но учитывая, что даже библиотека std может использовать ее для структуры данных, для которой она лучше всего подходит ( Vec ), если бы мне пришлось угадывать, я бы сказал, что основная причина в том, что этот API в настоящее время не работает.

Если бы мне пришлось гадать еще дальше, я бы сказал, что даже те, кто разработал этот API, не использовали его, главным образом потому, что ни одна коллекция std использует его (именно здесь я ожидаю, что этот API будет сначала протестирован) , а также потому, что использование _excess и Excess везде для обозначения usable_size / allocation_size чрезвычайно сбивает с толку / раздражает при программировании.

Вероятно, это связано с тем, что в API Excess -less было вложено больше работы, а когда у вас есть два API, их трудно синхронизировать, пользователям трудно обнаружить оба и знать, какой использовать, и, наконец, пользователям трудно предпочесть удобство правильным действиям.

Или, другими словами, если у меня есть два конкурирующих API, и я вкладываю 100% работы в улучшение одного и 0% работы в улучшение другого, неудивительно, что можно прийти к выводу, что один на практике значительно лучше, чем другие.

Насколько я могу судить, это единственные два вызова nallocx вне тестов jemalloc на Github:

https://github.com/facebook/folly/blob/f2925b23df8d85ebca72d62a69f1282528c086de/folly/detail/ThreadLocalDetail.cpp#L182
https://github.com/louishust/mysql5.6.14_tokudb/blob/4897660dee3e8e340a1e6c8c597f3b2b7420654a/storage/tokudb/ft-index/ftcxx/malloc_utils.hpp#L91

Ни один из них не похож на текущий alloc_excess API, а скорее используется автономно для вычисления размера выделения перед его выполнением.

Apache Arrow попытался использовать nallocx в своей реализации, но обнаружил, что ничего не вышло:

https://issues.apache.org/jira/browse/ARROW-464

По сути, это единственные ссылки на nallocx я могу найти. Почему так важно, чтобы первоначальная реализация API распределителя поддерживала такую ​​непонятную функцию?

Насколько я могу судить, это единственные два вызова nallocx вне тестов jemalloc на Github:

Сверху головы я знаю, что по крайней мере векторный тип facebook использует его через реализацию malloc в facebook ( политика роста malloc и fbvector ; это большой кусок векторов C ++ в facebook, использующий это), а также что Чапел использовал его для улучшения производительность их типа String ( здесь и проблема с

Почему так важно, чтобы первоначальная реализация API распределителя поддерживала такую ​​непонятную функцию?

Первоначальная реализация API распределителя не должна поддерживать эту функцию.

Но хорошая поддержка этой функции должна блокировать стабилизацию такого API.

Зачем ему блокировать стабилизацию, если ее можно будет добавить обратно с обратной совместимостью позже?

Зачем ему блокировать стабилизацию, если ее можно будет добавить обратно с обратной совместимостью позже?

Потому что, по крайней мере, для меня это означает, что только половина пространства дизайна была достаточно исследована.

Ожидаете ли вы, что на не лишние связанные части API будет влиять дизайн дополнительных связанных функций? Признаюсь, я следил за этой дискуссией без энтузиазма, но мне это кажется маловероятным.

Если мы не можем сделать этот API:

fn alloc(...) -> (*mut u8, usize) { 
   // worst case system API:
   let ptr = malloc(...);
   let excess = malloc_excess(...);
   (ptr, excess)
}
let (ptr, _) = alloc(...); // drop the excess

такой же эффективный, как этот:

fn alloc(...) -> *mut u8 { 
   // worst case system API:
   malloc(...)
}
let ptr = alloc(...);

тогда у нас есть большие проблемы.

Ожидаете ли вы, что на не лишние связанные части API будет влиять дизайн дополнительных связанных функций?

Итак, да, я ожидаю, что хороший API-интерфейс с избытком окажет огромное влияние на дизайн не связанных с лишним функций: он полностью удалит его.

Это предотвратило бы текущую ситуацию, когда два API не синхронизированы и в которых излишек API имеет меньше функциональных возможностей, чем один без лишнего. Хотя можно создать API без лишнего, поверх полного с избытком, обратное неверно.

Те, кто хочет сбросить Excess должны просто выбросить его.

Чтобы уточнить, если бы существовал какой-то способ добавить метод alloc_excess постфактум обратно совместимым способом, то вы бы согласились с этим? (но, конечно, стабилизация без alloc_excess означает, что добавление ее позже будет критическим изменением; я просто прошу, чтобы понять ваши рассуждения)

@joshlf Это очень просто сделать.

: bell: Сейчас наступает последний период для комментариев , как указано в обзоре выше . : колокол:

Те, кто хочет отбросить Излишек, должны просто отказаться от него.

В качестве альтернативы 0,01% людей, которым небезразлична избыточная мощность, могут использовать другой метод.

@sfackler Это то, что я получаю за двухнедельный перерыв от ржавчины - я забываю о методе по умолчанию impls :)

В качестве альтернативы 0,01% людей, которым небезразлична избыточная мощность, могут использовать другой метод.

Откуда у вас этот номер?

Все мои структуры данных Rust хранятся в памяти. Возможность делать это - единственная причина, по которой я использую Rust; если бы я мог просто упаковать все, я бы использовал другой язык. Так что меня все время не волнует Excess 0.01% , я забочусь об этом все время.

Я понимаю, что это зависит от домена, и что в других доменах люди никогда не будут интересоваться Excess , но я сомневаюсь, что только 0,01% пользователей Rust заботятся об этом (я имею в виду, что многие люди используют Vec и String , которые являются дочерними структурами данных для Excess ).

Я получил это число из-за того, что всего около 4 вещей используют nallocx по сравнению с набором вещей, которые используют malloc.

@gnzlbg

Вы предлагаете, что если бы мы сделали это «правильно» с самого начала, у нас было бы только fn alloc(layout) -> (ptr, excess) и не было бы fn alloc(layout) -> ptr вообще? Мне это кажется далеко не очевидным. Даже если избыток доступен, кажется естественным иметь последний API для случаев использования, где избыток не имеет значения (например, большинство древовидных структур), даже если он реализован как alloc_excess(layout).0 .

@rkruppe

Мне это кажется далеко не очевидным. Даже если избыток доступен, кажется естественным иметь последний API для случаев использования, где избыток не имеет значения (например, большинство древовидных структур), даже если он реализован как alloc_excess (layout) .0.

В настоящее время API с избыточным заполнением реализован поверх API без избыточного. Реализация Alloc для распределителя без излишков требует, чтобы пользователь предоставил методы alloc и dealloc .

Однако, если я хочу реализовать Alloc для избыточно-полного распределителя, мне нужно предоставить больше методов (по крайней мере, alloc_excess , но это возрастет, если мы перейдем к realloc_excess , alloc_zeroed_excess , grow_in_place_excess , ...).

Если бы мы поступили наоборот, то есть реализовали API без лишнего в качестве тонкости поверх полного с избытком, тогда реализации alloc_excess и dealloc достаточно для поддержки оба типа распределителей.

Пользователи, которым все равно, или которые не могут вернуть или запросить избыток, могут просто вернуть размер ввода или макет (что является небольшим неудобством), но пользователям, которые могут справиться и хотят справиться с избытком, не нужно реализовывать больше никаких методов.


@sfackler

Я получил это число из-за того, что всего около 4 вещей используют nallocx по сравнению с набором вещей, которые используют malloc.

Учитывая эти факты об использовании _excess в экосистеме Rust:

  • Всего 0 вещей используют _excess в экосистеме ржавчины
  • Всего 0 вещей используют _excess в библиотеке rust std
  • даже Vec и String могут правильно использовать _excess API в библиотеке rust std
  • _excess API нестабилен, не синхронизирован с API без лишнего, глючил до недавнего времени (даже не возвращал excess ), ...

    и учитывая эти факты об использовании _excess на других языках:

  • API jemalloc изначально не поддерживается программами C или C ++ из-за обратной совместимости

  • Программы C и C ++, которые хотят использовать избыточный API jemalloc, должны изо всех сил стараться использовать его:

    • отказ от системного распределителя памяти в jemalloc (или tcmalloc)

    • повторно реализовать библиотеку std своего языка (в случае C ++ реализовать несовместимую библиотеку std)

    • напишите весь свой стек поверх этой несовместимой библиотеки std

  • некоторые сообщества (firefox использует его, facebook переопределяет коллекции в стандартной библиотеке C ++, чтобы иметь возможность использовать его, ...) по-прежнему стараются изо всех сил использовать его.

Эти два аргумента кажутся мне правдоподобными:

  • API excess в std не может использоваться, поэтому библиотека std не может его использовать, следовательно, никто не может, поэтому он ни разу не используется в экосистеме Rust. .
  • Несмотря на то, что C и C ++ делают практически невозможным использование этого API, большие проекты с человеческими ресурсами идут на все, чтобы использовать его, поэтому по крайней мере некоторое потенциально крошечное сообщество людей очень заботится об этом.

Ваш аргумент:

  • Никто не использует _excess API, поэтому только 0,01% людей заботятся о нем.

не.

@alexcrichton Решение о переходе с -> Result<*mut u8, AllocErr> на -> *mut void может стать значительным сюрпризом для людей, которые следили за первоначальной разработкой RFC распределителя.

Я не согласен с точками вы делаете , но , тем не менее , казалось , как значительное число людей было бы готовы жить с «тяжелой weightness» в Result за увеличения вероятности пропуска нуль- проверьте возвращаемое значение.

  • Я игнорирую проблемы эффективности времени выполнения, налагаемые ABI, потому что я, как и @alexcrichton , предполагаю, что мы могли бы справиться с ними каким-либо образом с помощью уловок компилятора.

Есть ли какой-то способ добиться большей осведомленности об этом позднем изменении?

Один способ (из головы): изменить подпись сейчас, в PR самостоятельно, в главной ветке, пока Allocator все еще нестабильно. А потом посмотрите, кто жалуется на пиар (а кто празднует!).

  • Это слишком жестко? Похоже, что это по определению менее тяжеловесно, чем совмещать такое изменение со стабилизацией ...

По вопросу о том, возвращать ли *mut void или возвращать Result<*mut void, AllocErr> : Возможно, нам следует пересмотреть идею отдельных свойств распределителя «высокого уровня» и «низкого уровня», как обсуждалось во второй части RFC распределителя .

(Очевидно, если бы у меня были серьезные возражения против возвращаемого значения *mut void , я бы отправил его как проблему через fcpbot. Но на данный момент я в значительной степени доверяю мнению команды libs, возможно некоторая часть из-за усталости от этой саги о распределителе.)

@pnkfelix

Решение перейти с -> Result<*mut u8, AllocErr> на -> *mut void может стать значительным сюрпризом для людей, которые следили за первоначальной разработкой RFC распределителя.

Последнее означает, что, как уже говорилось, единственная ошибка, которую мы хотим выразить, - это OOM. Таким образом, чуть более легкое промежуточное звено, которое по-прежнему имеет преимущество защиты от случайного отказа при проверке ошибок, - это -> Option<*mut void> .

@gnzlbg

Избыточный API в std не может использоваться, поэтому библиотека std не может его использовать, поэтому никто не может, поэтому он ни разу не используется в экосистеме Rust.

Тогда иди исправь.

@pnkfelix

По вопросу о том, следует ли возвращать mut void или возвращать Result < mut void, AllocErr>: Возможно, нам следует пересмотреть идею отдельных свойств распределителя «высокого уровня» и «низкого уровня», как обсуждалось в дубле II. RFC распределителя.

В основном это были наши мысли, за исключением того, что высокоуровневый API будет в самом Alloc как alloc_one , alloc_array и т. Д. Мы даже можем позволить им сначала развиваться в экосистеме как расширение. черты характера, чтобы увидеть, к каким API сходятся люди.

@pnkfelix

Причина, по которой я сделал Layout только реализацией Clone, а не Copy, заключается в том, что я хотел оставить открытой возможность добавления дополнительной структуры к типу Layout. В частности, меня все еще интересует попытка макета отслеживать любую структуру типа, используемую для ее создания (например, 16-массив struct {x: u8, y: [char; 215]}), чтобы у распределителей возможность предоставления инструментальных процедур, которые сообщают о том, из каких типов состоит их текущее содержимое.

С этим где-то экспериментировали?

@sfackler Я уже сделал большую часть этого, и все это можно сделать с помощью дублированного API (без лишних методов + _excess ). Мне было бы хорошо, если бы у меня было два API и прямо сейчас не было полного _excess API.

Единственное, что меня все еще немного беспокоит, это то, что для реализации распределителя прямо сейчас нужно реализовать alloc + dealloc , но alloc_excess + dealloc тоже должно работать. Можно ли было бы дать alloc реализацию по умолчанию в терминах alloc_excess позже, или это невозможно или критическое изменение? На практике большинство распределителей в любом случае будут реализовывать большинство методов, так что это не имеет большого значения, это больше похоже на желание.


jemallocator реализует Alloc дважды (для Jemalloc и &Jemalloc ), где реализация Jemalloc для некоторого method просто (&*self).method(...) который перенаправляет вызов метода реализации &Jemalloc . Это означает, что необходимо вручную синхронизировать обе реализации Alloc для Jemalloc . Я не знаю, может ли быть трагичным получение другого поведения для реализаций &/_ .


Мне было очень трудно выяснить, что люди на самом деле делают с чертой Alloc на практике. Единственные обнаруженные мной проекты, которые используют его, в любом случае будут использовать его по ночам (серво, окислительно-восстановительный потенциал) и используют его только для изменения глобального распределителя. Меня очень беспокоит, что я не смог найти ни одного проекта, который использовал бы его в качестве параметра типа коллекции (может быть, мне просто не повезло, а такие есть?). Я особенно искал примеры реализации SmallVec и ArrayVec поверх типа Vec (поскольку std::Vec не имеет Alloc type параметра еще нет), а также задавался вопросом, как будет работать клонирование между этими типами ( Vec s с другим Alloc ator) (то же самое, вероятно, относится к клонированию Box es с разными Alloc s). Есть ли где-нибудь примеры того, как эти реализации будут выглядеть?

Единственные проекты, которые я обнаружил, которые его используют, в любом случае будут использовать каждую ночь (сервопривод, окислительно-восстановительный потенциал).

Как бы то ни было, Servo пытается по возможности отказаться от нестабильных функций: https://github.com/servo/servo/issues/5286

Это тоже проблема курицы и яйца. Многие проекты еще не используют Alloc потому что он все еще нестабилен.

Мне не совсем понятно, зачем нам вообще нужен полный набор _excess API. Изначально они существовали для отражения экспериментального * allocm API jemalloc, но несколько лет назад они были удалены в версии 4.0, чтобы не дублировать всю поверхность API. Кажется, мы могли бы последовать их примеру?

Можно ли было бы позже предоставить alloc реализацию по умолчанию с точки зрения alloc_excess, или это невозможно или критическое изменение?

Мы можем добавить реализацию по умолчанию alloc в терминах alloc_excess , но alloc_excess должна иметь реализацию по умолчанию в терминах alloc . Все работает нормально, если вы реализуете один или оба, но если вы не реализуете ни то, ни другое, ваш код будет компилироваться, но бесконечно рекурсивно. Это уже возникало раньше (возможно, для Rand ?), И мы могли бы каким-то образом сказать, что вам нужно реализовать хотя бы одну из этих функций, но нам все равно, какая.

Меня очень беспокоит, что я не смог найти ни одного проекта, который использовал бы его в качестве параметра типа коллекции (может быть, мне просто не повезло, а такие есть?).

Я не знаю никого, кто бы это делал.

Единственные проекты, которые я обнаружил, которые его используют, в любом случае будут использовать каждую ночь (сервопривод, окислительно-восстановительный потенциал).

Одна большая вещь, мешающая этому продвинуться, заключается в том, что коллекции stdlib еще не поддерживают параметрические распределители. Это в значительной степени исключает и большинство других ящиков, поскольку большинство внешних коллекций используют внутренние под капотом ( Box , Vec и т. Д.).

Единственные проекты, которые я обнаружил, которые его используют, в любом случае будут использовать каждую ночь (сервопривод, окислительно-восстановительный потенциал).

Одна большая вещь, мешающая этому продвинуться, заключается в том, что коллекции stdlib еще не поддерживают параметрические распределители. Это в значительной степени исключает и большинство других ящиков, поскольку большинство внешних коллекций используют внутренние под капотом (Box, Vec и т. Д.).

Это относится ко мне - у меня есть игрушечное ядро, и если бы я мог, я бы использовал Vec<T, A> , но вместо этого у меня должен быть глобальный фасад распределителя с внутренним изменением, что очень грубо.

@remexre, как параметризация ваших структур данных позволяет избежать глобального состояния с внутренней изменчивостью?

Полагаю, глобальное состояние, изменяемое внутри, по-прежнему будет, но гораздо безопаснее иметь настройку, в которой глобальный распределитель не может использоваться до тех пор, пока память не будет полностью отображена, чем иметь глобальную функцию set_allocator .


РЕДАКТИРОВАТЬ : Только что понял, что не ответил на вопрос. Прямо сейчас у меня есть что-то вроде:

struct BumpAllocator{ ... }
struct RealAllocator{ ... }
struct LinkedAllocator<A: 'static + AreaAllocator> {
    head: Mutex<Option<Cons<A>>>,
}
#[global_allocator]
static KERNEL_ALLOCATOR: LinkedAllocator<&'static mut (AreaAllocator + Send + Sync)> =
    LinkedAllocator::new();

где AreaAllocator - это свойство, которое позволяет мне (во время выполнения) проверять, что распределители случайно не «перекрываются» (с точки зрения диапазонов адресов, в которые они выделяют). BumpAllocator используется только на очень раннем этапе, для временного пространства при отображении остальной памяти для создания RealAllocator s.

В идеале я хотел бы иметь Mutex<Option<RealAllocator>> (или оболочку, которая делает его «только для вставки») единственным распределителем, и чтобы все, что было выделено на ранней стадии, параметризовалось BumpAllocator ранней загрузки.

@sfackler

Мне не совсем понятно, зачем нам вообще нужен полный набор _excess API. Изначально они существовали для отражения экспериментального * allocm API jemalloc, но несколько лет назад они были удалены в версии 4.0, чтобы не дублировать всю поверхность API. Кажется, мы могли бы последовать их примеру?

В настоящее время shrink_in_place вызывает xallocx который возвращает реальный размер выделения. Поскольку shrink_in_place_excess не существует, он отбрасывает этот размер, и пользователи должны вызывать nallocx для его повторного вычисления, стоимость которого действительно зависит от того, насколько велико выделение.

Так что по крайней мере некоторые функции распределения jemalloc, которые мы уже используем, возвращают нам полезный размер, но текущий API не позволяет нам его использовать.

@remexre

Когда я работал над своим игрушечным ядром, моей целью тоже было избегать использования глобального распределителя, чтобы гарантировать, что распределение не произойдет, пока не будет установлен распределитель. Рад слышать, что я не единственный!

Мне не нравится слово Heap для глобального распределителя по умолчанию. Почему не Default ?

Еще одно уточнение: RFC 1974 помещает все это в std::alloc но в настоящее время это в std::heap . Какое место предлагается для стабилизации?

@jethrogb "Куча" - довольно канонический термин для "того, что malloc дает вам указатели" - что вас беспокоит по поводу этого термина?

@sfackler

"эта штука malloc дает вам указатели на"

Если не считать, что это то, что System .

Ага, конечно. Global может быть, другое имя? Поскольку вы используете #[global_allocator] для его выбора.

Может быть несколько распределителей кучи (например, libc и jemalloc с префиксом). Как насчет переименования std::heap::Heap в std::heap::Default и #[global_allocator] в #[default_allocator] ?

Тот факт, что это то, что вы получите, если не укажете иное (предположительно, когда, например, Vec получает дополнительный параметр / поле типа для распределителя), более важен, чем тот факт, что у него нет "per -instances "состояние (или на самом деле экземпляры).

Последний период комментариев подошел к концу.

Что касается FCP, я думаю, что подмножество API, которое было предложено для стабилизации, имеет очень ограниченное применение. Например, он не поддерживает ящик jemallocator .

В каком смысле? jemallocator, возможно, придется пометить некоторые импликации нестабильных методов за флагом функции, но это все.

Если jemallocator в стабильном Rust не может реализовать, например Alloc::realloc , вызывая je_rallocx но нужно полагаться на alloc + copy + dealloc impl по умолчанию, то это неприемлемая замена для Стандартная библиотека alloc_jemalloc crate IMO.

Конечно, можно было бы что-нибудь скомпилировать, но это не особо полезная вещь.

Почему? В C ++ вообще нет концепции перераспределения в API распределителя, и это, похоже, не нанесло вред языку. Это явно не идеально, но я не понимаю, почему это недопустимо.

Коллекции C ++ обычно не используют realloc, потому что конструкторы перемещения C ++ могут запускать произвольный код, а не потому, что realloc бесполезен.

И сравнение проводится не с C ++, а с текущей стандартной библиотекой Rust со встроенной поддержкой jemalloc. Переход на стандартный распределитель и выход из него только с этим подмножеством Alloc API будет регрессией.

И realloc является примером. jemallocator в настоящее время также реализует alloc_zeroed , alloc_excess , usable_size , grow_in_place и т. д.

alloc_zeroed предлагается стабилизировать. Насколько я могу судить (посмотрите дальше), существует буквально ноль случаев использования alloc_excess . Не могли бы вы показать код, который будет регрессировать, если он вернется к реализации по умолчанию.

В более общем плане, однако, я не понимаю, почему это аргумент против стабилизации части этих API. Если вы не хотите использовать jemallocator, вы можете больше не использовать его.

Можно ли сделать Layout::array<T>() const fn?

Может паниковать, так что не сейчас.

Может паниковать, так что не сейчас.

Понятно ... Я бы согласился на const fn Layout::array_elem<T>() который был бы эквивалентом Layout::<T>::repeat(1).0 без паники.

@mzabaluev Я думаю, что то, что вы описываете, эквивалентно Layout::new<T>() . В настоящее время он может вызвать панику, но это только потому, что он реализован с использованием Layout::from_size_align а затем .unwrap() , и я ожидаю, что это можно сделать по-другому.

@joshlf Я думаю, что эта структура имеет размер 5, а как элементы массива они размещаются через каждые 8 ​​байтов из-за выравнивания:

struct Foo {
    bar: u32,
    baz: u8
}

Я не уверен, что массив Foo будет включать заполнение последнего элемента для расчета его размера, но это мое сильное ожидание.

В Rust размер объекта всегда кратен его выравниванию, так что адрес n th элемента массива всегда равен array_base_pointer + n * size_of<T>() . Таким образом, размер объекта в массиве всегда совпадает с размером самого объекта. Смотрите страницу Rustonomicon на сайте repr (Rust) для получения более подробной информации.

Хорошо, оказывается, что структура дополняется своим выравниванием, но, AFAIK, это не стабильная гарантия, за исключением #[repr(C)] .
В любом случае, создание Layout::new как const fn также приветствуется.

Это задокументированное (и поэтому гарантированное) поведение стабильной функции:

https://doc.rust-lang.org/std/mem/fn.size_of.html

Возвращает размер типа в байтах.

Более конкретно, это смещение в байтах между последовательными элементами в массиве с этим типом элемента, включая выравнивание. Таким образом, для любого типа T и длины n [T; n] имеет размер n * size_of::<T>() .

Благодарю. Я только что понял, что любая const fn, которая умножает результат на Layout::new , в свою очередь вызывает панику (если только это не делается с помощью saturating_mul или чего-то подобного), поэтому я вернулся к исходной точке. Продолжая вопрос о панике в проблеме отслеживания const fn.

Макрос panic!() в настоящее время не поддерживается в константных выражениях, но паники из проверенной арифметики генерируются компилятором, и это ограничение не распространяется:

error[E0080]: constant evaluation error
 --> a.rs:1:16
  |
1 | const A: i32 = i32::max_value() * 2;
  |                ^^^^^^^^^^^^^^^^^^^^ attempt to multiply with overflow

error: aborting due to previous error

Это связано с Alloc::realloc но не со стабилизацией минимального интерфейса ( realloc не является его частью):

В настоящее время, поскольку Vec::reserve/double call RawVec::reserve/double который вызывает Alloc::realloc , по умолчанию Alloc::realloc копирует мертвые векторные элементы (в диапазоне [len(), capacity()) ). . В абсурдном случае огромного пустого вектора, который хочет вставить capacity() + 1 элементов и, таким образом, перераспределить, стоимость касания всей этой памяти не является незначительной.

Теоретически, если реализация по умолчанию Alloc::realloc также будет принимать диапазон «bytes_used», она может просто скопировать соответствующую часть при перераспределении. На практике, по крайней мере, jemalloc переопределяет Alloc::realloc default impl вызовом rallocx . Будет ли выполнение alloc / dealloc dance, копирование только соответствующей памяти быстрее или медленнее, чем вызов rallocx , вероятно, будет зависеть от многих вещей (управляет ли rallocx развернуть блок на месте? сколько ненужной памяти скопирует rallocx ? и т. д.).

https://github.com/QuiltOS/rust/tree/allocator-error Я начал демонстрировать, как, на мой взгляд, связанный тип ошибки решает наши коллекции и проблемы обработки ошибок, выполняя само обобщение. В частности, обратите внимание, как в модулях я меняю то, что я

  • Всегда повторно используйте реализацию Result<T, A::Err> для реализации T
  • Никогда не unwrap или что-либо еще частичное
  • Нет oom(e) за пределами AbortAdapter .

Это означает, что вносимые мной изменения вполне безопасны и совершенно бессмысленны! Работа как с возвратом ошибки, так и с прерыванием ошибки не должна требовать дополнительных усилий для поддержания мысленных инвариантов - всю работу выполняет средство проверки типов.

Я помню --- я думаю в RFC @Gankro ? или ветка pre-rfc --- люди из Gecko / Servo, которые говорят, что было бы хорошо не допускать, чтобы ошибки коллекций были частью их типа. Что ж, я могу добавить #[repr(transparent)] в AbortAdapter чтобы коллекции можно было безопасно преобразовывать между Foo<T, A> и Foo<T, AbortAdapter<A>> (в безопасных оболочках), позволяя свободно переключаться вперед и назад, не дублируя каждый метод. [Для обратной совместимости коллекции стандартной библиотеки в любом случае необходимо будет дублировать, но пользовательские методы не обязательно должны быть, поскольку с Result<T, !> в наши дни довольно легко работать.]

К сожалению, код не может полностью проверить тип, потому что изменение параметров типа элемента lang (поле) сбивает компилятор с толку (сюрприз!). Коммит блока, вызывающий ICE, является последним - все до того, как он будет хорош. @eddyb исправил rustc в # 47043!

edit @joshlf Мне сообщили о вашем https://github.com/rust-lang/rust/pull/45272 , и я включил его сюда. Благодаря!

Постоянная память (например, http://pmem.io ) - следующая важная вещь, и Rust должен хорошо работать с ней.

Недавно я работал над оболочкой Rust для распределителя постоянной памяти (в частности, libpmemcto). Какие бы решения ни были приняты относительно стабилизации этого API, он должен: -

  • Иметь возможность поддерживать эффективную оболочку вокруг распределителя постоянной памяти, такого как libpmemcto;
  • Уметь указывать (параметризовать) типы коллекций с помощью распределителя (на данный момент нужно дублировать Box, Rc, Arc и т. Д.)
  • Уметь клонировать данные между распределителями
  • Уметь поддерживать структуры, хранящиеся в постоянной памяти, с полями, которые повторно инициализируются при создании экземпляра пула постоянной памяти, т. Е. Некоторые структуры постоянной памяти должны иметь поля, которые только временно хранятся в куче. Мои текущие варианты использования - это ссылка на пул постоянной памяти, используемый для выделения, и временные данные, используемые для блокировок.

В стороне, разработка pmem.io (Intel PMDK) интенсивно использует модифицированный распределитель jemalloc, так что использование jemalloc в качестве примера потребителя API будет разумным.

Можно ли уменьшить объем этого, чтобы сначала охватить только GlobalAllocator s, пока мы не приобретем больше опыта в использовании Alloc ators в коллекциях?

IIUC, это уже будет обслуживать потребности servo и позволит нам параллельно экспериментировать с параметризацией контейнеров. В будущем мы можем либо переместить коллекции, чтобы использовать вместо них GlobalAllocator либо просто добавить общий вид Alloc для GlobalAllocator чтобы их можно было использовать для всех коллекций.

Мысли?

@gnzlbg Чтобы #[global_allocator] был полезен (помимо выбора heap::System ), трейт Alloc должен быть стабильным, чтобы его можно было реализовать с помощью ящиков, таких как https: / /crates.io/crates/jemallocator. На данный момент нет типа или признака с именем GlobalAllocator Вы предлагаете новый API?

на данный момент нет типа или свойства с именем GlobalAllocator, вы предлагаете какой-нибудь новый API?

Я предложил переименовать "минимальный" API, который @alexcrichton предложил стабилизировать здесь, с Alloc на GlobalAllocator чтобы представлять только глобальные распределители, и оставил дверь открытой для параметризации коллекций другим признак распределителя в будущем (что не означает, что мы не можем параметризовать их при помощи признака GlobalAllocator ).

IIUC servo настоящее время требуется только для переключения глобального распределителя (в отличие от возможности параметризации некоторых коллекций распределителем). Так что, возможно, вместо того, чтобы пытаться стабилизировать решение, которое должно быть проверено на будущее для обоих вариантов использования, мы можем решить только проблему глобального распределителя сейчас и выяснить, как параметризовать коллекции распределителями позже.

Не знаю, имеет ли это смысл.

Сервопривод IIUC в настоящее время должен только иметь возможность переключать глобальный распределитель (в отличие от возможности параметризации некоторых коллекций распределителем).

Это правильно, но:

  • Если типаж и его метод стабильны и могут быть реализованы, то их также можно вызывать напрямую, минуя std::heap::Heap . Таким образом, это не только глобальный распределитель признаков, это особенность распределителей (даже если мы в конечном итоге создадим другой для коллекций, общих над распределителями), а GlobalAllocator - не очень хорошее имя.
  • В ящике jemallocator в настоящее время реализованы alloc_excess , realloc , realloc_excess , usable_size , grow_in_place и shrink_in_place которые не являются часть предлагаемого минимального API. Они могут быть более эффективными, чем impl по умолчанию, поэтому их удаление приведет к снижению производительности.

Оба пункта имеют смысл. Я просто подумал, что единственный способ значительно ускорить стабилизацию этой функции - это исключить зависимость от нее, которая также является хорошей чертой для параметризации коллекций над ней.

[Было бы неплохо, если бы Servo был похож на (стабильный | официальный ящик Mozilla), и Cargo мог бы обеспечить это, чтобы немного ослабить здесь давление.]

Сервопривод @ Ericson2314 - не единственный проект, который хочет использовать эти API.

@ Ericson2314 Я не понимаю, что это значит, не могли бы вы перефразировать?

Для контекста: Servo в настоящее время использует ряд нестабильных функций (включая #[global_allocator] ), но мы пытаемся постепенно отойти от этого (либо путем обновления до компилятора, который стабилизировал некоторые функции, либо путем поиска стабильных альтернатив. ) Это отслеживается на https://github.com/servo/servo/issues/5286. Так что стабилизировать #[global_allocator] было бы неплохо, но это не блокирует работу сервопривода.

Firefox полагается на тот факт, что Rust std по умолчанию использует системный распределитель при компиляции cdylib , и этот mozjemalloc, который в конечном итоге связан с тем же двоичным файлом, определяет такие символы, как malloc и free эта «тень» (я не знаю правильной терминологии компоновщика) из libc. Таким образом, выделения из кода Rust в Firefox заканчиваются использованием mozjemalloc. (Это в Unix, я не знаю, как это работает в Windows.) Это работает, но мне это кажется хрупким. Firefox использует стабильный Rust, и я бы хотел, чтобы он использовал #[global_allocator] для явного выбора mozjemalloc, чтобы сделать всю установку более надежной.

@SimonSapin Чем больше я играю с распределителями и коллекциями, тем больше я склоняюсь к мысли, что мы пока не хотим параметризовать коллекции с помощью Alloc , потому что в зависимости от распределителя коллекция может предлагать другой API, меняется сложность некоторых операций, некоторые детали коллекции фактически зависят от распределителя и т. д.

Поэтому я хотел бы предложить способ, которым мы можем добиться здесь прогресса.

Шаг 1. Распределитель кучи

Мы могли бы сначала ограничиться тем, чтобы попытаться позволить пользователям выбирать распределитель для кучи (или распределитель системы / платформы / глобального / свободного хранилища, или как вы предпочитаете его называть) в стабильном Rust.

Единственное, что мы изначально параметризуем, это Box , которому нужно только выделить ( new ) и освободить ( drop ) память.

Эта характеристика распределителя может изначально иметь API, предложенный @alexcrichton (или несколько расширенный), и эта особенность распределителя может по ночам все еще иметь слегка расширенный API для поддержки коллекций std:: .

Когда мы там окажемся, пользователи, которые хотят перейти на стабильную версию, смогут это сделать, но могут получить снижение производительности из-за нестабильного API.

Шаг 2. Распределитель кучи без снижения производительности

На этом этапе мы можем повторно оценить пользователей, которые не могут перейти на стабильную версию из-за падения производительности, и решить, как расширить этот API и стабилизировать его.

Шаги с 3 по N: поддержка пользовательских распределителей в коллекциях std .

Во-первых, это сложно, поэтому этого может никогда не случиться, и я думаю, что этого никогда не случится - неплохо.

Когда я хочу параметризовать коллекцию с помощью настраиваемого распределителя, у меня либо проблема с производительностью, либо с удобством использования.

Если у меня есть проблема с удобством использования, мне обычно нужен другой API коллекции, который использует функции моего настраиваемого распределителя, как, например, мой ящик SliceDeque . Здесь мне не поможет параметризация коллекции пользовательским распределителем.

Если у меня возникнут проблемы с производительностью, пользовательскому распределителю все равно будет очень сложно мне помочь. Я собираюсь рассматривать Vec в следующих разделах, потому что это коллекция, которую я перерабатываю чаще всего.

Уменьшение количества вызовов системного распределителя (оптимизация малых векторов)

Если я хочу выделить некоторые элементы внутри объекта Vec чтобы уменьшить количество вызовов системного распределителя, сегодня я просто использую SmallVec<[T; M]> . Однако SmallVec не является Vec :

  • перемещение Vec составляет O (1) по количеству элементов, но перемещение SmallVec<[T; M]> составляет O (N) для N <M и O (1) впоследствии,

  • указатели на элементы Vec становятся недействительными при перемещении, если len() <= M но не иначе, то есть, если len() <= M операции, такие как into_iter необходимо переместить элементы в сам объект-итератор, а не просто указатели.

Можем ли мы сделать Vec универсальным вместо распределителя для поддержки этого? Возможно все, но я думаю, что самые важные затраты:

  • это делает реализацию Vec более сложной, что может повлиять на пользователей, не использующих эту функцию.
  • документация Vec станет более сложной, потому что поведение некоторых операций будет зависеть от распределителя.

Я считаю, что этими расходами пренебречь нельзя.

Используйте шаблоны распределения

Фактор роста Vec адаптирован к конкретному распределителю. В std мы можем адаптировать его к обычным jemalloc / malloc / ... но если вы используете настраиваемый распределитель, есть вероятность, что выбранный нами фактор роста по умолчанию не будет лучшим вариантом для вашего варианта использования. Должен ли каждый распределитель иметь возможность указывать фактор роста для векторных схем распределения? Не знаю, но чутье подсказывает мне: наверное, нет.

Используйте дополнительные возможности вашего системного распределителя памяти

Например, распределитель с чрезмерным выделением ресурсов доступен для большинства целей уровня 1 и уровня 2. В системах, подобных Linux и Macos, распределитель кучи по умолчанию избыточно выделяет, в то время как Windows API предоставляет VirtualAlloc который можно использовать для резервирования памяти (например, в Vec::reserve/with_capacity ) и фиксации памяти в push .

В настоящее время черта Alloc не раскрывает способ реализации такого распределителя в Windows, потому что он не разделяет концепции фиксации и резервирования памяти (в Linux можно взломать распределитель без избыточной фиксации). просто касаясь каждой страницы один раз). Он также не предоставляет распределителю возможность указать, совершает ли он чрезмерную фиксацию по умолчанию для alloc .

То есть нам нужно будет расширить API Alloc для поддержки этого для Vec , и это будет IMO для небольшого выигрыша. Потому что, когда у вас есть такой распределитель, семантика Vec снова меняется:

  • Vec больше не нуждается в увеличении, поэтому такие операции, как reserve имеют особого смысла.
  • push - это O(1) вместо амортизированного O(1) .
  • итераторы для живых объектов никогда не становятся недействительными, пока объект жив, что позволяет выполнять некоторые оптимизации

Воспользуйтесь дополнительными функциями вашего системного распределителя памяти

Некоторые системные аллокаторы, такие как cudaMalloc / cudaMemcpy / ..., различают закрепленную и незакрепленную память, позволяют выделять память в непересекающихся адресных пространствах (поэтому нам понадобится связанный тип указателя в Alloc trait), ...

Но их использование в коллекциях, таких как Vec, снова меняет семантику некоторых операций тонкими способами, например, вызывает ли индексирование вектора неожиданное поведение undefined или нет, в зависимости от того, делаете ли вы это из ядра графического процессора или с хоста.

Подведение итогов

Я думаю, что попытка придумать API Alloc который можно использовать для параметризации всех коллекций (или даже только Vec ), сложно, возможно, слишком сложно.

Может быть, после того, как мы получим правильные распределители global / system / platform / heap / free-store и Box , мы сможем переосмыслить коллекции. Может быть, мы сможем повторно использовать Alloc , может быть, нам понадобится VecAlloc, VecDequeAlloc , HashMapAlloc`, ... или, может быть, мы просто скажем: «Знаете что, если вам это действительно нужно , просто скопируйте и вставьте стандартную коллекцию в ящик и отлейте ее в свой распределитель ». Возможно, лучшим решением будет просто упростить эту задачу, поместив коллекции std в отдельный ящик (или ящики) в детской и используя только стабильные функции, возможно, реализованные в виде набора строительных блоков.

В любом случае, я думаю, что пытаться решить все эти проблемы сразу и придумать черту Alloc которая подходит для всего, слишком сложно. Мы на шаге 0. Я думаю, что лучший способ быстро перейти к шагу 1 и шагу 2 - это исключить коллекции из поля зрения, пока мы не доберемся до них.

Когда мы там окажемся, пользователи, которые хотят перейти на стабильную версию, смогут это сделать, но могут получить снижение производительности из-за нестабильного API.

Выбор настраиваемого распределителя обычно связан с повышением производительности, поэтому я не знаю, кому послужит эта первоначальная стабилизация.

Выбор настраиваемого распределителя обычно связан с повышением производительности, поэтому я не знаю, кому послужит эта первоначальная стабилизация.

Все? По крайней мере, прямо сейчас. Большинство методов, на которые вы жалуетесь, отсутствуют в первоначальном предложении по стабилизации (например, alloc_excess ), AFAIK еще не используются ничем в стандартной библиотеке. Или это недавно изменилось?

Vec (и другие пользователи RawVec ) используют realloc в push

@SimonSapin

Крейт jemallocator в настоящее время реализует alloc_excess, realloc, realloc_excess, usable_size, grow_in_place и shrink_in_place

Из этих методов используются AFAIK realloc , grow_in_place и shrink_in_place , но grow_in_place - всего лишь наивная оболочка над shrink_in_place для jemalloc в по крайней мере, если бы мы реализовали нестабильный имплицит по умолчанию grow_in_place в терминах shrink_in_place в трейте Alloc , это сократило бы его до двух методов: realloc и shrink_in_place .

Выбор настраиваемого распределителя обычно связан с повышением производительности,

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

IIUC основным вариантом использования сервопривода было использование Firefox jemalloc вместо второго jemalloc, верно?

Даже если мы добавим realloc и shrink_in_place к трейту Alloc при начальной стабилизации, это только задержит жалобы на производительность.

Например, в тот момент, когда мы добавляем какой-либо нестабильный API к трейту Alloc который в конечном итоге используется коллекциями std , вы не сможете получить такую ​​же производительность в стабильной версии, как если бы иметь возможность жить по ночам. То есть, если мы добавим realloc_excess и shrink_in_place_excess к типу alloc и заставим Vec / String / ... использовать их, мы стабилизируем realloc и shrink_in_place капли не помогли бы вам.

IIUC основным вариантом использования сервопривода было использование Firefox jemalloc вместо второго jemalloc, верно?

Хотя у них есть общий код, Firefox и Servo - это два отдельных проекта / приложения.

Firefox использует mozjemalloc, который является форком старой версии jemalloc с множеством добавленных функций. Я думаю, что некоторый код unsafe FFI полагается на правильность и надежность на mozjemalloc, используемом Rust std.

Servo использует jemalloc, который на данный момент используется Rust по умолчанию для исполняемых файлов, но есть планы изменить это значение по умолчанию на системный распределитель. Servo также имеет некоторый код сообщения об использовании памяти unsafe который действительно полагается на надежность использования jemalloc. (Передача Vec::as_ptr() в je_malloc_usable_size .)

Servo использует jemalloc, который на данный момент используется Rust по умолчанию для исполняемых файлов, но есть планы изменить это значение по умолчанию на системный распределитель.

Было бы хорошо знать, предоставляют ли системные распределители в системах, которые являются сервоприводами, оптимизированные realloc и shrink_to_fit API, такие как jemalloc? realloccalloc ) очень распространены, но shrink_to_fit ( xallocx ) AFAIK специфичен для jemalloc . Возможно, лучшим решением было бы стабилизировать realloc и alloc_zeroed ( calloc ) в начальной реализации и оставить shrink_to_fit на потом. Это должно позволить серво работать с системными распределителями на большинстве платформ без проблем с производительностью.

Servo также имеет некоторый код сообщения о небезопасном использовании памяти, который действительно полагается на то, что jemalloc действительно используется. (Передача Vec :: as_ptr () в je_malloc_usable_size.)

Как вы знаете, в ящике jemallocator есть API для этого. Я ожидаю, что ящики, похожие на ящик jemallocator появятся у других распределителей, предлагающих аналогичные API, по мере того, как история глобального распределителя начнет стабилизироваться. Я вообще не думал о том, принадлежат ли эти API к трейту Alloc .

Я не думаю, что malloc_usable_size должно быть в трейте Alloc . Использование #[global_allocator] для уверенности в том, какой распределитель используется Vec<T> и отдельное использование функции из ящика jemallocator - нормально.

@SimonSapin, как только Alloc станет стабильным, у нас, вероятно, будет ящик типа jemallocator для Linux malloc и Windows. Эти ящики могут иметь дополнительную функцию для реализации частей нестабильного Alloc API (например, usable_size поверх malloc_usable_size ) и некоторых других вещей, которые не являются частью Alloc API, как отчеты о памяти поверх mallinfo . Как только появятся годные к употреблению ящики для систем, на которые нацелены сервоприводы, будет легче узнать, какие части признака Alloc отдавать приоритет стабилизации, и мы, вероятно, обнаружим более новые API, с которыми нужно хотя бы поэкспериментировать для некоторых распределители.

@gnzlbg Я немного скептически отношусь к тому, что написано на https://github.com/rust-lang/rust/issues/32838#issuecomment -358267292. Опуская все эти системные вещи, нетрудно обобщить коллекции для выделения - я сделал это. Попытка включить это кажется отдельной проблемой.

@SimonSapin У firefox нет нестабильной политики Rust? Думаю, я запутался: Firefox и Servo этого хотят, но если это так, то его вариант использования Firefox будет усиливать стабилизацию.

@sfackler Посмотрите, что ^. Я пытался провести различие между проектами, которые нуждаются в стабильности или хотят ее, но Servo находится по другую сторону этого разрыва.

У меня есть проект, который этого хочет и требует, чтобы он был стабильным. В Servo или Firefox как в потребителях Rust нет ничего особенного.

@ Ericson2314 Правильно, Firefox использует стабильную https://wiki.mozilla.org/Rust_Update_Policy_for_Firefox. Как я уже объяснял, сегодня есть рабочее решение, так что это не настоящий блокировщик чего-либо. Было бы лучше / надежнее использовать #[global_allocator] , вот и все.

Сервопривод действительно использует некоторые нестабильные функции, но, как уже упоминалось, мы пытаемся это изменить.

FWIW, параметрические распределители очень полезны для реализации распределителей. Бухгалтерский учет, менее чувствительный к производительности, становится намного проще, если вы можете использовать различные структуры данных внутри себя и параметризовать их с помощью более простого распределителя (например, bsalloc ). В настоящее время единственный способ сделать это в среде std - это двухфазная компиляция, в которой первая фаза используется для установки вашего более простого распределителя в качестве глобального распределителя, а вторая фаза используется для компиляции более крупного и более сложного распределителя. . В no-std это вообще невозможно сделать.

@ Ericson2314

Опуская все эти системные вещи, нетрудно обобщить коллекции для выделения - я сделал это. Попытка включить это кажется отдельной проблемой.

Есть ли у вас реализация ArrayVec или SmallVec поверх Vec + пользовательских распределителей, на которые я мог бы взглянуть? Это был первый пункт, о котором я упомянул, и он вообще не зависит от системы. Возможно, это были бы самые простые мыслимые два распределителя: один - это просто необработанный массив в качестве хранилища, а другой может быть построен поверх первого, добавив резерв к куче, когда массив исчерпает емкость. Основное отличие состоит в том, что эти распределители не являются «глобальными», но каждый из Vec s имеет свой собственный распределитель, независимый от всех остальных, и эти распределители сохраняют состояние.

Кроме того, я не призываю никогда этого не делать. Я просто утверждаю, что это очень сложно: C ++ пытается в течение 30 лет с частичным успехом: распределители GPU и GC работают из-за общих типов указателей, но реализуют ArrayVec и SmallVec поверх Vec не приводит к абстракции с нулевой стоимостью в области C ++ (в P0843r1 подробно обсуждаются некоторые проблемы для ArrayVec ).

Так что я бы просто предпочел, чтобы мы занялись этим после того, как стабилизируем части, которые действительно доставляют что-то полезное, при условии, что они не заставят преследовать настраиваемые распределители коллекций в будущем.


Я немного поговорил с @SimonSapin по IRC, и если бы мы расширили первоначальное предложение по стабилизации с помощью realloc и alloc_zeroed , тогда Rust в Firefox (который использует только стабильный Rust) смог бы использовать mozjemalloc в качестве глобального распределителя в стабильном Rust без каких-либо дополнительных хаков. Как упоминал @SimonSapin , у Firefox в настоящее время есть работоспособное решение для этого сегодня, поэтому, хотя это было бы хорошо, это не кажется очень важным приоритетом.

Тем не менее, мы могли бы начать с этого места, и как только мы там окажемся, переместим servo в стабильный #[global_allocator] без потери производительности.


@joshlf

FWIW, параметрические распределители очень полезны для реализации распределителей.

Не могли бы вы подробнее пояснить, что вы имеете в виду? Есть ли причина, по которой вы не можете параметризовать свои собственные распределители с помощью трейта Alloc ? Или ваш собственный особый признак распределителя и просто реализуйте признак Alloc в окончательных распределителях (эти два признака не обязательно должны быть равны)?

Я не понимаю, откуда взялся вариант использования «SmallVec = Vec + специальный распределитель». Я не часто упоминал об этом раньше (ни в Rust, ни в других контекстах) именно потому, что у него много серьезных проблем. Когда я думаю об «улучшении производительности с помощью специализированного распределителя», я думаю совсем не об этом.

Просматривая Layout API, мне было интересно узнать о различиях в обработке ошибок между from_size_align и align_to , где первый возвращает None в случае ошибки , а последний паникует (!).

Разве не было бы более полезным и последовательным добавить подходящим образом определенное и информативное перечисление LayoutErr и вернуть Result<Layout, LayoutErr> в обоих случаях (и, возможно, использовать его для других функций, которые в настоящее время возвращают Option тоже)?

@rkruppe

Я не понимаю, откуда взялся вариант использования «SmallVec = Vec + специальный распределитель». Я не часто упоминал об этом раньше (ни в Rust, ни в других контекстах) именно потому, что у него много серьезных проблем. Когда я думаю об «улучшении производительности с помощью специализированного распределителя», я думаю совсем не об этом.

Существует два независимых способа использования распределителей в Rust и C ++: системный распределитель, используемый всеми распределениями по умолчанию, и как аргумент типа для коллекции, параметризованной некоторой чертой распределителя, как способ создания объекта этой конкретной коллекции, который использует конкретный распределитель (который может быть системным или нет).

Дальнейшее внимание сосредоточено только на этом втором варианте использования: использование коллекции и типа распределителя для создания объекта этой коллекции, который использует конкретный распределитель.

По моему опыту работы с C ++, параметризация коллекции с помощью распределителя обслуживает два варианта использования:

  • повысить производительность объекта коллекции, заставив коллекцию использовать настраиваемый распределитель, ориентированный на конкретный шаблон распределения, и / или
  • добавить новую функцию в коллекцию, позволяющую ей делать то, чего раньше она не могла.

Добавление новых функций в коллекции

Это вариант использования распределителей, который я вижу в базах кода C ++ в 99% случаев. Тот факт, что добавление новой функции в коллекцию улучшает производительность, на мой взгляд, случайно. В частности, ни один из следующих распределителей не улучшает производительность, ориентируясь на шаблон распределения. Они делают это путем добавления функций, которые в некоторых случаях, как упоминает @ Ericson2314 , могут считаться "системными". Вот несколько примеров:

  • распределители стека для выполнения небольших оптимизаций буфера (см. статью Ховарда stack_alloc ). Они позволяют вам использовать std::vector или flat_{map,set,multimap,...} и передавая ему настраиваемый распределитель, который вы добавляете в небольшую оптимизацию буфера с ( SmallVec ) или без ( ArrayVec ) куча отступить. Это позволяет, например, поместить коллекцию с ее элементами в стек или статическую память (где в противном случае использовалась бы куча).

  • архитектуры сегментированной памяти (например, целевые объекты x86 с указателем шириной 16 бит и GPGPU). Например, C ++ 17 Parallel STL во время C ++ 14 был Технической спецификацией Parallel. Его предшественницей является библиотека Thrust от NVIDIA, которая включает в себя распределители, позволяющие классам контейнеров использовать память GPGPU (например, thrust :: device_malloc_allocator ) или закрепленную память (например, thust :: pinned_allocator ; закрепленная память обеспечивает более быструю передачу между хост-устройством в некоторые случаи).

  • распределители для решения проблем, связанных с параллелизмом, таких как ложное совместное использование (например, Intel Thread Building Blocks cache_aligned_allocator ) или требования чрезмерного выравнивания типов SIMD (например, Eigen3 aligned_allocator ).

  • Межпроцессная разделяемая память:

  • Сборка мусора: библиотека отложенного выделения памяти Херба Саттера использует определяемый пользователем тип указателя для реализации распределителей, которые собирают память для сбора мусора. Так что, например, когда вектор растет, старый фрагмент памяти сохраняется до тех пор, пока все указатели на эту память не будут уничтожены, что позволяет избежать недействительности итератора.

  • Инструментальные распределители памяти: blsma_testallocator из библиотеки программного обеспечения Bloomberg позволяет вам регистрировать схемы выделения / освобождения памяти (а также построение / уничтожение конкретных объектов C ++) для тех объектов, где вы ее используете. Вы не знаете, выделяется ли Vec после reserve ? Подключите такой распределитель, и они сообщат вам, если это произойдет. Некоторые из этих распределителей позволяют вам давать им имена, чтобы вы могли использовать их на нескольких объектах и ​​получать журналы, в которых указывается, какой объект что делает.

Это типы распределителей, которые я чаще всего вижу в C ++. Как я упоминал ранее, тот факт, что они улучшают производительность в некоторых случаях, на мой взгляд, является случайным. Важная часть состоит в том, что ни один из них не пытается нацелиться на конкретный шаблон распределения.

Шаблоны выделения адресов для повышения производительности.

AFAIK не существует широко используемых распределителей C ++, которые бы это делали, и я объясню, почему я думаю, что это через секунду. Следующие библиотеки предназначены для этого варианта использования:

Однако эти библиотеки на самом деле не предоставляют единого распределителя для конкретного случая использования. Вместо этого они предоставляют строительные блоки распределителя, которые можно использовать для создания настраиваемых распределителей, нацеленных на конкретный шаблон распределения в определенной части приложения.

Общий совет, который я помню из дней, проведенных на C ++, заключался в том, чтобы просто «не использовать их» (это самое последнее средство), потому что:

  • согласовать производительность системного распределителя очень сложно, превзойти его очень-очень сложно,
  • шансы на то, что шаблон распределения памяти другого приложения будет соответствовать вашему, невелики, поэтому вам действительно нужно знать свой шаблон распределения и знать, какие строительные блоки распределителя вам нужны, чтобы соответствовать ему
  • они не переносимы, потому что разные поставщики имеют разные реализации стандартной библиотеки C ++, которые действительно используют разные шаблоны распределения; поставщики обычно ориентируют свою реализацию на свои системные распределители. То есть решение, адаптированное для одного поставщика, может ужасно (хуже, чем системный распределитель) работать у другого.
  • есть много альтернатив, которые можно исчерпать, прежде чем пытаться их использовать: использование другой коллекции, резервирование памяти, ... Большинство альтернатив требуют меньших усилий и могут принести большие выигрыши.

Это не означает, что библиотеки для этого варианта использования бесполезны. Они есть, поэтому такие библиотеки, как foonathan / memory, процветают . Но, по крайней мере, по моему опыту, они намного реже используются в дикой природе, чем распределители, которые «добавляют дополнительные функции», потому что для достижения успеха вы должны превзойти системный распределитель, что требует больше времени, чем большинство пользователей готовы инвестировать (Stackoverflow полон вопросов типа «Я использовал Boost.Pool, и у меня ухудшилась производительность, что делать? Не использовать Boost.Pool.»).

Подведение итогов

IMO Я думаю, что это здорово, что модель распределителя C ++, хотя и далека от совершенства, поддерживает оба варианта использования, и я думаю, что если коллекции std Rust должны параметризоваться распределителями, они также должны поддерживать оба варианта использования, потому что, по крайней мере, в Распределители C ++ для обоих случаев оказались полезными.

Я просто думаю, что эта проблема немного ортогональна возможности настроить глобальный / системный / платформенный / default / heap / free-store распределитель конкретного приложения, и что попытка решить обе проблемы одновременно может задержать решение для одного из них без надобности.

То, что некоторые пользователи хотят делать с коллекцией, параметризованной распределителем, может сильно отличаться от того, что хотят делать другие пользователи. Если @rkruppe начинается с «сопоставления шаблонов распределения», а я начинаю с «предотвращения ложного совместного использования» или «использования небольшой оптимизации буфера с резервным копированием кучи», то сначала будет сложно понять потребности друг друга, а во-вторых, прийти в решении, которое работает для обоих.

@gnzlbg Спасибо за подробное описание. По большей части это не касается моего первоначального вопроса, и я не согласен с некоторыми из них, но хорошо, если он сформулирован так, чтобы мы не разговаривали друг с другом.

Мой вопрос касался именно этого приложения:

распределители стека для небольшой оптимизации буфера (см. статью Ховарда Хиннанта по stack_alloc). Они позволяют использовать std :: vector или flat_ {map, set, multimap, ...} и, передав ему настраиваемый распределитель, который вы добавляете в небольшую оптимизацию буфера с (SmallVec) или без (ArrayVec) отката кучи. Это позволяет, например, поместить коллекцию с ее элементами в стек или статическую память (где в противном случае использовалась бы куча).

Читая о stack_alloc, я теперь понимаю, как это может работать. Это не то, что люди обычно подразумевают под SmallVec (где буфер хранится встроенным в коллекцию), поэтому я пропустил эту опцию, но он побуждает решать проблему необходимости обновлять указатели при перемещении коллекции (а также удешевляет эти перемещения. ). Также обратите внимание, что short_alloc позволяет нескольким коллекциям совместно использовать один arena , что делает его еще более непохожим на типичные типы SmallVec. Это больше похоже на линейный распределитель / распределитель указателей с изгибом с плавным переходом к выделению кучи при исчерпании выделенного пространства.

Я не согласен с тем, что такой распределитель и cache_aligned_allocator принципиально добавляют новые функции. Они используются по- разному, и в зависимости от вашего определения «шаблона распределения» они могут не оптимизироваться для конкретного шаблона распределения. Однако они определенно оптимизированы для конкретных случаев использования и не имеют существенных поведенческих отличий от распределителей кучи общего назначения.

Однако я согласен с тем, что варианты использования, такие как отложенное выделение памяти Саттера, которые существенно меняют даже то, что означает «указатель», представляют собой отдельное приложение, которому может потребоваться отдельный дизайн, если мы хотим его поддерживать.

Читая о stack_alloc, я теперь понимаю, как это может работать. Это не то, что люди обычно подразумевают под SmallVec (где буфер хранится встроенным в коллекцию), поэтому я пропустил эту опцию, но он побуждает решать проблему необходимости обновлять указатели при перемещении коллекции (а также удешевляет эти перемещения. ).

Я упомянул stack_alloc потому что это единственный такой распределитель с «бумагой», но он был выпущен в 2009 году и предшествует C ++ 11 (C ++ 03 не поддерживает распределители с отслеживанием состояния в коллекциях).

Вкратце, как это работает в C ++ 11 (который поддерживает распределители с отслеживанием состояния):

  • станд :: вектор хранит Allocator объект внутри него так же , как Руст RawVec делает .
  • интерфейс Allocator имеет непонятное свойство под названием Allocator :: ropate_on_container_move_assignment (с этого момента POCMA), которое могут настраивать определяемые пользователем распределители; по умолчанию это свойство равно true . Если это свойство равно false , при назначении перемещения распределитель не может быть распространен, поэтому по стандарту требуется коллекция для перемещения каждого из ее элементов в новое хранилище вручную.

Итак, когда вектор с системным распределителем перемещается, сначала выделяется память для нового вектора в стеке, затем перемещается распределитель (который имеет нулевой размер), а затем перемещаются 3 указателя, которые все еще действительны. Такие ходы составляют O(1) .

OTOHO, когда вектор с распределителем POCMA == true перемещается, сначала выделяется память для нового вектора в стеке и инициализируется пустым вектором, затем старая коллекция - drain ed в новый, чтобы старый был пуст, а новый полный. Это перемещает каждый элемент коллекции индивидуально, используя их операторы присваивания перемещения. Этот шаг O(N) исправляет внутренние указатели элементов. Наконец, исходная теперь пустая коллекция отбрасывается. Обратите внимание, что это похоже на клон, но не потому, что сами элементы не клонированы, а перемещены в C ++.

Имеет ли это смысл?

Основная проблема этого подхода в C ++ заключается в следующем:

  • вектор роста-политика определяется реализацией
  • API распределителя не имеет методов _excess
  • Комбинация двух вышеуказанных проблем означает, что если вы знаете, что ваш вектор может содержать не более 9 элементов, у вас не может быть распределителя стека, который может содержать 9 элементов, потому что ваш вектор может попытаться вырасти, когда он имеет 8 с коэффициентом роста 1,5, поэтому вам нужно пессимизировать и выделить место для 18 элементов.
  • сложность векторной операции меняется в зависимости от свойств распределителя (POCMA - это лишь одно из многих свойств, которыми обладает C ++ Allocator API; написание распределителей C ++ нетривиально). Это затрудняет определение API вектора, потому что иногда копирование или перемещение элементов между разными распределителями одного и того же типа связано с дополнительными затратами, которые меняют сложность операций. Это также делает чтение спецификации огромной болью. Многие онлайн-источники документации, такие как cppreference, указывают на общий случай и неясные детали того, что меняется, если одно свойство распределителя истинно или ложно, маленькими буквами, чтобы не беспокоить 99% пользователей.

Многие люди работают над улучшением API распределителя C ++, чтобы исправить эти проблемы, например, путем добавления методов _excess и обеспечения их использования в стандартных коллекциях.

Я не согласен с тем, что такой распределитель и cache_aligned_allocator принципиально добавляют новые функции.

Возможно, я имел в виду, что они позволяют использовать коллекции std в ситуациях или для типов, для которых вы не могли их использовать раньше. Например, в C ++ вы не можете поместить элементы вектора в сегмент статической памяти вашего двоичного файла без чего-то вроде распределителя стека (но вы можете написать свою собственную коллекцию, которая делает это). OTOH, стандарт C ++ не поддерживает чрезмерно выровненные типы, такие как типы SIMD, и если вы попытаетесь выделить один из кучи с помощью new вы вызовете поведение undefined (вам нужно использовать posix_memalign или подобное) . Использование объекта обычно проявляет неопределенное поведение через segfault (*). Такие вещи, как aligned_allocator позволяют размещать эти типы в кучу и даже помещать их в коллекции std, не вызывая неопределенного поведения, используя другой распределитель. Конечно, новый распределитель будет иметь другие шаблоны распределения (эти распределители в основном перераспределяют всю память, кстати ...), но люди используют их для того, чтобы иметь возможность делать то, что они не могли делать раньше.

Очевидно, что Rust - это не C ++. А у C ++ есть проблемы, которых нет в Rust (и наоборот). Распределитель, который добавляет новую функцию в C ++, может быть ненужным в Rust, который, например, не имеет проблем с типами SIMD.

(*) Пользователи Eigen3 сильно страдают от этого, потому что, чтобы избежать неопределенного поведения при использовании контейнеров C ++ и STL, вам необходимо защитить контейнеры от типов SIMD или типов, содержащих типы SIMD ( документы Eigen3 ), а также вам необходимо защитить себя от когда-либо использовать new для ваших типов, перегрузив для них оператор new ( больше документов Eigen3 ).

@gnzlbg спасибо, меня тоже смутил пример smallvec. Для этого потребовались бы неподвижные типы и своего рода alloca в Rust - два RFC на рассмотрении, а затем еще дополнительная работа - так что я не сомневаюсь, что сейчас это касается. Существующая стратегия smallvec, заключающаяся в том, чтобы всегда использовать все необходимое пространство стека, пока что подходит.

Я также согласен с @rkruppe в том, что в вашем пересмотренном списке новые возможности распределителя не обязательно должны быть известны коллекции, использующей распределитель. Иногда полный Collection<Allocator> имеет новые свойства (например, существующие полностью в закрепленной памяти), но это просто естественное следствие использования распределителя.

Единственное исключение, которое я вижу здесь, - это распределители, которые выделяют только один размер / тип (это делают NVidia, как и распределители slab). У нас мог бы быть отдельный признак ObjAlloc<T> который был бы реализован для обычных распределителей: impl<A: Alloc, T> ObjAlloc<T> for A . Тогда коллекции будут использовать границы ObjAlloc, если им просто нужно выделить несколько элементов. Но я чувствую себя несколько глупо, даже говоря об этом, поскольку это должно быть выполнено с обратной совместимостью позже.

Имеет ли это смысл?

Конечно, но это не имеет отношения к Rust, поскольку у нас нет конструкторов перемещения. Таким образом, (подвижный) распределитель, который напрямую содержит память, на которую он передает указатели, просто невозможен, точка.

Например, в C ++ вы не можете поместить элементы вектора в сегмент статической памяти вашего двоичного файла без чего-то вроде распределителя стека (но вы можете написать свою собственную коллекцию, которая делает это).

Это не изменение поведения. Есть много веских причин контролировать, где коллекции берут свою память, но все они связаны с «внешними факторами», такими как производительность, сценарии компоновщика, контроль над структурой памяти всей программы и т. Д.

Такие вещи, как align_allocator, позволяют размещать эти типы в кучу и даже помещать их в коллекции std, не вызывая неопределенного поведения, используя другой распределитель.

Вот почему я специально упомянул cache_aligned_allocator TBB, а не align_allocator Эйгена. cache_aligned_allocator, похоже, не гарантирует какого-либо конкретного выравнивания в своей документации (он просто говорит, что это «обычно» 128 байт), и даже если бы это было так, оно обычно не использовалось бы для этой цели (поскольку его выравнивание, вероятно, слишком велико для обычных Типы SIMD и слишком малы для таких вещей, как DMA с выравниванием по страницам). Как вы заявляете, его цель - избежать ложного обмена.

@gnzlbg

FWIW, параметрические распределители очень полезны для реализации распределителей.

Не могли бы вы подробнее пояснить, что вы имеете в виду? Есть ли причина, по которой вы не можете параметризовать свои собственные распределители с помощью трейта Alloc? Или ваш собственный признак распределителя и просто реализуйте признак Alloc в последних распределителях (эти два признака не обязательно должны быть равны)?

Думаю, я не понял; позвольте мне попытаться объяснить лучше. Скажем, я реализую распределитель, который собираюсь использовать:

  • Как глобальный распределитель
  • В нестандартной среде

Допустим, я хотел бы использовать Vec под капотом для реализации этого распределителя. Я не могу просто использовать Vec напрямую в том виде, в котором он существует сегодня, потому что

  • Если я глобальный распределитель, то его использование приведет к рекурсивной зависимости от меня.
  • Если я нахожусь в среде no-std, Vec нет в том виде, в каком он существует сегодня

Таким образом, мне нужно иметь возможность использовать Vec который параметризован на другом распределителе, который я использую внутри для простой внутренней бухгалтерии. Это цель bsalloc (и источник имени - он используется для начальной загрузки других распределителей).

В elfmalloc мы по-прежнему можем быть глобальным распределителем:

  • При компиляции статически компилируйте jemalloc как глобальный распределитель
  • Создание файла общих объектов, который может быть динамически загружен другими программами.

Обратите внимание, что в этом случае важно, чтобы мы не компилировали себя, используя системный распределитель в качестве глобального распределителя, потому что после загрузки мы повторно вводим рекурсивную зависимость, потому что в этот момент мы являемся системным распределителем.

Но не работает, когда:

  • Кто-то хочет использовать нас в качестве глобального распределителя памяти в Rust "официальным" способом (в отличие от создания сначала общего объектного файла)
  • Мы находимся в нестандартной среде

OTOH, стандарт C ++ не поддерживает чрезмерно выровненные типы, такие как типы SIMD, и если вы попытаетесь выделить в кучу один с помощью new, вы вызовете неопределенное поведение (вам нужно использовать posix_memalign или аналогичный).

Поскольку наша текущая черта Alloc принимает выравнивание в качестве параметра, я предполагаю, что этот класс проблем (проблема «Я не могу работать без другого выравнивания») исчезнет для нас?

@gnzlbg - подробное касается постоянной памяти *.

Этот вариант использования необходимо учитывать. В частности, это сильно влияет на то, что делать правильно:

  • Что используется более одного распределителя, и особенно, когда он используется для постоянной памяти, он никогда не будет системным распределителем ; (действительно, может быть несколько распределителей постоянной памяти)
  • Стоимость «повторной реализации» стандартных коллекций высока и приводит к несовместимости кода со сторонними библиотеками.
  • Время жизни распределителя не обязательно равно 'static .
  • Для объектов, хранящихся в постоянной памяти, требуется дополнительное состояние, которое должно быть заполнено из кучи, то есть им необходимо повторно инициализировать состояние. Это особенно верно для мьютексов и т.п. То, что раньше было одноразовым, больше не утилизируется.

У Rust есть прекрасная возможность перехватить инициативу и сделать его первоклассной платформой, которая заменит жесткие диски, твердотельные накопители и даже хранилища с подключением к PCI.

* Не удивительно, потому что до недавнего времени это было немного особенным. Сейчас он широко поддерживается в Linux, FreeBSD и Windows.

@raphaelcohn

Это действительно не место для отработки постоянной памяти. Ваша школа - не единственная школа, касающаяся интерфейса с постоянной памятью - например, может оказаться, что преобладающий подход состоит в том, чтобы просто обращаться с ней как с более быстрым диском из соображений целостности данных.

Если у вас есть вариант использования постоянной памяти таким образом, вероятно, было бы лучше сначала как-то сделать это где-нибудь в другом месте. Создайте прототип, внесите несколько более конкретных изменений в интерфейс распределителя и, в идеале, убедитесь, что эти изменения стоят того влияния, которое они будут иметь в среднем случае.

@rpjohnst

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

Текущий Intel PMDK, на котором сосредоточено много усилий для поддержки низкоуровневого пользовательского пространства, больше подходит к нему как к выделенной обычной памяти с указателями - например, к памяти, аналогичной той, что используется через mmap . В самом деле, если кто-то хочет работать с постоянной памятью в Linux, я считаю, что это в значительной степени ваш единственный порт захода на данный момент. По сути, один из самых продвинутых инструментов для его использования - если хотите, преобладающий - рассматривает его как выделенную память.

Что касается его прототипа - ну, это именно то, что я сказал, что сделал:

Недавно я работал над оболочкой Rust для распределителя постоянной памяти (в частности, libpmemcto).

(Вы можете использовать раннюю версию моего ящика по адресу https://crates.io/crates/nvml . В модуле cto_pool гораздо больше экспериментов с контролем версий).

Мой прототип построен с учетом того, что необходимо для замены механизма хранения данных в реальной крупномасштабной системе. Подобный образ мышления стоит за многими моими проектами с открытым исходным кодом. За многие годы я нашел лучшие библиотеки, а также лучшие стандарты, которые основаны на реальном использовании.

Ничего похожего на попытку приспособить реальный распределитель к текущему интерфейсу. Честно говоря, опыт использования интерфейса Alloc , затем копирования всего Vec , а затем его настройки, был болезненным. Во многих случаях предполагается, что распределители не передаются, например Vec::new() .

При этом в своем первоначальном комментарии я сделал несколько замечаний о том, что потребуется от распределителя и что потребуется от пользователя такого распределителя. Я думаю, что это очень актуально для обсуждения интерфейса распределителя.

Хорошей новостью является то, что ваши первые 3 пункта из https://github.com/rust-lang/rust/issues/32838#issuecomment -358940992 используются в других сценариях использования.

Я просто хотел добавить, что энергонезависимую память в список не добавил
потому что в списке перечислены варианты использования распределителей, параметризующих контейнеры в
мир C ++, которые «широко» используются, по крайней мере, по моему опыту (те
аллокаторы, о которых я говорил, в основном из очень популярных библиотек, используемых многими).
Хотя я знаю об усилиях Intel SDK (некоторые из их библиотек
целевой C ++) Я лично не знаю проектов, использующих их (есть ли у них
распределитель, который можно использовать с std :: vector? Я не знаю). Это не
означают, что они не используются и не важны. Мне было бы интересно узнать
об этом, но основная мысль моего поста заключалась в том, что параметризация
распределители по контейнерам очень сложны, и мы должны попытаться сделать
работать с системными распределителями, не закрывая двери для контейнеров
(но мы займемся этим позже).

21 января 2018 г., в 17:36, Джон Эриксон [email protected] написал:

Хорошая новость - это ваши первые 3 пункта из # 32838 (комментарий)
https://github.com/rust-lang/rust/issues/32838#issuecomment-358940992
используются другими вариантами использования.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-359261305 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AA3Npk95PZBZcm7tknNp_Cqrs_3T1UkEks5tM2ekgaJpZM4IDYUN
.

Я попытался прочитать большую часть того, что уже было написано, так что это может быть уже здесь, и в этом случае извините, если я это пропустил, но вот идет:

Что-то, что довольно часто встречается в играх (в C / C ++), - это использование «покадрового выделения царапин». Это означает, что существует линейный / выпуклый распределитель, который используется для выделений, которые действуют в течение определенного периода времени (в игровой фрейм), а затем «уничтожен».

В этом случае "уничтожено", что означает, что вы возвращаете распределитель в исходное положение. Никакого "разрушения" объектов не происходит, поскольку эти объекты должны быть типа POD (таким образом, деструкторы не выполняются)

Интересно, согласуется ли что-то подобное с текущим дизайном распределителя в Rust?

(править: не должно быть разрушения объектов)

@emoon

Что-то, что довольно часто встречается в играх (в C / C ++), - это использование «покадрового выделения царапин». Это означает, что существует линейный / выпуклый распределитель, который используется для выделений, которые действуют в течение определенного периода времени (в игровой фрейм), а затем «уничтожен».

В этом случае "уничтожено", что означает, что вы возвращаете распределитель в исходное положение. Происходит «уничтожение» объектов вообще, поскольку эти объекты должны быть типа POD (таким образом, деструкторы не выполняются)

Должно быть выполнимо. Сверху моей головы вам понадобится один объект для самой арены и другой объект, который является маркером для каждого кадра на арене. Затем вы можете реализовать Alloc для этого дескриптора, и, предполагая, что вы используете высокоуровневые безопасные оболочки для распределения (например, представьте, что Box становится параметрическим для Alloc ), lifetimes гарантирует, что все выделенные объекты будут отброшены до того, как будет удален покадровый дескриптор. Обратите внимание, что dealloc все равно будет вызываться для каждого объекта, но если dealloc не выполнялось, тогда вся логика удаления и освобождения может быть полностью или в основном оптимизирована.

Вы также можете использовать настраиваемый тип интеллектуального указателя, который не реализует Drop , что упростило бы многие вещи в другом месте.

Благодаря! Я допустил опечатку в исходном посте. Это значит, что нет разрушения объектов.

Каков текущий консенсус для людей, которые не являются экспертами в распределителях и не могут следить за этой цепочкой: планируем ли мы поддерживать специальные распределители для типов коллекций stdlib?

@alexreg Я не уверен, каков окончательный план, но подтверждено 0 технических трудностей при этом. OTOH у нас нет хорошего способа показать это в std потому что переменные типа по умолчанию являются подозрительными, но у меня нет проблем с тем, чтобы просто сделать это только alloc , поэтому мы может беспрепятственно добиваться прогресса на стороне библиотеки.

@ Ericson2314 Хорошо, приятно слышать. Реализованы ли переменные типа по умолчанию? Или, может быть, на этапе RFC? Как вы говорите, если они ограничены только вещами, связанными с alloc / std::heap , все должно быть в порядке.

Я действительно думаю, что AllocErr должен быть ошибкой. Это было бы более согласовано с другими модулями (например, io).

impl Error for AllocError вероятно, имеет смысл и не повредит, но я лично считаю, что свойство Error бесполезно.

Сегодня я смотрел на функцию Layout :: from_size_align, и ограничение « align не должно превышать 2 ^ 31 (т.е. 1 << 31 )» не имело для меня смысла. И git blame указал на # 30170.

Я должен сказать, что это было довольно вводящее в заблуждение сообщение о фиксации, в котором говорилось о том, что align подходит для u32, что является лишь случайностью, когда на самом деле «фиксируется» (больше обходится) неправильно ведет себя системный распределитель.

Это приводит меня к следующему замечанию: пункт «OSX / alloc_system ошибается при огромных выравниваниях» здесь не следует проверять. Хотя прямая проблема уже решена, я не думаю, что это исправление подходит в долгосрочной перспективе: неправильное поведение системного распределителя не должно препятствовать реализации распределителя, который ведет себя. И произвольное ограничение Layout :: from_size_align делает это.

@glandium. Полезно ли запрашивать выравнивание до кратных 4 гигабайт или более?

Я могу представить себе случаи, когда кто-то может захотеть, чтобы распределение 4GiB было согласовано с 4GiB, что в настоящее время невозможно, но вряд ли больше. Но я не думаю, что следует добавлять произвольные ограничения только потому, что мы сейчас не думаем о таких причинах.

Я могу представить себе случаи, когда кто-то может захотеть, чтобы распределение 4GiB было согласовано с 4GiB.

Что это за случаи?

Я могу представить себе случаи, когда кто-то может захотеть, чтобы распределение 4GiB было согласовано с 4GiB.

Что это за случаи?

Конкретно, я просто добавил поддержку произвольно больших выравниваний в mmap-alloc , чтобы поддерживать выделение больших, выровненных блоков памяти для использования в elfmalloc . Идея состоит в том, чтобы выровнять плиту памяти по размеру, чтобы, учитывая указатель на объект, выделенный из этой плиты, вам просто нужно было замаскировать младшие биты, чтобы найти содержащую плиту. В настоящее время мы не используем плиты размером 4 ГБ (для таких больших объектов мы переходим непосредственно к mmap), но нет причин, по которым мы не могли бы этого сделать, и я мог полностью представить приложение с большими требованиями к ОЗУ, которое хотело это (то есть, если он выделял объекты размером несколько ГБ достаточно часто, чтобы не принимать накладные расходы mmap).

Вот возможный вариант использования выравнивания> 4 ГиБ: выравнивание по границе большой страницы. Уже существуют платформы, поддерживающие страницы размером более 4 ГиБ. В этом документе IBM говорится, что «процессор POWER5 + поддерживает четыре размера страниц виртуальной памяти: 4 КБ, 64 КБ, 16 МБ и 16 ГБ». Даже x86-64 не за горами: «огромные страницы» обычно занимают 2 МиБ, но также поддерживает 1 ГиБ.

Все нетипизированные функции в трейте Alloc имеют дело с *mut u8 . Это означает, что они могут принимать или возвращать нулевые указатели, и весь ад вырвется наружу. Должны ли они вместо этого использовать NonNull ?

Есть много указателей, которые они могли бы вернуть, от которых весь ад
вырваться.
В воскресенье, 4 марта 2018 г., в 3:56 Майк Хомми [email protected] написал:

Все нетипизированные функции в трейте Alloc имеют дело с * mut u8.
Это означает, что они могут принимать или возвращать нулевые указатели, и все, черт возьми,
вырваться. Должны ли они вместо этого использовать NonNull?

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370223269 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABY2UR2dRxDtdACeRUh_djM-DExRuLxiks5ta9aFgaJpZM4IDYUN
.

Более веская причина использовать NonNull состоит в том, что это позволит Result s, возвращаемым в настоящее время из методов Alloc (или Options , если мы переключимся на это в будущее) быть меньше.

Более веская причина использования NonNull заключается в том, что это позволит уменьшить результаты, возвращаемые в настоящее время из методов Alloc (или параметров, если мы переключимся на это в будущем).

Не думаю, что у AllocErr есть два варианта.

Есть много указателей, которые они могут вернуть, от которых вырвется весь ад.

Но нулевой указатель явно более неправильный, чем любой другой указатель.

Мне нравится думать, что система типов ржавчины помогает с ножными ружьями и используется для кодирования инвариантов. В документации для alloc четко сказано: «Если этот метод возвращает Ok(addr) , то возвращаемый адрес будет ненулевым адресом», но его тип возврата - нет. Как бы то ни было, Ok(malloc(layout.size())) будет правильной реализацией, хотя это явно не так.

Обратите внимание, есть также примечания о том, что размер Layout должен быть ненулевым, поэтому я также утверждаю, что он должен кодировать это как NonZero.

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

Из всех возможных ошибок при использовании (редактировании: и реализации) распределителей передача нулевого указателя является одной из самых простых для отслеживания (вы всегда получаете чистый segfault при разыменовании, по крайней мере, если у вас есть MMU и вы этого не сделали. очень странные вещи с ним), и обычно это одна из самых тривиальных проблем, которые нужно исправить. Верно, что небезопасные интерфейсы могут пытаться предотвратить использование ножных ружей, но эта ножная ружье кажется непропорционально маленькой (по сравнению с другими возможными ошибками и многословием кодирования этого инварианта в системе типов).

Кроме того, кажется вероятным, что реализации распределителя будут просто использовать непроверенный конструктор NonNull «для производительности»: поскольку в правильном распределителе все равно не будет возвращаться null, он захочет пропустить NonNell::new(...).unwrap() . В этом случае вы фактически не получите никакой ощутимой защиты от использования ножного оружия, только больше шаблонов. (Преимущества размера Result , если они реальны, все же могут быть веской причиной для этого.)

реализации распределителя будут просто использовать непроверенный конструктор NonNull

Дело не столько в помощи реализации распределителя, сколько в помощи их пользователям. Если MyVec содержит NonNull<T> а Heap.alloc() уже возвращает NonNull , это на один проверенный или небезопасный неконтролируемый вызов, который мне нужно сделать, меньше.

Обратите внимание, что указатели - это не только возвращаемые типы, они также являются типами ввода, например, для dealloc и realloc . Предполагается ли, что эти функции защищаются от того, что их ввод может быть нулевым или нет? Документация обычно говорит «нет», но система типов говорит «да».

Точно так же с layout.size (). Должны ли функции распределения каким-либо образом обрабатывать запрошенный размер, равный 0, или нет?

(Увеличение размера результата, если оно реально, может быть веской причиной для этого.)

Я сомневаюсь, что есть преимущества в размере, но с чем-то вроде # 48741 были бы преимущества кодогенерации.

Если мы продолжим этот принцип большей гибкости для пользователей API, указатели должны быть NonNull в возвращаемых типах, но не в аргументах. (Это не означает, что эти аргументы должны проверяться на null во время выполнения.)

Я думаю, что подход, основанный на законе Постела, здесь неправильный. Есть ли
в каком случае передача нулевого указателя методу Alloc допустима? Если не,
тогда эта гибкость в основном дает ножному ружью немного больше
чувствительный триггер.

5 марта 2018 г. в 8:00 «Саймон Сапин» [email protected] написал:

Если мы продолжим этот принцип большей гибкости для пользователей API,
указатели должны иметь значение NonNull в возвращаемых типах, но не в аргументах. (Этот
не означает, что эти аргументы должны проверяться на null во время выполнения.)

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-370327018 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AA_2L8zrOLyUv5mUc_kiiXOAn1f60k9Uks5tbOJ0gaJpZM4IDYUN
.

Дело не столько в помощи реализации распределителя, сколько в помощи их пользователям. Если MyVec содержит значение NonNullи Heap.alloc () уже возвращает NonNull, то есть на один проверенный или небезопасный неконтролируемый вызов, который мне нужно сделать меньше.

Ах, это имеет смысл. Не ремонтирует ножное ружье, но централизует ответственность за него.

Обратите внимание, что указатели - это не только возвращаемые типы, они также являются типами ввода, например, для освобождения и перераспределения. Предполагается ли, что эти функции защищаются от того, что их ввод может быть нулевым или нет? Документация обычно говорит «нет», но система типов говорит «да».

Есть ли случай, когда передача нулевого указателя методу Alloc допустима? Если нет, то такая гибкость в основном дает ножному ружью немного более чувствительный спусковой крючок.

Пользователь обязательно должен прочитать документацию и помнить об инвариантах. Многие инварианты вообще не могут быть реализованы через систему типов - если бы они могли, функция была бы небезопасной с самого начала. Таким образом, это исключительно вопрос того, действительно ли размещение NonNull в каком-либо интерфейсе поможет пользователям,

  • напоминание им прочитать документацию и подумать об инвариантах
  • предлагая удобство (точка @SimonSapin относительно возвращаемого значения alloc )
  • дает некоторое материальное преимущество (например, оптимизация макета)

Я не вижу серьезных преимуществ в том, чтобы, например, преобразовать аргумент dealloc в NonNull . Я вижу примерно два класса использования этого API:

  1. Относительно тривиальное использование, когда вы вызываете alloc , где-то храните возвращаемый указатель, а через некоторое время передаете сохраненный указатель в dealloc .
  2. Сложные сценарии, включающие FFI, много арифметических операций с указателями и т. Д., Где есть важная логика, обеспечивающая передачу правильного значения в dealloc в конце.

Взятие здесь NonNull основном помогает только первому типу варианта использования, потому что они сохранят NonNull в каком-нибудь хорошем месте и просто передадут его NonNull без изменений. Теоретически это может предотвратить некоторые опечатки (передача foo когда вы имели в виду bar ), если вы манипулируете несколькими указателями, и только один из них NonNull , но это не похоже слишком часто или важно. Недостатком того, что dealloc принимает необработанный указатель (при условии, что alloc возвращает NonNull что, по убеждению @SimonSapin , должно произойти), это требует as_ptr в вызов dealloc, который потенциально раздражает, но никак не влияет на безопасность.

Второй вариант использования не помогает, потому что он, вероятно, не может продолжать использовать NonNull протяжении всего процесса, поэтому ему придется вручную воссоздавать NonNull из необработанного указателя, который он получил любыми средствами. Как я утверждал ранее, это, скорее всего, станет неконтролируемым утверждением / unsafe а не фактической проверкой времени выполнения, поэтому никакие ножные ружья не будут предотвращены.

Это не означает, что я поддерживаю использование dealloc необработанного указателя. Я просто не вижу заявленных преимуществ перед ножными ружьями. Согласованность типов, вероятно, просто выигрывает по умолчанию.

Извините, но я прочитал это как «Многие инварианты вообще не могут быть реализованы через систему типов ... поэтому давайте даже не пробовать». Не позволяйте лучшему быть врагом хорошего!

Я думаю, что это больше связано с компромиссом между гарантиями, предоставляемыми NonNull и эргономикой, потерянной из-за необходимости переходить туда и обратно между NonNull и необработанными указателями. В любом случае у меня нет особо твердого мнения - ни одна из сторон не кажется необоснованной.

@cramertj Да, но я действительно не Alloc предназначен для неясных, скрытых и в значительной степени небезопасных случаев использования. Что ж, в непонятном, трудночитаемом коде я хотел бы быть максимально безопасным - именно потому, что они так редко затрагиваются, что, вероятно, первоначального автора поблизости не будет. И наоборот, если код читают годы спустя, к черту эгономику. Во всяком случае, это контрпродуктивно. Код должен быть очень ясным, чтобы незнакомый читатель мог лучше понять, что, черт возьми, происходит. Меньше шума <более четкие инварианты.

Второй тип использования не помогает, потому что он, вероятно, не может продолжать использовать NonNull протяжении всего процесса, поэтому ему придется вручную воссоздавать NonNull из необработанного указателя, который он получил любыми средствами.

Это просто нарушение координации, а не техническая неизбежность. Конечно, сейчас многие небезопасные API-интерфейсы могут использовать необработанные указатели. Так что что-то должно привести к переходу на улучшенный интерфейс с использованием NonNull или других оболочек. Тогда другой код может более легко последовать этому примеру. Я не вижу причин постоянно прибегать к трудночитаемым, неинформативным необработанным указателям в зеленом, полностью на Rust и небезопасном коде.

Привет!

Я просто хочу сказать, что как автор / сопровождающий пользовательского распределителя Rust я поддерживаю NonNull . Практически по всем причинам, которые уже были выложены в этой ветке.

Также я хотел бы отметить, что @glandium является разработчиком форка jemalloc для firefox, а также имеет большой опыт взлома распределителей.

Я мог бы написать гораздо больше, отвечая на @ Ericson2314 и другие, но это быстро превратилось в очень отстраненную и философскую дискуссию, поэтому я сокращаю ее здесь. Я возражал против того, что, по моему мнению, является преувеличением преимуществ NonNull в подобном API (конечно, есть и другие преимущества). Это не значит, что нет никаких преимуществ для безопасности, но, как сказал @cramertj , есть компромиссы, и я думаю, что "профессиональная" сторона преувеличена. Тем не менее, я уже сказал, что склоняюсь к использованию NonNull в разных местах по другим причинам - по той причине, что @SimonSapin дал в alloc , в dealloc для согласованности. Так что давайте сделаем это и больше не будем отклоняться от темы.

Если есть несколько вариантов использования NonNull с которыми все согласны, это отличное начало.

Мы, вероятно, захотим обновить Unique и друзей, чтобы использовать NonNull вместо NonZero чтобы снизить трение хотя бы в пределах liballoc . Но это действительно похоже на то, что мы в любом случае уже делаем на одном уровне абстракции над распределителем, и я не могу придумать никаких причин, чтобы не делать этого и на уровне распределителя. Для меня это разумное изменение.

(Уже есть две реализации трейта From которые безопасно преобразуют между Unique<T> и NonNull<T> .)

Учитывая, что мне нужно что-то очень похожее на API распределителя в стабильной ржавчине, я извлек код из репозитория ржавчины и поместил его в отдельный ящик:

Это / могло / использоваться для итерации экспериментальных изменений в API, но на данный момент это простая копия того, что находится в репозитории Rust.

[Не по теме] Да, было бы неплохо, если бы std мог использовать такие ящики с нестабильным кодом вне дерева, чтобы мы могли экспериментировать с нестабильными интерфейсами в стабильном коде. Это одна из причин, почему мне нравится иметь фасад std .

std может зависеть от копии ящика из crates.io, но если ваша программа также зависит от того же ящика, она все равно не будет "выглядеть" как тот же ящик / типы / черты для rustc, поэтому я не Не понимаю, как это поможет. В любом случае, независимо от фасада, отключение нестабильных функций на стабильном канале - это очень осознанный выбор, а не случайность.

Похоже, мы договорились об использовании NonNull. Каким образом это должно произойти? просто пиар это делает? RFC?

Я смотрел на некоторую сборку, созданную из вещей бокса, и пути ошибок довольно велики. Что-то с этим делать?

Примеры:

pub fn bar() -> Box<[u8]> {
    vec![0; 42].into_boxed_slice()
}

pub fn qux() -> Box<[u8]> {
    Box::new([0; 42])
}

компилируется в:

example::bar:
  sub rsp, 56
  lea rdx, [rsp + 8]
  mov edi, 42
  mov esi, 1
  call __rust_alloc_zeroed<strong i="11">@PLT</strong>
  test rax, rax
  je .LBB1_1
  mov edx, 42
  add rsp, 56
  ret
.LBB1_1:
  mov rax, qword ptr [rsp + 8]
  movups xmm0, xmmword ptr [rsp + 16]
  movaps xmmword ptr [rsp + 32], xmm0
  mov qword ptr [rsp + 8], rax
  movaps xmm0, xmmword ptr [rsp + 32]
  movups xmmword ptr [rsp + 16], xmm0
  lea rdi, [rsp + 8]
  call __rust_oom<strong i="12">@PLT</strong>
  ud2

example::qux:
  sub rsp, 104
  xorps xmm0, xmm0
  movups xmmword ptr [rsp + 58], xmm0
  movaps xmmword ptr [rsp + 48], xmm0
  movaps xmmword ptr [rsp + 32], xmm0
  lea rdx, [rsp + 8]
  mov edi, 42
  mov esi, 1
  call __rust_alloc<strong i="13">@PLT</strong>
  test rax, rax
  je .LBB2_1
  movups xmm0, xmmword ptr [rsp + 58]
  movups xmmword ptr [rax + 26], xmm0
  movaps xmm0, xmmword ptr [rsp + 32]
  movaps xmm1, xmmword ptr [rsp + 48]
  movups xmmword ptr [rax + 16], xmm1
  movups xmmword ptr [rax], xmm0
  mov edx, 42
  add rsp, 104
  ret
.LBB2_1:
  movups xmm0, xmmword ptr [rsp + 16]
  movaps xmmword ptr [rsp + 80], xmm0
  movaps xmm0, xmmword ptr [rsp + 80]
  movups xmmword ptr [rsp + 16], xmm0
  lea rdi, [rsp + 8]
  call __rust_oom<strong i="14">@PLT</strong>
  ud2

Это довольно большой объем кода, который можно добавить в любое место, создающее блоки. Сравните с 1.19, в котором не было API распределителя:

example::bar:
  push rax
  mov edi, 42
  mov esi, 1
  call __rust_allocate_zeroed<strong i="18">@PLT</strong>
  test rax, rax
  je .LBB1_2
  mov edx, 42
  pop rcx
  ret
.LBB1_2:
  call alloc::oom::oom<strong i="19">@PLT</strong>

example::qux:
  sub rsp, 56
  xorps xmm0, xmm0
  movups xmmword ptr [rsp + 26], xmm0
  movaps xmmword ptr [rsp + 16], xmm0
  movaps xmmword ptr [rsp], xmm0
  mov edi, 42
  mov esi, 1
  call __rust_allocate<strong i="20">@PLT</strong>
  test rax, rax
  je .LBB2_2
  movups xmm0, xmmword ptr [rsp + 26]
  movups xmmword ptr [rax + 26], xmm0
  movaps xmm0, xmmword ptr [rsp]
  movaps xmm1, xmmword ptr [rsp + 16]
  movups xmmword ptr [rax + 16], xmm1
  movups xmmword ptr [rax], xmm0
  mov edx, 42
  add rsp, 56
  ret
.LBB2_2:
  call alloc::oom::oom<strong i="21">@PLT</strong>

Если это действительно важно, то это действительно раздражает. Однако, может быть, LLVM оптимизирует это для более крупных программ?

В последней версии Firefox каждую ночь совершается 1439 звонков на номер __rust_oom . Однако Firefox не использует распределитель rust, поэтому мы получаем прямые вызовы malloc / calloc, за которыми следует нулевая проверка перехода к коду подготовки oom, который обычно состоит из двух movq и lea, заполняя AllocErr и получая его адрес чтобы передать его __rust__oom . По сути, это лучший сценарий, но это все еще 20 байтов машинного кода для двух movq и lea.

Я смотрю на ripgrep, их 85, и все они находятся в идентичных функциях _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn . Все они имеют длину 16 байт. Есть 685 вызовов этих функций-оболочек, большинству из которых предшествует код, аналогичный тому, что я вставил в https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485.

@nox сегодня искал возможность включить проход mergefunc llvm, интересно, имеет ли это какое-то значение здесь.

mergefunc видимому, не избавляется от нескольких идентичных _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn функций (пробовал с -C passes=mergefunc в RUSTFLAGS ).

Но что имеет большое значение, так это LTO, который фактически заставляет Firefox напрямую вызывать malloc, оставляя создание AllocErr прямо перед вызовом __rust_oom . Это также делает ненужным создание Layout перед вызовом распределителя, оставляя его при заполнении AllocErr .

Это заставляет меня думать, что функции распределения, за исключением __rust_oom вероятно, должны быть помечены как встроенные.

Кстати, посмотрев сгенерированный код для Firefox, я думаю, что в идеале было бы желательно использовать moz_xmalloc вместо malloc . Это невозможно без комбинации свойств Allocator и возможности замены глобального распределителя кучи, но возникает возможная потребность в настраиваемом типе ошибки для свойства Allocator: moz_xmalloc является непогрешимым и никогда не возвращается в случае неудача. IOW, он сам обрабатывает OOM, и в этом случае код ржавчины не должен вызывать __rust_oom . Поэтому было бы желательно, чтобы функции распределителя опционально возвращали ! вместо AllocErr .

Мы обсудили создание структуры AllocErr нулевого размера, что также может здесь помочь. Если указатель также стал NonNull , все возвращаемое значение может иметь размер указателя.

https://github.com/rust-lang/rust/pull/49669 вносит ряд изменений в эти API с целью стабилизации подмножества, охватывающего глобальные распределители. Проблема отслеживания для этого подмножества: https://github.com/rust-lang/rust/issues/49668. В частности, вводится новый GlobalAlloc .

Позволит ли этот PR нам делать такие вещи, как Vec::new_with_alloc(alloc) where alloc: Alloc ближайшее время?

@alexreg нет

@sfackler Хм, а почему бы и нет? Что нам нужно, чтобы это сделать? В противном случае я не совсем понимаю смысл этого PR, если только он не касается простого изменения глобального распределителя.

@alexreg

В противном случае я не совсем понимаю смысл этого PR, если только он не касается простого изменения глобального распределителя.

Я думаю, это просто для изменения глобального распределителя.

@alexreg Если вы имеете в виду стабильную поддерживается RawVec и, вероятно, можно добавить как #[unstable] за Vec для всех, кто хочет поработать над этим.

И да, как упоминалось в PR, его цель состоит в том, чтобы разрешить изменение глобального распределителя или распределение (например, в настраиваемом типе коллекции) без злоупотребления Vec::with_capacity .

FWIW, ящик allocator_api упомянутый в https://github.com/rust-lang/rust/issues/32838#issuecomment -376793369, имеет RawVec<T, A> и Box<T, A> на главном устройстве. ветка (еще не выпущена). Я думаю об этом как об инкубаторе для того, как могут выглядеть общие коллекции по типу распределения (плюс тот факт, что мне действительно нужен тип Box<T, A> для стабильной ржавчины). Я еще не начал портировать vec.rs, чтобы добавить Vec<T, A> , но PR приветствуются. vec.rs большой.

Я отмечу, что «проблемы» кодогенерации, упомянутые в https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485, должны исчезнуть с изменениями в # 49669.

Теперь, когда мы еще немного подумали об использовании трейта Alloc для помощи в реализации распределителя на уровнях, есть две вещи, которые, как я думаю, были бы полезны (по крайней мере, для меня):

  • как упоминалось ранее, при желании можно указать другой тип AllocErr . Это может быть полезно, чтобы сделать его ! или, теперь, когда AllocErr пуст, чтобы он мог передавать больше информации, чем «сбой».
  • при желании можно указать другой тип Layout . Представьте, что у вас есть два уровня распределителей: один для выделения страниц, а другой для более крупных регионов. Последний может полагаться на первый, но если они оба используют один и тот же тип Layout , тогда оба слоя должны выполнить свою собственную проверку: на самом низком уровне этот размер и выравнивание кратны размеру страницы, и на более высоком уровне этот размер и выравнивание соответствуют требованиям для более крупных регионов. Но эти проверки излишни. С помощью специализированных типов Layout проверка может быть делегирована созданию Layout а не самому распределителю, а преобразования между типами Layout позволят пропустить избыточные проверки.

@cramertj @SimonSapin @glandium Хорошо, спасибо за разъяснения. Я могу просто отправить PR для некоторых других основных типов коллекций. Что лучше сделать с вашим репозиторием / ящиком allocator-api,

@alexreg, учитывая количество Alloc в # 49669, вероятно, лучше сначала дождаться его объединения.

@glandium Достаточно уж далеко от приземления. Я только что заметил репо https://github.com/pnkfelix/collections-prime ... что это по отношению к вашему?

Я бы добавил еще один открытый вопрос:

  • Можно ли паниковать Alloc::oom ? В настоящее время в документации говорится, что этот метод должен прервать процесс. Это имеет значение для кода, который использует распределители, поскольку они должны быть разработаны для правильной обработки раскрутки без утечки памяти.

Я думаю, что мы должны допускать панику, поскольку сбой в локальном распределителе не обязательно означает, что глобальный распределитель также выйдет из строя. В худшем случае будет вызван глобальный распределитель oom , который прервет процесс (в противном случае будет нарушен существующий код).

@alexreg Это не так. Это просто похоже на простую копию того, что находится в std / alloc / collections. Ну, копия двухлетней давности. Мой ящик гораздо более ограничен по объему (опубликованная версия имеет только черту Alloc несколько недель назад, в основной ветке есть только RawVec и Box поверх что), и одна из моих целей - сохранить устойчивую ржавчину.

@glandium Хорошо, в этом случае, вероятно, для меня имеет смысл подождать, пока этот PR появится, затем создать PR против мастера ржавчины и пометить вас, чтобы вы знали, когда он будет объединен с мастером (и затем могу объединить его в свой ящик) , справедливо?

@alexreg имеет смысл. Вы / можете / начать работать над этим сейчас, но это, скорее всего, вызовет некоторый отток с вашей стороны, если / когда байкшеддинг изменит ситуацию в этом PR.

@glandium У меня есть кое-что, чем заняться сейчас с Rust, но я займусь этим, когда этот пиар будет одобрен. Будет здорово в ближайшее время получить общее распределение / коллекции кучи распределителя как на ночь, так и на стабильную. :-)

Можно ли запаниковать в Alloc :: oom? В настоящее время в документации говорится, что этот метод должен прервать процесс. Это имеет значение для кода, который использует распределители, поскольку они должны быть разработаны для правильной обработки раскрутки без утечки памяти.

@Amanieu Этот RFC был объединен: https://github.com/rust-lang/rfcs/pull/2116 Документы и реализация, возможно, просто еще не обновлены.

В API есть одно изменение, для которого я собираюсь отправить PR:

Разделите черту Alloc на две части: «реализация» и «помощники». Первые будут такими функциями, как alloc , dealloc , realloc и т. Д., А вторые - alloc_one , dealloc_one , alloc_array и т. д. Хотя есть некоторые гипотетические преимущества от возможности иметь индивидуальную реализацию для последнего, это далеко не самая распространенная потребность, и когда вам нужно реализовать общие оболочки (которые, как я обнаружил, невероятно распространены, до того момента, как я начал писать для этого настраиваемую производную), вам все равно нужно реализовать их все, потому что обертка может их настраивать.

OTOH, если разработчик трейта Alloc пытается делать необычные вещи, например, в alloc_one , им не гарантируется, что dealloc_one будет вызван для этого распределения. Для этого есть несколько причин:

  • Помощники не используются постоянно. Только один пример, raw_vec использует смесь alloc_array , alloc / alloc_zeroed , но использует только dealloc .
  • Даже при последовательном использовании, например, alloc_array / dealloc_array , можно безопасно преобразовать Vec в Box , которое затем будет использовать dealloc .
  • Затем есть некоторые части API, которые просто не существуют (нет обнуленной версии alloc_one / alloc_array )

Таким образом, даже несмотря на то, что существуют реальные варианты использования для специализации, например, alloc_one (и, на самом деле, у меня есть такая потребность в mozjemalloc), лучше вместо этого использовать специализированный распределитель.

На самом деле, это хуже, чем то, что в репозитории ржавчины есть ровно одно использование alloc_array и отсутствие использования alloc_one , dealloc_one , realloc_array , dealloc_array . Даже синтаксис блока не использует alloc_one , он использует exchange_malloc , который принимает size и align . Так что эти функции больше предназначены для удобства клиентов, чем для разработчиков.

С чем-то вроде impl<A: Alloc> AllocHelpers for A (или AllocExt , какое бы имя ни было выбрано), у нас по-прежнему будет удобство этих функций для клиентов, не позволяя разработчикам стрелять себе в ногу, если они думают они будут делать необычные вещи, переопределяя их (и облегчая людям, реализующим прокси-распределители).

В API есть одно изменение, для которого я собираюсь отправить PR

Сделал это в № 50436

@glandium

(и вообще-то мне очень нужен мозжемаллок),

Не могли бы вы подробнее рассказать об этом варианте использования?

mozjemalloc имеет базовый распределитель, который целенаправленно дает утечку. За исключением одного вида объектов, где хранится свободный список. Я могу делать это, располагая распределители по уровням, а не трюки с alloc_one .

Требуется ли освободить место с точным выравниванием, которое вы выделили?

Чтобы подтвердить, что ответ на этот вопрос - ДА , у меня есть прекрасная цитата от самих Microsoft :

Выровненный_аллок (), вероятно, никогда не будет реализован, поскольку C11 определил его способом, несовместимым с нашей реализацией (а именно, что free () должен иметь возможность обрабатывать сильно выровненные выделения)

Использование системного распределителя в Windows всегда требует знания выравнивания при освобождении, чтобы правильно высвободить сильно выровненные выделения, поэтому можем ли мы просто отметить этот вопрос как решенный?

Использование системного распределителя в Windows всегда требует знания выравнивания при освобождении, чтобы правильно высвободить сильно выровненные выделения, поэтому можем ли мы просто отметить этот вопрос как решенный?

Обидно, но так оно и есть. Тогда давайте откажемся от перераспределенных векторов. :смущенный:

Давайте откажемся от перераспределенных векторов, тогда

Как так? Вам просто нужен Vec<T, OverAlignedAlloc<U16>> который и выделяет, и освобождает с избыточным выравниванием.

Как так? Вам просто нужен Vec<T, OverAlignedAlloc<U16>> который и выделяет, и освобождает с избыточным выравниванием.

Я должен был быть более конкретным. Я имел в виду перемещение векторов с чрезмерным выравниванием в API вне вашего контроля, то есть в тот, который принимает Vec<T> а не Vec<T, OverAlignedAlloc<U16>> . (Например, CString::new() .)

Вам лучше использовать

#[repr(align(16))]
struct OverAligned16<T>(T);

а затем Vec<OverAligned16<T>> .

Вам лучше использовать

Это зависит от. Предположим, вы хотите использовать встроенные функции AVX (ширина 256 бит, требование выравнивания 32 байта) для вектора f32 s:

  • Vec<T, OverAlignedAlloc<U32>> решает проблему, можно использовать встроенные функции AVX непосредственно в элементах вектора (в частности, при выровненной загрузке памяти), а вектор по-прежнему превращается в срез &[f32] что делает его эргономичным в использовании.
  • Vec<OverAligned32<f32>> самом деле не решает проблему. Каждый f32 занимает 32 байта из-за требования выравнивания. Введенное заполнение предотвращает прямое использование операций AVX, поскольку f32 s больше не находятся в непрерывной памяти. И я лично считаю, что с deref to &[OverAligned32<f32>] немного утомительно иметь дело.

Для одного элемента в Box , Box<T, OverAligned<U32>> vs Box<OverAligned32<T>> оба подхода более эквивалентны, и второй подход действительно может быть предпочтительнее. В любом случае хорошо иметь оба варианта.

Размещено это с изменениями в трейте Alloc: https://internals.rust-lang.org/t/pre-rfc-changing-the-alloc-trait/7487

Сообщение об отслеживании в начале этого выпуска ужасно устарело (последний раз редактировалось в 2016 году). Нам нужен обновленный список активных проблем, чтобы продолжить обсуждение продуктивно.

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

Существует несколько потоков различий от «того, что в настоящее время реализовано в ночное время» до «того, что было предложено в исходном RFC Alloc», порождающих тысячи комментариев по различным каналам (репозиторий rfc, проблема отслеживания rust-lang, RFC глобального выделения, внутренние сообщения, многие огромные PR и т. д.), и то, что стабилизируется в GlobalAlloc RFC, не сильно отличается от того, что было предложено в исходном RFC.

Это то, что нам нужно в любом случае, чтобы завершить обновление документации и справочника, и это также будет полезно в текущих обсуждениях.

Я думаю, что прежде чем мы даже подумаем о стабилизации признака Alloc , мы должны сначала попробовать реализовать поддержку распределителя во всех коллекциях стандартных библиотек. Это должно дать нам некоторый опыт того, как эта черта будет использоваться на практике.

Я думаю, что прежде чем мы даже подумаем о стабилизации признака Alloc , мы должны сначала попробовать реализовать поддержку распределителя во всех коллекциях стандартных библиотек. Это должно дать нам некоторый опыт того, как эта черта будет использоваться на практике.

Да, конечно. Особенно Box , поскольку мы еще не знаем, как избежать того, чтобы Box<T, A> заняло два слова.

Да, конечно. Особенно Box, поскольку мы еще не знаем, как избежать Boxвозьму два слова.

Я не думаю, что нам следует беспокоиться о размере Box<T, A> для начальной реализации, но это то, что можно добавить позже обратно совместимым способом, добавив черту DeAlloc которая поддерживает только освобождение.

Пример:

trait DeAlloc {
    fn dealloc(&mut self, ptr: NonNull<Opaque>, layout: Layout);
}

trait Alloc {
    // In addition to the existing trait items
    type DeAlloc: DeAlloc = Self;
    fn into_dealloc(self) -> Self::DeAlloc {
        self
    }
}

impl<T: Alloc> DeAlloc for T {
    fn dealloc(&mut self, ptr: NonNull<Opaque>, layout: Layout) {
        Alloc::dealloc(self, ptr, layout);
    }
}

Я думаю, что прежде чем мы даже подумаем о стабилизации трейта Alloc, мы должны сначала попробовать реализовать поддержку распределителя во всех коллекциях стандартных библиотек. Это должно дать нам некоторый опыт того, как эта черта будет использоваться на практике.

Я думаю, что @ Ericson2314 работал над этим, https://github.com/rust-lang/rust/issues/42774. Было бы неплохо получить от него обновления.

Я не думаю, что нам следует беспокоиться о размере Box<T, A> для начальной реализации, но это то, что можно добавить позже обратно совместимым способом, добавив черту DeAlloc которая поддерживает только освобождение.

Это один из подходов, но мне не совсем понятно, что он определенно лучший. Он имеет явные недостатки, например, что а) он работает только тогда, когда возможен поиск указателя → распределителя (это не верно, например, для большинства распределителей арены) и, б) он добавляет значительные накладные расходы к dealloc (а именно, чтобы выполнить обратный поиск). Может оказаться, что лучшее решение этой проблемы - это более универсальная система эффектов или контекста, такая как это предложение или это предложение . Или, может быть, что-то совсем другое. Поэтому я не думаю, что мы должны предполагать, что это будет легко решить способом, который обратно совместим с текущим воплощением признака Alloc .

@joshlf Принимая во внимание тот факт, что Box<T, A> имеет доступ к себе только при удалении, это лучшее, что мы можем сделать только с безопасным кодом. Такой шаблон может быть полезен для распределителей, похожих на арену, у которых нет операции dealloc и только свободная память при удалении распределителя.

Для более сложных систем, где распределитель принадлежит контейнеру (например, LinkedList ) и управляет множественными выделениями, я ожидаю, что Box не будет использоваться внутри. Вместо этого внутренние компоненты LinkedList будут использовать необработанные указатели, которые выделяются и освобождаются с помощью экземпляра Alloc который содержится в объекте LinkedList . Это позволит избежать удвоения размера каждого указателя.

Учитывая тот факт, что Box<T, A> имеет доступ к себе только при удалении, это лучшее, что мы можем сделать только с безопасным кодом. Такой шаблон может быть полезен для распределителей, похожих на арену, у которых нет операции dealloc и только свободная память при удалении распределителя.

Верно, но Box не знает, что dealloc не работает.

Для более сложных систем, где распределитель принадлежит контейнеру (например, LinkedList ) и управляет множественными распределениями, я ожидаю, что Box не будет использоваться внутри. Вместо этого внутренние компоненты LinkedList будут использовать необработанные указатели, которые выделяются и освобождаются с помощью экземпляра Alloc который содержится в объекте LinkedList . Это позволит избежать удвоения размера каждого указателя.

Я думаю, было бы действительно стыдно требовать от людей использования небезопасного кода для написания каких-либо коллекций. Если цель состоит в том, чтобы сделать все коллекции (предположительно, включая коллекции, не входящие в стандартную библиотеку) необязательно параметрическими на распределителе, а Box не является параметрическим для распределителя, то автор коллекций не должен использовать Box или используйте небезопасный код (и имейте в виду, что всегда нужно освобождать вещи - это один из наиболее распространенных типов небезопасности памяти в C и C ++, поэтому в этом случае трудно исправить небезопасный код). Это похоже на неудачную сделку.

Верно, но Box не знает, что dealloc не работает.

Почему бы не адаптировать то, что делает C ++ unique_ptr ?
То есть: хранить указатель на распределитель, если он "с сохранением состояния", и не сохранять его, если распределитель "без состояния"
(например, глобальная оболочка вокруг malloc или mmap ).
Для этого потребуется разделить текущий трейн Alloc на две характеристики: StatefulAlloc и StatelessAlloc .
Я понимаю, что это очень грубо и неизящно (и, вероятно, кто-то уже предлагал это в предыдущих обсуждениях).
Несмотря на свою неэлегантность, это решение простое и обратно совместимое (без потери производительности).

Я думаю, было бы действительно стыдно требовать от людей использования небезопасного кода для написания каких-либо коллекций. Если цель состоит в том, чтобы сделать все коллекции (предположительно, включая коллекции, не входящие в стандартную библиотеку) необязательно параметрическими для распределителя, а Box не является параметрическим для распределителя, то автор коллекций должен либо вообще не использовать Box, либо использовать небезопасный код (и имейте в виду, что не забывайте всегда освобождать вещи - один из наиболее распространенных типов небезопасности памяти в C и C ++, поэтому в этом случае трудно получить правильный небезопасный код). Это похоже на неудачную сделку.

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

Поэтому обращение к unsafe при реализации контейнеров на основе узлов может быть неизбежным злом, по крайней мере, в краткосрочной перспективе.

@eucpp Обратите внимание, что unique_ptr не хранит распределитель - он хранит Deleter :

Средство удаления должно быть ссылкой на объект FunctionObject или lvalue на объект FunctionObject или ссылкой lvalue на функцию, вызываемую с аргументом типа unique_ptr:: указатель`

Я считаю, что это примерно эквивалентно разделению трейтов Alloc и Dealloc .

@cramertj Да, вы правы. Тем не менее, требуются две черты - с сохранением состояния и без состояния Dealloc .

Разве ZST Dealloc не будет достаточно?

Вторник, 12 июня 2018 г., 15:08 Евгений Моисеенко [email protected]
написал:

@cramertj https://github.com/cramertj Да, вы правы. Тем не менее, два
требуются черты - Dealloc с отслеживанием состояния и без состояния.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32838#issuecomment-396716689 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AEAJtWkpF0ofVc18NwbfV45G4QY6SCFBks5t8B_AgaJpZM4IDYUN
.

Разве ZST Dealloc не будет достаточно?

@remexre, наверное :)

Я не знал, что компилятор ржавчины поддерживает ZST из коробки.
В C ++ это потребует по крайней мере некоторых уловок для оптимизации пустой базы.
Я новичок в Rust, поэтому извиняюсь за очевидные ошибки.

Я не думаю, что нам нужны отдельные черты для stateful и stateless.

Если Box дополнен параметром A type, он будет содержать значение A напрямую, а не ссылку или указатель на A . Этот тип может быть нулевого размера для (де) распределителя без сохранения состояния. Или A может быть чем-то вроде ссылки или дескриптора распределителя с отслеживанием состояния, который может совместно использоваться несколькими выделенными объектами. Поэтому вместо impl Alloc for MyAllocator вы можете сделать что-то вроде impl<'r> Alloc for &'r MyAllocator

Между прочим, Box который знает только, как освободить, а не как выделить, не будет реализовывать Clone .

@SimonSapin Я бы ожидал, что Clone ing потребует повторного указания распределителя, так же, как при создании нового Box (то есть это не будет сделано с использованием Clone черта).

@cramertj Разве это не будет непоследовательно по сравнению с Vec и другими контейнерами, реализующими Clone ?
Каковы недостатки хранения экземпляра Alloc внутри Box а не Dealloc ?
Тогда Box может реализовать Clone а также clone_with_alloc .

Я не говорю о том, что черты разделения действительно сильно влияют на Clone - impl будет просто выглядеть как impl<T, A> Clone for Box<T, A> where A: Alloc + Dealloc + Clone { ... } .

@sfackler Я бы не возражал против этого impl, но я также ожидал бы иметь clone_into или что-то, что использует предоставленный распределитель.

Имеет ли смысл метод alloc_copy для Alloc ? Это можно было бы использовать для обеспечения более быстрой реализации memcpy ( Copy/Clone ) для больших распределений, например, путем клонирования страниц с копированием при записи.

Это было бы довольно круто и тривиально было бы предоставить реализацию по умолчанию.

Что будет использовать такую ​​функцию alloc_copy ? impl Clone for Box<T, A> ?

Да, то же самое для Vec .

Изучив его еще раз, кажется, что подходы к созданию страниц копирования при записи в одном и том же диапазоне процессов - от взломанного до невозможного, по крайней мере, если вы хотите сделать это более чем на один уровень. Так что alloc_copy не будет большим преимуществом.

Вместо этого может быть полезен более общий аварийный люк, который позволяет будущие махинации с виртуальной памятью. Т.е. если выделение велико, все равно поддерживается mmap и не имеет состояния, то распределитель может обещать не обращать внимания на будущие изменения в распределении. Затем пользователь мог переместить эту память в канал, отменить отображение или аналогичные вещи.
В качестве альтернативы может быть тупой распределитель mmap-all-the-things и функция try-передачи.

Вместо этого более общий аварийный люк, который позволяет будущей виртуальной памяти

Распределители памяти (malloc, jemalloc, ...) обычно не позволяют вам украсть у них какую-либо память и обычно не позволяют запрашивать или изменять свойства памяти, которой они владеют. Итак, какое отношение этот общий аварийный выход имеет к распределителям памяти?

Кроме того, поддержка виртуальной памяти сильно различается между платформами, настолько, что для эффективного использования виртуальной памяти часто требуются разные алгоритмы для каждой платформы, часто с совершенно разными гарантиями. Я видел несколько переносимых абстракций в виртуальной памяти, но я еще не видел ни одной, которая не была бы искажена до такой степени, что была бы бесполезной в некоторых ситуациях из-за их «переносимости».

Вы правы. Любой такой вариант использования (я в основном думал об оптимизации для конкретной платформы), вероятно, лучше всего обслуживается с помощью специального распределителя в первую очередь.

Есть ли какие-либо мысли по поводу API составного распределителя, описанного Андреем Александреску в его презентации на CppCon? Видео доступно на YouTube здесь: https://www.youtube.com/watch?v=LIb3L4vKZ7U (он начинает описывать свой предложенный дизайн около 26:00, но беседа достаточно интересная, вы можете предпочесть ее просмотреть) .

Похоже, неизбежным выводом из всего этого является то, что библиотеки коллекций должны быть общим распределением WRT, а сам программист приложений должен иметь возможность свободно составлять распределители и коллекции на строительной площадке.

Есть ли какие-либо мысли по поводу API составного распределителя, описанного Андреем Александреску в его презентации на CppCon?

Текущий Alloc API позволяет писать составные распределители (например, MyAlloc<Other: Alloc> ), и вы можете использовать черты и специализацию для достижения практически всего, что было достигнуто в докладе Андрея. Однако, помимо «идеи», что это должно быть возможно, практически ничего из выступления Андрея не может быть применимо к Rust, поскольку способ, которым Андрей строит API, основан на неограниченных дженериках + SFINAE / static if с самого начала и системе дженериков Rust. полностью отличается от этого.

Я хотел бы предложить стабилизировать остальные методы Layout . Они уже полезны с текущим API глобального распределителя.

Это все те методы, которые вы имеете в виду?

  • pub fn align_to(&self, align: usize) -> Layout
  • pub fn padding_needed_for(&self, align: usize) -> usize
  • pub fn repeat(&self, n: usize) -> Result<(Layout, usize), LayoutErr>
  • pub fn extend(&self, next: Layout) -> Result<(Layout, usize), LayoutErr>
  • pub fn repeat_packed(&self, n: usize) -> Result<Layout, LayoutErr>
  • pub fn extend_packed(&self, next: Layout) -> Result<(Layout, usize), LayoutErr>
  • pub fn array<T>(n: usize) -> Result<Layout, LayoutErr>

@gnzlbg Да.

@Amanieu: Звучит нормально, но проблема уже огромна. Рассмотреть возможность подачи отдельного вопроса (или даже стабилизационного PR), который мы могли бы FCP отдельно?

из распределителей и времени жизни :

  1. (для распределителя impls): перемещение значения распределителя не должно делать недействительными его невыполненные блоки памяти.

    Все клиенты могут предполагать это в своем коде.

    Таким образом, если клиент выделяет блок из распределителя (назовите его a1), а затем a1 перемещается в новое место (например, vialet a2 = a1;), тогда для клиента остается разумным освободить этот блок через a2.

Означает ли это, что Allocator должен быть Unpin ?

Хороший улов!

Поскольку черта Alloc все еще нестабильна, я думаю, что нам все равно придется изменить правила, если мы захотим, и изменить эту часть RFC. Но об этом действительно стоит помнить.

@gnzlbg Да, я знаю об огромных различиях в системах дженериков и о том, что не все, что он описывает, можно реализовать в Rust таким же образом. Тем не менее, я работал над библиотекой время от времени с момента публикации, и у меня все хорошо получается.

Означает ли это, что распределитель _должен быть Unpin ?

Это не так. Unpin описывает поведение типа, заключенного в Pin , особой связи с этим API нет.

Но нельзя ли использовать Unpin для принудительного применения упомянутого ограничения?

Еще один вопрос относительно dealloc_array : почему функция возвращает Result ? В текущей реализации это может потерпеть неудачу в двух случаях:

  • n равно нулю
  • переполнение емкости для n * size_of::<T>()

Для первого у нас есть два случая (как и в документации, разработчик может выбирать между ними):

  • Распределение возвращает Ok на обнуленном n => dealloc_array также должно возвращать Ok .
  • Распределение возвращает Err на обнуленном n => нет указателя, который можно передать в dealloc_array .

Второе обеспечивается следующим ограничением безопасности:

макет [T; n] должен соответствовать этому блоку памяти.

Это означает, что мы должны вызвать dealloc_array с тем же n что и при распределении. Если можно выделить массив с элементами n , n допустимо для T . В противном случае распределение не удалось бы.

Изменить: Что касается последнего пункта: даже если usable_size возвращает более высокое значение, чем n * size_of::<T>() , это все еще действительно. В противном случае реализация нарушает это ограничение признака:

Размер блока должен находиться в диапазоне [use_min, use_max] , где:

  • [...]
  • use_max - это емкость, которая была (или была бы) возвращена, когда (если) блок был выделен посредством вызова alloc_excess или realloc_excess .

Это справедливо только потому, что для свойства требуется unsafe impl .

Для первого у нас есть два случая (как и в документации, разработчик может выбирать между ними):

  • Распределение возвращает Ok на обнуленном n

Откуда вы взяли эту информацию?

Все методы Alloc::alloc_ в документации указывают, что поведение распределений нулевого размера не определено в их предложении «Безопасность».

Документы core::alloc::Alloc (соответствующие части выделены):

Примечание относительно типов нулевого размера и макетов нулевого размера: многие методы в трейте Alloc заявляют, что запросы на выделение должны иметь ненулевой размер, иначе может возникнуть неопределенное поведение.

  • Однако некоторые высокоуровневые методы распределения ( alloc_one , alloc_array ) четко определены для типов нулевого размера и могут опционально поддерживать их : решение о том, возвращать ли Err , или чтобы вернуть Ok с некоторым указателем.
  • Если реализация Alloc решает вернуть Ok в этом случае (т. Е. Указатель обозначает недоступный блок нулевого размера), то этот возвращаемый указатель должен считаться «выделенным в данный момент». В таком распределителе все методы, которые принимают в качестве входных данных указатели, выделенные в данный момент, должны принимать эти указатели нулевого размера, не вызывая неопределенного поведения.

  • Другими словами, если указатель нулевого размера может вытекать из распределителя, то этот распределитель также должен принять этот указатель, возвращающийся в его методы освобождения и перераспределения .

Итак, одно из условий ошибки dealloc_array определенно подозрительно:

/// # Safety
///
/// * the layout of `[T; n]` must *fit* that block of memory.
///
/// # Errors
///
/// Returning `Err` indicates that either `[T; n]` or the given
/// memory block does not meet allocator's size or alignment
/// constraints.

Если [T; N] не соответствует размеру распределителя или ограничениям выравнивания, то AFAICT не соответствует блоку памяти распределения, и поведение не определено (согласно пункту безопасности).

Другое условие ошибки - «Всегда возвращает Err при арифметическом переполнении». что довольно общий характер. Трудно сказать, является ли это полезным состоянием ошибки. Для каждой реализации признака Alloc можно придумать другую, которая могла бы выполнять некоторую арифметику, которая теоретически могла бы быть завершена, так что


Документы core::alloc::Alloc (соответствующие части выделены):

Конечно. Мне кажется странным, что так много методов (например, Alloc::alloc ) заявляют, что выделения нулевого размера являются неопределенным поведением, но тогда мы просто предоставляем Alloc::alloc_array(0) с поведением, определяемым реализацией. В некотором смысле Alloc::alloc_array(0) - это лакмусовая бумажка для проверки, поддерживает ли распределитель выделения нулевого размера или нет.

Если [T; N] не соответствует размеру распределителя или ограничениям выравнивания, то AFAICT не соответствует блоку памяти распределения, и поведение не определено (согласно пункту безопасности).

Да, я думаю, что это состояние ошибки можно отбросить, поскольку оно избыточно. Либо нам нужно условие безопасности, либо условие ошибки, но не то и другое одновременно.

Другое условие ошибки - «Всегда возвращает Err при арифметическом переполнении». что довольно общий характер. Трудно сказать, является ли это полезным состоянием ошибки.

ИМО, он охраняется той же оговоркой о безопасности, что и выше; если емкость [T; N] переполнится, он не подходит , что блок памяти для освобождения. Может быть, @pnkfelix мог бы

В некотором смысле Alloc::alloc_array(1) - это лакмусовая бумажка для проверки, поддерживает ли распределитель выделения нулевого размера или нет.

Вы имели в виду Alloc::alloc_array(0) ?

ИМО, он охраняется той же оговоркой о безопасности, что и выше; если емкость [T; N] переполнится, это не _ подходит_ для освобождения этого блока памяти.

Обратите внимание, что эта черта может быть реализована пользователями для своих собственных распределителей, и что эти пользователи могут переопределить реализации этих методов по умолчанию. Поэтому при рассмотрении вопроса о том, должно ли это возвращать Err для арифметического переполнения или нет, следует не только сосредоточиться на том, что делает текущая реализация метода по умолчанию, но также подумать о том, что может иметь смысл для пользователей, реализующих их для других распределители.

Вы имели в виду Alloc::alloc_array(0) ?

Да, прости.

Обратите внимание, что эта черта может быть реализована пользователями для своих собственных распределителей, и что эти пользователи могут переопределить реализации этих методов по умолчанию. Поэтому при рассмотрении вопроса о том, должно ли это возвращать Err для арифметического переполнения или нет, следует не только сосредоточиться на том, что делает текущая реализация метода по умолчанию, но также подумать о том, что может иметь смысл для пользователей, реализующих их для других распределители.

Понятно, но для реализации Alloc требуется unsafe impl а разработчики должны соблюдать правила безопасности, упомянутые в https://github.com/rust-lang/rust/issues/32838#issuecomment -467093527 .

Каждый API, указывающий здесь на проблему отслеживания, является признаком Alloc или связан с признаком Alloc . @ rust-lang / libs, считаете ли вы полезным оставить его открытым в дополнение к https://github.com/rust-lang/rust/issues/42774?

Простой исходный вопрос: какова мотивация гибкости ZST? Мне кажется, что, учитывая, что во время компиляции мы знаем, что тип является ZST, мы можем полностью оптимизировать как выделение (для возврата постоянного значения), так и освобождение. Учитывая это, мне кажется, что мы должны сказать одно из следующего:

  • Разработчик всегда должен поддерживать ZST, и он не может вернуть Err для ZST.
  • Всегда UB выделяет ZST, и в этом случае ответственность за короткое замыкание несет вызывающий абонент.
  • Есть своего рода метод alloc_inner который реализует вызывающий объект, и метод alloc с реализацией по умолчанию, который выполняет короткое замыкание; alloc должен поддерживать ZST, но alloc_inner НЕ может вызываться для ZST (это просто для того, чтобы мы могли добавить логику короткого замыкания в одном месте - в определении признака - по порядку чтобы сэкономить разработчикам некоторый шаблон)

Есть ли причина, по которой необходима гибкость, которую мы имеем с текущим API?

Есть ли причина, по которой необходима гибкость, которую мы имеем с текущим API?

Это компромисс. Вероятно, свойство Alloc используется чаще, чем реализовано, поэтому имеет смысл максимально упростить использование Alloc, предоставив встроенную поддержку ZST.

Это будет означать, что разработчики трейта Alloc должны будут позаботиться об этом, но для меня важнее то, что тем, кто пытается развить трейт Alloc, нужно будет помнить о ZST при каждом изменении API. Это также усложняет документацию API, объясняя, как обрабатываются ZST (или могут быть, если они «определены реализацией»).

Распределители C ++ придерживаются этого подхода, когда распределитель пытается решить множество различных проблем. Это не только усложнило их реализацию и развитие, но и усложнило их практическое использование пользователями из-за того, как все эти проблемы взаимодействуют в API.

Я думаю, что обработка ZST и выделение / освобождение необработанной памяти - это две ортогональные и разные проблемы, и поэтому мы должны сохранять простой API-интерфейс Alloc, просто не обрабатывая их.

Пользователи Alloc, такие как libstd, должны будут обрабатывать ZST, например, для каждой коллекции. Это определенно проблема, которую стоит решить, но я не думаю, что черта Alloc подходит для этого. Я бы ожидал, что утилита для решения этой проблемы появится в libstd по необходимости, и когда это произойдет, мы, возможно, сможем попробовать RFC для такой утилиты и выставить ее в std :: heap.

Все это звучит разумно.

Я думаю, что обработка ZST и выделение / освобождение необработанной памяти - это две ортогональные и разные проблемы, и поэтому мы должны сохранять простой API-интерфейс Alloc, просто не обрабатывая их.

Разве это не означает, что мы должны сделать так, чтобы API явно не обрабатывал ZST, а не определялся реализацией? ИМО, ошибка «неподдерживаемая» не очень полезна во время выполнения, поскольку подавляющее большинство вызывающих не смогут определить запасной путь, и поэтому им придется предположить, что ZST в любом случае не поддерживаются. Кажется чистым просто упростить API и объявить, что они _ никогда_ не поддерживаются.

Будет ли специализация использоваться пользователями alloc для обработки ZST? Или просто if size_of::<T>() == 0 чеки?

Будет ли специализация использоваться пользователями alloc для обработки ZST? Или просто if size_of::<T>() == 0 чеки?

Последнего должно быть достаточно; соответствующие пути кода будут тривиально удалены во время компиляции.

Разве это не означает, что мы должны сделать так, чтобы API явно не обрабатывал ZST, а не определялся реализацией?

Для меня важным ограничением является то, что если мы запрещаем выделение памяти нулевого размера, методы Alloc должны иметь возможность предполагать, что переданный им Layout не имеет нулевого размера.

Есть несколько способов добиться этого. Можно было бы добавить еще одно предложение Safety ко всем методам Alloc указав, что если Layout имеет нулевой размер, поведение не определено.

В качестве альтернативы мы могли бы запретить Layout s нулевого размера, тогда в Alloc не нужно ничего говорить о распределении нулевого размера, поскольку это не может безопасно произойти, но это будет иметь некоторые недостатки.

Например, некоторые типы, такие как HashMap создают Layout из нескольких Layout s, и хотя последний Layout может иметь ненулевой размер, промежуточные могут быть (например, в HashSet ). Таким образом, этим типам нужно будет использовать «что-то еще» (например, тип LayoutBuilder ), чтобы создать свои окончательные Layout s, и заплатить за чек «ненулевого размера» (или использовать метод _unchecked ) при преобразовании в Layout .

Будет ли специализация использоваться пользователями alloc для обработки ZST? Или просто если size_of ::() == 0 проверок?

Мы пока не можем специализироваться на ZST. Прямо сейчас весь код использует size_of::<T>() == 0 .

Есть несколько способов добиться этого. Можно было бы добавить еще одно предложение Safety ко всем методам Alloc указав, что если Layout имеет нулевой размер, поведение не определено.

Было бы интересно подумать, есть ли способы сделать это гарантией времени компиляции, но даже debug_assert что макет имеет ненулевой размер, должно быть достаточно, чтобы отловить 99% ошибок.

Я не обращал внимания на дискуссии о распределителях, так что сожалею об этом. Но мне давно хотелось, чтобы у распределителя был доступ к типу значения, которое он распределяет. Могут быть конструкции распределителей, которые могли бы его использовать.

Тогда у нас, вероятно, будут те же проблемы, что и у C ++ и его api распределителя.

Но мне давно хотелось, чтобы у распределителя был доступ к типу значения, которое он распределяет. Т

Зачем тебе это нужно?

@gnzblg @brson Сегодня у меня был потенциальный вариант использования, когда я кое-что узнал о типе выделяемого значения.

Я работаю над глобальным распределителем, который может переключаться между тремя базовыми распределителями - локальным потоком, глобальным с блокировками и одним для использования сопрограмм - идея в том, что я могу ограничить сопрограмму, представляющую сетевое соединение, до максимума. объем используемой динамической памяти (при отсутствии возможности управлять распределителями в коллекциях, особенно в стороннем коде) *.

Возможно, было бы удобно узнать, выделяю ли я значение, которое может перемещаться между потоками (например, Arc), или значение, которое не будет. А может и нет. Но это возможный сценарий. На данный момент у глобального распределителя есть переключатель, который пользователь использует, чтобы указать ему, из какого распределителя делать распределения (не требуется для перераспределения или освобождения; мы можем просто посмотреть адрес памяти для этого).

* [Это также позволяет мне использовать локальную память NUMA везде, где это возможно, без какой-либо блокировки и, с моделью 1 ядро ​​1 поток, ограничивать общее использование памяти].

@raphaelcohn

Я работаю над глобальным распределителем

Я не думаю, что что-либо из этого может (или могло бы) применяться к трейту GlobalAlloc , а у трейта Alloc уже есть общие методы, которые могут использовать информацию о типе (например, alloc_array<T>(1) выделяет один T , где T - это фактический тип, поэтому распределитель может учитывать этот тип при выполнении распределения). Я думаю, что для целей этого обсуждения было бы более полезно увидеть код, реализующий распределители памяти, которые используют информацию о типах. Я не слышал хороших аргументов в пользу того, почему эти методы должны быть частью некоторой общей черты распределителя, а не просто быть частью API распределителя или какой-либо другой чертой распределителя.

Я думаю, было бы также очень интересно узнать, какие из типов, параметризованных с помощью Alloc , вы собираетесь комбинировать с распределителями, использующими информацию о типах, и каков вы ожидаете результата.

AFAICT, единственный интересный тип для этого - Box потому что он выделяет T напрямую. Практически все другие типы в std никогда не выделяют T , а выделяют некоторый частный внутренний тип, о котором ваш распределитель ничего не может знать. Например, Rc и Arc могут выделить (InternalRefCounts, T) , List / BTreeSet / и т. Д. Выделить типы внутренних узлов, Vec / Deque / ... выделить массивы T s, но не сами T s и т. Д.

Для Box и Vec мы могли бы добавить обратно совместимыми способами черту BoxAlloc и ArrayAlloc с общими импами для Alloc которые могли бы специализируются на том, чтобы перехватить их поведение, если когда-либо возникнет необходимость в решении этих проблем общим способом. Но есть ли причина, по которой предоставление ваших собственных типов MyAllocBox и MyAllocVec которые вступают в сговор с вашим распределителем для использования информации о типах, не является жизнеспособным решением?

Поскольку теперь у нас есть специальный репозиторий для рабочей группы распределителей , а список в OP устарел, этот вопрос может быть закрыт, чтобы обсуждение и отслеживание этой функции было в одном месте?

Хороший замечание @TimDiekmann! Я собираюсь пойти дальше и закрыть это в пользу обсуждений в этом репозитории.

Это все еще проблема отслеживания, на которую указывает какой-то атрибут #[unstable] . Я думаю, что его не следует закрывать, пока эти функции не будут либо стабилизированы, либо не рекомендованы к использованию. (Или мы могли бы изменить атрибуты, чтобы указать на другую проблему.)

Да, нестабильные функции, указанные в git master, обязательно должны иметь открытую проблему отслеживания.

Согласовано. Также добавлено уведомление и ссылка на ОП.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги