Rust: 分配器特征和std :: heap

创建于 2016-04-08  ·  412评论  ·  资料来源: rust-lang/rust

feature此功能有一个专门的工作组,请将评论和疑虑直接提交给工作组的仓库

原始帖子:


FCP提案: https :
FCP复选框: https :


rust-lang / rfcs#1398和std::heap模块的跟踪问题。

  • [x]将struct Layouttrait Allocator和默认实现放入alloc板条箱(https://github.com/rust-lang/rust/pull/42313)
  • [x]决定零件应该放在哪里(例如,默认impls依赖于alloc板箱,但是Layout / Allocator _could_位于libcore ...中) (https://github.com/rust-lang/rust/pull/42313)
  • 源代码中的[] fixme:审计默认实现(在Layout用于溢出错误,(可能会根据需要切换到overflowing_add和overflowing_mul)。
  • [x]决定是否应将realloc_in_place替换为grow_in_placeshrink_in_placecomment )(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认为由于“ fits”的定义,此问题已在https://github.com/rust-lang/rust/pull/42313中解决)
  • []与上一个问题类似:是否需要以与您分配的确切对齐方式进行分配? (见2017年6月5日的评论)
  • [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

https://github.com/rust-lang/rust/pull/42313之后的std::heap

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最初发展的人们感到意外。

我不会不同意您的观点,但是尽管如此,似乎仍有相当一部分人愿意承受Result的“沉重”负担,而这可能会增加错过null的机会,检查返回的值。

  • 我忽略了ABI带来的运行时效率问题,因为我像@alexcrichton一样,假设我们可以通过编译器技巧以某种方式处理这些问题。

有没有一些方法,我们可以得到关于自己的延迟变化的知名度越来越高?

一种方法(不在我的脑海中):现在在PR上的master分支上自己更改签名,而Allocator仍然不稳定。 然后查看谁在PR上抱怨(以及谁在庆祝!)。

  • 这太过分了吗? 从定义上看,它似乎比将这种变化与稳定化结合起来没有那么费力。

所有412条评论

不幸的是,我在RFC讨论中没有给予足够的重视以提及此问题,但我认为应该用两个函数grow_in_placeshrink_in_place来代替realloc_in_place原因:

  • 我想不出一个单独的用例(缺少实现reallocrealloc_in_place ),在这种情况下,分配的大小是增加还是减少。 使用更专业的方法可以使事情更加清晰。
  • 增长和收缩分配的代码路径趋向于根本不同-增长涉及测试相邻的内存块是否空闲并声明它们的所有权,而收缩则涉及分割适当大小的子块并释放它们。 尽管realloc_in_place内部的分支成本很小,但使用growshrink更好地捕获分配器需要执行的不同任务。

请注意,可以将这些功能向后兼容地添加到realloc_in_place旁边,但这将限制默认情况下根据其他功能实现的功能。

为了保持一致, realloc可能还希望分为growsplit ,但这是我所知道的具有可重载的realloc函数的唯一优点。能够使用mmap的remap选项,该选项没有这种区别。

此外,我认为reallocrealloc_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 { .. } ,以便每个常规分配器都可以充当所有对象的特定于对象的分配器。

未来的工作将是修改集合以将您的特征用于它们的节点,而不是直接使用普通的ole(通用)分配器。

@pnkfelix

(在这一点上,我正在等待#[may_dangle]支持将火车驶入Beta通道,以便随后我可以将其用于std集合,作为分配器集成的一部分)

我想这发生了吗?

@ Ericson2314是的,出于实验目的,我自己写绝对是一种选择,但是我认为在互操作性方面进行标准化可以带来很多好处(例如,我计划还实现一个slab分配器,但是如果我的代码的第三方用户可以在我的杂志缓存层中使用某人_else's_ slab分配器,则很好。 我的问题仅仅是ObjectAllocator<T>特质或类似的特征是否值得讨论。 尽管似乎最好使用其他RFC? 我对一个RFC属于多少以及何时属于单独的RFC的准则并不十分熟悉...

@joshlf

该提案的对象分配器类型或特征在哪里适合? 会留给将来的RFC吗? 还有吗

是的,这将是另一个RFC。

我对一个RFC属于多少以及何时属于单独的RFC的准则并不十分熟悉...

这取决于RFC本身的范围,该范围由编写者决定,然后每个人都会给出反馈。

但是实际上,由于这是已经接受的RFC的跟踪问题,因此对于该线程而言,考虑扩展和设计更改并不是真的。 您应该在RFC仓库上重新打开一个新的。

@joshlf啊,我认为ObjectAllocator<T>应该是一个特征。 我的意思是原型特征不是特定的分配器。 是的,正如@steveklabnik所说,该特征值得拥有自己的RFC。


@steveklabnik是的,现在在其他地方讨论会更好。 但是@joshlf也提出了这个问题,以免暴露出公认的但未实现的API设计中迄今为止无法预料的缺陷。 从这个意义上讲,它与该线程中较早的帖子匹配。

@ Ericson2314是的,我认为那是您的意思。 我想我们在同一页上:)

@steveklabnik听起来不错; 我将讨论自己的实现并提交RFC(如果最终看起来不错)。

@joshlf我没有任何理由将自定义分配器放入编译器或标准库。 一旦该RFC生效,您就可以轻松地发布自己的板条箱,该板条箱可以进行任意类型的分配(即使是像jemalloc这样的成熟分配器也可以自定义实现!)。

@alexreg这与特定的自定义分配器Allocator )是任何低级分配器的类型一样,我要询问的特征( ObjectAllocator<T> )是任何分配器的类型。可以分配/取消分配和构造/删除类型T

@alexreg请参阅我有关将标准库集合与自定义对象特定的分配器一起使用的早期知识。

可以,但是我不确定这是否属于标准库。 可以轻松放入另一个箱子,而不会损失功能或可用性。

2017年1月4日,21:59,Joshua Liebow-Feeser [email protected]写道:

@alexreg https://github.com/alexreg这不是关于特定的自定义分配器,而是一个特性,它指定所有在特定类型上参数化的分配器的类型。 因此,就像RFC 1398定义了任何低级分配器类型的特征(Allocator)一样,我要询问的是特征(ObjectAllocator),即可以分配/取消分配和构造/删除类型T的任何分配器的类型。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499064上查看,或使线程https://github.com/notifications/unsubscribe-auth/静音

我认为您想将标准库集合(任何堆分配的值)与任意自定义分配器一起使用; 即不限于特定对象的。

2017年1月4日,22:01,John Ericson [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建议的内容)。

我认为您想将标准库集合(任何堆分配的值)与任意自定义分配器一起使用; 即不限于特定对象的。

理想情况下,您希望两者都接受两种类型的分配器。 使用特定于对象的缓存有非常明显的好处。 例如,平板分配和杂志缓存都具有非常显着的性能优势-如果您好奇的话,请查看我上面链接的论文。

但是对象分配器特征可以只是通用分配器特征的子特征。 就我而言,就这么简单。 当然,某些类型的分配器可能比通用分配器更有效,但是编译器和标准都不需要(或者确实应该)知道这一点。

2017年1月4日22:13,Joshua Liebow-Feeser [email protected]写道:

可以,但是我不确定这是否属于标准库。 可以轻松放入另一个箱子,而不会损失功能或可用性。

是的,但是您可能希望某些标准库功能依赖它(例如, @ Ericson2314 https://github.com/Ericson2314建议的内容)。

我认为您想将标准库集合(任何堆分配的值)与任意自定义分配器一起使用; 即不限于特定对象的。

理想情况下,您希望两者都接受两种类型的分配器。 使用特定于对象的缓存有非常明显的好处。 例如,平板分配和杂志缓存都具有非常显着的性能优势-如果您好奇的话,请查看我上面链接的论文。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270502231上查看,或使线程https://github.com/notifications/unsubscribe-auth/静音

但是对象分配器特征可以只是通用分配器特征的子特征。 就我而言,就这么简单。 当然,某些类型的分配器可能比通用分配器更有效,但是编译器和标准都不需要(或者确实应该)知道这一点。

嗯,所以问题在于语义不同。 Allocator分配并释放原始字节Blob。 另一方面, ObjectAllocator<T>会分配已经构造的对象,并且还负责删除这些对象(包括能够缓存构造的对象,这些对象稍后可以在构造新分配的对象时分发) ,这很昂贵)。 特质看起来像这样:

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

这与Allocator不兼容, Allocator ,调用者有责任先释放对象的drop 。 这真的很重要-了解类型T允许ObjectAllocator<T>做诸如调用Tdrop方法以及从free(t)t移入free ,呼叫者_cannot_首先丢掉t -而是ObjectAllocator<T>的责任。 从根本上说,这两个特征是互不相容的。

嗯,我明白了。 我认为该建议已经包含了类似的内容,即在字节级别上的“更高级别”分配器。 在这种情况下,一个完美的建议!

2017年1月4日,22:29,Joshua Liebow-Feeser [email protected]写道:

但是对象分配器特征可以只是通用分配器特征的子特征。 就我而言,就这么简单。 当然,某些类型的分配器可能比通用分配器更有效,但是编译器和标准都不需要(或者确实应该)知道这一点。

嗯,所以问题在于语义不同。 分配器分配并释放原始字节Blob。 对象分配器另一方面,将分配已经构造的对象,并且还负责删除这些对象(包括能够缓存构造的对象,这些对象可以稍后在构建新分配的对象时分发出去,这很昂贵)。 特质看起来像这样:

特质ObjectAllocator{
fn alloc()-> T;
fn free(t T);
}
这与分配器不兼容,分配器的方法处理原始指针并且没有类型概念。 此外,对于分配器,调用者有责任先删除要释放的对象。 这真的很重要-了解T类型允许ObjectAllocator做诸如调用T的drop方法之类的事情,由于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,我相信它将获得大量支持! 并感谢您的澄清(我完全没有跟上该RFC的细节)。

2017年1月5日,00:53,Joshua Liebow-Feeser [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

用于测试自定义分配器的板条箱将很有用。

如果我遗漏了一些明显的内容,请原谅我,但是有没有理由在此RFC中描述的Layout特性不实现Copy以及Clone ,因为这仅仅是荚?

我想不到。

很抱歉在流程中提出这么晚,但是...

可能值得增加对类似于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还是比较陌生,因此就必须编写代码来审查新手的代码而言,这可能只会为维护人员带来更多工作。 我不想踩任何人的脚趾,也不想为人们做更多的工作。

好的,我们最近见过面来评估分配器的状态,我认为

我们讨论的一件事是,由于可能的推理问题,更改所有libstd类型可能为时过早,但是无论如何,将Allocator特性和Layout并入似乎是一个好主意。 std::heap生态系统中的模块进行实验别处!

@joshlf如果您想在这里提供帮助,我认为

@alexcrichton我认为您的链接已断开? 它指向这里。

我们讨论的一件事是,由于可能的推断问题,更改所有libstd类型可能为时过早

添加特性是一个很好的第一步,但是如果不重构现有的API来使用它,它们的使用就不会太多。 在https://github.com/rust-lang/rust/issues/27336#issuecomment -300721558中,我建议我们可以立即重构立面后面的板条箱,但在std添加新包装。 烦人,但能让我们取得进步。

@alexcrichton获取对象分配器的过程是什么? 到目前为止,我的实验(即将公开;如果您感到好奇,我可以将您添加到私人GH回购中),这里的讨论使我相信分配器特征和对象之间将有近乎完美的对称性分配器特征。 例如,你有这样的事情(我改变Address*mut u8与对称性*mut TObjectAllocator<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希望提供一个提供Allocator特性和Layout类型的PR,即使它没有集成到libstd任何集合中。

如果我正确理解这一点,那么我可以为此建立一个PR。 我之所以没有这样做,是因为我一直试图至少与原型RawVecVec集成。 (目前我已经完成了RawVec ,但是Vec更具挑战性,因为许多其他结构都基于它构建,例如DrainIntoIter等...)

实际上,我当前的分支似乎可以实际构建(并通过了与RawVec集成测试),所以我继续将其发布:#42313

@hawkw问:

如果我遗漏了一些明显的内容,请原谅我,但是由于RFC所描述的Layout特性只是POD,所以没有实现Copy和Clone的原因吗?

我使Layout只实现Clone而不是Copy是我想保持开放性,为Layout类型添加更多结构。 特别是,我仍然对尝试Layout尝试跟踪用于构造它的任何类型结构(例如struct { x: u8, y: [char; 215] } 16数组)的尝试感兴趣,以便分配器可以选择公开报告其当前内容来自哪种类型的检测例程。

这几乎肯定是必须具有的可选功能,也就是说,似乎潮流正反对强制开发人员使用类型丰富的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的对齐要求是什么,并且Windows上当前dealloc实现使用align来确定如何正确地免费。 @ruuda您可能对此事实感兴趣。

这里的稳定点也是“确定对fn dealloc的对齐要求是什么,并且Windows上当前的dealloc实现使用align来确定如何正确地免费。

是的,我认为这是我最初遇到此问题的方式; 因此,我的程序在Windows上崩溃了。 由于HeapAlloc不提供对齐保证,因此allocate分配更大的区域并将原始指针存储在标头中,但是作为优化,如果仍然可以满足对齐要求,可以避免这种情况。 我想知道是否有一种方法可以将HeapAlloc转换为不需要对齐就可以对齐的分配器,而又不会失去这种优化。

@ruuda

由于HeapAlloc不作对齐保证

它确实为32位提供了8字节的最小对齐保证,为64位提供了16字节的最小对齐保证,只是没有提供任何方法来保证更高的对齐。

Windows上CRT提供的_aligned_malloc可以提供更高对齐的分配,但是值得注意的是,它必须_aligned_free配对,因为free是非法的。 因此,如果您不知道分配是通过malloc还是_aligned_malloc那么您就陷入了与Windows上alloc_system相同的难题。不知道deallocate的对齐方式。 CRT没有提供可以与free配对的标准aligned_alloc函数,因此即使Microsoft也无法解决此问题。 (尽管它一个C11函数,并且Microsoft不支持C11,所以这是一个很弱的参数。)

请注意, deallocate只关心对齐方式,以了解它是否过度对齐,实际值本身无关紧要。 如果您想要真正独立于对齐的deallocate ,则可以简单地将所有分配视为过度对齐,但是在小分配上会浪费很多内存。

@alexcrichton写道

当前Allocator::oom的签名为:

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

但是,提请我注意的是,Gecko至少也喜欢了解OOM上的分配大小。 我们可能希望在稳定时添加诸如Option<Layout>类的上下文为何发生OOM。

AllocErr已经带有LayoutAllocErr::Exhausted变种。 我们也可以只将LayoutAllocErr::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,作为讨论零大小分配是否需要与其要求的对齐方式匹配的特定问题的地方。

(哦,等等,零大小的分配在用户分配器中是非法的!)

由于alloc::heap::allocate函数和朋友现在在Nightly中消失了,因此我更新了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 IMO在优化这种低级API的人机工程学方面没有多大意义。

@SimonSapin

我觉得人体工程学不是很好。 我们从导入一个项目到从两个不同的模块导入三个项目。

我对我的代码库有完全相同的感觉-现在很笨拙。

allocator.alloc(Layout::from_size_align(…))?使用便捷方法是否有意义

您是指Alloc特质,还是仅代表Heap ? 这里要考虑的一件事是,现在存在第三个错误条件: Layout::from_size_align返回Option ,因此除分配时可以得到的常规错误外,它还可能返回None

或者, Alloc特性是否在前奏中,还是用例的利基?

IMO对优化这种低级API的人体工程学没有多大意义。

我同意这可能太低级了,无法前奏,但是我仍然认为优化人体工程学是有价值的(至少是自私的-这真是一个令人讨厌的重构)。

@SimonSapin您之前没有处理过OOM吗? 同样在std所有三种类型都可以在std::heap模块中使用(它们应该在一个模块中)。 您还没有处理过尺寸过大的情况吗? 还是零尺寸类型?

您以前没有处理OOM吗?

当它存在时, alloc::heap::allocate函数返回一个没有Result的指针,并且在OOM处理中没有任何选择。 我认为它中止了这一过程。 现在,我添加了.unwrap()来恐慌线程。

他们应该在一个模块中

我现在看到heap.rs包含pub use allocator::*; 。 但是,当我单击rustdoc页面上列出的impl中的Alloc以获取Heap我被发送到alloc::allocator::Alloc

至于其余的,我还没有研究。 我正在向新的编译器移植很多年前编写的代码。 我认为这些是FreeType(一个C库)的回调。

当它存在时,alloc :: heap :: allocate函数返回没有结果的指针,并且在OOM处理中没有选择。

它确实给了您一个选择。 它返回的指针可能是空指针,这表明堆分配器分配失败。 这就是为什么我很高兴它切换到Result所以人们不会忘记处理这种情况。

哦,好吧,也许FreeType最终进行了空检查,我不知道。 无论如何,是的,返回结果是好的。

给定#30170和#43097,我很想通过简单地指定用户不能请求> = 1 << 32对齐来解决OS X问题。

强制执行此操作的一种非常简单的方法:更改Layout接口,以使alignu32而不是usize

@alexcrichton您对此有想法吗? 我应该做一个做这件事的公关吗?

@pnkfelix Layout::from_size_align仍然会占用usize并在u32溢出时返回错误,对吗?

@SimonSapin如果静态的先决条件是传递值> = 1 << 32是不安全的,那么有什么理由让它继续执行usize align?

如果答案是“好一些分配器可能支持对齐方式> = 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 {
    // ...
}

值得注意的是:

  • 只有稳定allocalloc_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提出了一个有趣的观点,即可能分离AllocDealloc特征以优化alloc需要一些数据,但是dealloc不需要额外的信息,因此Dealloc类型的大小可以为零。

这个问题曾经解决过吗? 分离两个特征的缺点是什么?

@joshlf

这是否消除了分配器指定不支持的选项?

是的,不是,这意味着不立即在稳定的rust中支持这种操作,但是我们可以在不稳定的Rust中继续支持它。

您是说要真正删除掉那些遗留的东西,还是只是因为不稳定而留给他们?

确实! 同样,尽管我只是建议一个稳定的API表面积,但是我们可以将所有其他方法保持为不稳定状态。 随着时间的流逝,我们可以继续稳定更多的功能。 我认为最好从保守的角度开始。


@SimonSapin

这是今天AllocErr :: Exhausted的构造函数吗? 如果是这样,它是否不应该具有Layout参数?

哈哈好点! 我有点想保留如果确实需要将Error设为零大小类型的可能性,但是我们当然可以使布局采用方法保持不稳定并在必要时使其稳定。 还是您认为保留布局的Error应该在第一遍中稳定下来?


@cramertj

我个人还没有看到这样的问题/担忧(我想我错过了!),但是我个人认为这不是值得的。 一般而言,两个特征是样板的两倍,例如,现在每个人都必须在集合中键入Alloc + Dealloc 。 我希望这样的专门用途不会希望通知其他所有最终用户亲自使用的界面。

@cramertj @alexcrichton

我个人还没有看到这样的问题/担忧(我想我错过了!),但是我个人认为这不是值得的。

总的来说,我同意这样做是不值得的,但有一个明显的例外: BoxBox<T, A: Alloc>会,给定的当前定义Alloc ,都为至少两个词大(指针它已经具有与所涉及的参考Alloc起码),但全局单例(可以实现为ZST)除外。 存储这种常见和基本类型所需的空间中的2倍(或更多)爆炸对我来说很重要。

@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>>>,
}

与ZST Alloc相比,由具有1个单词Alloc的树构造的树在整个数据结构中的大小将增加约1.7倍。 这对我来说似乎很糟糕,而这类应用程序正是将Alloc作为特征的全部意义所在。

@cramertj

我们可以添加如下内容:

我们还将拥有实际的特征别名:) https://github.com/rust-lang/rust/issues/41517

@glaebhoerl是的,但是由于还没有实现,因此稳定似乎还有一段Allocator手动提示,我认为我们可以在到达时向后兼容地切换到特征别名;)

@joshlf

存储这种常见和基本类型所需的空间中的2倍(或更多)爆炸对我来说很重要。

我以为今天所有的实现都是零大小的类型或大的指针,对吗? 某些指针大小类型可以为零大小的可能的优化不是吗? (或类似的东西?)


@cramertj

我们可以添加如下内容:

确实! 然后,我们将一个特征变为三个。 过去,我们从未有过具有此类特征的丰富经验。 例如, Box<Both>不转换为Box<OnlyOneTrait> 。 我敢肯定,我们可以等待语言功能使所有这些变得平稳,但是看来这些功能距离最佳还有很长的路要走。


@SimonSapin

稳定整个事情还需要解决什么?

我不知道。 我想从绝对最小的事情开始,这样就减少了辩论。

我以为今天所有的实现都是零大小的类型或大的指针,对吗? 某些指针大小类型可以为零大小的可能的优化不是吗? (或类似的东西?)

是的,这个想法是,给定一个指向从您的分配器类型分配的对象的指针,您可以找出它来自哪个实例(例如,使用内联元数据)。 因此,您需要取消分配的唯一信息是类型信息,而不是运行时信息。

回到关于分配的对齐方式,我看到两种方法:

  • 根据建议进行稳定(在分配时对齐)。 除非包含Layout否则不可能放弃手动分配的内存的所有权。 特别是,不可能以比要求更严格的对齐方式构建VecBoxString或其他std容器(例如,因为您没有不想让盒装元素跨越缓存行),而不必稍后手动对其进行解构和释放(这并非总是一种选择)。 另一个不可能的示例是使用simd操作填充Vec ,然后将其赠送。

  • 不需要在解除分配上对齐,并从基于Windows的HeapAllocalloc_system删除小分配优化。 始终存储路线。 @alexcrichton ,在提交该代码时,您还记得为什么将其放在第一位吗? 我们是否有证据表明它为实际应用程序节省了大量内存? (使用微基准,可以根据分配大小使结果以任何一种方式显示出来-除非HeapAlloc仍然对大小进行四舍五入。)

无论如何,这是一个非常困难的权衡; 内存和性能的影响将在很大程度上取决于应用程序的类型,并且针对哪个应用程序进行优化也是特定于应用程序的。

我认为我们实际上可能是Just Fine(TM)。 引用Alloc docs

一些方法要求布局适合存储块。
布局“适合”内存块的含义是什么(或
等效地,对于要“适合”布局的存储块而言)是
必须满足以下两个条件:

  1. 块的起始地址必须与layout.align()对齐。

  2. 块的大小必须在[use_min, use_max]范围内,其中:

    • use_minself.usable_size(layout).0 ,并且

    • use_max是曾经(或曾经)的容量
      当(如果)通过调用分配块时返回
      alloc_excessrealloc_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 :

例如, Box<Both>不转换为Box<OnlyOneTrait> 。 我敢肯定,我们可以等待语言功能使所有这些变得平稳,但是看来这些功能距离最佳还有很长的路要走。

另一方面,特质对象的转换似乎毫无争议地是可取的,并且主要是实现它的工作量/带宽/意愿。 最近有一个线程: https :

@ruuda我是最初编写alloc_system实现的人。 alexcrichton只是在<time period>的出色分配器重构期间移动了它。

当前实现要求您使用与分配给定内存块相同的对齐方式进行释放。 无论文档要求什么,这都是当前的现实,每个人都必须遵守,直到Windows上的alloc_system被更改为止。

Windows上的分配始终使用MEMORY_ALLOCATION_ALIGNMENT的倍数(尽管他们记得您为字节分配的大小)。 MEMORY_ALLOCATION_ALIGNMENT在32位上为8,在64位上为16。 对于过度对齐的类型,由于对齐大于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

我个人认为,今天在Windows上实现alloc_system比具有将分配的所有权放弃到另一个容器(如Vec的能力更大。 AFAIK虽然没有数据可测量始终填充对齐方式而无需对齐对释放的影响。

@joshlf

我认为该评论是错误的,正如在Windows上指出的alloc_system依赖于传递给取消分配的对齐方式一样。

考虑到99.99%的分配不会过度对齐,您是否真的要在所有这些分配上产生这种开销?

开销取决于应用程序,还是要针对内存或性能进行优化,取决于应用程序。 我的怀疑是,对于大多数应用程序来说,它们都很好,但是少数少数人对内存深为关注,而它们确实负担不起这些额外的字节。 另外一小部分人需要控制对齐,而他们确实需要对齐。

@alexcrichton

我认为该评论是错误的,正如在Windows上指出的alloc_system依赖于传递给取消分配的对齐方式一样。

这是否意味着Windows上的alloc_system实际上没有正确实现Alloc特性(因此也许我们应该更改Alloc特性的要求)?


@ retep998

如果我正确地阅读了您的评论,那么是否要为所有分配提供对齐开销,而不管我们是否需要能够以其他对齐方式进行分配? 也就是说,如果我以64字节对齐方式分配64字节,并且还以64字节对齐方式取消分配,则您描述的开销仍然存在。 因此,它不是能够以不同的对齐方式进行重新分配的功能,而是请求比正常对齐更大的功能。

@joshlf当前由alloc_system引起的开销是由于请求比正常更大的对齐方式。 如果您的对齐方式小于或等于MEMORY_ALLOCATION_ALIGNMENT ,则不会有alloc_system引起的开销。

但是,如果我们更改实现以允许以不同的对齐方式几乎适用于

我知道了说得通。

对Heap和&Heap都实现Alloc是什么意思? 在什么情况下,用户将使用其中一个提示与另一个提示?

这是第一个标准库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但是实现不需要。 值得注意的是,这意味着像HeapSystem是线程安全的,在分配时不需要同步。

但是,更重要的是,使用#[global_allocator]要求它所附加的静态类型T ,具有Alloc for &T 。 (aka所有全局分配器必须是线程安全的)

对于*mut u8我认为*mut ()可能很有趣,但是我个人并不觉得自己被迫“正确地做到这一点”。

*mut u8的主要优点是使用带字节偏移量的.offset非常方便。

对于*mut u8我认为*mut ()可能很有趣,但是我个人并不觉得自己必须自己“正确解决”。

如果我们在稳定的接口中使用*mut u8 ,我们是否就锁定自己了? 换句话说,一旦我们稳定了这一点,将来我们将没有机会“做到这一点”。

另外,如果将来我们进行类似RFC 2040的优化, *mut ()对我来说似乎有点危险。

*mut u8的主要优点是,使用.offset和字节偏移量非常方便。

是的,但是您可以轻松地执行let ptr = (foo as *mut u8) ,然后按照自己的喜好去做。 如果有引人注目的替代方案,那么在API中坚持使用*mut u8似乎没有足够的动机(公平地说,我不确定是否存在)。

另外,如果将来我们进行类似RFC 2040的优化,* mut()对我来说似乎有点危险。

这种优化可能永远不会发生-它会破坏太多现有代码。 即使这样做,它也会应用于&()&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

这对像对象这样的结构hack有用吗? 假设我有一个看起来像这样的Node<T>

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

和值类型:

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

现在,我想在一次分配中分配大小为7的字符串Node<V> 。 理想情况下,我想分配大小为16的align 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编译器认为不是打包的打包。但是,您_can_可以告诉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提供详细答案! 我的用例的TLDR是,我可以得到大小为16的Node<V> ,但前提是V是repr(packed) 。 否则,我能做的最好的事情就是19号(12 + 7)。

@SimonSapin不确定; 我会尝试。

还没有真正跟上这个线程,但我坚决反对尚未稳定东西。 我们在解决棘手问题方面尚未取得进展:

  1. 分配者多态集合

    • 甚至没有non肿的盒子!

  2. 可疑的收藏

我认为基本特征的设计影响这些特征的解决方案:在过去的几个月中,我很少有时间在Rust上,但有时会对此争论不休。 我怀疑我是否还有时间在这里充分说明自己的观点,所以我只能希望我们至少首先为所有这些问题写一个完整的解决方案:有人证明我错了,不可能严格(强制正确使用),灵活,以及符合当前特征的人体工程学。 甚至只需检查顶部的框即可。

回复: @ Ericson2314的评论

我认为与该观点和@alexcrichton稳定某物的愿望之间的冲突有关的一个相关问题是:稳定最小接口可以带来多少收益? 特别是,很少有消费者会直接调用Alloc方法(即使大多数集合都可能会使用Box或其他类似的容器),所以真正的问题变成了:稳定购买力的用户不直接调用Alloc方法? 老实说,我能想到的唯一严重的用例是,它为分配器多态集合(为更广泛的用户使用)铺平了道路,但似乎在#27336上受阻,这与正在解决。 也许我还缺少其他大型用例,但基于快速分析,我倾向于摆脱稳定性,因为它仅具有边际优势,但以将我们锁定在设计中为代价,后来我们发现其次优。

@joshlf它允许人们定义和使用自己的全局分配器。

嗯,好点。 是否可以在不稳定Alloc情况下稳定指定全局分配器? 即,实现Alloc的代码必须是不稳定的,但是可能会封装在自己的板条箱中,并且将该分配器标记为全局分配器的机制本身也会保持稳定。 还是我误解了稳定/不稳定和稳定的编译器/夜间编译器如何相互作用?

@joshlf记住,按照https://github.com/rust-lang/rust/issues/42774#issuecomment -317279035的说法,# 其他问题-存在这些特征的问题,这就是为什么我现在要开始着手解决这个问题的原因。 一旦讨论了这些问题,大家讨论起来比讨论#27336之后的预期期货要容易得多。

@joshlf但是您不能使用稳定的编译器来编译定义全局分配器的板条箱。

@sfackler嗯,是的,我很害怕误解:P

我发现名称Excess(ptr, usize)有点令人困惑,因为usize不是请求分配大小的excess (而是分配的额外大小),而是total分配的大小。

IMO TotalRealUsable或任何表示大小是分配总大小或实际大小的名称都比“超额”好,我发现误导。 _excess方法也是如此。

我同意上面的@gnzlbg ,我认为一个简单的(ptr,usize)元组就可以了。

请注意,建议不要在第一遍中稳定Excess

发布此线程以在reddit上进行讨论,其中有些人对此表示担忧: https :

在今天与@ rust-lang / libs进行了进一步讨论之后,我想对稳定建议进行一些调整,可以总结为:

  • alloc_zeroed添加到稳定化方法的集合中,否则具有与alloc相同的签名。
  • 使用extern { type void; }支持在API中将*mut u8更改*mut void ,解决@dtolnay的问题,并提供一种在整个生态系统中统一c_void的方法。
  • alloc的返回类型更改为*mut void ,删除ResultError

也许最有争议的是最后一点,所以我也想对此进行详细说明。 这是今天与libs团队讨论的结果,特别是围绕(a)基于Result的接口的ABI效率比返回指针的效率低,以及(b)今天几乎没有“生产”分配器而引起的。提供学习“仅此而已”以外的任何功能的能力。 为了提高性能,我们基本上可以使用内联纸等,但是仍然存在Error是额外的负载,很难在最低层将其删除。

返回错误有效负载的想法是,分配器可以提供特定于实现的自省功能,以了解分配失败的原因,否则几乎所有使用者都只需要知道分配成功还是失败。 此外,这旨在成为一个非常低级的API,实际上并不会经常调用它(相反,应该将封装得很好的类型化API改为调用)。 从这个意义上说,对于这个位置我们拥有最可用和最符合人体工程学的API并不是最重要的,而是在不牺牲性能的情况下启用用例就显得尤为重要。

*mut u8的主要优点是使用带字节偏移量的.offset非常方便。

在满足我们的库还建议impl *mut void { fn offset }不与冲突现有的offset用于定义T: Sized 。 也可以是byte_offset

+1使用*mut voidbyte_offset 。 外部类型功能的稳定性是否存在问题,还是我们可以回避该问题,因为只有定义是不稳定的(而liballoc可以在内部做不稳定的事情),而不是使用(例如let a: *mut void = ...是(不稳定)?

是的,我们不需要阻止外部类型的稳定化。 即使删除了外部类型支持,我们为此定义的void总是最糟糕的魔术类型。

在libs会议中是否有关于AllocDealloc是否应作为单独特征的进一步讨论?

我们没有具体说明这一点,但我们总体上认为,除非有特别令人信服的理由,否则我们不应偏离现有技术。 特别是,C ++的分配器概念没有类似的划分。

在这种情况下,我不确定这是否合适。 在C ++中,所有内容都已显式释放,因此没有等效于Box内容需要存储其自己的分配器的副本(或对其的引用)。 这就是导致我们大爆炸的原因。

@joshlf unique_ptr等效于Boxvector等效于Vecunordered_map等效于HashMap

@cramertj啊,有趣,我只看集合类型。 那时看来这可能是一件事情。 我们以后总是可以通过毯子隐式添加它,但是避免它可能更干净。

一揽子隐含方法可能更干净,实际上:

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团队讨论的结果,特别是围绕(a)基于结果的接口如何具有比返回指针的效率低的ABI,以及(b)今天几乎没有“生产”分配器提供学习的能力除了“仅此而已”。 为了提高性能,我们大多数情况下都可以使用内联等进行覆盖,但是错误仍然是额外的有效负载,很难在最低层删除。

我担心这将使使用返回的指针非常容易,而无需检查null。 似乎还可以通过返回Result<NonZeroPtr<void>, AllocErr>并使AllocErr大小为零来消除开销而不增加这种风险?

NonZeroPtrptr::Sharedptr::Unique的合并,如https://github.com/rust-lang/rust/issues/27730#issuecomment-316236397中所建议。)

@SimonSapinResult<NonZeroPtr<void>, AllocErr>需要稳定三种类型,所有这些都是全新的,并且其中一些在历史上一直在竭力稳定。 甚至不需要void类的东西(我认为很不错)。

我同意“无需检查null就可以轻松使用它”,但这又是一个非常低级的API,不打算大量使用,因此我认为我们不应该针对调用程序的人体工程学进行优化。

人们还可以在可能具有更复杂的返回类型(如Result<NonZeroPtr<void>, AllocErr>的低级alloc之上构建较高级别的抽象,例如alloc_one Result<NonZeroPtr<void>, AllocErr>

我同意AllocErr在实际中不会有用,但是Option<NonZeroPtr<void>>呢? 不会造成意外使用而无开销的API,是Rust使C与众不同的原因之一,而返回C样式的空指针感觉就像向我倒退了一步。 说它是“不打算大量使用的非常低级的API”,就像是说我们不应该在意不常见的微控制器架构上的内存安全性,因为它们是非常低级且不常用的。

不论此函数的返回类型如何,与分配器的每次交互都涉及不安全的代码。 无论返回类型是Option<NonZeroPtr<void>>还是*mut void低级分配API都可能会滥用。

特别Alloc::alloc是级别较低且不打算大量使用的API。 诸如Alloc::alloc_one<T>Alloc::alloc_array<T>类的方法会更常用,并且返回类型为“更细”。

状态的AllocError是不值得的,但是实现Error且具有Displayallocation failure Display的大小为零的类型很好。 如果我们走NonZeroPtr<void>路线,我认为Result<NonZeroPtr<void>, AllocError>Option<NonZeroPtr<void>>更可取。

为什么急于稳定:( !! Result<NonZeroPtr<void>, AllocErr>对于客户来说无疑是更好的选择。说这是一个“非常低级的API”,不需要很好就是令人沮丧的雄心勃勃。所有级别的代码都应该尽可能的安全和可维护;晦涩的代码不经常被编辑(从而在人们的短期记忆中分页),就更是如此!

此外,如果我们要拥有用户编写的allocate-polymorphic集合(我当然希望如此),那就是直接使用分配器打开大量相当复杂的代码。

关于重新脱销,从操作上讲,我们几乎肯定希望每个基于树的集合仅引用/克隆同种异体体。 这意味着将分配器传递给每个要销毁的custom-allocator-box。 但是,在没有线性类型的Rust中如何最好地做到这一点是一个悬而未决的问题。 与我之前的评论相反,我会在集合实现中使用一些不安全的代码,因为理想的用例会更改Box的实现,而不是split allocator和deallocator特性的实现。 即我们可以在不限制线性的情况下取得稳定的进展。

@sfackler我认为我们需要一些关联的类型将解除分配器连接到分配器; 可能无法对其进行改造。

@ Ericson2314由于人们希望使用分配器来处理现实世界中的真实事物,因此有一种“急于稳定”的趋势。 这不是一个科学项目。

该关联类型将用于什么?

@sfackler的人仍然可以每晚固定/,并且关心这种高级功能的人应该对此感到满意。 [如果问题是不稳定的rustc与不稳定的Rust,那是一个需要解决策略的问题。]相反,糟糕的API烘烤会永远束缚我们,除非我们想使用新的2.0 std拆分生态系统。

关联的类型会将取消分配器与分配器相关联。 每个人都需要知道彼此,才能起作用。 [仍然存在使用正确类型的错误(de)分配器的问题,但是我接受,没有人远程提出解决方案。]

如果人们只能每晚固定下来,为什么我们根本没有稳定的版本? 与分配器API进行直接交互的人员的数量要比想要通过例如替换全局分配器来利用这些API的人员小得多。

您是否可以编写一些代码来说明为什么分配器需要知道其关联分配器的类型? 为什么C ++的分配器API不需要类似的映射?

如果人们只能每晚固定下来,为什么我们根本没有稳定的版本?

表示语言的稳定性。 您针对这种情况编写的代码将永不中断。 在较新的编译器上。 如果需要非常糟糕的东西,则需要每晚固定一次,因此不值得等待被认为质量值得保证的功能的最终迭代。

与分配器API进行直接交互的人员的数量要比想要通过例如替换全局分配器来利用这些API的人员小得多。

啊哈! 这将用于将jemalloc移出树等吗? 没有人提议稳定允许选择全局分配器的骇客骇客,仅仅是堆静态本身? 还是我看错了建议?

提议可以选择允许稳定全局分配器的骇客程序,以使其稳定,这是允许我们将jemalloc移出树的一半。 这个问题是另一半。

#[global_allocator]属性稳定化: https :

伊克斯

@ Ericson2314您认为选择全局分配器会是一种多么

(在https://github.com/rust-lang/rust/issues/27389#issuecomment-342285805中响应)

该提案已修改为使用* mut void。

@rfcbot解决了* mut u8

@rfcbot评论

在对IRC进行了一些讨论之后,我同意了这一点,因为我们不打算在Alloc上稳定Box泛型,而是在某些Dealloc特征上使用如@sfackler此处建议的那样,适当的毯子暗示。 如果我误解了意图,请告诉我。

@cramertj只是为了澄清,有可能在事实之后添加Alloc定义吗?

@joshlf是的,它看起来像这样: https :

如何为给定的Alloc指定Dealloc Alloc ? 我想像这样吗?

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

我想这使我们陷入了棘手的WRT https://github.com/rust-lang/rust/issues/29661。

是的,我不认为有一种方法可以使DeallocAlloc现有定义(不具有关联类型)向后兼容,而无需使用默认值。

如果您希望能够自动获取与分配器相对应的解除分配器,则不仅需要关联类型,还需要一个函数来产生解除分配器值。

但是,将来可以通过将这些内容附加到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)
    }
}

值得注意的是,我们实际上需要能够产生解除分配器的实例,而不仅仅是知道其类型。 你也可以参数化BoxAlloc和存储A::Dealloc ,而不是与类型推断其可能的帮助。 我们可以通过将Deallocdeallocator移到一个单独的特征上来完成这项工作:

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专业化还有一段路要走吗? 换句话说,从理论上讲,您希望执行以下操作,但是我认为它还行不通吗?

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 ,然后继续进行分配。

我正在考虑一个假想的“ oneshot”分配器,尽管我不知道它是多少。

这样的分配器可能存在,但是按值获取self将要求_all_分配器以这种方式工作,并且将阻止在调用deallocator之后允许分配的所有分配器。

在我们考虑稳定之前,我仍然希望在集合中看到其中的一些实现和使用。

您是否认为https://github.com/rust-lang/rust/issues/27336https://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
}

我知道这很痛苦,但是似乎我们在这里讨论的变化足够大,如果我们决定继续使用split alloc / dealloc特征,我们应该首先在std中进行尝试,然后重新进行FCP。

等待这些东西实施的时间表是什么?

grow_in_place方法不会返回任何形式的多余容量。 它当前使用布局调用usable_size ,将分配扩展到至少适合该布局,但是如果分配超出该布局,则用户将无法知道。

我很难理解allocrealloc方法相对于alloc_excessrealloc_excess

分配器需要找到合适的存储块来执行分配:这需要知道存储块的大小。 无论是分配器然后返回指针,还是元组“存储块的指针和大小”都不会产生任何可测量的性能差异。

因此allocrealloc只是增加了API的面,似乎鼓励编写性能较低的代码。 我们为什么要在API中完全使用它们? 他们的优势是什么?


编辑:或者换句话说:API中所有可能分配的函数都应返回Excess ,这基本上消除了所有_excess方法的需要。

多余仅在涉及可能增长的数组的用例中才有意义。 例如,它对BoxBTreeMap没用或没有意义。 计算多余的部分可能会产生一定的成本,并且肯定会有更复杂的API,因此在我看来,不关心多余容量的代码应该被迫为此付出代价。

计算多余的部分可能会有些成本

你能给个例子吗? 我不知道,也无法想象,一个能够分配内存但不知道实际分配多少内存的分配器(这就是Excess是:分配的实际内存量;我们应该重命名)。

唯一可能会引起一些争议的常用Alloc算子是POSIX malloc ,即使它总是内部计算Excess ,也不会将其公开为C的一部分。 API。 但是,以Excess返回请求的大小是可以的,可移植的,简单的,并且完全不会产生任何费用,这就是每个使用POSIX malloc都已经在假设的事情。

jemalloc以及基本上任何其他Alloc ator都提供API,该API返回Excess而不会产生任何费用,因此对于那些分配器,返回Excess为零成本也是如此。

计算多余的部分可能会产生一定的成本,并且肯定会有更复杂的API,因此在我看来,不关心多余容量的代码应该被迫为此付出代价。

现在,每个人都已经付出了具有两个用于分配内存的API的分配器特性的代价。 尽管可以在Excess -full一个上构建一个Excess -less API,但事实并非如此。 所以我想知道为什么没有这样做:

  • Alloc特征方法始终返回Excess
  • 为所有以下用户添加一个ExcessLessAlloc特征,该特征将从Alloc方法中删除Excess :1)足够使用Alloc但2)不关心当前分配的实际内存量(在我看来,这是一个利基市场,但我仍然认为拥有这样的API很不错)
  • 如果有一天有人发现一种通过Alloc - Excess -less方法使用快速路径实现Alloc ator的方法,我们总是可以为其提供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 :

在这种方案中,通常要做的事情是先拥有一组大小为class的对象,然后再进行classes [size / SIZE_QUANTUM] .alloc()。 在那个世界上,弄清楚使用了什么大小的类需要额外的指令:例如,让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是一个错误修复(不是性能修复),我们的jemalloc层的当前状态存在很多问题,但是由于该PR至少返回了它应该返回的内容:

  • 就GCC而言, nallocxconst函数,即真正的纯函数。 这意味着它没有副作用,其结果仅取决于其参数,它不访问全局状态,其参数不是指针(因此该函数无法访问全局状态将它们抛出),并且对于C / C ++程序LLVM可以使用此方法如果不使用结果,可取消呼叫的信息。 AFAIK Rust当前无法将FFI C功能标记为const fn或类似功能。 因此,这是可以解决的第一件事,只要内联和优化工作正常,对于不使用超出realloc_excess ,这将使
  • nallocx总是为mallocx内的对齐分配计算的,也就是说,所有代码已经在计算它,但是mallocx丢弃了它的结果,因此在这里我们实际上计算了两次,并在某些情况下, nallocx差不多一样贵mallocx ...我有一个jemallocator的是有这样的事情在它的枝干一些基准,但是这必须由固定上游jemalloc通过提供一个不会丢掉它的API。 但是,此修复程序仅影响当前正在使用Excess
  • 然后就是我们要两次计算align标志的问题,但这是LLVM可以在我们这方面进行优化(并且很难修复)的问题。

是的,看起来像是更多代码,但是这些额外的代码实际上是我们实际上两次调用过的代码,因为第一次调用时,我们将结果丢弃了。 修复并非不可能,但是我还没有时间进行修复。


编辑: @sfackler今天我设法为它腾出一些时间,并且能够使alloc_excess alloc在jemallocs慢路径中相对于https :

是吗

是。

因此,这是可以解决的第一件事,只要内联和优化工作正常,对于不使用多余资源的用户来说,这将使realloc_excess成本为零。

当用作全局分配器时,这些都不能被内联。

即使其API并非对于所有用途都最符合人体工程学,但这样的API至少将允许所有用途,这是表明设计没有缺陷的必要条件。

Github上实际上有零代码调用alloc_excess 。 如果这是至关重要的功能,为什么没有人使用过它呢? C ++的分配API不提供对多余容量的访问。 如果有实际的具体证据表明它们可以改善性能并且任何人实际上都在意使用它们,那么将来以向后兼容的方式添加/稳定这些功能似乎非常简单。

当用作全局分配器时,这些都不能被内联。

然后,至少对于LTO构建,这是我们应该尝试解决的问题,因为像jemalloc这样的全局分配器依赖于此: nallocx是设计的方式,也是第一个建议jemalloc的开发人员让我们对alloc_excess性能感到满意的是,我们应该内联这些调用,并且应该正确传播C属性,以便编译器从不包含这些调用的调用站点中删除nallocx调用使用Excess ,就像C和C ++编译器一样。

即使我们不能这样做,也可以通过修补jemalloc API来使Excess API保持零成本(我在rust-lang / jemalloc分支)。 我们既可以自己维护该API,也可以尝试将其放到上游,但是要使它放到上游,我们必须充分说明为什么这些其他语言可以执行这些优化而Rust无法做到。 或者我们必须要有另一个论点,例如对于需要Excess用户来说,这个新API的速度明显快于mallocx + nallocx Excess

如果这是至关重要的功能,为什么没有人使用过它呢?

这是个好问题。 std::Vec是使用Excess API的发布者,但它目前不使用它,我以前的所有评论都指出“ Excess中缺少此内容API”是我试图让Vec使用它。 Excess API:

  • 根本没有返回Excesshttps :
  • 缺少grow_in_place_excessalloc_zeroed_excess
  • 无法在FFI中正确传播C属性: https :
  • ...(我怀疑它到此为止)

我不知道为什么没人使用这个API。 但是考虑到即使std库也无法将其用于最适合的数据结构( Vec ),如果我不得不猜测,我要说的主要原因是该API目前已损坏。

如果我不得不进一步猜测,我想说的是,即使是那些设计此API的人也没有使用过它,主要是因为没有单个std集合使用它(这是我希望首先对这个API进行测试的地方) ,也因为在各处使用_excessExcess意味着usable_size / allocation_size极易使程序感到困惑/烦恼。

这可能是因为在Excess -less API中投入了更多工作,并且当您拥有两个API时,很难使它们保持同步,用户很难发现两者并知道要使用哪个API,最后,用户很难选择方便而不是做正确的事。

换句话说,如果我有两个相互竞争的API,并且我将100%的工作用于改进一个API,而将0%的工作用于改进另一个API,则得出一个结论是实际上已经在实践中这一结论就不足为奇了比其他更好。

据我所知,这是在Github上的jemalloc测试之外对nallocx的仅有的两个调用:

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的初始实现支持这种晦涩的功能很重要?

据我所知,在Github上的jemalloc测试之外,这是对nallocx的仅有的两个调用:

从我的头顶开始,我知道至少facebook的向量类型通过facebook的malloc实现来使用它( mallocfbvector增长策略;这是facebook上C ++的向量的很大一部分都使用了此方法),并且Chapel也使用它来改进他们的String类型的性能(此处跟踪问题)。 所以也许今天不是Github最好的一天?

为什么分配器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不同步,而且其中extra-api的功能要比少过的api少。 虽然可以在一个多余的API之上构建一个多余的API,但事实并非如此。

那些想要删除Excess应该删除它。

要澄清的是,如果在事实之后有某种以向后兼容的方式添加alloc_excess方法的方法,那么您会满意吗? (但是,当然,如果没有alloc_excess就能稳定下来,则以后再添加它会是一个重大更改;我只是想问一下,所以我理解您的理由)

@joshlf这样做非常简单。

:bell:根据上面的评论

那些想要删除多余部分的人应该删除它。

另外,约有0.01%的人会担心多余的容量,可以使用另一种方法。

@sfackler这是我因生锈休息两周而得到的-我忘记了默认方法impls :)

另外,约有0.01%的人会担心多余的容量,可以使用另一种方法。

您在哪里得到这个号码?

我所有的Rust数据结构在内存中都是平坦的。 能够做到这一点是我使用Rust的唯一原因。 如果我能将所有内容都装箱,我将使用另一种语言。 因此,我并不关心Excess0.01%的时间,我一直都在乎它。

我知道这是特定于域的,在其他域中人们永远不会在乎Excess ,但是我怀疑只有0.01%的Rust用户在乎这一点(我的意思是,很多人都在使用VecString ,它们是Excess的后代子数据结构。

我得到这个数字的原因是,与使用malloc的事物集相比,总共使用nallocx的事物总数约为4。

@gnzlbg

您是否建议,如果我们从一开始就“正确”地做到了,那么我们将只有fn alloc(layout) -> (ptr, excess)而根本没有fn alloc(layout) -> ptr ? 在我看来,这似乎还很遥远。 即使可以使用多余的东西,对于多余的东西都不重要的用例(例如,大多数树结构),即使将后者实现为alloc_excess(layout).0 ,也可以使用后者的API。

@rkruppe

在我看来,这似乎还很遥远。 即使可以使用多余的东西,对于多余的东西都不重要的用例(例如,大多数树结构),即使将后者实现为alloc_excess(layout).0,使用后者的API也是很自然的。

当前,多余的API是在多余的API之上实现的。 为少用的分配器实现Alloc要求用户提供allocdealloc方法。

但是,如果我想为超额分配器实现Alloc ,我需要提供更多方法(至少alloc_excess ,但是如果我们进入realloc_excess ,这种方法会越来越多, alloc_zeroed_excessgrow_in_place_excess ,...)。

如果我们要反过来做,那就是在多余的API之上实现多余的API,然后实现alloc_excessdealloc两种类型的分配器。

不在乎,无法返回或查询多余项的用户可以只返回输入大小或布局(这是一个很小的麻烦),但是可以处理并想要处理多余项的用户则无需实施任何其他方法。


@sfackler

我得到这个数字的原因是,与使用malloc的事物集相比,总共使用nallocx的事物总数约为4。

鉴于以下有关Rust生态系统中_excess使用情况的事实:

  • 0个事物在rust生态系统中总共使用_excess
  • rust std库中共0件事,总共使用_excess
  • 甚至VecString都不能在rust std库中正确使用_excess API
  • _excess API不稳定,与少用的API不同步,直到最近才出现故障(根本没有返回excess ),...

    并考虑到以下有关_excess在其他语言中的用法的事实:

  • 由于向后兼容,C或C ++程序本身不支持jemalloc的API

  • 想要使用jemalloc多余API的C和C ++程序需要通过以下方式摆脱使用它的方式:

    • 选择退出系统分配器并进入jemalloc(或tcmalloc)

    • 重新实现其语言的标准库(对于C ++,实现不兼容的标准库)

    • 在不兼容的std库的顶部写入整个堆栈

  • 一些社区(firefox使用它,facebook重新实现了C ++标准库中的集合以能够使用它,...)仍然不愿意使用它。

这两个参数对我来说似乎是合理的:

  • stdexcess API不可用,因此std库不能使用它,因此没有人可以使用,这就是为什么即使在Rust生态系统中也没有使用它的原因。
  • 尽管C和C ++几乎不可能使用此API,但是拥有大量人力的大型项目都竭尽全力使用它,因此至少有一些潜在的微小人群对此非常关心。

您的论点:

  • 没有人使用_excess API,因此只有0.01%的人在乎它。

才不是。

@alexcrichton决定从-> Result<*mut u8, AllocErr>切换到-> *mut void可能使遵循分配器RFC最初发展的人们感到意外。

我不会不同意您的观点,但是尽管如此,似乎仍有相当一部分人愿意承受Result的“沉重”负担,而这可能会增加错过null的机会,检查返回的值。

  • 我忽略了ABI带来的运行时效率问题,因为我像@alexcrichton一样,假设我们可以通过编译器技巧以某种方式处理这些问题。

有没有一些方法,我们可以得到关于自己的延迟变化的知名度越来越高?

一种方法(不在我的脑海中):现在在PR上的master分支上自己更改签名,而Allocator仍然不稳定。 然后查看谁在PR上抱怨(以及谁在庆祝!)。

  • 这太过分了吗? 从定义上看,它似乎比将这种变化与稳定化结合起来没有那么费力。

关于是否返回*mut void或返回Result<*mut void, AllocErr> :可能我们应该重新讨论分开的“高级”和“低级”分配器特征的想法,如所讨论的采取Allocator RFC的II

(显然,如果我对*mut void返回值有严重的异议,那么我会通过fcpbot将它作为关注点提出。但是在这一点上,我非常信任libs团队的判断,也许是在部分原因是由于此分配器传奇的疲劳。)

@pnkfelix

-> Result<*mut u8, AllocErr>切换到-> *mut void可能使遵循分配器RFC最初发展的人们感到意外。

正如所讨论的,后者意味着,我们只想表达的唯一错误是OOM。 因此,介于两者之间的重量稍轻,仍然具有防止意外未能检查错误的好处,就是-> Option<*mut void>

@gnzlbg

std中多余的API不可用,因此std库无法使用它,因此没有人可以使用,这就是为什么在Rust生态系统中甚至不使用它的原因。

然后修复它。

@pnkfelix

关于是否返回mut void或返回Result < mut void,AllocErr>:可能有必要重新考虑分开的“高级”和“低级”分配器特征的想法,如第二部分所述分配器RFC。

这些基本上是我们的想法,除了高级API本身会以alloc_onealloc_array等形式出现在Alloc本身中。我们甚至可以让那些人首先在生态系统中进行扩展特质,以了解人们融合了哪些API。

@pnkfelix

我使Layout仅实现Clone而不是Copy的原因是我想让向Layout类型添加更多结构的可能性保持开放。 尤其是,我仍然对尝试让Layout尝试跟踪用于构造它的任何类型结构(例如struct {x:u8,y:[char; 215]}的16数组)感兴趣,以便分配器可以可以公开报告其当前内容来自哪种类型的检测例程的选项。

在某个地方进行过实验吗?

@sfackler我已经完成了大部分操作,并且所有这些操作都可以使用重复的API(没有多余的+ _excess方法)完成。 如果我有两个API并且现在没有完整的_excess API,我会很好。

唯一让我有些担心的是,现在要实现一个分配器就需要实现alloc + dealloc ,但是alloc_excess + dealloc也应该可以工作。 有没有可能给alloc的默认实现来讲alloc_excess更高版本,或者是一个不可能或者重大更改? 实际上,大多数分配器无论如何都将实现大多数方法,因此这不是什么大问题,而更像是一个愿望。


jemallocator两次实现Alloc (对于Jemalloc&Jemalloc ),其中对于某些methodJemalloc实现只是(&*self).method(...)会将方法调用转发到&Jemalloc实现。 这意味着必须手动同步JemallocAlloc两种实现。 我不知道&/_实现的不同行为是否可以悲剧。


我发现很难发现人们实际上在实践中使用Alloc特性正在做什么。 我发现正在使用它的唯一项目无论如何都将保持夜间使用(伺服,氧化还原),并且仅使用它来更改全局分配器。 令我非常担心的是,我找不到任何将其用作集合类型参数的项目(也许我只是倒霉,还有一些?)。 我特别在寻找在类似Vec的类型上实现SmallVecArrayVec示例(因为std::Vec没有Alloc类型参数),还想知道在这些类型之间进行克隆(使用不同的Alloc ator的Vec s)如何工作(克隆Box es具有不同的Alloc s)。 是否有示例说明这些实现在某处的样子?

我发现使用它的唯一项目无论如何都将每晚使用(伺服,氧化还原)

对于它的价值,Servo尝试在可能的情况下摆脱不稳定的功能: https :

这也是鸡与蛋的问题。 许多项目尚未使用Alloc因为它仍然不稳定。

对我来说还不是很清楚,为什么我们首先应该拥有完整的_excess API。 它们最初的存在是为了模仿jemalloc的实验性* allocm API,但几年前在4.0中将其删除,以免复制整个API表面。 看来我们可以效仿他们的领导?

以后是否可以根据alloc_excess赋予alloc一个默认的实现,或者这是不可能的还是重大的改变?

我们可以根据alloc_excess添加一个默认实现alloc ,但是alloc_excess需要一个具有alloc的默认实现。 如果您实现一个或两者,则一切工作都很好,但是如果您既不实现,则代码可以编译,但是无限递归。 这是以前出现过的(也许是Rand吗?),我们可以用某种方式说您需要至少实现其中一个功能,但我们不在乎。

令我非常担心的是,我找不到任何将其用作集合类型参数的项目(也许我只是倒霉,还有一些?)。

我不知道有人这样做。

我发现使用它的唯一项目无论如何都将每晚使用(伺服,氧化还原)

阻止这种情况发展的一大原因是stdlib集合尚不支持参数分配器。 这几乎也排除了其他大多数包装箱,因为大多数外部收藏使用内部的内部包装( BoxVec等)。

我发现使用它的唯一项目无论如何都将每晚使用(伺服,氧化还原)

阻止这种情况发展的一大原因是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。 它们最初的存在是为了模仿jemalloc的实验性* allocm API,但几年前在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]重命名std::heap::Heap #[default_allocator]怎么样?

如果您不另外指定,它就是您所得到的(大概在例如Vec为分配器获取一个额外的类型参数/字段时),这比不具有“ per -instances”状态(或确实是实例)。

最终意见征询期现已完成。

关于FCP,我认为为稳定而提出的API子集用途非常有限。 例如,它不支持jemallocator板条箱。

用什么方式? jemallocator可能必须在功能标志后面标记一些不稳定方法的隐式实现,仅此而已。

如果稳定Rust上的jemallocator无法通过调用je_rallocx来实现例如Alloc::realloc ,但是需要依赖默认的alloc + copy + dealloc impl,那么这不是可接受的替代方法标准库的alloc_jemalloc板箱IMO。

当然,您可以编译一些东西,但这并不是特别有用的东西。

为什么? C ++在其分配器API中根本没有任何重新分配的概念,并且似乎没有削弱该语言。 这显然不是理想的,但是我不明白为什么它是不可接受的。

C ++集合通常不使用realloc,因为C ++ move构造函数可以运行任意代码,因为realloc没有用。

而且比较不是与C ++进行的,而是与具有内置jemalloc支持的当前Rust标准库进行的。 仅使用Alloc API的此子集切换到std分配器并退出std分配器将是一种回归。

realloc为例。 jemallocator当前还实现alloc_zeroedalloc_excessusable_sizegrow_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>() 。 因此,数组中对象的大小始终与其自身的大小相同。 有关更多详细信息,请参见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或类似的方法完成的),所以我回到平方。 与持续的问题有关的常量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调用RawVec::reserve/double并调用Alloc::realloc ,所以Alloc::realloc的默认隐式复制了无效的向量元素(在[len(), capacity())范围内) 。 在一个巨大的空向量想要插入capacity() + 1元素并因此进行重新分配的荒谬情况下,触摸所有内存的代价并不微不足道。

从理论上讲,如果默认的Alloc::realloc实现也采用了“ bytes_used”范围,则可以在重新分配时复制相关部分。 实际上,至少jemalloc会调用rallocx来覆盖Alloc::realloc default impl。 是否执行alloc / dealloc舞蹈仅复制相关内存比调用rallocx更快或更慢取决于很多事情( rallocx可以扩展该块的位置( rallocx将复制多少不必要的内存?等)。

https://github.com/QuiltOS/rust/tree/allocator-error我已经开始演示我认为关联错误类型如何通过进行归纳本身来解决我们的集合和错误处理问题。 特别要注意的是,我如何在模块中进行更改

  • 始终将Result<T, A::Err>实现重用于T实现
  • 切勿unwrap或其他任何部分
  • oom(e)之外没有AbortAdapter

这意味着我正在进行的更改非常安全,而且也非常容易! 处理错误返回和错误中止都不需要付出额外的努力来维护精神不变性-类型检查器完成了所有工作。

我记得---我认为在@Gankro的RFC中? 还是pre-rfc线程--- Gecko / Servo人说,最好不要将集合的易错性纳入其类型。 好吧,我可以添加#[repr(transparent)]AbortAdapter使藏品可以安全地之间被转化Foo<T, A>Foo<T, AbortAdapter<A>> (内是安全的包装),允许人们自由来回切换,而无需重复每种方法。 [对于后向兼容,无论如何都需要复制标准库集合,但是用户方法不必是Result<T, !>因为如今它们很容易使用。

不幸的是,代码无法完全进行类型检查,因为更改lang项目(框)的类型参数会混淆编译器(惊奇!)。 导致ICE的Box提交是最后一个-一切都还不错。 @eddyb在#47043中修复了rustc!

编辑@joshlf,让我知道了您的https://github.com/rust-lang/rust/pull/45272 ,并将其合并到这里。 谢谢!

持久性内存(例如http://pmem.io )是下一个大问题,Rust需要定位以使其良好运行。

我最近一直在为永久内存分配器(特别是libpmemcto)开发Rust包装器。 关于此API的稳定性的任何决定,都需要:

  • 能够支持围绕持久性内存分配器(如libpmemcto)的高性能包装器;
  • 能够通过分配器指定(参数化)集合类型(目前,需要复制Box,Rc,Arc等)
  • 能够跨分配器克隆数据
  • 能够支持使用持久性存储池实例化时重新初始化具有字段的持久性存储结构,即,某些持久性存储结构需要具有仅临时存储在堆中的字段。 我当前的用例是对用于分配的持久性内存池和用于锁的临时数据的引用。

顺便说一句,pmem.io开发(英特尔的PMDK)在后台大量使用了经过修改的jemalloc分配器-因此,谨慎使用jemalloc作为示例API使用者是明智的。

在我们获得在集合中使用Alloc指示符的更多经验之前,是否有可能将其范围缩小到仅覆盖GlobalAllocator s?

IIUC已经可以满足servo的需求,并使我们能够并行地对容器进行参数化实验。 将来,我们可以将集合移到使用GlobalAllocator来代替,也可以只为GlobalAllocator添加Alloc隐含暗示,以便将它们用于所有集合。

有什么想法吗?

@gnzlbg为了使#[global_allocator]属性有用(除了选择heap::System ),还必须稳定Alloc特性,以便可以通过https://等板条箱实现GlobalAllocator类型或特征,您是否正在提议一些新的API?

目前没有名为GlobalAllocator的类型或特征,您是否正在提议一些新的API?

我建议是重命名“最小”的API,@alexcrichton建议以稳定这里AllocGlobalAllocator代表全球唯一的分配器,并让门打开由不同的参数进行调整的集合将来的分配器特征(这并不意味着我们无法通过GlobalAllocator特征对其进行参数化)。

IIUC servo当前仅需要能够切换全局分配器(而不是也能够通过分配器对某些集合进行参数化)。 因此,也许我们没有试图稳定一个应该在两个用例中都经过验证的解决方案,而是现在只能解决全局分配器问题,并弄清楚以后如何通过分配器对集合进行参数化。

不知道这是否有意义。

IIUC伺服器当前仅需要能够切换全局分配器(与之相反,也能够通过分配器对某些集合进行参数化)。

没错,但是:

  • 如果一个特征及其方法是稳定的,因此可以实现,那么也可以直接调用它而无需经历std::heap::Heap 。 因此,它不仅是一个特征全局分配器,而且是分配器的一个特征(即使我们最终为通用的分配器分配了一个不同的集合),而GlobalAllocator并不是一个特别好的名字。
  • jemallocator板条箱当前实现alloc_excessreallocrealloc_excessusable_sizegrow_in_placeshrink_in_place部分建议的最小API。 这些参数可能比默认的impl更有效,因此删除它们将导致性能下降。

这两点都说得通。 我只是认为,显着加速此功能稳定的唯一方法是消除对它的依赖,这也是对其上的参数化集合的良好特征。

[如果Servo像(稳定的|官方Mozilla箱子),并且货运可以强制执行此操作,以减轻这里的压力,那将是很好的。]

@ Ericson2314伺服不是唯一想要使用这些API的项目。

@ Ericson2314我不明白这意味着什么,您可以改写吗?

就上下文而言:Servo当前使用了许多不稳定的功能(包括#[global_allocator] ),但是我们正试图逐步摆脱这种状态(通过更新到已稳定某些功能的编译器,或者找到稳定的替代品)。 )可在https://github.com/servo/servo/issues/5286中进行跟踪#[global_allocator]会很好,但不会阻止任何Servo工作。

Firefox依赖于这样的事实,Rust std在编译cdylib时默认为系统分配器,而最终被链接到同一二进制文件中的mozjemalloc定义了诸如mallocfree符号。 #[global_allocator]显式选择mozjemalloc,以使整个设置更可靠。

@SimonSapin我在分配器和集合中使用的次数越多,我越倾向于认为我们不希望通过Alloc对集合进行参数化,因为取决于分配器,一个集合可能希望提供一个不同的API,某些操作的复杂性会发生变化,某些收集细节实际上取决于分配器,等等。

因此,我想提出一种可以在这里取得进展的方法。

步骤1:堆分配器

首先,我们可以限制自己,以尝试让用户在稳定的Rust中为堆选择分配器(或系统/平台/全局/免费存储分配器,或者您更愿意为其命名)。

我们最初对其进行参数化的唯一内容是Box ,它只需要分配( new )和取消分配( drop )内存。

此分配器特征最初可以具有@alexcrichton提议(或有所扩展)的API,并且该分配器特征可以在夜间仍然具有稍微扩展的API以支持std::集合。

一旦我们到达那里,想要迁移到稳定版的用户将能够做到这一点,但是由于API不稳定,可能会降低性能。

步骤2:堆分配器不影响性能

到那时,我们可以重新评估由于性能下降而无法稳定的用户,并决定如何扩展此API并使其稳定。

步骤3至N:在std集合中支持自定义分配器。

首先,这很难,所以它可能永远不会发生,而且我认为永远不会发生不是一件坏事。

当我想使用自定义分配器对集合进行参数化设置时,我会遇到性能问题或可用性问题。

如果我遇到可用性问题,我通常希望使用其他收集API,该API可以利用我的自定义分配器的功能,例如SliceDeque板条箱可以。 通过自定义分配器对集合进行参数设置对我没有帮助。

如果我遇到性能问题,自定义分配器仍然很难帮助我。 我将仅在下一部分中考虑Vec ,因为这是我最常重新实现的集合。

减少系统分配器调用的数量(小型向量优化)

如果我想在Vec对象中分配一些元素以减少对系统分配器的调用次数,那么今天我只使用SmallVec<[T; M]> 。 但是, SmallVec不是Vec

  • 移动Vec的元素个数为O(1),但移动SmallVec<[T; M]>的N <M和O(1)则为O(N),

  • 如果len() <= M指向Vec元素的指针在移动时无效,但不是这样,也就是说,如果像into_iter这样的len() <= M操作需要将元素移到迭代器对象本身,而不仅仅是获取指针。

我们可以通过分配器使Vec通用来支持这一点吗? 一切皆有可能,但我认为最重要的成本是:

  • 这样做会使Vec更加复杂,这可能会影响不使用此功能的用户
  • Vec的文档将变得更加复杂,因为某些操作的行为将取决于分配器。

我认为这些费用不可忽略。

利用分配模式

Vec的增长因子是为特定分配器量身定制的。 在std我们可以将其调整为常见的jemalloc / malloc / ...,但是如果您使用的是自定义分配器,则可能是我们选择的增长因子默认情况下,它不是最适合您的用例的。 每个分配器是否应该能够为类似vec的分配模式指定增长因子? 我不知道,但是我的直觉告诉我:可能不是。

利用系统分配器的其他功能

例如,在第1层和第2层的大多数目标中都可以使用过量使用分配器。 在类似Linux和Macos的系统中,堆分配器默认情况下过量使用,而Windows API公开VirtualAlloc可用于保留内存(例如,在Vec::reserve/with_capacity )并在push上提交内存

目前, Alloc特质无法提供在Windows上实现这种分配器的方法,因为它没有将提交和保留内存的概念分开(在Linux上,可以侵入非过度分配的分配器。只需触摸每个页面一次)。 它也没有公开分配器声明默认情况下是否过量使用alloc

也就是说,我们需要扩展Alloc API来支持Vec ,而这对于IMO来说是徒劳的。 因为当您拥有这样的分配器时, Vec语义会再次更改:

  • Vec不需要再增长,因此像reserve这样的操作几乎没有意义。
  • pushO(1)而不是摊销后的O(1)
  • 只要对象是活动的,对活动对象的迭代器就永远不会失效,这允许进行一些优化

利用系统分配器的更多额外功能

某些系统alloctor像cudaMalloc / cudaMemcpy / ...区分固定和非固定内存,允许您在不相交的地址空间上分配内存(因此我们需要在地址空间中关联一个Pointer类型) Alloc特质),...

但是像Vec这样在集合上使用它们确实会再次以微妙的方式更改某些操作的语义,例如对向量建立索引是否突然调用未定义的行为,这取决于您是从GPU内核还是从主机执行。

包起来

我认为尝试提出一个可用于对所有集合进行参数化的Alloc API很难(甚至可能只是Vec ),这可能很难。

也许在正确使用global / system / platform / heap / free-store分配器和Box ,我们可以重新考虑集合。 也许我们可以重用Alloc ,也许我们需要一个VecAlloc, VecDequeAlloc , HashMapAlloc`,...或者我们只是说,“您知道吗,如果您真的需要这个,只需将标准集合复制粘贴到板条箱中,然后将其成型为分配器即可。” 也许最好的解决方案是通过在托儿所自己的板条箱中放置std集合并仅使用稳定的功能(可能作为一组构建模块来实现)来简化此操作。

无论如何,我认为尝试在此处立即解决所有这些问题并尝试提出对所有事物都有利的Alloc特性太难了。 我们现在处于第0步。我认为快速进入第1步和第2步的最佳方法是将集合放在图片之外,直到我们在那里为止。

一旦我们到达那里,想要迁移到稳定版的用户将能够做到这一点,但是由于API不稳定,可能会降低性能。

选择一个自定义分配器通常是关于提高性能的,所以我不知道这种最初的稳定将服务于谁。

选择一个自定义分配器通常是关于提高性能的,所以我不知道这种最初的稳定将服务于谁。

大家吗至少现在。 您最初抱怨的稳定性建议中缺少的大多数方法(例如alloc_excess )都是标准库中尚未使用AFAIK的方法。 还是最近改变了?

Vec (和RawVec其他用户)在push使用realloc push

@SimonSapin

jemallocator板条箱当前实现alloc_excess,realloc,realloc_excess,usable_size,grow_in_place和shrink_in_place

通过这些方法,使用了AFAIK reallocgrow_in_placeshrink_in_place ,但是grow_in_place只是jemalloc的shrink_in_place的天真包装器,至少因此,如果我们在Alloc特性中以shrink_in_place形式实现grow_in_place的默认不稳定隐式,将其缩减为两种方法: reallocshrink_in_place

选择自定义分配器通常可以提高性能,

尽管这是事实,但使用没有这些方法的更合适的分配器可能会比具有这些方法的不良分配器获得更高的性能。

IIUC伺服的主要用例是使用Firefox jemalloc,而不是再使用第二个jemalloc,对吗?

即使我们在最初的稳定中将reallocshrink_in_placeAlloc特性中,也只会延迟性能投诉。

例如,当我们将任何不稳定的API添加到std集合最终使用的Alloc特性中时,您将无法获得与稳定版本相同的性能。能够每晚上班。 也就是说,如果我们将realloc_excessshrink_in_place_excess到alloc特质,并使Vec / String / ...使用它们,我们就稳定了reallocshrink_in_place不会对您有任何帮助。

IIUC伺服的主要用例是使用Firefox jemalloc,而不是再使用第二个jemalloc,对吗?

尽管它们共享一些代码,但Firefox和Servo是两个单独的项目/应用程序。

Firefox使用mozjemalloc,它是jemalloc的旧版本的分支,并添加了许多功能。 我认为某些unsafe FFI代码依赖于Rust std使用的mozjemalloc的正确性和完整性。

Servo使用的jemalloc恰好是Rust可执行文件的默认值,但是有计划将该默认值更改为系统的分配器。 Servo还具有一些unsafe内存使用情况报告代码,这些代码依赖于确实使用的jemalloc的健全性。 (将Vec::as_ptr()传递到je_malloc_usable_size 。)

Servo使用的jemalloc恰好是Rust可执行文件的默认值,但是有计划将该默认值更改为系统的分配器。

最好知道伺服目标系统中的系统分配器是否像jemalloc一样提供优化的reallocshrink_to_fit API? realloc (和calloc )很常见,但是shrink_to_fitxallocx )是特定于jemalloc AFAIK。 也许最好的解决方案是在最初的实现中稳定reallocalloc_zeroedcalloc ),并让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特性稳定下来,对于Linux malloc和Windows,我们可能会有一个像jemallocator这样的箱子。 这些板条箱可能具有额外的功能,可以实现不稳定的Alloc API可以实现的部分(例如,在usable_size之上的malloc_usable_size )以及其他一些功能不属于Alloc API的一部分,例如mallinfo之上的内存报告。 一旦针对伺服目标的系统有了可用的包装箱,就可以更容易地知道Alloc特性的哪些部分优先考虑稳定化,并且我们可能会发现应该至少对某些API进行试验的较新的API。分配器。

@gnzlbg我对https://github.com/rust-lang/rust/issues/32838#issuecomment -358267292中的内容有些怀疑。 撇开所有这些特定于系统的内容,不难概括出alloc的集合-我已经做到了。 试图合并这似乎是一个单独的挑战。

@SimonSapin firefox是否有没有不稳定的Rust策略? 我觉得我很困惑:Firefox和Servo都希望这样做,但是如果这样,它的Firefox用例将增加稳定的压力。

@sfackler看到^。 我试图在需要与希望保持稳定的项目之间进行区分,但是Servo处于这种分歧的另一侧。

我有一个项目想要这个并且要求它稳定。 作为Rust的使用者,Servo或Firefox并没有什么特别神奇的。

@ Ericson2314正确,Firefox使用稳定版: https//wiki.mozilla.org/Rust_Update_Policy_for_Firefox 正如我所解释的,尽管今天有可行的解决方案,所以这并不是任何事情的真正阻碍。 使用#[global_allocator]会更好/更强大,仅此而已。

伺服系统确实使用了一些不稳定的功能,但是如前所述,我们正在尝试对此进行更改。

FWIW,参数分配器对于实现分配器非常有用。 如果您可以在内部使用各种数据结构并通过一些更简单的分配器(例如bsalloc )对它们进行参数化,则许多对性能不太敏感的簿记将变得更加容易。 当前,在std环境中执行此操作的唯一方法是进行两阶段编译,其中第一阶段用于将较简单的分配器设置为全局分配器,第二阶段用于编译更大,更复杂的分配器。 在没有标准的情况下,根本没有办法做到。

@爱立信

撇开所有这些特定于系统的内容,不难概括出alloc的集合-我已经做到了。 试图合并这似乎是一个单独的挑战。

Vec +我可以查看的自定义分配器之上,您是否有ArrayVecSmallVec实现? 这是我提到的第一点,而且根本不是特定于系统的。 可以说这可能是最简单的两个分配器,一个分配器只是一个原始数组作为存储,而另一个分配器可以在第一个分配器耗尽时通过在堆中添加后备来建立在第一个分配器之上。 主要的区别是那些分配器不是“全局”的,但是每个Vec都有自己的分配器,独立于所有其他分配器,并且这些分配器是有状态的。

另外,我也不主张永远不要这样做。 我只是在说这很难,C ++尝试了30年,仅获得部分成功:GPU分配器和GC分配器由于通用指针类型而起作用,但是实现了ArrayVecSmallVec上的顶部Vec不会导致在C ++土地零成本抽象( P0843r1讨论了一些用于问题ArrayVec详细地)。

因此,我更愿意在稳定可交付有用内容的组件之后继续这样做,只要这些组件将来不再追求自定义集合分配器即可。


我在IRC上与@SimonSapin进行了交谈,如果我们要使用reallocalloc_zeroed扩展初始稳定建议,那么Firefox中的Rust(仅使用稳定的Rust)将可以使用mozjemalloc作为稳定Rust中的全局分配器,不需要任何额外的技巧。 正如@SimonSapin所提到的

不过,我们可以从那里开始,一旦到达那里,就将servo到稳定的#[global_allocator]而不会造成性能损失。


@joshlf

FWIW,参数分配器对于实现分配器非常有用。

您能详细说明一下您的意思吗? 您为什么不能使用Alloc特性参数化自定义分配器? 还是您自己的自定义分配器特征,然后在最终分配器上实现Alloc特征(这两个特征不一定必须相等)?

我不知道“ SmallVec = Vec +特殊分配器”的用例从何而来。 我以前没有提到过很多东西(无论是在Rust还是在其他情况下),正是因为它有很多严重的问题。 当我想到“使用专用分配器提高性能”时,我根本没有想到。

查看Layout API,我想知道from_size_alignalign_to之间在错误处理方面的区别,如果发生错误,前者返回None ,而后者会慌张(!)。

在这两种情况下添加适当定义的和信息丰富的LayoutErr枚举并返回Result<Layout, LayoutErr>会更有用和更一致(并且可能将其用于当前返回Option的其他函数)

@rkruppe

我不知道“ SmallVec = Vec +特殊分配器”的用例从何而来。 我以前没有提到过很多东西(无论是在Rust还是在其他情况下),正是因为它有很多严重的问题。 当我想到“使用专用分配器提高性能”时,我根本没有想到。

在Rust和C ++中,有两种独立的使用分配器的方式:系统分配器,默认情况下所有分配都使用该系统分配器,并作为由某些分配器特征参数化的集合的类型参数,作为创建该特定集合的对象的一种方式,使用特定的分配器(可以是系统分配器,也可以不是系统分配器)。

接下来的内容仅关注第二种用例:使用集合和分配器类型创建使用特定分配器的该集合的对象。

以我在C ++中的经验,使用分配器对集合进行参数化处理有两种用例:

  • 通过使集合使用针对特定分配模式的自定义分配器来提高集合对象的性能,和/或
  • 向集合中添加新功能,使其可以执行以前无法执行的操作。

向集合添加新功能

这是分配器的用例,我有99%的情况在C ++代码库中看到。 在我看来,向集合中添加新功能可以提高性能这一事实是偶然的。 特别是,以下所有分配器都没有针对目标分配模式来提高性能。 他们这样做是通过添加功能,在某些情况下,如@ Ericson2314所提到的,可以被认为是“特定于系统的”。 这些是一些示例:

  • 用于进行小型缓冲区优化的堆栈分配器(请参阅Howard Hinnantstd::vectorflat_{map,set,multimap,...}并通过向其传递自定义分配器,从而添加了具有( SmallVec )或不包含( ArrayVec )的小型缓冲区优化堆回落。 例如,这允许将具有其元素的集合放在堆栈或静态内存(否则将使用堆的位置)上。

  • 分段内存架构(例如16位宽的指针x86目标和GPGPU)。 例如,在C ++ 14期间,C ++ 17并行STL是并行技术规范。 它是同一作者的前驱库,是NVIDIA的Thrust库,其中包括分配器,以允许容器容器使用GPGPU内存(例如推力:: device_malloc_allocator )或固定的内存(例如推力:: pinned_allocator ;固定的内存允许主机之间的更快传输)一些案例)。

  • 分配程序来解决与并行性相关的问题,例如错误共享(例如Intel Thread Building Blocks cache_aligned_allocator )或SIMD类型的过度对齐要求(例如Eigen3的aligned_allocator )。

  • 进程间共享内存: Boost.Interprocess具有分配器,这些分配器使用OS进程间共享内存功能(例如,系统V共享内存)分配集合的内存。 这允许直接使用std容器来管理用于在不同进程之间进行通信的内存。

  • 垃圾收集:Herb Sutter的延迟内存分配库使用用户定义的指针类型来实现垃圾收集内存的分配器。 这样,例如,当向量增大时,旧的内存块将保持活动状态,直到指向该内存的所有指针都被破坏为止,从而避免了迭代器无效。

  • 有工具的分配器:彭博软件库的blsma_testallocator允许您记录使用它的对象的内存分配/释放(以及C ++特定的对象构造/破坏)模式。 您不知道Vec之后是否分配reserve吗? 插入这样的分配器,它们会告诉您是否发生这种情况。 这些分配器中的一些可以让您为它们命名,以便您可以在多个对象上使用它,并获取记录以表明哪个对象在做什么。

这些是我在C ++中最常看到的分配器类型。 正如我之前提到的,在我看来,它们在某些情况下会提高性能这一事实是偶然的。 重要的是,它们都没有试图针对特定的分配模式。

定位分配模式以提高性能。

AFAIK没有执行此操作的任何广泛使用的C ++分配器,我将在第二秒解释为什么我认为这是这样做的。 以下库针对此用例:

但是,这些库实际上并未为特定用例提供单个分配器。 相反,它们提供了分配器构造块,您可以使用它们来构建针对应用程序特定部分中的特定分配模式的自定义分配器。

我回想起C ++时代的一般建议是“不要使用它们”(它们是最后的手段),因为:

  • 匹配系统分配器的性能非常困难,击败它非常非常困难,
  • 别人的应用程序内存分配模式与您匹配的机会很小,因此您确实需要了解您的分配模式,并知道与之匹配的分配器构造块
  • 它们不具有可移植性,因为不同的供应商具有不同的C ++标准库实现,它们使用不同的分配模式。 供应商通常将其实现目标放在其系统分配器上。 也就是说,为一个供应商量身定制的解决方案可能在另一个供应商中表现糟糕(比系统分配器差)。
  • 在尝试使用这些替代方法之前,有许多替代方法可能会用尽:使用其他集合,保留内存等。大多数替代方法都省力省力,可以带来更大的胜利。

这并不意味着该用例的库没有用。 它们是,这就是为什么诸如foonathan / memory之类的库正在开花的原因。 但是至少以我的经验,它们比“添加额外功能”的分配器在野外使用的方式少,因为要赢得胜利,您必须击败系统分配器,这比大多数用户愿意投资的时间要长(Stackoverflow已满“我使用Boost.Pool,但性能变差,该怎么办?不使用Boost.Pool。”)。

包起来

IMO,我认为C ++分配器模型虽然远非完美,但同时支持这两种用例,这是很棒的,而且我认为,如果Rust的std集合要由分配器进行参数化,它们也应该支持两种用例,至少事实证明,这两种情况的C ++分配器都是有用的。

我只是认为这个问题与能够自定义特定应用程序的global / system / platform / default / heap / free-store分配器有点正交,并且尝试同时解决这两个问题可能会延迟一个解决方案他们不必要地。

一些用户想要对由分配器参数化的集合执行的操作可能与其他用户想要执行的操作有所不同。 如果@rkruppe是从“匹配分配模式”开始的,而我是从“防止错误共享”或“使用带有堆回退的小缓冲区优化”开始的,那么首先很难理解彼此的需求,其次才是适用于两者的解决方案。

@gnzlbg感谢您的全面撰写。 大多数问题都没有解决我最初的问题,我不同意其中的一些问题,但是最好将其清楚地说明出来,以免彼此交谈。

我的问题专门针对此应用程序:

堆栈分配器,用于进行小缓冲区优化(请参阅Howard Hinnant的stack_alloc文件)。 它们使您可以使用std :: vector或flat_ {map,set,multimap,...},并通过向其传递自定义分配器,从而添加了具有(SmallVec)或不具有(ArrayVec)堆回退功能的小型缓冲区优化。 例如,这允许将具有其元素的集合放在堆栈或静态内存(否则将使用堆的位置)上。

阅读关于stack_alloc的知识,我现在知道它是如何工作的。 这不是人们通常所说的SmallVec(缓冲区以内联方式存储在集合中)的原因,这就是我错过该选项的原因,但它避免了在集合移动时必须更新指针的问题(并使这些移动更便宜) )。 另请注意,short_alloc允许多个集合共享一个arena ,这使其与典型的SmallVec类型更加不同。 它更像是一个线性/凸点指针分配器,在用尽大量空间时可以很好地回退到堆分配。

我不同意这种分配器和cache_aligned_allocator从根本上增加了新功能。 它们的用法不同,根据您对“分配模式”的定义,它们可能无法针对特定的分配模式进行优化。 但是,它们当然可以针对特定的用例进行优化,并且与通用堆分配器没有明显的行为差异。

但是,我确实同意,像Sutter的延迟内存分配这样的用例是一个单独的应用程序,如果我们要完全支持它,则它们可能需要单独的设计,而这种情况会大大改变“指针”的含义。

阅读关于stack_alloc的知识,我现在知道它是如何工作的。 这不是人们通常所说的SmallVec(缓冲区以内联方式存储在集合中)的原因,这就是我错过该选项的原因,但它避免了在集合移动时必须更新指针的问题(并使这些移动更便宜) )。

我提到stack_alloc是因为它是唯一带有“ paper”的分配器,但它于2009年发布,并且在C ++ 11之前(C ++ 03在集合中不支持有状态分配器)。

简而言之,在C ++ 11(支持状态分配器)中的工作方式是:

  • 的std ::向量存储的Allocator对象里面,就像铁锈RawVec确实
  • Allocator接口具有一个模糊的属性,称为Allocator :: propagate_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 ++分配器API具有的许多属性之一;编写C ++分配器并非易事)。 由于有时在相同类型的不同分配器之间复制或移动元素会产生额外的开销,这会改变操作的复杂性,因此指定向量的API会很麻烦。 这也使阅读规范变得非常痛苦。 许多在线文档来源(例如cppreference)都把一般情况放在了首位,并且模糊的细节说明了如果一个分配器属性用小写字母表示是对还是错,该如何更改,以免打扰99%的用户。

有很多人致力于改进C ++的分配器API来解决这些问题,例如,通过添加_excess方法并保证符合标准的集合使用它们。

我不同意这种分配器和cache_aligned_allocator从根本上添加了新功能。

也许我的意思是,它们允许您在以前无法使用它们的情况下或某些类型中使用std集合。 例如,在C ++中,如果没有堆栈分配器之类的东西,就不能将矢量的元素放在二进制文件的静态内存段中(但您可以编写自己的集合来执行此操作)。 OTOH,C ++标准不支持SIMD类型之类的过度对齐的类型,如果您尝试用new堆分配一个,您将调用未定义的行为(您需要使用posix_memalign或类似的名称) 。 使用对象通常会通过段错误(*)来显示未定义的行为。 aligned_allocator使您可以使用其他分配器来堆分配这些类型,甚至将它们放入std集合中,而无需调用未定义的行为。 当然,新的分配器将具有不同的分配模式(这些分配器基本上会使所有内存过度对齐...),但是人们使用它们的目的是能够做一些以前无法做的事情。

显然,Rust不是C ++。 C ++存在Rust所没有的问题(反之亦然)。 在Rust中,可能不需要在C ++中添加新功能的分配器,例如,SIMD类型没有任何问题。

(*)Eigen3的用户深受其苦,因为为了避免在使用C ++和STL容器时出现不确定的行为,您需要保护容器免受SIMD类型或包含SIMD类型的类型( Eigen3 docs )的new在类型上使用new更多Eigen3 docs )。

@gnzlbg谢谢,我也被smallvec的例子弄糊涂了。 这将需要不可移动的类型和Rust中的某种分配-审查中的两个RFC,然后需要更多的后续工作--因此,我现在对此没有任何疑问。 始终使用您需要的所有堆栈空间的现有smallvec策略目前看来还不错。

我也同意@rkruppe在您的修订名单,分配的新功能,不必使用分配器已知集合。 有时完整的Collection<Allocator>具有新属性(可以说完全存在于固定内存中),但这只是使用分配器的自然结果。

我在这里看到的一个例外是仅分配单个大小/类型的分配器(NVidia的分配器和平板分配器都这样做)。 我们可以为普通分配器实现一个单独的ObjAlloc<T>特性: impl<A: Alloc, T> ObjAlloc<T> for A 。 然后,如果集合只需要分配一些项目,则它们将使用ObjAlloc边界。 但是,即使提出这个问题,我也觉得很愚蠢,因为以后应该可以向后兼容。

那有意义吗?

可以,但是因为我们没有move构造函数,所以与Rust并没有真正的关系。 因此,一个(可移动的)分配器直接包含它将指针传递给它的内存是不可能的。

例如,在C ++中,如果没有堆栈分配器之类的东西,就不能将矢量的元素放在二进制文件的静态内存段中(但您可以编写自己的集合来执行此操作)。

这不是行为上的改变。 有许多有效的理由来控制集合从何处获取内存,但是所有这些都与“外部性”相关,例如性能,链接程序脚本,对整个程序的内存布局的控制等。

像aligned_allocator之类的东西使您可以使用其他分配器来堆分配这些类型,甚至将它们放入std集合中,而无需调用未定义的行为。

这就是为什么我特别提到了TBB的cache_aligned_allocator而不是Eigen的aligned_allocator的原因。 cache_aligned_allocator似乎不保证其文档中有任何特定的对齐方式(它只是说它“通常”为128字节),即使这样做了,通常也不会用于此目的(因为它的对齐方式对于普通用户而言可能太大了) SIMD类型,对于页面对齐的DMA来说太小了。 如您所说,其目的是避免错误共享。

@gnzlbg

FWIW,参数分配器对于实现分配器非常有用。

您能详细说明一下您的意思吗? 您为什么无法使用Alloc特性参数化自定义分配器? 还是您自己的自定义分配器特征,仅在最终分配器上实现Alloc特征(这两个特征不一定相等)?

我想我不清楚。 让我尝试更好地解释。 假设我正在实现一个期望使用的分配器:

  • 作为全局分配器
  • 在没有标准的环境中

假设我想在后台使用Vec来实现此分配器。 我不能直接使用今天存在的Vec ,因为

  • 如果我是全局分配器,那么使用它只会对我自己造成递归依赖
  • 如果我处在没有标准的环境中,那么今天就没有Vec

因此,我需要的是能够使用在我内部用于简单内部簿记的另一个分配器上参数化的Vec 。 这是bsalloc的目标(也是名称的来源-用于引导其他分配器)。

在elfmalloc中,我们仍然可以通过以下方式成为全局分配器:

  • 编译自己时,静态编译jemalloc作为全局分配器
  • 生成可以由其他程序动态加载的共享对象文件

请注意,在这种情况下,不要使用系统分配器作为全局分配器进行编译很重要,因为一旦加载,我们将重新引入递归依赖项,因为在这一点上,我们系统分配器。

但是在以下情况下不起作用:

  • 有人想以“官方”方式将我们用作Rust中的全局分配器(与首先创建共享对象文件相反)
  • 我们处在没有标准的环境中

OTOH,C ++标准不支持SIMD类型之类的过度对齐类型,并且如果您尝试使用new进行堆分配,则会调用未定义的行为(您需要使用posix_memalign或类似名称)。

由于我们当前的Alloc特性将对齐方式作为参数,因此我认为这类问题(“如果没有不同的对齐方式,我将无法工作”问题)对我们而言已经消失了?

@gnzlbg-全面的文章(谢谢),但是所有用例都不能涵盖持久性内存*。

必须考虑该用例。 特别是,它强烈影响正确的做法是:

  • 使用了多个分配器,尤其是当该分配器用于持久性内存时,它将永远不是系统分配器; (实际上,可能有多个持久性内存分配器)
  • 重新实现标准集合的成本很高,并导致与第三方库的代码不兼容。
  • 分配器的生存期不一定是'static
  • 存储在持久性内存中的对象需要必须从堆中填充的其他状态,即它们需要重新初始化状态。 对于互斥锁等尤其如此。 曾经是一次性的东西不再被处置。

Rust具有极好的机会在这里抓住主动权,并使其成为替代HD,SSD甚至PCI连接存储的一流平台。

*确实,这并不奇怪,因为直到最近它还是有点特别。 现在,它已在Linux,FreeBSD和Windows中得到广泛支持。

@raphaelcohn

这确实不是解决持久性内存的地方。 您并不是关于持久性内存接口的唯一思想流派-例如,出于数据完整性的原因,可能会发现流行的方法只是将其视为更快的磁盘。

如果您有以这种方式使用持久性存储器的用例,则最好先以某种方式在其他地方使用该用例。 对其进行原型处理,对分配器接口进行一些更具体的更改,并理想地证明这些更改值得它们对平均情况产生的影响。

@rpjohnst

我不同意。 这正是它所属的地方。 我想避免做出因过于狭窄的焦点和寻找证据而导致的设计决定。

当前的Intel PMDK-集中精力进行低级用户空间的支持-越来越多地使用带指针的已分配常规内存(类似于通过mmap分配的内存)进行处理。 确实,如果您想在Linux上使用持久性内存,那么,我认为这几乎是您目前唯一的选择。 本质上,使用它的最先进的工具箱之一-如果可以的话,流行一种-将其视为已分配的内存。

至于原型制作-嗯,这正是我说过的:-

我最近一直在为永久内存分配器(特别是libpmemcto)开发Rust包装器。

(您可以在https://crates.io/crates/nvml上使用我的板条箱的早期版本。 cto_pool模块中的源代码控制还有更多实验)。

我的原型是考虑到在实际的大规模系统中替换数据存储引擎所需的内容而构建的。 我的许多开源项目背后都有类似的心态。 多年来,我发现最好的库以及最好的标准,它们是从_from_实际用法中得出的。

没有什么比将实际的分配器适合当前接口更合适了。 坦白说,使用Alloc接口,然后复制整个Vec ,然后进行调整的体验是很痛苦的。 很多地方都假定未传入分配器,例如Vec::new()

在此过程中,我在原始评论中对分配器需要什么以及这种分配器的用户需要什么进行了观察。 我认为这些在关于分配器接口的讨论线程上非常有效。

好消息是,您的https://github.com/rust-lang/rust/issues/32838#issuecomment -358940992的前三个要点已由其他用例共享。

我只是想补充一点,我没有将非易失性内存添加到列表中
因为该列表列出了分配器在容器中参数化用例的用例
至少根据我的经验(被广泛使用)的C ++世界
我提到的alloctor主要来自许多人使用的非常流行的库。
虽然我知道Intel SDK的努力(其中一些库
目标C ++)我个人不知道使用它们的任何项目(它们有吗
可以与std :: vector一起使用的分配器? 我不知道)。 这不是
表示它们不被使用也不重要。 我很想知道
关于这些,但是我的帖子的重点是参数化
容器分配器非常复杂,我们应该尝试使
在不关闭容器门的情况下使用系统分配器的进度
(但我们稍后应解决)。

在2018年1月21日星期日,17:36,John Ericson [email protected]写道:

好消息是您从#32838的头3个要点开始(评论)
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 ),则生命周期将确保在删除每帧句柄之前删除所有分配的对象。 请注意,仍然会为每个对象调用dealloc ,但是如果dealloc是空操作,则整个拖放分配逻辑可能会完全或大部分被优化掉。

您还可以使用不实现Drop的自定义智能指针类型,这会使其他地方的事情变得更容易。

谢谢! 我在原始帖子中打了错字。 就是说没有破坏对象。

对于那些不是分配器方面的专家并且不能遵循此线程的人,当前的共识是什么:我们计划支持stdlib集合类型的自定义分配器吗?

@alexreg我不确定最终的计划是什么,但是这样做确实有0个技术难题。 OTOH我们没有一个好方法来在std公开它,因为默认类型变量是可疑的,但是我将它变成alloc没问题-目前只有这样,所以我们可以在lib方面无阻碍地取得进步。

@ Ericson2314好,很高兴听到。 是否已实现默认类型变量? 还是在RFC阶段? 如您所说,如果仅将它们限制在与alloc / std::heap相关的事物上,那应该没问题。

我真的认为AllocErr应该是错误的。 它将与另一个模块(例如io)更加一致。

impl Error for AllocError可能很有意义并且没有伤害,但是我个人发现Error特质是没有用的。

我今天正在看Layout :: from_size_align函数,并且“ align不得超过2 ^ 31(即1 << 31 )”限制对我来说没有意义。 git blame指向#30170。

我必须说那是一个相当具有欺骗性的提交消息,它说的是在u32中适合align ,这只是偶然的,当“固定”的实际东西(更多解决方法)是系统分配器行为异常时。

这使我这个注:“OSX / alloc_system是巨大的比马车”项目在这里应该进行检查。 尽管直接的问题已得到解决,但从长远来看,我认为解决方案不正确:因为系统分配器的行为不当不应阻止实现行为正常的分配器。 而对Layout :: from_size_align的任意限制就可以做到这一点。

@glandium请求对齐到4 GB或更大的倍数是否有用?

我可以想象这样的情况,其中有人可能希望将4GiB的分配与4GiB对齐,这目前是不可能的,但几乎没有。 但是我不认为仅仅因为我们现在不考虑这种原因就应该添加任意限制。

我可以想象这样的情况:有人可能希望将4GiB的分配与4GiB对齐

那是什么情况

我可以想象这样的情况:有人可能希望将4GiB的分配与4GiB对齐

那是什么情况

具体来说,我刚刚在mmap-alloc中添加了对任意大对齐方式的支持,以支持分配大而对齐的内存块供elfmalloc 。 这样做的想法是使内存块与其​​大小对齐,以便给定从该块分配的对象的指针,您只需要掩盖低位以找到包含的块。 我们目前不使用大小为4GB的平板(对于大对象,我们直接使用mmap),但是没有理由我们不能这么做,我完全可以想象一个需要大RAM需求的应用程序(也就是说,如果它足够频繁地分配多个GB对象,而又不想接受mmap的开销)。

这是> 4GiB对齐的可能用例:对齐大页面边界。 已经有支持> 4 GiB页面的平台。 该IBM文档说:“ POWER5 +处理器支持四种虚拟内存页面大小:4 KB,64 KB,16 MB和16 GB。” 甚至x86-64也相距不远:“大页面”通常为2 MiB,但它也支持1 GiB。

Alloc特性中的所有非类型化函数都在处理*mut u8 。 这意味着它们可以采用或返回空指针,并且所有地狱都会崩溃。 他们应该改用NonNull吗?

他们可以返回很多指针,所有这些指针都会从中返回
冲出重围。
在2018年3月4日,星期日,凌晨3:56,Mike Hommey [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另一个更有说服力的原因是,它将允许当前从Alloc方法(或Options ,如果我们在中切换)返回的Result s未来)。

使用NonNull的一个更有说服力的理由是,它将允许Alloc方法(或Option,如果将来将其切换为将来)返回的当前结果较小。

我不认为这是因为AllocErr具有两个变体。

他们可以返回许多指针,所有这些指针都可能从中消失。

但是空指针显然比其他任何指针都更错误。

我喜欢认为rust类型系统有助于使用脚枪,并用于编码不变式。 alloc的文档明确指出“如果此方法返回Ok(addr) ,则返回的地址将为非空地址”,但其返回类型则不会。 实际上, Ok(malloc(layout.size()))显然不是有效的实现。

注意,也有一些关于Layout大小需要为非零的说明,因此我也认为它应该将其编码为NonZero

并不是因为所有这些功能本质上都是不安全的,所以我们不应该采取一些预防措施。

在使用(编辑:和实现)分配器的所有可能的错误中,传递空指针是最容易追踪的方法之一(至少在拥有MMU且没有这样做的情况下,您总是会在取消引用时得到清晰的段错误)非常奇怪的事情),通常也是最不重要的问题之一。 确实,不安全的接口可以尝试防止踩踏,但是这种踩踏似乎显得不成比例地变小(与其他可能的错误以及在类型系统中对该不变式进行编码的冗长性相比)。

此外,似乎分配器实现可能只是使用未经检查的NonNull构造函数“以提高性能”:由于在正确的分配器中无论如何永远不会返回null,因此它会跳过NonNell::new(...).unwrap() 。 在那种情况下,您实际上将无法获得任何切实的脚枪防护,只能获得更多样板。 ( Result大小的好处,如果是真实的,可能仍然是令人信服的原因。)

分配器实现将只使用NonNull的未经检查的构造函数

重点不是帮助分配器实现,而是帮助用户。 如果MyVec包含一个NonNull<T>并且Heap.alloc()已经返回了一个NonNull ,那么我需要进行一次较少检查或不安全检查的呼叫。

注意,指针不仅是返回类型,而且还是deallocrealloc输入类型。 那些功能是否应该强化以防止其输入可能为null? 文档倾向于拒绝,但是类型系统倾向于拒绝。

与layout.size()类似。 分配函数是否应该以某种方式处理请求的大小为0?

(结果大小的好处,如果是真实的,可能仍然是一个令人信服的理由。)

我怀疑大小会有所好处,但是如果使用#48741,就会有代码生成好处。

如果我们继续为API用户提供更灵活的原则,则指针在返回类型中应为NonNull ,而在参数中则应为。 (这并不意味着在运行时应对这些参数进行空检查。)

我认为Postel的法律方法是错误的选择。 有没有
将空指针传递给Alloc方法有效的情况? 如果不,
那么灵活性基本上就是给脚枪多一点
敏感触发。

2018年3月5日,上午8:00,“ Simon Sapin” [email protected]写道:

如果我们继续遵循对API用户更加灵活的原则,
指针在返回类型中应为NonNull,而在参数中则不能。 (这个
并不意味着这些参数应该在运行时进行空检查。)

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件,在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,这是我需要进行的较少检查或不安全检查的调用。

嗯,这很有道理。 不修复脚枪,但集中责任。

注意,指针不仅是返回类型,而且还是dealloc和realloc的输入类型。 那些功能是否应该强化以防止其输入可能为null? 文档倾向于拒绝,但是类型系统倾向于拒绝。

在任何情况下,将空指针传递给Alloc方法都是有效的吗? 如果没有,那么这种灵活性基本上只是给步枪一个稍微敏感的触发。

用户绝对必须阅读文档并牢记不变性。 许多不变量根本无法通过类型系统强制执行-如果可以,该函数从一开始就不会不安全。 因此,这仅仅是一个问题,即是否将NonNull放在任何给定的界面中是否会真正帮助用户

  • 提醒他们阅读文档并考虑不变量
  • 提供便利( @SimonSapin的point wrt alloc的返回值)
  • 提供一些实质性的优势(例如,布局优化)

我看不出将dealloc变成NonNull强大优势。 我大致看到该API的两类用法:

  1. 相对简单的用法,在其中调用alloc ,将返回的指针存储在某个位置,然后过一会儿将存储的指针传递到dealloc
  2. 复杂的场景涉及FFI,大量的指针运算等,其中涉及确保您最后将正确的东西传递给dealloc的重要逻辑。

在这里采用NonNull基本上只会帮助第一种用例,因为那些会将NonNull在一个不错的地方,然后将其不变地传递给NonNull 。 从理论上讲,如果您要处理多个指针,并且其中只有一个是NonNull ,则可以防止某些拼写错误(当您表示bar foo时,传递dealloc使用原始指针的缺点(假设alloc返回NonNull ,而@SimonSapin认为我应该发生)将是需要输入as_ptr dealloc调用,这可能很烦人,但不会以任何方式影响安全性。

第二种用例无济于事,因为它可能无法在整个过程中继续使用NonNull ,因此它必须从它获得的原始指针中手动重新创建NonNull无论如何。 正如我之前所说,这很可能成为未经检查的/ unsafe断言,而不是实际的运行时检查,因此不会避免使用脚枪。

这并不是说我赞成dealloc使用原始指针。 我只是看不到任何声称有脚枪优势。 默认情况下,类型的一致性可能只会获胜。

很抱歉,但我读这句话是“许多不变式根本无法通过类型系统强制执行,因此,我们甚至不尝试”。 不要让完美成为善良的敌人!

我认为这更多是关于NonNull提供的保证与因必须在NonNull和原始指针之间来回转换而失去的人体工程学之间的权衡。 无论哪种方式,我都没有特别强烈的意见-双方似乎都没有道理。

@cramertj是的,但是我真的Alloc是用于晦涩,隐藏和很大程度上不安全的用例。 好吧,在晦涩难懂的代码中,我想拥有尽可能多的安全性-正是因为它们很少被接触到,所以原始作者可能不在身边。 反之,如果多年后仍要阅读该代码,则应采用人机工程学。 如果有的话,它适得其反。 该代码应努力做到非常明确,以便不熟悉的读者可以更好地了解到底发生了什么。 更少的噪音<更清晰的不变式。

第二种用例无济于事,因为它可能无法在整个过程中一直使用NonNull ,因此它必须从它获得的原始指针中手动重新创建NonNull无论如何。

这仅仅是协调失败,而不是技术必然性。 当然,目前许多不安全的API可能都使用原始指针。 因此,必须使用某种方法来引导使用NonNull或其他包装程序切换到高级接口的方式。 然后其他代码可以更容易地效仿。 我发现有0个理由会不断依赖未开发的,纯净的,不安全的代码中难以理解的,毫无信息的原始指针。

嗨!

我只想说,作为Rust自定义分配器的作者/维护者,我赞成NonNull 。 几乎出于该线程已经列出的所有原因。

另外,我想指出的是, @ glandium是firefox的jemalloc分支的维护者,并且也有许多破解分配器的经验。

对于@ Ericson2314等,我可以写很多更多的回复,但是它很快就变成了一个非常独立和哲学的辩论,因此在这里我将其简短。 我反对我认为这种API夸大了NonNull安全好处(当然还有其他好处)。 这并不是说没有安全利益,而是正如@cramertj所说的那样,在权衡下,我认为“赞成”一方被夸大了。 无论如何,我已经说过,出于其他原因,我倾向于在各个地方使用NonNull ,原因是@SimonSapin为保持一致性,在dealloc给出了alloc 。 因此,让我们这样做,不要再切线了。

如果每个人都有一些NonNull用例,那是一个很好的开始。

我们可能希望更新Unique并让朋友使用NonNull而不是NonZero来使摩擦至少保持在liballoc低水平。 但这看起来确实像我们已经在分配器的一个抽象级别上做的事情,并且我也无法想到没有在分配器级别上执行此操作的任何原因。 对我来说似乎是一个合理的改变。

From特性的两个实现已在Unique<T>NonNull<T>之间安全地转换。)

考虑到在稳定的锈中我需要非常类似于分配器API的东西,我从锈仓库中提取了代码,并将其放在单独的箱子中:

/可以/用于迭代对API进行的实验性更改,但到目前为止,它只是rust repo中内容的简单副本。

[关闭主题]是的,如果std可以使用树外稳定代码箱这样的话,那就太好了,这样我们就可以在稳定代码中试验不稳定的接口了。 这就是我喜欢使用std门面的原因之一。

std可能取决于crates.io上的一个箱子的副本,但是如果您的程序也依赖于相同的箱子,那么它就不会“看起来”相同的箱子/类型/特征来进行锈蚀,所以我不看不出有什么帮助。 无论如何,无论立面如何,在稳定通道上都无法使用不稳定的功能是一个非常刻意的选择,并非偶然。

看来我们使用NonNull有一些约定。 真正实现这一目标的前进方向是什么? 只是公关吗? RFC?

无关地,我一直在研究由Boxing东西生成的一些程序集,并且错误路径相当大。 应该对此做些什么?

例子:

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 。 本质上,这是最好的情况,但是对于两个movq和lea来说,仍然是20字节的机器代码。

我看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函数(在RUSTFLAGS-C passes=mergefunc一起尝试)。

但是最大的不同是LTO,这实际上是使Firefox直接调用malloc的原因,而在调用__rust_oom之前保留AllocErr的创建。 这也使得在调用分配器之前无需创建Layout ,而在填充AllocErr时将其保留。

这使我认为除__rust_oom之外的分配函数可能应标记为内联。

顺便说一句,在查看了Firefox的生成代码之后,我认为理想情况下最好使用moz_xmalloc而不是malloc 。 如果没有分配器特征的结合并能够替换全局堆分配器,这是不可能的,但是可能需要分配器特征的自定义错误类型: moz_xmalloc绝对可靠,在以下情况下永远不会返回失败。 IOW,它自己处理OOM,在这种情况下,锈代码不需要调用__rust_oom 。 这将使分配器函数有选择地返回!而不是AllocErr

我们已经讨论了将AllocErr设置为零大小的结构,这也可能会有所帮助。 当指针也设为NonNull ,整个返回值可以是指针大小的。

https://github.com/rust-lang/rust/pull/49669对这些API进行了许多更改,目的是稳定覆盖全局分配器的子集。 该子集的跟踪问题: https新的GlobalAlloc特性

此公关会允许我们很快做Vec::new_with_alloc(alloc)alloc: Alloc事情吗?

@alexreg

@sfackler嗯,为什么不呢? 在此之前我们需要什么? 否则,我真的不明白这个PR的意义,除非它只是用于更改全局分配器。

@alexreg

否则,我真的不明白这个PR的意义,除非它只是用于更改全局分配器。

我认为这只是为了更改全局分配器。

@alexreg如果您的意思是稳定,那么有很多尚未解决的设计问题我们还没有准备好稳定。 在Nightly上,此功能RawVec支持,对于愿意进行此操作的任何人,可以将其以#[unstable]Vec添加为#[unstable]

是的,正如PR中提到的那样,其重点是允许更改全局分配器或分配(例如,以自定义集合类型)而不放弃Vec::with_capacity

FWIW, https: //github.com/rust-lang/rust/issues/32838#issuecomment -376793369中提到的allocator_api板条箱上的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 repo / crate, @ glandium或rust master这样做吗?

@alexreg考虑到#49669中Alloc特性的重大更改量,最好等待它首先合并。

@glandium足够公平。 这似乎并没有远离着陆。 我也刚刚注意到https://github.com/pnkfelix/collections-prime回购...与您的有什么关系?

我将再添加一个开放的问题:

  • 是否允许Alloc::oom恐慌? 目前,文档说此方法必须中止该过程。 这对使用分配器的代码有影响,因为必须将它们设计为正确处理展开,而不会泄漏内存。

我认为我们应该允许恐慌,因为本地分配器的失败并不一定意味着全局分配器也会失败。 在最坏的情况下,将调用全局分配器的oom ,这将中止进程(否则将破坏现有代码)。

@alexreg不是。 它似乎只是std / alloc / collections中内容的简单副本。 好吧,它有两年的历史了。 我的箱子的范围更加有限(截至几周前,已发布的版本仅具有Alloc特质,master分支的顶部仅具有RawVecBox ),而我的目标之一就是保持其稳定的防锈性能。

@glandium好的,在这种情况下,我可能要等到PR着陆之后,再针对锈母版创建PR并标记您,以便您知道何时将其合并到母版中(然后可以将其合并到板条箱中) ,公平吗?

@alexreg很有道理。 您现在可以开始研究它,但是如果/当自行车棚改变了该公关中的情况时,这可能会在您的终端引起混乱。

@glandium我现在还有其他事情

是否允许Alloc :: oom惊慌? 目前,文档说此方法必须中止该过程。 这对使用分配器的代码有影响,因为必须将它们设计为正确处理展开,而不会泄漏内存。

@Amanieu该RFC已合并: https :

我正在考虑对API进行一项更改以提交PR:

Alloc特质分为两部分:“实现”和“助手”。 前者是allocdeallocrealloc等功能,而后者是alloc_onedealloc_onealloc_array等。虽然能够为后者实现自定义实现有一些假设的好处,但它与最常见的需求相去甚远,而且当您需要实现通用包装时(我发现这种情况非常普遍,到我实际上已经开始为此编写自定义派生类的地步),您仍然需要实现所有它们,因为包装可能正在自定义它们。

OTOH,如果Alloc特质实现者确实尝试在alloc_one做一些花哨的事情,则不能保证会为该分配调用dealloc_one 。 原因有很多:

  • 辅助程序使用不一致。 仅举一个例子, raw_vec使用alloc_arrayalloc / alloc_zeroed ,但仅使用dealloc
  • 即使持续使用例如alloc_array / dealloc_array ,仍然可以安全地将Vec转换为Box ,然后再使用dealloc
  • 然后,API的某些部分不存在( alloc_one / alloc_array零版本)

因此,即使有一些特殊的实际用例,例如alloc_one (事实上​​,我确实有对mozjemalloc的需求),但最好还是使用专门的分配器。

实际上,比这更糟糕的是,在锈库中,仅使用alloc_array ,而不使用alloc_onedealloc_onerealloc_arraydealloc_array 。 甚至连box语法都不使用alloc_one ,它使用exchange_malloc ,这需要一个sizealign 。 因此,与实现者相比,这些功能为客户提供的更多便利。

使用impl<A: Alloc> AllocHelpers for A (或AllocExt ,无论选择什么名称)之类的东西,我们仍然可以为客户提供这些功能的便利,同时不允许实现者以为自己开枪他们会通过重写它们来做一些花哨的事情(并使人们更容易实现代理分配器)。

我正在考虑对API进行一次更改以提交PR

在#50436中这样做

g

(事实上​​,我确实需要mozjemalloc),

您能详细说明这个用例吗?

mozjemalloc有一个有目的泄漏的基本分配器。 除一种对象外,它保留一个空闲列表。 我可以通过分层分配器来做到这一点,而不是使用alloc_one完成技巧。

是否需要以与您分配的确切对齐方式进行分配?

只是为了强调这个问题的答案是自己引述了

由于C11指定了与我们的实现不兼容的方式,aligned_alloc()可能永远不会实现(也就是说,free()必须能够处理高度对齐的分配)

在Windows上使用系统分配器将始终需要在分配时知道对齐方式,以便正确地释放高度对齐的分配,因此我们能否仅将问题标记为已解决?

在Windows上使用系统分配器将始终需要在分配时知道对齐方式,以便正确地释放高度对齐的分配,因此我们能否仅将问题标记为已解决?

这是一种耻辱,但事实就是如此。 然后让我们放弃过度对齐的向量。 :困惑:

让我们放弃过度对齐的向量

怎么来的? 您只需要Vec<T, OverAlignedAlloc<U16>>都可以通过过度对齐来分配和取消分配。

怎么来的? 您只需要Vec<T, OverAlignedAlloc<U16>>都可以通过过度对齐来分配和取消分配。

我应该更具体一些。 我的意思是将超标向量移到您控制范围之外的API中,即采用Vec<T>而不是Vec<T, OverAlignedAlloc<U16>>的API。 (例如CString::new() 。)

你宁可使用

#[repr(align(16))]
struct OverAligned16<T>(T);

然后Vec<OverAligned16<T>>

你宁可使用

那要看。 假设您要在f32 s向量上使用AVX内部函数(256位宽,32字节对齐要求):

  • Vec<T, OverAlignedAlloc<U32>>解决了这个问题,可以直接在向量元素上使用AVX内在函数(特别是对齐的内存负载),并且向量仍会重新引用成&[f32]切片,使其符合人体工程学。
  • Vec<OverAligned32<f32>>并不能真正解决问题。 由于对齐要求,每个f32占用32个字节的空间。 引入的填充会阻止直接使用AVX操作,因为f32不再位于连续内存中。 而且我个人觉得对&[OverAligned32<f32>]的取消处理有点乏味。

对于BoxBox<T, OverAligned<U32>>Box<OverAligned32<T>>的单个元素,两种方法都更等效,第二种方法的确更可取。 无论如何,同时拥有这两种选择都是不错的选择。

发布此wrt对Alloc特性的更改: https :

本期顶部的跟踪帖子太过时了(最近一次编辑是在2016年)。 我们需要更新的活动关注列表以有效地继续讨论。

最新的设计文档(包含当前未解决的问题以及设计决策的依据)也将极大地帮助讨论。

从“每晚实施的内容”到“最初的Alloc RFC所建议的内容”之间存在多种差异,在不同的渠道上产生了成千上万的评论(rfc repo,rust-lang跟踪问题,全局alloc 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特性来添加它释放。

那是一种方法,但是对我来说,这绝对不是最好的方法。 它具有明显的缺点,例如,a)仅在可能进行指针->分配器查找时才起作用(这不适用于大多数竞技场分配器),并且b)大大增加了dealloc开销。 本提案本提案这样的更具通用性的效果或上下文系统。 或者完全不同的东西。 因此,我不认为我们应该以与Alloc特性的当前化身向后兼容的方式轻松解决此问题。

@joshlf考虑到Box<T, A>仅在被删除时才可以访问自身的事实,这是我们只能使用安全代码才能做到的最好的事情。 这种模式对于像竞技场一样的分配器很有用,当分配器被删除时,它们具有无操作的dealloc且只有可用内存。

对于分配器由容器拥有(例如LinkedList )并管理多个分配的更复杂的系统,我希望Box不会在内部使用。 而是LinkedList内部函数将使用原始指针,该原始指针将通过LinkedList对象中包含的Alloc实例进行分配和释放。 这样可以避免每个指针的大小加倍。

考虑到Box<T, A>仅在被删除时才可以访问自身的事实,这是我们只能使用安全代码才能做到的最好的事情。 这种模式对于像竞技场一样的分配器很有用,当分配器被删除时,它们具有无操作的dealloc且只有可用内存。

是的,但是Box不知道dealloc是无操作的。

对于更复杂的系统,其中分配器由容器拥有(例如LinkedList )并管理多个分配,我希望Box不会在内部使用。 相反, LinkedList内部函数将使用原始指针,该原始指针将通过LinkedList对象中包含的Alloc实例进行分配和释放。 这样可以避免每个指针的大小加倍。

我认为要求人们使用不安全的代码来编写所有集合真的很可惜。 如果目标是使所有集合(可能包括标准库之外的集合)在分配器上可选地参数化,并且Box不是分配器参数化的,那么集合作者不得使用Box根本不使用

是的,但是Box不知道dealloc是no-op。

为什么不适应C ++ unique_ptr作用?
也就是说:如果分配器是“有状态的”,则存储指向分配器的指针,如果分配器是“无状态的”,则不存储指针
(例如,在mallocmmap周围的全局包装器)。
这将需要将当前的Alloc训练分为两个特征: StatefulAllocStatelessAlloc
我意识到这是非常不礼貌和不礼貌的(可能有人在先前的讨论中已经提出过)。
尽管该解决方案很笨拙,但它是简单且向后兼容的(无性能损失)。

我认为要求人们使用不安全的代码来编写所有集合真的很可惜。 如果目标是使所有集合(可能包括标准库之外的集合)在分配器上可选地参数化,并且Box不是分配器参数化的,那么集合作者必须完全不使用Box或使用不安全的代码(并且请记住,记住永远释放事物是C和C ++中最常见的内存不安全类型之一,因此,这是很难获得正确的不安全代码)。 这似乎是一个不幸的交易。

恐怕一种效果或上下文系统的实现可能会花费太多时间(如果原则上可行),这种实现可能允许一个人以安全的方式编写基于节点的容器(如列表,树等)。
我没有找到解决此问题的论文或学术语言(请实际纠正此类问题,请纠正我)。

因此,至少在短期内,采用unsafe来实现基于节点的容器可能是必不可少的。

@eucpp注意, unique_ptr不存储分配器,而是存储Deleter

Deleter必须是FunctionObject或对FunctionObject的左值引用或对函数的左值引用,并且可以使用unique_ptr类型的参数进行调用:: pointer`

我认为这大致相当于我们提供了分开的AllocDealloc特征。

@cramertj是的,您是对的。 尽管如此,还是需要两个特征-有状态和无状态Dealloc

ZST解除分配不够吗?

2018年6月12日,星期二,下午3:08 Evgeniy Moiseenko [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解除分配不够吗?

@remexre我想它会是:)

我不知道rust编译器开箱即用地支持ZST。
在C ++中,至少需要一些有关空基优化的技巧。
我在Rust刚很新,所以对一些明显的错误感到抱歉。

我认为对于有状态与无状态我们不需要单独的特征。

使用Box A类型参数扩展Box ,它将直接包含A的值,而不是A的引用或指针。 该类型可以是无状态分配器的零大小。 或A本身可以是对有状态分配器的引用或句柄,可以在多个已分配对象之间共享。 因此,您可能想要执行impl<'r> Alloc for &'r MyAllocator类的操作,而不是impl Alloc for MyAllocator impl<'r> Alloc for &'r MyAllocator

顺便说一句,只知道如何分配而不知道如何分配的Box不会实现Clone

@SimonSapin我希望Clone ing要求再次指定分配器,就像创建新的Box一样(也就是说,不会使用Clone来完成)

@cramertjVec和其他实现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我不会反对这种暗示,但是我也希望有一个clone_into或使用提供的分配器的东西。

Allocalloc_copy方法有意义吗? 这可用于为大型分配提供更快的memcpy( Copy/Clone )实现,例如通过写页面的写复制克隆。

这将是很酷的,并且为它提供默认实现很简单。

这样的alloc_copy函数将使用什么? impl Clone for Box<T, A>

是的,同上Vec

研究了更多内容后,似乎可以在相同的过程范围内(在骇客和不可能之间)创建写时复制页面的方法,至少在您要进行一个以上层次的工作时如此。 因此, alloc_copy不会有太大的好处。

取而代之的是,允许将来使用虚拟内存的更一般的逃生舱口可能有用。 即,如果分配很大,无论如何都要得到mmap的支持,并且是无状态的,那么分配器可以保证对分配的未来更改不予理会。 然后,用户可以将该内存移至管道,取消映射或进行类似操作。
另外,可能有一个愚蠢的mmap-all-the-things分配器和一个try-transfer函数。

取而代之的是更通用的逃生舱口,它允许将来使用虚拟内存

内存分配器(malloc,jemalloc等)通常不允许您从它们中窃取任何类型的内存,并且通常不允许您查询或更改其拥有的内存的属性。 那么,这种一般的逃生舱口与内存分配器有什么关系?

此外,平台之间对虚拟内存的支持也有很大差异,以至于有效使用虚拟内存通常要求每个平台使用不同的算法,并且保证的方式也完全不同。 我已经看到了一些关于虚拟内存的可移植抽象,但是我还没有看到由于它们的“可移植性”而在某些情况下没有失效的抽象。

你是对的。 首先,使用自定义分配器可以最好地满足任何此类用例(我主要考虑的是特定于平台的优化)。

对Andrei Alexandrescu在他的CppCon演示中描述的可组合分配器API有何想法? 该视频可在YouTube上找到,网址为: //www.youtube.com/watch?v=LIb3L4vKZ7U (他在26:00左右开始描述他的拟议设计,但是演讲很有趣,您可能更喜欢看一遍) 。

听起来这是不可避免的结论,那就是集合库应该是通用的WRT分配,而应用程序程序员本人应该应该能够在施工现场自由组成分配器和集合。

对Andrei Alexandrescu在他的CppCon演示中描述的可组合分配器API有何想法?

当前的Alloc API允许编写可组合的分配器(例如MyAlloc<Other: Alloc> ),并且您可以使用traits和specialization来实现Andreis所讲的几乎所有功能。 但是,除了应该能够做到的“想法”之外,Andrei的演讲几乎没有任何内容适用于Rust,因为从一开始就使用Rust的泛型系统,而Andrei构建API的方式是基于不受约束的泛型+ SFINAE / static。与那个完全不同。

我想建议稳定其余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移至新位置(例如,小孔a2 = a1;),则客户端通过a2解除分配该块仍然是合理的。

这是否意味着分配者必须Unpin

接得好!

由于Alloc特质仍然不稳定,因此,如果我们想修改RFC的这一部分,我认为我们仍然可以更改规则。 但这确实是要牢记的。

@gnzlbg是的,我知道泛型系统之间的巨大差异,并不是他详细介绍的所有内容都可以在Rust中实现。 但是,自发布以来,我一直在不停地研究图书馆,并且我取得了良好的进展。

这是否意味着分配者_必须_为Unpin

没有。 Unpin与类型包装在Pin时的行为有关,对此API没有特定的连接。

但是不能使用Unpin强制执行上述约束吗?

关于dealloc_array另一个问题:函数为什么返回Result ? 在当前的实现中,这可能在两种情况下失败:

  • n为零
  • n * size_of::<T>()容量溢出

对于第一种情况,我们有两种情况(如文档中所述,实现者可以在这两种情况之间进行选择):

  • 分配返回Ok上归零n => dealloc_array也应该返回Ok
  • 分配在归零后的n =>上返回Err ,没有指针可以传递给dealloc_array

第二点是通过以下安全约束来确保的:

[T; n]的布局必须适合该内存块。

这意味着,我们必须使用与分配中相同的n调用dealloc_array 。 如果可以分配具有n元素的数组,则nT 。 否则,分配将失败。

编辑:关于最后一点:即使usable_size返回的值大于n * size_of::<T>() ,这仍然有效。 否则,实现将违反此特征约束:

块的大小必须在[use_min, use_max]范围内,其中:

  • [...]
  • use_max是(如果)通过调用alloc_excessrealloc_excess分配块时(或将要返回的)容量。

这仅适用,因为特征需要unsafe impl

对于第一种情况,我们有两种情况(如文档中所述,实现者可以在这两种情况之间进行选择):

  • 分配在归零后的n上返回Ok n

您从何处获得此信息?

文档中的所有Alloc::alloc_方法都指定零大小分配的行为在其“安全”子句下未定义。

core::alloc::Alloc文档(突出显示的相关部分):

关于零大小的类型和零大小的布局的注释: Alloc特质状态下的许多方法都指出分配请求必须为非零大小,否则可能导致未定义的行为。

  • 但是,一些较高级别的分配方法alloc_onealloc_array )在零大小类型上定义良好,可以选择支持它们:取决于实现者是否返回Err ,或使用某些指针
  • 如果在这种情况下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 ”。 这是非常通用的。 很难说这是否是有用的错误条件。

IMO,它受到与上述相同的安全条款的保护; 如果[T; N]的容量溢出,则该内存块不适合释放。 也许@pnkfelix可以详细说明吗?

从某种意义上说, Alloc::alloc_array(1)是一个石蕊测试,用于检查分配器是否支持零大小的分配。

您是说Alloc::alloc_array(0)吗?

IMO,它受到与上述相同的安全条款的保护; 如果[T; N]的容量溢出,则不会_fit_取消分配该内存块。

请注意,用户可以为自己的自定义分配器实现此特征,并且这些用户可以覆盖这些方法的默认实现。 因此,在考虑是否应该为算术溢出返回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,并且他们无法为ZST返回Err
  • 分配ZST始终是UB,在这种情况下,呼叫者有责任短路
  • 调用者可以实现某种alloc_inner方法,而带有默认实现的alloc方法可以实现短路。 alloc必须支持ZST,但是可能不会为ZST调用alloc_inner (这是为了使我们可以在特征定义中的单个位置添加短路逻辑,以便按顺序添加)为实施者节省一些样板)

