Skip to content

[vec] growth-strategy optimization #45434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/doc/book
Submodule book updated 46 files
+2 −4 README.md
+0 −3 second-edition/dictionary.txt
+86 −90 second-edition/nostarch/chapter02.md
+148 −148 second-edition/nostarch/chapter03.md
+671 −426 second-edition/nostarch/chapter13.md
+203 −89 second-edition/nostarch/chapter14.md
+484 −649 second-edition/nostarch/chapter16.md
+ second-edition/nostarch/odt/chapter13.docx
+ second-edition/nostarch/odt/chapter14.docx
+4 −4 second-edition/src/ch01-02-hello-world.md
+17 −24 second-edition/src/ch02-00-guessing-game-tutorial.md
+0 −2 second-edition/src/ch03-01-variables-and-mutability.md
+10 −13 second-edition/src/ch03-02-data-types.md
+15 −17 second-edition/src/ch03-03-how-functions-work.md
+1 −2 second-edition/src/ch03-04-comments.md
+4 −10 second-edition/src/ch03-05-control-flow.md
+2 −2 second-edition/src/ch05-01-defining-structs.md
+1 −1 second-edition/src/ch05-02-example-structs.md
+1 −1 second-edition/src/ch05-03-method-syntax.md
+3 −3 second-edition/src/ch06-02-match.md
+1 −1 second-edition/src/ch07-00-modules.md
+1 −1 second-edition/src/ch07-01-mod-and-the-filesystem.md
+1 −1 second-edition/src/ch07-03-importing-names-with-use.md
+9 −9 second-edition/src/ch10-02-traits.md
+4 −4 second-edition/src/ch10-03-lifetime-syntax.md
+1 −1 second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md
+11 −6 second-edition/src/ch13-00-functional-features.md
+381 −280 second-edition/src/ch13-01-closures.md
+206 −124 second-edition/src/ch13-02-iterators.md
+74 −34 second-edition/src/ch13-03-improving-our-io-project.md
+32 −11 second-edition/src/ch13-04-performance.md
+4 −1 second-edition/src/ch14-00-more-about-cargo.md
+57 −13 second-edition/src/ch14-01-release-profiles.md
+98 −54 second-edition/src/ch14-02-publishing-to-crates-io.md
+38 −18 second-edition/src/ch14-03-cargo-workspaces.md
+5 −2 second-edition/src/ch14-04-installing-binaries.md
+36 −52 second-edition/src/ch16-00-concurrency.md
+117 −180 second-edition/src/ch16-01-threads.md
+86 −161 second-edition/src/ch16-02-message-passing.md
+177 −190 second-edition/src/ch16-03-shared-state.md
+64 −61 second-edition/src/ch16-04-extensible-concurrency-sync-and-send.md
+1 −1 second-edition/src/ch18-01-all-the-places-for-patterns.md
+2 −2 second-edition/src/ch18-02-refutability.md
+24 −10 second-edition/src/ch20-06-graceful-shutdown-and-cleanup.md
+3 −13 second-edition/theme/index.hbs
+3 −3 second-edition/tools/docx-to-md.xsl
2 changes: 1 addition & 1 deletion src/doc/nomicon
202 changes: 157 additions & 45 deletions src/liballoc/raw_vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use core::mem;
use core::ops::Drop;
use core::ptr::{self, Unique};
use core::slice;
use heap::{Alloc, Layout, Heap};
use heap::{Alloc, Layout, Heap, Excess};
use super::boxed::Box;

/// A low-level utility for more ergonomically allocating, reallocating, and deallocating
Expand Down Expand Up @@ -165,6 +165,79 @@ impl<T, A: Alloc> RawVec<T, A> {
}
}

