Wstęp

Zdarzają się sytuacje, kiedy chcemy przechować nasze obiekty w plikach/bazie. Doskonale nadaje się do tego serializacja. Przekształca ona instancję obiektu do postaci którą łatwo da się zapisać jako strumień bajtów. Więcej o serializacji można przeczytać tutaj.

Serializacja w Rubym

Serializacja w Rubym jest serializacją Marshala. Wykonuje się ją stosunkowo łatwo. Jeśli chodzi o ActiveRecord, mam do tego przygotowaną klasę która zrzuca mi obiekty do bazy (tutaj wersja skrócona):

class PendingObject < ActiveRecord::Base

  # Iterator do operowania na wszystkich oczekujących obiektach
  def self.each
    self.all.each do |el|
      yield el, el.restore
    end
  end

  def store(object)
    self.object = Marshal.dump(object)
    self.save!
  end

  def restore
    Marshal.load(self.object)
  end

end

Oczywiście to co jest wyżej nie jest do końca “elastyczne”, ponieważ nie mamy szybkiego (bez iterowania całości) dostępu do obiektów danego typu (danej klasy), niemniej jednak sami możecie to stosunkowo prosto rozbudować.

Wróćmy jednak do tematu. Tak zrealizowana serializacja działa bez problemu, przechowując nasze “żywe” instancje w bazie danych MySQL (w Postgresie też działa). Dane powinny być przechowywane jako typ Binary:

      t.binary :object

Mongo, Mongoid i jego problemy

Niestety przeniesienie tego rozwiązania tak aby działało z Mongo, okazało się troszkę trudniejsze. Zwykłe “kopiuj,wklej” okazało się nieskuteczne:

class PendingObject

  include Mongoid::Document
  include Mongoid::Timestamps

  field :object, :type => Binary

  # Iterator do operowania na wszystkich oczekujących obiektach
  def self.each
    self.all.each do |el|
      yield el, el.restore
    end
  end

  def store(object)
    self.object = Marshal.dump(object)
    self.save!
  end

  def restore
    Marshal.load(self.object)
  end

end

Niezależnie od tego czy typ pola to Binary czy też String, Mongoid zwracał następujący wyjątek:

String not valid UTF-8

O ile w wypadku stringa jestem to w stanie zrozumieć, o tyle kompletnie nie rozumiem dlaczego zwracał to kiedy chciałem zapisać wyjście jako strumień bajtów.

Base64 na ratunek

Rozwiązaniem okazała się konwersja zserializowanego obiektu do Base64. Wprawdzie obiekt będzie zajmował około 30-35% więcej miejsca, niemniej jednak jest to dla mnie w zupełności akceptowalne. Jeśli chodzi o kwestie wydajnościowe to przeprowadziłem dwie grupy testów, tak aby ustalić jaki jest spadek wydajności:

  • Serializacja
  • Serializacja i odczyt obiektów

Procedura testowa wyglądała w następujący sposób:

  1. Utworzenie pewnego (niewielkiego) obiektu
  2. Wykonanie kolejno serializacji od 1 do 100 000 – ze skokiem co 1000 bez konwersji do Base64
  3. Uruchomienie skryptu ponownie wraz z kowersją do Base64
  4. Wykonanie serii inicjalizacji instancji obiektów (tylko) – jako wartość porównawcza
  5. Analiza i wykresy

Dla pewności wykonałem 10 pełnych iteracji całości testów a następnie uśredniłem wyniki (zrobiłem to aby wyeliminować chwilowe “zmuły” procesora na inne zadania).

Benchmark

Kod benchmarku jest stosunkowo prosty. Składa się z kilku części:

  1. Przygotowanie iteracji
  2. Dummy object – obiekt który będzie nam służył do serializacji
  3. PendingObject – patrz wyżej
  4. ResultStorer – obiekt który zapisuje dla nas wyniki w odpowiednim pliku
  5. Benchmark – “obudowa” służąca do pomiaru czasu i odpalania testów
  6. Pętelki :)

Całość do ściągnięcia tutaj(benchmar.rb).

Wyniki, porównania i wykresy

Najpierw “czysta” inicjalizacja obiektów – bez serializacji. Można zauważyć, że jest tutaj ładnie i (stosunkowo) liniowo a wartości są znikome. Utworzenie 100 tysięcy obiektów zajmuje około 0.25 sekundy.

Pora na dane ciekawsze :) inicjalizacja obiektów (dla porównania), inicjalizacja oraz serializacja (narazie w jedną stronę i bez base64):

Wprost widać, że serializacja nie jest szybka. Spowalnia cały proces około 10krotnie. Jednak wciąż jest to tylko 2,5 sekundy na 100 000 obiektów. Dołóżmy teraz serializację oraz zamianę na Base64 (pozostawiając poprzednie wartości również na wykresie):


Widać tutaj, że konwersja do Base64 nie jest aż tak dużym narzutem czasowym jakby mogło się wydawać (a przynajmniej mi się tak wydawało). Spadek wydajności jest stale na poziomie 10-12%. Nie jest to wartość nie do zaakceptowania (tym bardziej, że dla 100 000 obiektów wciąż jest to 2,7 sekundy).

Pora na najciekawszy wykres: czystą inicjalizację oraz deserializację (łącznie z Base64). Pisząc “deserializację” mam na myśli czas potrzebny na odwrócenie strumienia bajtów w obiekt (czas potrzebny na serializację obiektu testowego nie jest uwzględniony – policzyłem serializację oraz serializację wraz z deserializacją i odjąłem czasy od siebie):

Widzimy tutaj (co już nie powinno nikogo dziwić ;), że deserializacja z Base64 zajmuje około 12 do 14% więcej czasu niż czysta deserializacja. Tak jak poprzednio, nie jest to wartość na poziomie niemożliwym do zaakceptowania (100 tysięcy obiektów poniżej 2 sekund).

Pora na wykres zbiorczy zawierający wszystkie dane czyli inicjalizację “czystą”, serializację, serializację Base64, deserializację, deserializację z Base64 oraz proces serializacja-deserializacja oraz jego Base64 odpowiednik:

Wnioski i podsumowanie

Z wykonanych przeze mnie obliczeń wynika, że całkowity spadek wydajności w przypadku konwersji zserializowanego obiektu do Base64 (wliczając w to proces odwrotny) utrzymuje się na poziomie 23-26%. Biorąc pod uwagę, że rzadko kiedy serializujemy takie ilości obiektów (100k i więcej) w tak krótkim czasie, uważam że jest to wartość do zaakceptowania. Oczywiście jeśli nie musimy (ponieważ korzystamy np z MySQLa i Binary) – to nie ma sensu tego używać. Jeśli jednak wykorzystujemy np Mongoida (lub inny system bazodanowy z problemami z Binary) i chcemy w nim przechowywać zserializowane obiekty – takie rozwiązanie wydaje się całkiem sensowne.Nawet wliczając narzut na bazę (rozmiar po serializacji), całkowity spadek wydajności nie powinien przekroczyć 30-35%.

Ciekawą rzeczą która wynika z wykresów jest to, że deserializacja jest bardziej “czuła” na obciążenie procesora (wykres jest bardziej poszarpany).

Podsumowując, jeśli szkoda nam czasu na szukanie lepszego rozwiązania i nie planujemy serializować milionów obiektów – ew. serializacja z konwersją do Base64 jest jak najbardziej akceptowalna.