2014-07-03 17 views
6

Oczekiwano, że czas działania Array#shift i Array#unshift będzie równy Θ(n). Powodem jest to, że maszyna musi przechodzić przez każdy element tablicy i przypisywać go do klucza w lewo lub w prawo do niego.Czas pracy Array # unshift vs Array # shift

W przypadku Array#unshift, zakładając, że tylko jedna wartość jest przekazywana jako argument i jest wiele elementów tablicy, założyłem, że przypisanie wartości dla array[0] nie ma znaczącego wpływu na czas działania. Innymi słowy, gdy liczba elementów macierzy jest wysoka, a liczba zmiennych przekazywanych do Array#unshift jest niska, oczekiwałem, że Array#shift i Array#unshift będą miały taki sam czas działania.

Podczas wykonywania testów porównawczych w Ruby 2.1.2 te założenia nie są prawdziwe. Czemu?

Kod:

require 'benchmark' 

GC.disable  

number_of_elements = 25_600_000 

a1 =[] 
a2 = [] 
a3 = [] 
a4 = [] 
q1 = Queue.new 
q2 = Queue.new 

puts number_of_elements 

number_of_elements.times do 
    q1.enq(true) 
    q2.enq(true) 
    a1 << true 
    a2 << true 
    a3 << true 
    a4 << true 
end 

number_of_operations = 1 

Benchmark.bm do |bm| 
    puts "Queue#enq('test')" 
    bm.report do  
    number_of_operations.times { q1.enq('test') } 
    end 

    puts "Queue#deq" 
    bm.report do  
    number_of_operations.times { q2.deq } 
    end 

    puts "Array#shift" 
    bm.report do 
    number_of_operations.times { a1.shift } 
    end 

    puts "Array#unshift" 
    bm.report do 
    number_of_operations.times { a2.unshift('test') }  
    end 

    puts "Array#pop" 
    bm.report do 
    number_of_operations.times { a3.pop } 
    end 

    puts "Array#<<" 
    bm.report do 
    number_of_operations.times { a4 << 'test' } 
    end  
end 

Wynik:

25600000 
     user  system  total  real 
Queue#enq('test') 
    0.000000 0.000000 0.000000 ( 0.000006) 
Queue#deq 
    0.010000 0.020000 0.030000 ( 0.029928) 
Array#shift 
    0.010000 0.020000 0.030000 ( 0.032203) 
Array#unshift 
    0.080000 0.060000 0.140000 ( 0.143272) 
Array#pop 
    0.000000 0.000000 0.000000 ( 0.000004) 
Array#<< 
    0.000000 0.000000 0.000000 ( 0.000007) 
+0

Co stanie się, jeśli zwiększysz liczbę operacji? – Ajedi32

+0

#[email protected]: 0.016307, #[email protected]: 0.059878, # shift @ 64m: 0.098583, # shift @ 128m: 0.344900. #[email protected]: 0.059736, #[email protected]: 0.126382, # unshift @ 64m: 0.285351, # unshift @ 128m: 0.993967. Porównując tylko te proporcje, wydaje się, że #shift działa przy Θ (3,4n) i #unshift lekko wykładniczo. – migu

+1

Czy zapoznałeś się z kodem źródłowym Ruby, aby zrozumieć, w jaki sposób implementowane są tablice i jak działa 'shift' /' unshift'? Istnieje wiele rzeczy, które mogą zaistnieć za kulisami, które spowodują zmętnienie twoich wyników. Dodanie elementu do początku tablicy niekoniecznie przydzieli pojedynczą nową pozycję dla tablicy, może przydzielić wiele lub po prostu przesunie wskaźnik tablicy startów; podobnie, 'shift'ing element może po prostu zaktualizować wskaźnik, aby wskazywał na nowy adres indeksu-0. –

Odpowiedz

1

W MRI Ruby 2.1.2 unshift ma realloc tablicę i skopiuj go w całości:

   static VALUE 
rb_ary_unshift_m(int argc, VALUE *argv, VALUE ary) 
{ 
    long len = RARRAY_LEN(ary); 

    [...] 

    ary_ensure_room_for_unshift(ary, argc); 
    ary_memcpy(ary, 0, argc, argv); 
    ARY_SET_LEN(ary, len + argc); 
    return ary; 
} 

shift najwyraźniej nie zawsze rób coś takiego:

   static VALUE 
rb_ary_shift_m(int argc, VALUE *argv, VALUE ary) 
{ 
    VALUE result; 
    long n; 

    [...] 

    rb_ary_modify_check(ary); 
    result = ary_take_first_or_last(argc, argv, ary, ARY_TAKE_FIRST); 
    n = RARRAY_LEN(result); 
    if (ARY_SHARED_P(ary)) { 
     if (ARY_SHARED_OCCUPIED(ARY_SHARED(ary))) { 
      ary_mem_clear(ary, 0, n); 
     } 
     ARY_INCREASE_PTR(ary, n); 
    } 
    else { 
     RARRAY_PTR_USE(ary, ptr, { 
      MEMMOVE(ptr, ptr + n, VALUE, RARRAY_LEN(ary)-n); 
     }); /* WB: no new reference */ 
    } 
    ARY_INCREASE_LEN(ary, -n); 

    return result; 
} 
+0

W niektórych przypadkach tablica ma dodatkową przestrzeń z przodu przeznaczoną na unshift: https://github.com/ruby/ruby/commit/fdbd3716781817c840544796d04a7d41b856d9f4 – Ibrahim

+0

Powyższe jest od 2.0, więc ta odpowiedź jest trochę myląca . Wydaje się, że dla rozmiarów tablic mniejszych niż 64, ta optymalizacja nie nastąpi, prawdopodobnie dlatego, że O (n) nie jest tak źle, gdy n <64 – Ibrahim