是否有理由需要我们使用当前API的灵活性?

是否有理由需要我们使用当前API的灵活性?

这是一个权衡。 可以说,使用Alloc特质的次数要多于实现,因此可能有意义的是,通过为ZST提供内置支持,使使用Alloc尽可能容易。

这意味着Alloc特质的实现者将需要注意这一点,但对我而言更重要的是,那些试图发展Alloc特质的人将需要在每次API更改时牢记ZST。 通过说明如何处理ZST(或者如果是“实现定义的”,可能会这样),也会使API的文档复杂化。

C ++分配器采用这种方法,其中分配器尝试解决许多不同的问题。 由于所有这些问题如何在API中相互作用,这不仅使它们更难实现和发展,而且使用户更难以实际使用。

我认为处理ZST和分配/取消分配原始内存是两个正交且不同的问题,因此我们应该通过不处理它们来保持Alloc trait API的简单性。

像libstd这样的Alloc用户将需要处理ZST,例如在每个集合上。 这绝对是一个值得解决的问题,但我认为Alloc特质并不是解决问题的地方。 我希望有一个实用程序可以解决这个问题,它会在必要时在libstd中弹出,当发生这种情况时,我们可以尝试使用RFC这样的实用程序并将其公开在std :: heap中。

听起来一切合理。

我认为处理ZST和分配/取消分配原始内存是两个正交且不同的问题,因此我们应该通过不处理它们来保持Alloc trait API的简单性。

难道不意味着我们应该让API显式地不处理ZST,而不是由实现定义? IMO,一个“不受支持的”错误在运行时不是很有帮助,因为绝大多数调用者将无法定义后备路径,因此必须假定无论如何都不支持ZST。 似乎更干净,只是简化了API并声明它们被_never_支持。

alloc用户将使用专业化来处理ZST吗? 或者只是if size_of::<T>() == 0支票?

alloc用户将使用专业化来处理ZST吗? 或者只是if size_of::<T>() == 0支票?

后者应足够; 适当的代码路径在编译时将被删除。

难道不意味着我们应该让API显式地不处理ZST,而不是由实现定义?

对我来说,一个重要的约束是,如果我们禁止零大小的分配,则Alloc方法应该能够假定传递给他们的Layout不是零大小的。

有多种方法可以实现此目的。 一种方法是在所有Alloc方法中添加另一个Safety子句,说明如果Layout的大小为零,则行为是不确定的。

或者,我们可以禁止大小为零的Layout s,然后Alloc不需要说零大小的分配,因为这不能安全地发生,但是这样做会带来一些不利影响。

例如,一些类型,如HashMap积累的Layout从多个Layout s,而同时最后Layout可能不是零大小时,中间的可能是(例如HashSet )。 因此,这些类型将需要使用“其他”(例如LayoutBuilder类型)来建立其最终的Layout s,并支付“非零大小”支票(或使用转换为Layout时的_unchecked )方法。