/// Given a `current_capacity` and a desired `capacity_increase` returns a
/// suitable capacity for the `RawVec` such that `suitable_capacity >=
/// current_capacity + capacity_increase`.
///
/// # Panics
///
/// Panics on overflow if `current_capacity + capacity_increase >
/// std::usize::MAX`.
///
/// # Growth strategy
///
/// RawVec grows differently depending on:
///
/// - 1. initial size: grows from zero to four elements or at least 64 bytes;
/// use `with_capacity` to avoid a growth from zero.
///
/// - 2. vector size:
/// - small vectors (<= 4096 bytes) grow with a factor of 2x.
/// - medium-sized and large vectors grow with a growth factor of 1.5x.
///
/// # Growth factor
///
/// The growth factor of medium-sized and large vectors is chosen to allow
/// reusing memory from previously-freed allocations in subsequent allocations.
///
/// Depending on the growth factor, previously freed memory can be reused after
///
/// - 4 reallocations for a growth factor of 1.5x,
/// - 3 reallocations for a growth factor of 1.45x,
/// - 2 reallocations for a growth factor of 1.3x,
///
/// Which growth-factor is better [is application
/// dependent](https://stackoverflow.com/questions/1100311/
/// what-is-the-ideal-growth-rate-for-a-dynamically-allocated-array),
/// also some claim that [the golden ration (1.618) is
/// optimal](https://crntaylor.wordpress.com/2011/07/15/
/// optimal-memory-reallocation-and-the-golden-ratio/).
/// The trade-off is having to wait for many reallocations to be able to
/// reuse old memory.
///
/// A factor of 2x _never_ allows reusing any previously-freed memory but
/// `jemalloc`'s memory blocks < 4096 bytes cannot grow in place, that is,
/// trying to grow a vector < 4096 bytes is always going to require allocating
/// new memory and copying the contents over. Since `jemalloc`'s memory pools
/// for small allocations grow with powers of 2 it makes sense to keep a
/// growth-factor of 2x for these allocations.
///
#[inline(always)]
fn amortized_new_capacity(elem_size: usize, current_capacity: usize,
capacity_increase: usize) -> usize {
// Computes the capacity from the `current_capacity` following the
// growth-strategy. The function `alloc_guard` ensures that
// `current_capacity <= isize::MAX` so that `current_capacity * N` where `N
// <= 2` cannot overflow.
let growth_capacity = match current_capacity {
// Empty vector => 4 elements or at least 64 bytes
0 => (64 / elem_size).max(4),

// Small vectors: jemalloc cannot grow in place blocks smaller than 4096
// bytes so until then the memory of previously-freed allocations
// cannot be reused by subsequent allocations:
c if c < 4096 / elem_size => 2 * c,

// Medium and large vectors (>= 4096 bytes): a growth factor of 1.5
// allows the memory of a previously-freeed allocation to be reused
// after 4 subsequent allocations:
c => (c / 2 + 1) * 3,
};
cmp::max(growth_capacity,
current_capacity.checked_add(capacity_increase).unwrap())
}


impl<T> RawVec<T, Heap> {
/// Reconstitutes a RawVec from a pointer, capacity.
///
Expand Down Expand Up @@ -439,19 +512,6 @@ impl<T, A: Alloc> RawVec<T, A> {
}
}

/// Calculates the buffer's new size given that it'll hold `used_cap +
/// needed_extra_cap` elements. This logic is used in amortized reserve methods.
/// Returns `(new_capacity, new_alloc_size)`.
fn amortized_new_size(&self, used_cap: usize, needed_extra_cap: usize) -> usize {
// Nothing we can really do about these checks :(
let required_cap = used_cap.checked_add(needed_extra_cap)
.expect("capacity overflow");
// Cannot overflow, because `cap <= isize::MAX`, and type of `cap` is `usize`.
let double_cap = self.cap * 2;
// `double_cap` guarantees exponential growth.
cmp::max(double_cap, required_cap)
}

/// Ensures that the buffer contains at least enough space to hold
/// `used_cap + needed_extra_cap` elements. If it doesn't already have
/// enough capacity, will reallocate enough space plus comfortable slack
Expand Down Expand Up @@ -504,6 +564,8 @@ impl<T, A: Alloc> RawVec<T, A> {
/// # vector.push_all(&[1, 3, 5, 7, 9]);
/// # }
/// ```
#[inline]
#[cold]
pub fn reserve(&mut self, used_cap: usize, needed_extra_cap: usize) {
unsafe {
// NOTE: we don't early branch on ZSTs here because we want this
Expand All @@ -517,27 +579,30 @@ impl<T, A: Alloc> RawVec<T, A> {
return;
}

let new_cap = self.amortized_new_size(used_cap, needed_extra_cap);
let elem_size = mem::size_of::<T>();
let new_cap = amortized_new_capacity(elem_size,
used_cap,
needed_extra_cap);

let new_layout = match Layout::array::<T>(new_cap) {
Some(layout) => layout,
None => panic!("capacity overflow"),
};
// FIXME: may crash and burn on over-reserve
alloc_guard(new_layout.size());
let res = match self.current_layout() {
let Excess(res, usable_size) = match self.current_layout() {
Some(layout) => {
let old_ptr = self.ptr.as_ptr() as *mut u8;
self.a.realloc(old_ptr, layout, new_layout)
self.a.realloc_excess(old_ptr, layout, new_layout)
}
None => self.a.alloc(new_layout),
None => self.a.alloc_excess(new_layout),
};
let uniq = match res {
Ok(ptr) => Unique::new_unchecked(ptr as *mut T),
Err(e) => self.a.oom(e),
};
self.ptr = uniq;
self.cap = new_cap;
self.cap = usable_size / mem::size_of::<T>();
}
}

Expand Down Expand Up @@ -576,7 +641,10 @@ impl<T, A: Alloc> RawVec<T, A> {
return false;
}

let new_cap = self.amortized_new_size(used_cap, needed_extra_cap);
let elem_size = mem::size_of::<T>();
let new_cap = amortized_new_capacity(elem_size,
used_cap,
needed_extra_cap);

// Here, `cap < used_cap + needed_extra_cap <= new_cap`
// (regardless of whether `self.cap - used_cap` wrapped).
Expand All @@ -588,7 +656,10 @@ impl<T, A: Alloc> RawVec<T, A> {
alloc_guard(new_layout.size());
match self.a.grow_in_place(ptr, old_layout, new_layout) {
Ok(_) => {
self.cap = new_cap;
// FIXME: grow_in_place swallows the usable_size, so we
// need to recompute it here:
let (_, new_usable_size) = self.a.usable_size(&new_layout);
self.cap = new_usable_size / mem::size_of::<T>();
true
}
Err(_) => {
Expand Down Expand Up @@ -760,38 +831,79 @@ mod tests {
v.reserve(50, 150); // (causes a realloc, thus using 50 + 150 = 200 units of fuel)
assert_eq!(v.a.fuel, 250);
}
/*
#[test]
fn amortized_new_capacity_tests() {
// empty vector:
assert_eq!(amortized_new_capacity(1, 0, 1), 64);
assert_eq!(amortized_new_capacity(64, 0, 1), 1);
assert_eq!(amortized_new_capacity(128, 0, 1), 1);

// small vector:
assert_eq!(amortized_new_capacity(1, 2048, 1), 4096); // 2x

// medium vector
assert_eq!(amortized_new_capacity(1, 5000, 1), 7500); // 1.5x

// large vector
assert_eq!(amortized_new_capacity(1, 140000, 1), 280000); // 2x
}

#[test]
fn reserve_does_not_overallocate() {
fn reserve_() {
// Allocation from empty vector:
{
let mut v: RawVec<u32> = RawVec::new();
// First `reserve` allocates like `reserve_exact`
v.reserve(0, 9);
assert_eq!(9, v.cap());
}
let mut v: RawVec<u8> = RawVec::new();
assert_eq!(v.cap(), 0);

// empty vectors allocate at least 64 bytes
v.reserve(0, 63);
assert_eq!(v.cap(), 64);
}
{
let mut v: RawVec<u32> = RawVec::new();
v.reserve(0, 7);
assert_eq!(7, v.cap());
// 97 if more than double of 7, so `reserve` should work
// like `reserve_exact`.
v.reserve(7, 90);
assert_eq!(97, v.cap());
}

// empty vectors allocate at least 64 bytes
v.reserve(0, 9);
assert_eq!(16, v.cap());
}
// Exponential growth:
{
let mut v: RawVec<u32> = RawVec::new();
v.reserve(0, 12);
assert_eq!(12, v.cap());
v.reserve(12, 3);
// 3 is less than half of 12, so `reserve` must grow
// exponentially. At the time of writing this test grow
// factor is 2, so new capacity is 24, however, grow factor
// of 1.5 is OK too. Hence `>= 18` in assert.
assert!(v.cap() >= 12 + 12 / 2);
let mut v: RawVec<u8> = RawVec::new();
assert_eq!(v.cap(), 0);

// empty vectors allocate at least 64 bytes
v.reserve(0, 1);
assert_eq!(v.cap(), 64);

// small vectors grow with a factor of two:
v.reserve(64, 1);
assert_eq!(v.cap(), 128);

// The required capacity is 128 + 3968 = 4096 which is larger than
// 2*128 = 256, so here `reserve` allocates "exactly" 4096 elements
// modulo extra size returned by the allocator:
v.reserve(128, 3968);
assert!(v.cap() >= 4096 && v.cap() <= 4096 + 128);

// 1 <= 1.5 * cap, so here the "medium" sized vector uses a growth
// factor of 1.5:
let cap = v.cap();
v.reserve(cap, 1);
assert!(v.cap() >= cap * 3 / 2 && v.cap() <= cap * 3 / 2 + 512);

// we reserve enough to make the vector "large"
let cap = v.cap();
v.reserve(cap, 4096 * 32);
assert!(v.cap() >= cap + 4096 * 32
&& v.cap() <= cap + 4096 * 40);

// large vectors grow with a growth-factor equals 2x:
let cap = v.cap();
v.reserve(cap, 1);
assert_eq!(v.cap(), cap * 2);
assert!(v.cap() >= cap * 2 && v.cap() <= cap * 2 + 4096);
}
}


*/
}
19 changes: 13 additions & 6 deletions src/liballoc/vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ use core::ops;
use core::ptr;
use core::ptr::Shared;
use core::slice;
use core::intrinsics;

use borrow::ToOwned;
use borrow::Cow;
Expand Down Expand Up @@ -707,6 +708,11 @@ impl<T> Vec<T> {
self.pop().unwrap()
}

#[inline(always)]
fn is_full(&self) -> bool {
self.len == self.buf.cap()
}

/// Inserts an element at position `index` within the vector, shifting all
/// elements after it to the right.
///
Expand All @@ -729,8 +735,8 @@ impl<T> Vec<T> {
assert!(index <= len);

// space for the new element
if len == self.buf.cap() {
self.buf.double();
if unsafe { intrinsics::unlikely(self.is_full()) } {
self.buf.reserve(self.len, 1);
}

unsafe {
Expand Down Expand Up @@ -964,8 +970,8 @@ impl<T> Vec<T> {
pub fn push(&mut self, value: T) {
// This will panic or abort if we would allocate > isize::MAX bytes
// or if the length increment would overflow for zero-sized types.
if self.len == self.buf.cap() {
self.buf.double();
if unsafe { intrinsics::unlikely(self.is_full()) } {
self.buf.reserve(self.len, 1);
}
unsafe {
let end = self.as_mut_ptr().offset(self.len as isize);
Expand Down Expand Up @@ -2532,8 +2538,9 @@ impl<'a, T> Placer<T> for PlaceBack<'a, T> {
fn make_place(self) -> Self {
// This will panic or abort if we would allocate > isize::MAX bytes
// or if the length increment would overflow for zero-sized types.
if self.vec.len == self.vec.buf.cap() {
self.vec.buf.double();
if unsafe { intrinsics::unlikely(self.vec.is_full()) } {
let len = self.vec.len();
self.vec.buf.reserve(len, 1);
}
self
}
Expand Down
3 changes: 2 additions & 1 deletion src/liballoc/vec_deque.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use core::ops::{Index, IndexMut, Place, Placer, InPlace};
use core::ptr;
use core::ptr::Shared;
use core::slice;
use core::intrinsics;

use core::hash::{Hash, Hasher};
use core::cmp;
Expand Down Expand Up @@ -1752,7 +1753,7 @@ impl<T> VecDeque<T> {
// This may panic or abort
#[inline]
fn grow_if_necessary(&mut self) {
if self.is_full() {
if unsafe { intrinsics::unlikely(self.is_full()) } {
let old_cap = self.cap();
self.buf.double();
Copy link
Contributor

@arthurprs arthurprs Oct 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this become reserve(1) as well? nevermind, I just saw bluss comment.

unsafe {
Expand Down
2 changes: 1 addition & 1 deletion src/liblibc
Submodule liblibc updated 63 files
+4 −16 .travis.yml
+69 −53 Cargo.lock
+2 −2 Cargo.toml
+0 −3 README.md
+4 −0 appveyor.yml
+1 −1 ci/README.md
+15 −0 ci/android-accept-licenses.sh
+12 −15 ci/android-install-sdk.sh
+2 −2 ci/docker/aarch64-linux-android/Dockerfile
+0 −27 ci/docker/aarch64-unknown-linux-musl/Dockerfile
+2 −2 ci/docker/arm-linux-androideabi/Dockerfile
+0 −25 ci/docker/arm-unknown-linux-musleabihf/Dockerfile
+2 −2 ci/docker/i686-linux-android/Dockerfile
+0 −11 ci/docker/mips64el-unknown-linux-gnuabi64/Dockerfile
+0 −11 ci/docker/powerpc64le-unknown-linux-gnu/Dockerfile
+0 −14 ci/docker/sparc64-unknown-linux-gnu/Dockerfile
+0 −5 ci/docker/x86_64-unknown-linux-gnux32/Dockerfile
+1 −8 ci/run.sh
+1 −1 libc-test/Cargo.toml
+6 −38 libc-test/build.rs
+62 −37 src/dox.rs
+30 −38 src/redox.rs
+0 −110 src/redox/net.rs
+0 −26 src/unix/bsd/apple/mod.rs
+0 −18 src/unix/bsd/freebsdlike/freebsd/mod.rs
+0 −10 src/unix/bsd/mod.rs
+0 −2 src/unix/bsd/netbsdlike/mod.rs
+7 −71 src/unix/bsd/netbsdlike/netbsd/mod.rs
+49 −296 src/unix/haiku/mod.rs
+10 −51 src/unix/mod.rs
+0 −9 src/unix/newlib/mod.rs
+0 −345 src/unix/notbsd/android/b32/arm.rs
+3 −265 src/unix/notbsd/android/b64/aarch64.rs
+0 −43 src/unix/notbsd/android/mod.rs
+5 −366 src/unix/notbsd/linux/mips/mips32.rs
+4 −326 src/unix/notbsd/linux/mips/mips64.rs
+0 −43 src/unix/notbsd/linux/mips/mod.rs
+20 −54 src/unix/notbsd/linux/mod.rs
+8 −371 src/unix/notbsd/linux/musl/b32/arm.rs
+6 −375 src/unix/notbsd/linux/musl/b32/mips.rs
+0 −16 src/unix/notbsd/linux/musl/b32/mod.rs
+0 −16 src/unix/notbsd/linux/musl/b32/x86.rs
+0 −334 src/unix/notbsd/linux/musl/b64/aarch64.rs
+49 −11 src/unix/notbsd/linux/musl/b64/mod.rs
+0 −76 src/unix/notbsd/linux/musl/b64/powerpc64.rs
+0 −71 src/unix/notbsd/linux/musl/b64/x86_64.rs
+16 −15 src/unix/notbsd/linux/musl/mod.rs
+5 −366 src/unix/notbsd/linux/other/b32/arm.rs
+0 −17 src/unix/notbsd/linux/other/b32/mod.rs
+5 −371 src/unix/notbsd/linux/other/b32/powerpc.rs
+0 −12 src/unix/notbsd/linux/other/b32/x86.rs
+5 −295 src/unix/notbsd/linux/other/b64/aarch64.rs
+18 −33 src/unix/notbsd/linux/other/b64/mod.rs
+0 −351 src/unix/notbsd/linux/other/b64/not_x32.rs
+5 −377 src/unix/notbsd/linux/other/b64/powerpc64.rs
+0 −26 src/unix/notbsd/linux/other/b64/sparc64.rs
+0 −331 src/unix/notbsd/linux/other/b64/x32.rs
+348 −31 src/unix/notbsd/linux/other/b64/x86_64.rs
+15 −40 src/unix/notbsd/linux/other/mod.rs
+5 −338 src/unix/notbsd/linux/s390x.rs
+0 −17 src/unix/notbsd/mod.rs
+0 −13 src/unix/solaris/mod.rs
+0 −8 src/unix/uclibc/mod.rs
2 changes: 1 addition & 1 deletion src/tools/cargo
Submodule cargo updated 158 files
2 changes: 1 addition & 1 deletion src/tools/clippy
2 changes: 1 addition & 1 deletion src/tools/rls
Submodule rls updated from 9ad92e to 93b47d
2 changes: 1 addition & 1 deletion src/tools/rustfmt