4 April, 2016 ARCO Meeting, Odense Last revision: 17 October, 2018 Worst-case-efficient dynamic arrays in practice Jyrki Katajainen Department of Computer Science University of Copenhagen paper code goto my research information system at http://www.diku.dk/~jyrki/ slides
C array N 0 1 i A : p • contiguous memory segment • fast random access : • capacity fixed (denoted N ) A [ i ] ≡ ∗ ( A + i ) • uninitialized • iteration : • no bounds checking p = A ; ++ p ; ∗ p ;
C ++ array n N 0 1 i A : p • contiguous memory segment • fast random access : • size varies ( ++ , −− , denoted n ) A [ i ] ≡ ∗ ( A . begin ( ) + i ) • capacity varies (denoted N ) • iteration : • initialized (up to n ) p = A . begin ( ); • bounds checking if desired ++ p ; ∗ p ;
Array interfaces V , A = std::allocator < V > V std::vector leda::array + V : value type + V : value type + A : allocator type + I : iterator type + I : iterator type + item: I + N : size type + N : size type . . . . . . + default constructor + default constructor + destructor + copy constructor + get allocator() const → A + copy assignment + begin() const → I + destructor + end() const → I + first item() → I + last item() → I + size() const → N + next item( I ) → I + resize( N ) → void + prev item( I ) → I + capacity() const → N + begin() → I + reserve( N ) → void + end() → I + shrink to fit() → void + operator [ ]( N ) const → V const & + low() const → N + operator [ ]( N ) → V & + high() const → N LEDA user manual + push back( V const &) → void + size() const → N + push back( V &&) → void + resize( N ) → void C ++ standard + get( N ) → V & + pop back() → void + insert( I , V const &) → I + set( N , V ) → void + erase( I ) → I + operator [ ]( N ) → V & . . . . . . 62 member functions 36 member functions 7 free functions
Bridge design pattern V , A = std::allocator < V > , K = std::vector < V , A > V , A = std::allocator < V > cphstl::vector array kernel + V : value type + V : value type + A : allocator type + A : allocator type + K : array kernel < V , A > + K : kernel type + I : rank iterator < K > . . . + J : rank iterator < K const > . . . + N : size type – A rgs : any argument-pack type . . . – index to address( N ) const → V * V , K = std::vector < V , std::allocator < V >> + default constructor + destructor cphleda::array + get allocator() const → A + V : value type + swap( K &) → void + K : kernel type + begin() const → I + end() const → I . . . + size() const → N . . . + capacity() const → N + operator [ ]( N ) const → V const & + operator [ ]( N ) → V & + emplace back( A rgs &&...) → void minimal + push back( V const &) → void + push back( V &&) → void + pop back() → void
Usage example # include < algorithm > // std : : sort # include < cassert > // assert # include < iostream > // standard streams # include < memory > // std : : allocator # include ”sliced array . h++” // cphstl : : sliced array # include ” s t l − vector . h++” // cphstl : : vector int main ( int , char ∗∗ ) { using V = int ; using A = std : : allocator < V > ; using S = cphstl : : vector < V , A , cphstl : : sliced_array < V , A > > ; S sequence = { 4 , 2, 3, 1 } ; std : : sort ( sequence . begin ( ) , sequence . end ( )) ; assert ( std : : is_sorted ( sequence . begin ( ) , sequence . end ( ))) ; for ( V x : sequence ) { std : : cout < < ” ”; < x < } std : : cout < < ” \ n”; return 0; } jyrki@Janus:~/CPHSTL/Paper/Dynamic-arrays/Test$ make usage g++ -x c++ -std=c++17 -Wall -Wextra -fconcepts -I../Code usage.c++ ./a.out 1 2 3 4
Reflection-based implementation // shrink to fit namespace { template < typename T > concept bool has_shrink_to_fit = requires ( T x ) { { x . shrink_to_fit ( ) } → void ; // member function } ; } template < typename V , typename A , typename K > void vector < V , A , K > : : shrink_to_fit ( ) { constexpr ( has_shrink_to_fit < K > ) { i f kernel . shrink_to_fit ( ) ; } else { // do nothing } }
Goal In a minimal kernel, support all operations at O (1) worst- case cost. Assumptions : For allocators a , b , and integer ∆ ≥ 0: • p = a . allocate (∆) : O (1) worst case for any ∆ • a . deallocate ( p ,∆) : O (1) worst case for any ∆ • a . construct ( p , • ) : O (1) worst case • a . destroy ( p ) : O (1) worst case • a = b : O (1) worst case • word-RAM primitives: O (1) worst case
Memory-management tests 1. Repeat t times ( t = 10 6 ): a) Allocate space for a block of ∆ bytes ( ∆ = 2 k ). b) Deallocate the allocated block without using it. 2. Repeat the above r times ( r = 101) and report the median of the execution times for a single allocate- deallocate pair. memory-management tests; median of the execution times [ns] std :: allocator < char > a ; a . deallocate ( a . allocate (∆),∆); 2 5 2 6 2 7 2 8 2 9 2 10 2 11 2 12 2 13 2 14 2 15 2 16 2 17 2 18 2 19 2 20 2 21 2 22 2 23 2 24 2 25 2 26 35 35 61 61 60 67 68 66 66 68 68 68 50 51 52 52 52 51 51 52 2456 2644 free ( malloc (∆)); 2 5 2 6 2 7 2 8 2 9 2 10 2 11 2 12 2 13 2 14 2 15 2 16 2 17 2 18 2 19 2 20 2 21 2 22 2 23 2 24 2 25 2 26 27 27 49 50 48 57 57 55 55 57 57 57 37 39 39 39 39 39 39 39 2360 2366 I know, I may have a problem, but you have it too!
Textbook solution • doubling if ( n = N ) N 2 N 1. allocate 2 N X : Y : 2. move all from X to Y n = N 3. release X • halving if (4 n ≤ N ) N/ 2 N 1. allocate N/ 2 X : Y : 2. move all from X to Y 4 n ≤ N 3. release X
Folklore solution ( cphstl :: resizable_array ) 2 N N • doubling X : Y : if ( n = N ) n = N allocate 2 N N/ 2 N • halving X : Y : if (4 n ≤ N ) 4 n ≤ N allocate N/ 2 • incremental copying push_back : move 1 from the end of X to Y at the same relative position pop_back : move 2 from the end of X to Y at the same relative position ( X empty ) if release X
Slicing ( cphstl :: sliced_array ) • S : slice capacity (specified at compile time); • slice : C array of cap- acity S ; • # slices : ⌈ n/S ⌉ ; • last slice : can be non-full; • directory : resizable array S 0 1 2 • extra space : at most S elements plus O ( n/S ) pointers
Piling ( cphstl :: pile ) • slice : C array of fixed capacity (specified at run time); • slice capacities exponentially increasing; • # slices : ⌈ lg(max { 2 , n } ) ⌉ ; • last slice : can be non-full; • directory : resizable array 2 0 1 2 2 3 4 8 • extra space : at most n elements plus O (lg n ) pointers
Hashed array tree √ √ • N : fixed capacity (given at run time); • S = 2 ⌊ lg N/ 2 ⌋ ∈ ( N/ 2 . . N ]; • slice : C array of capacity S ; • # slices : ⌈ n/S ⌉ ; • last slice : can be non-full; • directory : C array of capacity ⌈ N/S ⌉ S 0 1 2 √ √ • extra space : at most N elements plus O ( N ) pointers
Piling hashed array trees ( cphstl :: space_efficient_array ) • pile of hashed array trees; • at level i : hashed array tree of capacity max { 2 , 2 i } ; • # levels : ⌈ lg(max { 2 , n } ) ⌉ ; • last slice in the last hashed array tree : can be non-full; • directory : resizable array 0 1 2 3 at most √ n elements plus O ( √ n ) pointers; space • extra space : bound Ω( √ n ) optimal
Summary of efficiencies • S : slice size; • n : current size solution elements pointers push_back | operator [ ] pop_back textbook O (1) O (1) amortized 6 n O (1) resizable O (1) O (1) 12 n O (1) sliced O (1) O (1) n + S O ( n/S ) pile O (1) O (1) 2 n O (lg n ) n + √ n O ( √ n ) space efficient O (1) O (1) • sources : [Sitarski 1996]; [Brodnik, Carlsson, Demaine, Munro, Sedgewick 1999]; [Katajainen, Mortensen 2001]
Space test • overhead : 100% · (space in use − n · sizeof ( int ) )/ n · sizeof ( int ) space overhead after n push_back operations The amount of extra space in use at a specific time 200 resizable vector pile sliced space efficient 150 space overhead [%] 100 50 1.0 0 1×10 6 3×10 6 5×10 6 7×10 6 9×10 6 0.1 1×10 6 3×10 6 # push _ back ’s [ n ]
Subscripting operator V & operator [ ]( N i ) { return ∗ index_to_address ( i ) ; } V ∗ index_to_address ( N i ) const { contiguous array i f ( i < 2) { return directory [0] + i ; V ∗ index_to_address ( N i ) const { } return A + i ; N h = whole_number_logarithm ( i ) ; } return directory [ h ] + i − (1 < < h ) ; } resizable array sliced array V ∗ index_to_address ( N i ) const { i f ( i < X_size ) { V ∗ index_to_address ( N i ) const { return X + i ; return directory [ i > shift ] + ( i bitand mask ) ; > } } return Y + i ; } space-efficient array pile V ∗ index_to_address ( N i ) const { i f ( i < 2) { N whole_number_logarithm ( N x ) { return directory [0] . index_to_address ( i ) ; asm (”bsr % 0, %0 \ n” } : ” = r”( x ) N h = whole_number_logarithm ( i ) ; : ”0” ( x ) N ∆ = i − (1 < < h ) ; ) ; return directory [ h ] . index_to_address (∆) ; return x ; } }
Recommend
More recommend