分配用户会使用专业化来处理ZST吗? 或者只是size_of ::()== 0检查?

我们还不能专门研究ZST。 现在所有代码都使用size_of::<T>() == 0

有多种方法可以实现此目的。 一种方法是在所有Alloc方法中添加另一个Safety子句,说明如果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因为它直接分配Tstd几乎所有其他类型从不分配T ,但是分配器不知道的某些私有内部类型。 例如, RcArc可以分配(InternalRefCounts, T)List / BTreeSet /等。分配内部节点类型Vec / Deque / ...分配T s数组,但不分配T s数组,依此类推。

对于BoxVec我们可以向后兼容的方式添加BoxAllocArrayAlloc特质,并带有Alloc总括分配器,分配器可以如果需要以通用方式解决这些问题,则专门劫持这些人的行为。 但是,为什么提供与分配器合用以利用类型信息的MyAllocBoxMyAllocVec类型不是可行的解决方案吗?

由于我们现在有一个用于分配器WG

@TimDiekmann好一点! 我将继续并关闭该页面,以支持该存储库中的讨论线程。

这仍然是某些#[unstable]属性指向的跟踪问题。 我认为在这些功能稳定或不推荐使用之前,不应关闭它。 (或者我们可以更改属性以指向其他问题。)

是的,在git master中引用的不稳定功能肯定应该有一个开放的跟踪问题。

同意还添加了一个通知并链接到OP。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

withoutboats picture withoutboats  ·  202评论

nikomatsakis picture nikomatsakis  ·  340评论

aturon picture aturon  ·  184评论

Mark-Simulacrum picture Mark-Simulacrum  ·  681评论

withoutboats picture withoutboats  ·  211评论