Skip to content

switch to a more efficient vector/string representation #8981

Closed
@thestinger

Description

@thestinger

They are currently represented them as *{ length, capacity, data[] }. It means we have to handle trickier uint overflow cases, which increases cost size a fair bit. It also means even an empty vector always requires a heap allocation.

C++ represents a vector as { length, capacity, *data }, so a fresh empty vector simply as a null data pointer. Unlike the OptVec optimization, it does not require an extra branch in every method. Calling realloc on a null pointer is well-defined, so you can ignore the special case.

This has the drawback of making a move involve a 3-word copy, not 1-word. However, passing around unique vectors by-move rather than slices is not common enough to be a very relevant performance issue.

Benchmarks on x86_64 Linux (linux 3.11.5, glibc 2.18):

Comparison ~[T] Proposal
Empty 29 ns/iter (+/- 1) 2 ns/iter (+/- 0)
Push 10k, starting at 16 capacity 45528 ns/iter (+/- 2584) 7611 ns/iter (+/- 407)
Push 10k, starting at 10k capacity 43783 ns/iter (+/- 1429) 6876 ns/iter (+/- 320)
Size size_of::<uint> 3 * size_of::<uint>
Empty heap allocation size minimum 2 * size_of::<uint> n/a
4 element heap allocation size 2 * size_of::<uint> + 4 * size_of::<T> 4 * size_of::<T>

Note that since jemalloc is very clever, at some sizes preallocating has next to no advantage for the new proposed vector too.

Implementation:

#[allow(ctypes)];

#[cfg(test)]
extern mod extra;

use std::mem::size_of;
use std::cast::transmute;
use std::unstable::raw::Slice;
use std::unstable::intrinsics::move_val_init;
use std::ptr::read_ptr;

// redefine these to use `*mut` ...

extern {
    fn malloc(size: uint) -> *mut u8;
    fn realloc(ptr: *mut u8, size: uint) -> *mut u8;
    fn free(ptr: *mut u8);
    fn abort() -> !;
}

#[fixed_stack_segment]
unsafe fn malloc_raw(size: uint) -> *mut u8 {
    let ptr = malloc(size);
    if ptr.is_null() {
        abort()
    }
    ptr
}

#[fixed_stack_segment]
unsafe fn realloc_raw(ptr: *mut u8, size: uint) -> *mut u8 {
    let ptr = realloc(ptr, size);
    if ptr.is_null() {
        abort()
    }
    ptr
}

#[fixed_stack_segment]
unsafe fn free_raw(ptr: *mut u8) {
    free(ptr)
}

struct Vec<T> {
    priv len: uint,
    priv cap: uint,
    priv ptr: *mut T
}

impl<T> Vec<T> {
    #[inline]
    fn new() -> Vec<T> {
        Vec { len: 0, cap: 0, ptr: 0 as *mut T }
    }

    #[inline]
    fn with_capacity(capacity: uint) -> Vec<T> {
        let size = capacity.checked_mul(&size_of::<T>()).unwrap();
        unsafe {
            Vec { len: 0, cap: capacity, ptr: malloc_raw(size) as *mut T }
        }
    }

    #[inline]
    fn shrink_to_fit(&mut self) {
        unsafe {
            self.cap = self.len;
            if self.len == 0 {
                free_raw(self.ptr as *mut u8);
                self.ptr = 0 as *mut T;
            } else {
                self.ptr = realloc_raw(self.ptr as *mut u8, self.cap * size_of::<T>()) as *mut T;
            }
        }
    }

    #[inline]
    fn push(&mut self, value: T) {
        unsafe {
            if self.len == self.cap {
                if self.cap == 0 { self.cap += 2 }
                let old_size = self.cap * size_of::<T>();
                self.cap = self.cap * 2;
                let size = old_size.checked_mul(&2).unwrap();
                self.ptr = realloc_raw(self.ptr as *mut u8, size) as *mut T;
            }

            let end = self.ptr.offset(self.len as int);
            move_val_init(&mut *end, value);
            self.len += 1;
        }
    }

    #[inline]
    fn as_slice<'r>(&'r self) -> &'r [T] {
        let slice = Slice { data: self.ptr as *T, len: self.len };
        unsafe { transmute(slice) }
    }

    #[inline]
    fn as_mut_slice<'r>(&'r mut self) -> &'r mut [T] {
        let slice = Slice { data: self.ptr as *T, len: self.len };
        unsafe { transmute(slice) }
    }
}

#[unsafe_destructor]
impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            for x in self.as_mut_slice().mut_iter() {
                read_ptr(x);
            }
            free_raw(self.ptr as *mut u8)
        }
    }
}

#[cfg(test)]
mod bench {
    use std;
    use super::Vec;
    use extra::test::BenchHarness;

    #[bench]
    fn empty(bh: &mut BenchHarness) {
        do bh.iter() {
            let _xs: Vec<int> = Vec::new();
        }
    }

    #[bench]
    fn empty_std(bh: &mut BenchHarness) {
        do bh.iter() {
            let _xs: ~[int] = ~[];
        }
    }

    static size: uint = 10000;

    #[bench]
    fn push(bh: &mut BenchHarness) {
        do bh.iter() {
            let mut xs = Vec::with_capacity(16);
            for i in range(0, size) {
                xs.push(i);
            }
        }
    }

    #[bench]
    fn push_std(bh: &mut BenchHarness) {
        do bh.iter() {
            let mut xs = std::vec::with_capacity(16);
            for i in range(0, size) {
                xs.push(i);
            }
        }
    }

    #[bench]
    fn push_preallocated(bh: &mut BenchHarness) {
        do bh.iter() {
            let mut xs = Vec::with_capacity(size);
            for i in range(0, size) {
                xs.push(i);
            }
        }
    }

    #[bench]
    fn push_preallocated_std(bh: &mut BenchHarness) {
        do bh.iter() {
            let mut xs = std::vec::with_capacity(size);
            for i in range(0, size) {
                xs.push(i);
            }
        }
    }
}

#[test]
fn push() {
    let mut xs = Vec::new();
    for x in std::iter::range_step(0, 100, 5) {
        xs.push(x)
    }
    let expected = std::iter::range_step(0, 100, 5).to_owned_vec();
    assert_eq!(xs.as_slice(), expected.as_slice());
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-slowIssue: Problems and improvements with respect to performance of generated code.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions