From df69ec1e52b313865ebd18f5c4af85d88ec8856e Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Sat, 18 Feb 2017 22:17:11 -0800 Subject: [PATCH 1/6] Global allocators round 3 --- text/0000-global-allocators.md | 248 +++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 text/0000-global-allocators.md diff --git a/text/0000-global-allocators.md b/text/0000-global-allocators.md new file mode 100644 index 00000000000..e51840a5cdf --- /dev/null +++ b/text/0000-global-allocators.md @@ -0,0 +1,248 @@ +- Feature Name: `allocator` +- Start Date: 2017-02-04 +- RFC PR: +- Rust Issue: + +# Summary +[summary]: #summary + +Overhaul the global allocator APIs to put them on a path to stabilization, and +switch the default allocator to the system allocator when the feature +stabilizes. + +This RFC is a refinement of the previous [RFC 1183][]. + +[RFC 1183]: https://github.com/rust-lang/rfcs/blob/master/text/1183-swap-out-jemalloc.md + +# Motivation +[motivation]: #motivation + +## API + +The unstable `allocator` feature allows developers to select the global +allocator which will be used in a program. A crate identifies itself as an +allocator with the `#![allocator]` annotation, and declares a number of +allocation functions with specific `#[no_mangle]` names and a C ABI. To +override the default global allocator, a crate simply pulls an allocator in +via an `extern crate`. + +There are a couple of issues with the current approach: + +A C-style ABI is error prone - nothing ensures that the signatures are correct, +and if a function is omitted that error will be caught by the linker rather than +compiler. The Macros 1.1 API is similar in that certain special functions must +be identified to the compiler, and in that case a special attribute +(`#[proc_macro_derive]`)is used rather than a magic symbol name. + +Since an allocator is automatically selected when it is pulled into the crate +graph, it is painful to compose allocators. For example, one may want to create +an allocator which records statistics about active allocations, or adds padding +around allocations to attempt to detect buffer overflows in unsafe code. To do +this currently, the underlying allocator would need to be split into two +crates, one which contains all of the functionality and another which is tagged +as an `#![allocator]`. + +## jemalloc + +Rust's default allocator has historically been jemalloc. While jemalloc does +provide significant speedups over certain system allocators for some allocation +heavy workflows, it has has been a source of problems. For example, it has +deadlock issues on Windows, does not work with Valgrind, adds ~300KB to +binaries, and has caused crashes on macOS 10.12. See [this comment][] for more +details. As a result, it is already disabled on many targets, including all of +Windows. While there are certainly contexts in which jemalloc is a good choice, +developers should be making that decision, not the compiler. The system +allocator is a more reasonable and unsurprising default choice. + +A third party crate allowing users to opt-into jemalloc would also open the door +to provide access to some of the library's other features such as tracing, arena +pinning, and diagnostic output dumps for code that depends on jemalloc directly. + +[this comment]: https://github.com/rust-lang/rust/issues/36963#issuecomment-252029017 + +# Detailed design +[design]: #detailed-design + +## Defining an allocator + +An allocator crate identifies itself as such by applying the `#![allocator]` +annotate at the crate root. It then defines a specific set of functions which +are tagged with attributes: + +```rust +#![allocator] + +/// Returns a pointer to `size` bytes of memory aligned to `align`. +/// +/// On failure, returns a null pointer. +/// +/// Behavior is undefined if the requested size is 0 or the alignment is not a +/// power of 2. The alignment must be no larger than the largest supported page +/// size on the platform. +#[allocator(allocate)] +pub fn allocate(size: usize, align: usize) -> *mut u8 { + ... +} + +/// Returns a pointer to `size` bytes of memory aligned to `align`, and +/// initialized with zeroes. +/// +/// On failure, returns a null pointer. +/// +/// Behavior is undefined if the requested size is 0 or the alignment is not a +/// power of 2. The alignment must be no larger than the largest supported page +/// size on the platform. +#[allocator(allocate_zeroed)] +pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8 { + ... +} + +/// Deallocates the memory referenced by `ptr`. +/// +/// The `ptr` parameter must not be null. +/// +/// The `old_size` and `align` parameters are the parameters that were used to +/// create the allocation referenced by `ptr`. +#[allocator(deallocate)] +pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize) { + ... +} + +/// Resizes the allocation referenced by `ptr` to `size` bytes. +/// +/// On failure, returns a null pointer and leaves the original allocation +/// intact. +/// +/// If the allocation was relocated, the memory at the passed-in pointer is +/// undefined after the call. +/// +/// Behavior is undefined if the requested size is 0 or the alignment is not a +/// power of 2. The alignment must be no larger than the largest supported page +/// size on the platform. +/// +/// The `old_size` and `align` parameters are the parameters that were used to +/// create the allocation referenced by `ptr`. +#[allocator(reallocate)] +pub fn reallocate(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> *mut u8 { + ... +} + +/// Resizes the allocation referenced by `ptr` to `size` bytes without moving +/// it. +/// +/// The new size of the allocation is returned. This must be at least +/// `old_size`. The allocation must always remain valid. +/// +/// Behavior is undefined if the requested size is 0 or the alignment is not a +/// power of 2. The alignment must be no larger than the largest supported page +/// size on the platform. +/// +/// The `old_size` and `align` parameters are the parameters that were used to +/// create the allocation referenced by `ptr`. +/// +/// This function is optional. The default implementation simply returns +/// `old_size`. +#[allocator(reallocate_inplace)] +pub fn reallocate_inplace(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> usize { + ... +} +``` + +Note that `useable_size` has been removed, as it is not used anywhere in the +standard library. + +The allocator functions must be publicly accessible, but can have any name and +be defined in any module. However, it is recommended to use the names above in +the crate root to minimize confusion. + +An allocator must provide all functions with the exception of +`reallocate_inplace`. New functions can be added to the API in the future in a +similar way to `reallocate_inplace`. + +## Using an allocator + +The functions that an allocator crate defines can be called directly, but most +usage will happen through the *global allocator* interface located in +`std::heap`. This module exposes a set of functions identical to those described +above, but that call into the global allocator. To select the global allocator, +a crate declares it via an `extern crate` annotated with `#[allocator]`: + +```rust +#[allocator] +extern crate jemalloc; +``` + +As its name would suggest, the global allocator is a global resource - all +crates in a dependency tree must agree on the selected global allocator. If two +or more distinct allocator crates are selected, compilation will fail. Note that +multiple crates can select a global allocator as long as that allocator is the +same across all of them. In addition, a crate can depend on an allocator crate +without declaring it to be the global allocator by omitting the `#[allocator]` +annotation. + +## Standard library + +The standard library will gain a new stable crate - `alloc_system`. This is the +default allocator crate and corresponds to the "system" allocator (i.e. `malloc` +etc on Unix and `HeapAlloc` etc on Windows). + +The `alloc::heap` module will be reexported in `std` and stabilized. It will +simply contain functions matching directly to those defined by the allocator +API. The `alloc` crate itself may also be stabilized at a later date, but this +RFC does not propose that. + +The existing `alloc_jemalloc` may continue to exist as an implementation detail +of the Rust compiler, but it will never be stabilized. Applications wishing to +use jemalloc can use a third-party crate from crates.io. + +# How We Teach This +[how-we-teach-this]: #how-we-teach-this + +The term "allocator" is the canonical one for this concept. It is unfortunately +shared with a similar but distinct concept described in [RFC 1398][], which +defined an `Allocator` trait over which collections be parameterized. This API +is disambiguated by referring specifically to the "global" or "default" +allocator. + +Global allocator selection would be a somewhat advanced topic - the system +allocator is sufficient for most use cases. It is a new tool that developers can +use to optimize for their program's specific workload when necessary. + +It should be emphasized that in most cases, the "terminal" crate (i.e. the bin, +cdylib or staticlib crate) should be the only thing selecting the global +allocator. Libraries should be agnostic over the global allocator unless they +are specifically designed to augment functionality of a specific allocator. + +Defining an allocator is an even more advanced topic that should probably live +in the _Nomicon_. + +[RFC 1398]: https://github.com/rust-lang/rfcs/pull/1398 + +# Drawbacks +[drawbacks]: #drawbacks + +Dropping the default of jemalloc will regress performance of some programs until +they manually opt back into that allocator, which may produce confusion in the +community as to why things suddenly became slower. + +The allocator APIs are to some extent designed after what jemalloc supports, +which is quite a bit more than the system allocator is able to. The Rust +wrappers for those simpler allocators have to jump through hoops to ensure that +all of the requirements are met. + +# Alternatives +[alternatives]: #alternatives + +We could require that at most one crate selects a global allocator in the crate +graph, which may simplify the implementation. + +The allocator APIs could be simplified to a more "traditional" +malloc/calloc/free API at the cost of an efficiency loss when using allocators +with more powerful APIs. + +# Unresolved questions +[unresolved]: #unresolved-questions + +It is currently forbidden to pass a null pointer to `deallocate`, though this is +guaranteed to be a noop with libc's `free` at least. Some kinds of patterns in C +are cleaner when null pointers can be `free`d - is the same true for Rust? From 56d68203ed164ae5b963320ba9cb268254a299bb Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Tue, 18 Apr 2017 23:46:42 +0200 Subject: [PATCH 2/6] Tweaks from feedback --- text/0000-global-allocators.md | 56 ++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/text/0000-global-allocators.md b/text/0000-global-allocators.md index e51840a5cdf..b988f9ca944 100644 --- a/text/0000-global-allocators.md +++ b/text/0000-global-allocators.md @@ -79,6 +79,8 @@ are tagged with attributes: /// Behavior is undefined if the requested size is 0 or the alignment is not a /// power of 2. The alignment must be no larger than the largest supported page /// size on the platform. +/// +/// This function is required. #[allocator(allocate)] pub fn allocate(size: usize, align: usize) -> *mut u8 { ... @@ -92,6 +94,9 @@ pub fn allocate(size: usize, align: usize) -> *mut u8 { /// Behavior is undefined if the requested size is 0 or the alignment is not a /// power of 2. The alignment must be no larger than the largest supported page /// size on the platform. +/// +/// This function is optional. If not provided, `allocate` will be called and +/// the resulting buffer will be zerored. #[allocator(allocate_zeroed)] pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8 { ... @@ -103,6 +108,8 @@ pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8 { /// /// The `old_size` and `align` parameters are the parameters that were used to /// create the allocation referenced by `ptr`. +/// +/// This function is required. #[allocator(deallocate)] pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize) { ... @@ -122,6 +129,11 @@ pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize) { /// /// The `old_size` and `align` parameters are the parameters that were used to /// create the allocation referenced by `ptr`. +/// +/// This function is optional. If not provided, an implementation will be +/// generated which calls `allocate` to obtain a new buffer, copies the old +/// memory contents to the new buffer, and then calls `deallocate` on the old +/// buffer. #[allocator(reallocate)] pub fn reallocate(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> *mut u8 { ... @@ -155,9 +167,9 @@ The allocator functions must be publicly accessible, but can have any name and be defined in any module. However, it is recommended to use the names above in the crate root to minimize confusion. -An allocator must provide all functions with the exception of -`reallocate_inplace`. New functions can be added to the API in the future in a -similar way to `reallocate_inplace`. +Note that new functions can be added to this API in the future as long as they +can have default implementations in a similar manner to other optional +functions. ## Using an allocator @@ -240,9 +252,47 @@ The allocator APIs could be simplified to a more "traditional" malloc/calloc/free API at the cost of an efficiency loss when using allocators with more powerful APIs. +The global allocator could be an instance of the `Allocator` trait. Since that +trait's methods take `&mut self`, things are a bit complicated however. The +allocator would most likely need to be a `const` type implementing `Allocator` +since it wouldn't be sound to interact with a static. This may cause confusion +since the allocator itself will therefor not be allowed to maintain any state +internally since a new instance will be created for each use. In addition, the +`Allocator` trait uses a `Layout` type as a higher level encapsulation of the +requested alignment and size of the allocation. The larger API surface area +will most likely cause this feature to have a significantly longer stabilization +period. + # Unresolved questions [unresolved]: #unresolved-questions It is currently forbidden to pass a null pointer to `deallocate`, though this is guaranteed to be a noop with libc's `free` at least. Some kinds of patterns in C are cleaner when null pointers can be `free`d - is the same true for Rust? + +The `Allocator` trait defines several methods that do not have corresponding +implementations here: + +* `oom`, which is called after a failed allocation to provide any allocator + specific messaging that may exist. +* `usable_size`, which is mentioned above as being unused, and should probably + be removed from this trait as well. +* `alloc_excess`, which is like `alloc` but returns the entire usable size + including any extra space beyond the requested size. +* Some other higher level convenience methods like `alloc_array`. + +Should any of these be added to the global allocator as well? It may make sense +to add `alloc_excess` to the allocator API. This can either have a default +implementation which simply calls `allocate` and returns the input size, or +calls `allocate` followed by `reallocate_inplace`. + +The existing `usable_size` function (proposed for removal) only takes a size and +align. A similar, but potentially more useful API is one that takes a pointer +to a heap allocated region and returns the usable size of it. This is supported +as a GNU extension `malloc_useable_size` in the system allocator, and in +jemalloc as well. An [issue][usable_size] has been filed to add this to support +this to aid heap usage diagnostic crates. It would presumably have to return an +`Option` to for allocators that do not have such a function, but this limits +its usefulness if support can't be guaranteed. + +[usable_size]: https://github.com/rust-lang/rust/issues/32075 From 770abead19c488d78ea1f6485509bdf76670a919 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Sun, 30 Apr 2017 22:38:38 -0700 Subject: [PATCH 3/6] Use a trait instead of free functions --- text/0000-global-allocators.md | 224 +++++++++++++++------------------ 1 file changed, 101 insertions(+), 123 deletions(-) diff --git a/text/0000-global-allocators.md b/text/0000-global-allocators.md index b988f9ca944..beae4339b56 100644 --- a/text/0000-global-allocators.md +++ b/text/0000-global-allocators.md @@ -30,9 +30,10 @@ There are a couple of issues with the current approach: A C-style ABI is error prone - nothing ensures that the signatures are correct, and if a function is omitted that error will be caught by the linker rather than -compiler. The Macros 1.1 API is similar in that certain special functions must -be identified to the compiler, and in that case a special attribute -(`#[proc_macro_derive]`)is used rather than a magic symbol name. +compiler. + +Allocators have some state, and with the current API, that state is forced to be +truly global since bare functions can't carry state. Since an allocator is automatically selected when it is pulled into the crate graph, it is painful to compose allocators. For example, one may want to create @@ -65,135 +66,120 @@ pinning, and diagnostic output dumps for code that depends on jemalloc directly. ## Defining an allocator -An allocator crate identifies itself as such by applying the `#![allocator]` -annotate at the crate root. It then defines a specific set of functions which -are tagged with attributes: +We introduce a new trait, `GlobalAllocator`. It is similar to the `Allocator` +trait described in [RFC 1398][], but is stripped down and the methods take +`&self` rather than `&mut self`. + +[RFC 1398]: https://github.com/rust-lang/rfcs/blob/master/text/1398-kinds-of-allocators.md ```rust -#![allocator] - -/// Returns a pointer to `size` bytes of memory aligned to `align`. -/// -/// On failure, returns a null pointer. -/// -/// Behavior is undefined if the requested size is 0 or the alignment is not a -/// power of 2. The alignment must be no larger than the largest supported page -/// size on the platform. -/// -/// This function is required. -#[allocator(allocate)] -pub fn allocate(size: usize, align: usize) -> *mut u8 { - ... + +#[lang = "global_allocator"] +pub unsafe trait GlobalAllocator: Send + Sync { + /// Returns a pointer to a newly allocated region of memory suitable for the + /// provided `Layout`. The contents of the memory are undefined. + /// + /// On failure, returns a null pointer. + pub fn allocate(&self, layout: Layout) -> *mut u8; + + /// Returns a pointer to a newly allocated region of memory suitable for the + /// provided `Layout`. The memory is guaranteed to contain zeroes. + /// + /// On failure, returns a null pointer. + pub fn allocate_zeroed(&self, layout: Layout) -> *mut u8 { + let ptr = self.allocate(layout); + if !ptr.is_null() { + ptr::write_bytes(ptr, 0, layout.size()); + } + ptr + } + + /// Deallocates the memory referenced by `ptr`. + /// + /// The pointer must correspond to a region of memory previously allocated + /// by this allocator with the provided layout. + pub unsafe fn deallocate(&self, ptr: *mut u8, layout: Layout); + + /// Resizes the allocation referenced by `ptr` a new layout. + /// + /// On failure, returns a null pointer and leaves the original allocation + /// intact. + /// + /// If the allocation was relocated, the memory at the passed-in pointer is + /// undefined after the call. + /// + /// The pointer must correspond to a region of memory previously allocated + /// by this allocator with the provided layout. + pub fn reallocate(&self, ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { + let new_ptr = self.alloc(layout); + if !new_ptr.is_null() { + ptr::copy_nonoverlapping(ptr, new_ptr, cmp::min(old_layout.size(), layout.size())); + self.deallocate(ptr); + } + new_ptr + } } +``` + +Two methods currently defined in the global allocatr API are not present on this +trait: `usable_size` which is used nowhere in the standard library, and +`reallocate_inplace`, which is only used in libarena. -/// Returns a pointer to `size` bytes of memory aligned to `align`, and -/// initialized with zeroes. -/// -/// On failure, returns a null pointer. -/// -/// Behavior is undefined if the requested size is 0 or the alignment is not a -/// power of 2. The alignment must be no larger than the largest supported page -/// size on the platform. -/// -/// This function is optional. If not provided, `allocate` will be called and -/// the resulting buffer will be zerored. -#[allocator(allocate_zeroed)] -pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8 { +A global allocator is a type implementing `GlobalAllocator` which can be +constructed in a constant expression. + +## Using an allocator + +While the `GlobalAllocator` trait can be used like any other, the most common +usage of a global allocator is through the functions defined in the +`std::heap` module. It contains free functions corresponding to each of the +methods defined on the `GlobalAllocator` trait: + +```rust +pub fn allocate(layout: Layout) -> *mut u8 { ... } -/// Deallocates the memory referenced by `ptr`. -/// -/// The `ptr` parameter must not be null. -/// -/// The `old_size` and `align` parameters are the parameters that were used to -/// create the allocation referenced by `ptr`. -/// -/// This function is required. -#[allocator(deallocate)] -pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize) { +pub fn allocate_zeroed(layout: Layout) -> *mut u8 { ... } -/// Resizes the allocation referenced by `ptr` to `size` bytes. -/// -/// On failure, returns a null pointer and leaves the original allocation -/// intact. -/// -/// If the allocation was relocated, the memory at the passed-in pointer is -/// undefined after the call. -/// -/// Behavior is undefined if the requested size is 0 or the alignment is not a -/// power of 2. The alignment must be no larger than the largest supported page -/// size on the platform. -/// -/// The `old_size` and `align` parameters are the parameters that were used to -/// create the allocation referenced by `ptr`. -/// -/// This function is optional. If not provided, an implementation will be -/// generated which calls `allocate` to obtain a new buffer, copies the old -/// memory contents to the new buffer, and then calls `deallocate` on the old -/// buffer. -#[allocator(reallocate)] -pub fn reallocate(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> *mut u8 { +pub unsafe fn deallocate(&self, ptr: *mut u8, layout: Layout) { ... } -/// Resizes the allocation referenced by `ptr` to `size` bytes without moving -/// it. -/// -/// The new size of the allocation is returned. This must be at least -/// `old_size`. The allocation must always remain valid. -/// -/// Behavior is undefined if the requested size is 0 or the alignment is not a -/// power of 2. The alignment must be no larger than the largest supported page -/// size on the platform. -/// -/// The `old_size` and `align` parameters are the parameters that were used to -/// create the allocation referenced by `ptr`. -/// -/// This function is optional. The default implementation simply returns -/// `old_size`. -#[allocator(reallocate_inplace)] -pub fn reallocate_inplace(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> usize { +pub fn reallocate(ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { ... } ``` -Note that `useable_size` has been removed, as it is not used anywhere in the -standard library. +Each of these functions simply delegates to the selected global allocator. The +allocator is selected by tagging a static value of a type implementing +`GlobalAllocator` with the `#[allocator]` annotation: -The allocator functions must be publicly accessible, but can have any name and -be defined in any module. However, it is recommended to use the names above in -the crate root to minimize confusion. - -Note that new functions can be added to this API in the future as long as they -can have default implementations in a similar manner to other optional -functions. - -## Using an allocator +```rust +extern crate my_allocator; -The functions that an allocator crate defines can be called directly, but most -usage will happen through the *global allocator* interface located in -`std::heap`. This module exposes a set of functions identical to those described -above, but that call into the global allocator. To select the global allocator, -a crate declares it via an `extern crate` annotated with `#[allocator]`: +use my_allocator::{MyAllocator, MY_ALLOCATOR_INIT}; -```rust #[allocator] -extern crate jemalloc; +static ALLOCATOR: MyAllocator = MY_ALLOCATOR_INIT; + +fn main() { + ... +} ``` -As its name would suggest, the global allocator is a global resource - all -crates in a dependency tree must agree on the selected global allocator. If two -or more distinct allocator crates are selected, compilation will fail. Note that -multiple crates can select a global allocator as long as that allocator is the -same across all of them. In addition, a crate can depend on an allocator crate -without declaring it to be the global allocator by omitting the `#[allocator]` -annotation. +Note that `ALLOCATOR` is still a normal static value - it can be used like any +other static would bed. ## Standard library +A small `alloc_api` crate will be created which will contain the `Layout` type. +The initial API will be more conservative than that described in [RFC 1398][], +possibly nothing more than a `from_size_align` constructor and accessors for +`size` and `align`. + The standard library will gain a new stable crate - `alloc_system`. This is the default allocator crate and corresponds to the "system" allocator (i.e. `malloc` etc on Unix and `HeapAlloc` etc on Windows). @@ -201,7 +187,7 @@ etc on Unix and `HeapAlloc` etc on Windows). The `alloc::heap` module will be reexported in `std` and stabilized. It will simply contain functions matching directly to those defined by the allocator API. The `alloc` crate itself may also be stabilized at a later date, but this -RFC does not propose that. +RFC does not propose that. `Layout` will be reexported in the `heap` module. The existing `alloc_jemalloc` may continue to exist as an implementation detail of the Rust compiler, but it will never be stabilized. Applications wishing to @@ -245,23 +231,14 @@ all of the requirements are met. # Alternatives [alternatives]: #alternatives -We could require that at most one crate selects a global allocator in the crate -graph, which may simplify the implementation. - -The allocator APIs could be simplified to a more "traditional" -malloc/calloc/free API at the cost of an efficiency loss when using allocators -with more powerful APIs. +We could loosen the requirement that the root crate is the only one which may +select the global allocator in favor of allowing any crate in the dependency +graph to do so. -The global allocator could be an instance of the `Allocator` trait. Since that -trait's methods take `&mut self`, things are a bit complicated however. The -allocator would most likely need to be a `const` type implementing `Allocator` -since it wouldn't be sound to interact with a static. This may cause confusion -since the allocator itself will therefor not be allowed to maintain any state -internally since a new instance will be created for each use. In addition, the -`Allocator` trait uses a `Layout` type as a higher level encapsulation of the -requested alignment and size of the allocation. The larger API surface area -will most likely cause this feature to have a significantly longer stabilization -period. +We could try to use the `Allocator` trait for global allocators. The `&mut self` +problem can b e solved via an implementation on a reference to the allocator +type in a way similar to `TcpStream`'s `Write` and `Read` implementations, but +this is pretty hacky. # Unresolved questions [unresolved]: #unresolved-questions @@ -277,6 +254,7 @@ implementations here: specific messaging that may exist. * `usable_size`, which is mentioned above as being unused, and should probably be removed from this trait as well. +* `realloc_inplace`, which attempts to resize an allocation without moving it. * `alloc_excess`, which is like `alloc` but returns the entire usable size including any extra space beyond the requested size. * Some other higher level convenience methods like `alloc_array`. From d80313cd047796efed3106f90172b87aaa665074 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 22 May 2017 12:01:45 -0700 Subject: [PATCH 4/6] Updates and refinements to various apis and guarantees --- text/0000-global-allocators.md | 113 +++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 20 deletions(-) diff --git a/text/0000-global-allocators.md b/text/0000-global-allocators.md index beae4339b56..e50ea59b037 100644 --- a/text/0000-global-allocators.md +++ b/text/0000-global-allocators.md @@ -73,9 +73,39 @@ trait described in [RFC 1398][], but is stripped down and the methods take [RFC 1398]: https://github.com/rust-lang/rfcs/blob/master/text/1398-kinds-of-allocators.md ```rust - +/// A trait implemented by objects that can be global allocators. +/// +/// Instances of this trait can be used to back allocations done through the +/// `std::heap` API. This trait represents the fundamental ability to allocate +/// memory in Rust. +/// +/// To use a global allocator you'll need to use the `#[global_allocator]` +/// attribute like so: +/// +/// ``` +/// extern crate my_allocator; +/// +/// #[global_allocator] +/// static ALLOCATOR: MyAllocator = my_allocator::INIT; +/// +/// fn main() { +/// let _b = Box::new(2); // uses `MyAllocator` above +/// } +/// ``` +/// +/// # Unsafety +/// +/// This trait is an `unsafe` trait as there are a number of guarantees a global +/// allocator must adhere to which aren't expressible through the type system. +/// First and foremost types that implement this trait must behave like, well, +/// allocators! All pointers returned from `allocate` that are active in a +/// program (disregarding those `deallocate`d) must point to disjoint chunks of +/// memory. In other words, allocations need to be distinct and can't overlap. +/// +/// Additionally it must be safe to allocate a chunk of memory on any thread of +/// a program and then deallocate it on any other thread of the program. #[lang = "global_allocator"] -pub unsafe trait GlobalAllocator: Send + Sync { +pub unsafe trait GlobalAllocator: Send + Sync + 'static { /// Returns a pointer to a newly allocated region of memory suitable for the /// provided `Layout`. The contents of the memory are undefined. /// @@ -110,7 +140,7 @@ pub unsafe trait GlobalAllocator: Send + Sync { /// /// The pointer must correspond to a region of memory previously allocated /// by this allocator with the provided layout. - pub fn reallocate(&self, ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { + pub unsafe fn reallocate(&self, ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { let new_ptr = self.alloc(layout); if !new_ptr.is_null() { ptr::copy_nonoverlapping(ptr, new_ptr, cmp::min(old_layout.size(), layout.size())); @@ -128,6 +158,12 @@ trait: `usable_size` which is used nowhere in the standard library, and A global allocator is a type implementing `GlobalAllocator` which can be constructed in a constant expression. +Note that the precise type signatures used here are a little up for debate. It's +expected that they will be settled (along with accompanying documentation as to +guarantees) as part of stabilization in [rust-lang/rust#27700][stab-issue]. + +[stab-issue]: https://github.com/rust-lang/rust/issues/27700 + ## Using an allocator While the `GlobalAllocator` trait can be used like any other, the most common @@ -148,21 +184,21 @@ pub unsafe fn deallocate(&self, ptr: *mut u8, layout: Layout) { ... } -pub fn reallocate(ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { +pub unsafe fn reallocate(ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { ... } ``` Each of these functions simply delegates to the selected global allocator. The allocator is selected by tagging a static value of a type implementing -`GlobalAllocator` with the `#[allocator]` annotation: +`GlobalAllocator` with the `#[global_allocator]` annotation: ```rust extern crate my_allocator; use my_allocator::{MyAllocator, MY_ALLOCATOR_INIT}; -#[allocator] +#[global_allocator] static ALLOCATOR: MyAllocator = MY_ALLOCATOR_INIT; fn main() { @@ -175,23 +211,60 @@ other static would bed. ## Standard library -A small `alloc_api` crate will be created which will contain the `Layout` type. -The initial API will be more conservative than that described in [RFC 1398][], -possibly nothing more than a `from_size_align` constructor and accessors for -`size` and `align`. +A `core::heap` module will be added with the `Layout` type and the +`GlobalAllocator` trait. The initial API of `Layout` will be more conservative +than that described in [RFC 1398][], possibly nothing more than a +`from_size_align` constructor and accessors for `size` and `align`. It is +intended that the API will grow over time with conveniences such as +`Layout::new::()`. + +The `alloc::heap` module will reexport these types from `core::heap`. It will +also provide top-level functions (like `allocate` above) which do not take an +allocator but instead are routed through the global allocator. The `alloc` +crate, however, will not provide a global allocator. Instead the compiler will +understand that crates which transitively depend on `alloc` will require an +allocator, with a per-target fallback default allocator used by the compiler. -The standard library will gain a new stable crate - `alloc_system`. This is the -default allocator crate and corresponds to the "system" allocator (i.e. `malloc` -etc on Unix and `HeapAlloc` etc on Windows). +The standard library will grow a `std::heap` module that reexports the contents +of `alloc::heap`. Additionally it will contain a system allocator definition: -The `alloc::heap` module will be reexported in `std` and stabilized. It will -simply contain functions matching directly to those defined by the allocator -API. The `alloc` crate itself may also be stabilized at a later date, but this -RFC does not propose that. `Layout` will be reexported in the `heap` module. +```rust +pub struct SystemAllocator; + +impl GlobalAllocator for SystemAllocator { + // ... +} +``` + +The `SystemAllocator` is defined as having zero size, no fields, and always +referring to the OS allocator (i.e. `malloc` etc on Unix and `HeapAlloc` etc on +Windows). + +The existing `alloc_system` and `alloc_jemalloc` crates will likely be +deprecated and eventually removed. The `alloc_system` crate is replaced with the +`SystemAllocator` structure in the standard library and the `alloc_jemalloc` +crate will become available on crates.io. The `alloc_jemalloc` crate will likely +look like: + +```rust +pub struct Jemalloc; + +impl GlobalAllocator for Jemalloc { + // ... +} +``` -The existing `alloc_jemalloc` may continue to exist as an implementation detail -of the Rust compiler, but it will never be stabilized. Applications wishing to -use jemalloc can use a third-party crate from crates.io. +It is not proposed in this RFC to switch the per-platform default allocator just +yet. Assuming everything goes smoothly, however, it will likely be defined as +`SystemAllocator` as platforms transition away from jemalloc-by-default once the +jemalloc-from-crates.io is stable and usable. + +The compiler will also no longer forbid cyclic the cyclic dependency between a +crate defining an implementation of an allocator and the `alloc` crate itself. +As a vestige of the current implementation this is only to get around linkage +errors where the liballoc rlib references symbols defined in the "allocator +crate". With this RFC the compiler has far more control over the ABI and linkage +here, so this restriction is no longer necessary. # How We Teach This [how-we-teach-this]: #how-we-teach-this From b60f63bcb49db674ca425ad2bc30a1ba8ed650f5 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 7 Jun 2017 21:52:19 -0400 Subject: [PATCH 5/6] More updates In particular, switch the proposal and alternative --- text/0000-global-allocators.md | 278 ++++++++++----------------------- 1 file changed, 85 insertions(+), 193 deletions(-) diff --git a/text/0000-global-allocators.md b/text/0000-global-allocators.md index e50ea59b037..9d8274e4d2f 100644 --- a/text/0000-global-allocators.md +++ b/text/0000-global-allocators.md @@ -17,7 +17,7 @@ This RFC is a refinement of the previous [RFC 1183][]. # Motivation [motivation]: #motivation -## API +## The current API The unstable `allocator` feature allows developers to select the global allocator which will be used in a program. A crate identifies itself as an @@ -66,132 +66,68 @@ pinning, and diagnostic output dumps for code that depends on jemalloc directly. ## Defining an allocator -We introduce a new trait, `GlobalAllocator`. It is similar to the `Allocator` -trait described in [RFC 1398][], but is stripped down and the methods take -`&self` rather than `&mut self`. - -[RFC 1398]: https://github.com/rust-lang/rfcs/blob/master/text/1398-kinds-of-allocators.md +Global allocators will use the `Allocator` trait defined in [RFC 1398][]. +However `Allocator`'s methods take `&mut self` since it's designed to be used +with individual collections. Since this allocator is global across threads, we +can't take `&mut self` references to it. So, instead of implementing `Allocator` +for the allocator type itself, it is implemented for shared references to the +allocator. This is a bit strange, but similar to `File`'s `Read` and `Write` +implementations, for example. ```rust -/// A trait implemented by objects that can be global allocators. -/// -/// Instances of this trait can be used to back allocations done through the -/// `std::heap` API. This trait represents the fundamental ability to allocate -/// memory in Rust. -/// -/// To use a global allocator you'll need to use the `#[global_allocator]` -/// attribute like so: -/// -/// ``` -/// extern crate my_allocator; -/// -/// #[global_allocator] -/// static ALLOCATOR: MyAllocator = my_allocator::INIT; -/// -/// fn main() { -/// let _b = Box::new(2); // uses `MyAllocator` above -/// } -/// ``` -/// -/// # Unsafety -/// -/// This trait is an `unsafe` trait as there are a number of guarantees a global -/// allocator must adhere to which aren't expressible through the type system. -/// First and foremost types that implement this trait must behave like, well, -/// allocators! All pointers returned from `allocate` that are active in a -/// program (disregarding those `deallocate`d) must point to disjoint chunks of -/// memory. In other words, allocations need to be distinct and can't overlap. -/// -/// Additionally it must be safe to allocate a chunk of memory on any thread of -/// a program and then deallocate it on any other thread of the program. -#[lang = "global_allocator"] -pub unsafe trait GlobalAllocator: Send + Sync + 'static { - /// Returns a pointer to a newly allocated region of memory suitable for the - /// provided `Layout`. The contents of the memory are undefined. - /// - /// On failure, returns a null pointer. - pub fn allocate(&self, layout: Layout) -> *mut u8; - - /// Returns a pointer to a newly allocated region of memory suitable for the - /// provided `Layout`. The memory is guaranteed to contain zeroes. - /// - /// On failure, returns a null pointer. - pub fn allocate_zeroed(&self, layout: Layout) -> *mut u8 { - let ptr = self.allocate(layout); - if !ptr.is_null() { - ptr::write_bytes(ptr, 0, layout.size()); - } - ptr - } - - /// Deallocates the memory referenced by `ptr`. - /// - /// The pointer must correspond to a region of memory previously allocated - /// by this allocator with the provided layout. - pub unsafe fn deallocate(&self, ptr: *mut u8, layout: Layout); - - /// Resizes the allocation referenced by `ptr` a new layout. - /// - /// On failure, returns a null pointer and leaves the original allocation - /// intact. - /// - /// If the allocation was relocated, the memory at the passed-in pointer is - /// undefined after the call. - /// - /// The pointer must correspond to a region of memory previously allocated - /// by this allocator with the provided layout. - pub unsafe fn reallocate(&self, ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { - let new_ptr = self.alloc(layout); - if !new_ptr.is_null() { - ptr::copy_nonoverlapping(ptr, new_ptr, cmp::min(old_layout.size(), layout.size())); - self.deallocate(ptr); - } - new_ptr - } +pub struct Jemalloc; + +impl<'a> Allocate for &'a Jemalloc { + // ... } ``` -Two methods currently defined in the global allocatr API are not present on this -trait: `usable_size` which is used nowhere in the standard library, and -`reallocate_inplace`, which is only used in libarena. +[RFC 1398]: https://github.com/rust-lang/rfcs/blob/master/text/1398-kinds-of-allocators.md -A global allocator is a type implementing `GlobalAllocator` which can be -constructed in a constant expression. +## Using an allocator -Note that the precise type signatures used here are a little up for debate. It's -expected that they will be settled (along with accompanying documentation as to -guarantees) as part of stabilization in [rust-lang/rust#27700][stab-issue]. +The `alloc::heap` module will contain several items: -[stab-issue]: https://github.com/rust-lang/rust/issues/27700 +```rust +/// Defined in RFC 1398 +pub struct Layout { ... } -## Using an allocator +/// Defined in RFC 1398 +pub unsafe trait Allocator { ... } -While the `GlobalAllocator` trait can be used like any other, the most common -usage of a global allocator is through the functions defined in the -`std::heap` module. It contains free functions corresponding to each of the -methods defined on the `GlobalAllocator` trait: +/// An `Allocator` which uses the system allocator. +/// +/// This uses `malloc`/`free` on Unix systems, and `HeapAlloc`/`HeapFree` on +/// Windows, for example. +pub struct System; -```rust -pub fn allocate(layout: Layout) -> *mut u8 { - ... -} +unsafe impl Allocator for System { ... } -pub fn allocate_zeroed(layout: Layout) -> *mut u8 { - ... -} +unsafe impl<'a> Allocator for &'a System { ... } -pub unsafe fn deallocate(&self, ptr: *mut u8, layout: Layout) { - ... -} +/// An `Allocator` which uses the configured global allocator. +/// +/// The global allocator is selected by defining a static instance of the +/// allocator and annotating it with `#[global_allocator]`. Only one global +/// allocator can be defined in a crate graph. +/// +/// # Note +/// +/// For techical reasons, only non-generic methods of the `Allocator` trait +/// will be forwarded to the selected global allocator in the current +/// implementation. +pub struct Heap; -pub unsafe fn reallocate(ptr: *mut u8, old_layout: Layout, layout: Layout) -> *mut u8 { - ... -} +unsafe impl Allocator for Heap { ... } + +unsafe impl<'a> Allocator for &'a Heap { ... } ``` -Each of these functions simply delegates to the selected global allocator. The -allocator is selected by tagging a static value of a type implementing -`GlobalAllocator` with the `#[global_allocator]` annotation: +This module will be reexported as `std::alloc`, which will be the location at +which it will be stabilized. The `alloc` crate is not proposed for stabilization +at this time. + +An example of setting the global allocator: ```rust extern crate my_allocator; @@ -207,38 +143,7 @@ fn main() { ``` Note that `ALLOCATOR` is still a normal static value - it can be used like any -other static would bed. - -## Standard library - -A `core::heap` module will be added with the `Layout` type and the -`GlobalAllocator` trait. The initial API of `Layout` will be more conservative -than that described in [RFC 1398][], possibly nothing more than a -`from_size_align` constructor and accessors for `size` and `align`. It is -intended that the API will grow over time with conveniences such as -`Layout::new::()`. - -The `alloc::heap` module will reexport these types from `core::heap`. It will -also provide top-level functions (like `allocate` above) which do not take an -allocator but instead are routed through the global allocator. The `alloc` -crate, however, will not provide a global allocator. Instead the compiler will -understand that crates which transitively depend on `alloc` will require an -allocator, with a per-target fallback default allocator used by the compiler. - -The standard library will grow a `std::heap` module that reexports the contents -of `alloc::heap`. Additionally it will contain a system allocator definition: - -```rust -pub struct SystemAllocator; - -impl GlobalAllocator for SystemAllocator { - // ... -} -``` - -The `SystemAllocator` is defined as having zero size, no fields, and always -referring to the OS allocator (i.e. `malloc` etc on Unix and `HeapAlloc` etc on -Windows). +other static would be. The existing `alloc_system` and `alloc_jemalloc` crates will likely be deprecated and eventually removed. The `alloc_system` crate is replaced with the @@ -249,14 +154,18 @@ look like: ```rust pub struct Jemalloc; -impl GlobalAllocator for Jemalloc { +unsafe impl Allocator for Jemalloc { + // ... +} + +unsafe impl<'a> Allocator for &'a Jemalloc { // ... } ``` It is not proposed in this RFC to switch the per-platform default allocator just yet. Assuming everything goes smoothly, however, it will likely be defined as -`SystemAllocator` as platforms transition away from jemalloc-by-default once the +`System` as platforms transition away from jemalloc-by-default once the jemalloc-from-crates.io is stable and usable. The compiler will also no longer forbid cyclic the cyclic dependency between a @@ -269,12 +178,6 @@ here, so this restriction is no longer necessary. # How We Teach This [how-we-teach-this]: #how-we-teach-this -The term "allocator" is the canonical one for this concept. It is unfortunately -shared with a similar but distinct concept described in [RFC 1398][], which -defined an `Allocator` trait over which collections be parameterized. This API -is disambiguated by referring specifically to the "global" or "default" -allocator. - Global allocator selection would be a somewhat advanced topic - the system allocator is sufficient for most use cases. It is a new tool that developers can use to optimize for their program's specific workload when necessary. @@ -296,54 +199,43 @@ Dropping the default of jemalloc will regress performance of some programs until they manually opt back into that allocator, which may produce confusion in the community as to why things suddenly became slower. -The allocator APIs are to some extent designed after what jemalloc supports, -which is quite a bit more than the system allocator is able to. The Rust -wrappers for those simpler allocators have to jump through hoops to ensure that -all of the requirements are met. +Depending on implementation of a trait for references to a type is unfortunate. +It's pretty strange and unfamiliar to many Rust developers. Many global +allocators are zero-sized as their state lives outside of the Rust structure, +but a reference to the allocator will be 4 or 8 bytes. If developers wish to use +global allocators as "normal" allocators in individual collections, allocator +authors may have to implement `Allocator` twice - for the type and references to +the type. One can forward to the other, but it's still work that would not need +to be done ideally. + +In theory, there could be a blanket implementation of `impl<'a, T> Allocator for +T where &'a T: Allocator`, but the compiler is unfortunately not able to deal +with this currently. + +The `Allocator` trait defines some functions which have generic arguments. +They're purely convenience functions, but if a global allocator overrides them, +the custom implementations will not be used when going through the `Heap` type. +This may be confusing. # Alternatives [alternatives]: #alternatives -We could loosen the requirement that the root crate is the only one which may -select the global allocator in favor of allowing any crate in the dependency -graph to do so. +We could define a separate `GlobalAllocator` trait with methods taking `&self` +to avoid the strange implementation for references requirement. This does +require the duplication of some or all of the API surface and documentation of +`Allocator` to a second trait with only a difference in receiver type. -We could try to use the `Allocator` trait for global allocators. The `&mut self` -problem can b e solved via an implementation on a reference to the allocator -type in a way similar to `TcpStream`'s `Write` and `Read` implementations, but -this is pretty hacky. +The `GlobalAllocator` trait could be responsible for simply returning a type +which implements `Allocator`. This avoids the duplication or the strange +implementation for references issues in the other possibilities, but can't be +defined in a reasonable way without HKT, and is a somewhat strange layer of +indirection. # Unresolved questions [unresolved]: #unresolved-questions -It is currently forbidden to pass a null pointer to `deallocate`, though this is -guaranteed to be a noop with libc's `free` at least. Some kinds of patterns in C -are cleaner when null pointers can be `free`d - is the same true for Rust? - -The `Allocator` trait defines several methods that do not have corresponding -implementations here: - -* `oom`, which is called after a failed allocation to provide any allocator - specific messaging that may exist. -* `usable_size`, which is mentioned above as being unused, and should probably - be removed from this trait as well. -* `realloc_inplace`, which attempts to resize an allocation without moving it. -* `alloc_excess`, which is like `alloc` but returns the entire usable size - including any extra space beyond the requested size. -* Some other higher level convenience methods like `alloc_array`. - -Should any of these be added to the global allocator as well? It may make sense -to add `alloc_excess` to the allocator API. This can either have a default -implementation which simply calls `allocate` and returns the input size, or -calls `allocate` followed by `reallocate_inplace`. - -The existing `usable_size` function (proposed for removal) only takes a size and -align. A similar, but potentially more useful API is one that takes a pointer -to a heap allocated region and returns the usable size of it. This is supported -as a GNU extension `malloc_useable_size` in the system allocator, and in -jemalloc as well. An [issue][usable_size] has been filed to add this to support -this to aid heap usage diagnostic crates. It would presumably have to return an -`Option` to for allocators that do not have such a function, but this limits -its usefulness if support can't be guaranteed. - -[usable_size]: https://github.com/rust-lang/rust/issues/32075 +Are `System` and `Heap` the right names for the two `Allocator` implementations +in `std::heap`? + +Should `std::heap` also have free functions which forward to the global +allocator? From 22fe7cb7f5fc284cbe3915e9f875ee0c2b726ddc Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 7 Jun 2017 22:01:07 -0400 Subject: [PATCH 6/6] Typo --- text/0000-global-allocators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-global-allocators.md b/text/0000-global-allocators.md index 9d8274e4d2f..461198e6ae9 100644 --- a/text/0000-global-allocators.md +++ b/text/0000-global-allocators.md @@ -77,7 +77,7 @@ implementations, for example. ```rust pub struct Jemalloc; -impl<'a> Allocate for &'a Jemalloc { +impl<'a> Allocator for &'a Jemalloc { // ... } ```