[MoodOfTheSong] 7. Optymalizacja

Kod, który opisałem we wpisie o liczeniu gęstości przejść przez zero na całym zbiorze danych wykonywał się dość długo. Doszedłem do wniosku, że marnuję zasoby sprzętowe ponieważ najcięższe obliczeniowo przekształcenia wykonuję na jednym rdzeniu procesora. Postanowiłem coś z tym zrobić.

Dla przypomnienia, oto kod, który bierze zbiór surowych sygnałów a zwraca zbiór liczb. Każda z tych liczb to średnia wartość cechy obliczonej dla danego sygnału:

        rates = list(
            map(
                lambda data: self.transform(data).mean(),
                self.directory.signals()
            )
        )

Co z nim jest nie tak? Odpowiada on mniej-więcej pętli:

rates = []
for data in self.directory.signals():
  rates.append(self.transform(data).mean())

Jak widać, dane są przetwarzane sekwencyjnie – jedna po drugiej. Jak to zrównoleglić?
Na przykład z każdą iteracją pętli tworzyć nowy wątek, a za pętlą czekać na zakończenie wszystkich. Albo wpisać „map parallel” w Google i po pięciu minutach z zadowoleniem odpalać zmodyfikowany kod:

        rates = list(
            multiprocessing.Pool().map(
                lambda data: self.transform(data).mean(),
                self.directory.signals()
            )
        )

…a to wszystko tylko po to, aby zobaczyć wyjątek:
AttributeError: Can't pickle local object 'Stats.value..'

Okazuje się, że taka wersja funkcji map nie obsługuje lambd.

Po chwili dalszego szukania jest rozwiązanie: użycie pythonowej metody __call__ i stworzenie klasy, która zastąpi lambdę:

class Do:
    def __init__(self, transform):
        self.transform = transform

    def __call__(self, data):
        return self.transform(data).mean()

Dzięki temu możemy użyć obiektu jako funkcji. Dość dziwna konstrukcja, nie wiem czy poza Pythonem stosowana, ale robi robotę:

        rates = list(
            multiprocessing.Pool().map(
                Do(self.transform),
                self.directory.signals()
            )
        )

I co? I wyjątek:

OSError: [Errno 12] Cannot allocate memory

Na szczęście kolejne kilka minut guglowania przyniosły w pełni działające rozwiązanie: zamiast funkcji map, należy użyć w tym przypadku imap_unordered. Wg dokumentacji, imap to a lazier version of map(), imap_unordered z kolei nie gwarantuje kolejności elementów, więc teoretycznie pozwala na większą optymalizację.

Nie wnikam w szczegóły, myślę, że aktualnie ważniejsze dla mnie jest to czy rozwiązanie działa. Przeprowadziłem więc testy, które dały następujące wyniki na moim laptopie (dwurdzeniowy procesor i5 M 520 @2.40GHz):

  • 15:13,24 dla zwykłej funkcji map
  • 7:59,75 dla wersji wielowątkowej

Zauważyłem też w dokumentacji imap następujące zdanie:

For very long iterables using a large value for chunksize can make the job complete much faster than using the default value of 1.

Postanowiłem więc spróbować różne ustawienia tego parametru funkcji imap_unordered. Oto wyniki formie chunksize – czas:

1 – 7:59,75
2 – 8:28,57
5 – 8:00,71
10 – 7:50,10
12 – 7:48,47
15 – 8:14,43

Dla większych wartości skrypt się zawieszał. Nie mam pojęcia czemu. Wyniki są do siebie na tyle zbliżone, że nie ma sensu ich brać pod uwagę dopóki nie wykonam porządniejszych testów niż jednorazowe uruchomienie. Na razie więc zostawiam wartość domyślną – 1, ale być może wrócę do tego parametru w przyszłości.

Ogólnie wniosek jest jeden – warto było poświęcić trochę czasu na optymalizację systemu, ale w niedalekiej przyszłości muszę pomyśleć o infrastrukturze z większą mocą obliczeniową. Dlatego właśnie niedawno założyłem konto na AWS 😉