O ’REILLY
Python WYDAJNE APLIKACJE W JĘZYKU PYTHON!
Helionie
M icha Gorelick, Ian Ozsvaid
Spis treści
P rze d m o w a .......................................................................................................................... 9 1. W ydajny kod Python .........................................................................................................15 Podstawowy system komputerowy ......................................................................................................15 Jednostki obliczeniow e...................................................................................................................... 16 Jednostki pamięci ................................................................................................................................19 Warstwy kom unikacji........................................................................................................................ 21 Łączenie ze sobą podstawowych elementów .................................................................................... 22 Porównanie wyidealizowanego przetwarzania z maszyną wirtualną języka Python ............ 23 Dlaczego warto używać języka Python? ............................................................................................. 26
2.
Użycie profilow ania do znajdowania wąskich g a rd e ł................................................. 29 Efektywne profilowanie .......................................................................................................................... 30 Wprowadzenie do zbioru Julii ............................................................................................................... 31 Obliczanie pełnego zbioru Julii ..............................................................................................................34 Proste metody pomiaru czasu — instrukcja print i dekorator......................................................37 Prosty pomiar czasu za pomocą polecenia time systemu Unix ....................................................40 Użycie modułu cProfile ............................................................................................................................41 Użycie narzędzia runsnake do wizualizacji danych wyjściowych modułu cProfile ..............46 Użycie narzędzia line_profiler do pomiarów dotyczących kolejnych wierszy kodu .............46 Użycie narzędzia memory_profiler do diagnozowania wykorzystania pamięci ....................51 Inspekcja obiektów w stercie za pomocą narzędzia heapy ........................................................... 56 Użycie narzędzia dowser do generowania aktywnego wykresu dla zmiennych z utworzonymi instancjami .....................................................................................58 Użycie modułu dis do sprawdzania kodu bajtowego narzędzia CPython ...............................60 Różne metody, różna złożoność ..................................................................................................... 62 Testowanie jednostkowe podczas optymalizacji w celu zachowania poprawności ...............64 Dekorator @profile bez operacji ..................................................................................................... 64 Strategie udanego profilowania kodu ................................................................................................. 66 Podsumowanie ............................................................................................................................................67
3
3.
Listy i k ro tk i........................................................................................................................ 69 Bardziej efektywne wyszukiwanie ........................................................................................................71 Porównanie list i krotek ...........................................................................................................................73 Listy jako tablice dynamiczne ................................................................................................................ 74 Krotki w roli tablic statycznych .............................................................................................................77 Podsumowanie ............................................................................................................................................78
Słowniki i z b io r y ................................................................................................................79
4.
Jak działają słowniki i zbiory? ................................................................................................................ 82 Wstawianie i pobieranie.................................................................................................................... 82 Usuwanie .............................................................................................................................................. 85 Zmiana wielkości ................................................................................................................................85 Funkcje mieszania i entropia............................................................................................................86 Słowniki i przestrzenie nazw ................................................................................................................. 89 Podsumowanie ............................................................................................................................................92
Iteratory i g e n e ra to ry ....................................................................................................... 93
5.
Iteratory dla szeregów nieskończonych .............................................................................................. 96 Wartościowanie leniwe generatora ...................................................................................................... 97 Podsumowanie ......................................................................................................................................... 101
6. Obliczenia macierzowe i w e k to ro w e ........................................................................... 103 Wprowadzenie do problemu ............................................................................................................... 104 Czy listy języka Python są wystarczająco dobre? ........................................................................... 107 Problemy z przesadną alokacją .................................................................................................... 109 Fragmentacja pamięci .............................................................................................................................111 Narzędzie perf ...................................................................................................................................113 Podejmowanie decyzji z wykorzystaniem danych wyjściowych narzędzia perf .......... 115 Wprowadzenie do narzędzia numpy ..........................................................................................116 Zastosowanie narzędzia numpy w przypadku problemu dotyczącego dyfuzji ................... 119 Przydziały pamięci i operacje wewnętrzne ............................................................................... 121 Optymalizacje selektywne: znajdowanie tego, co wymaga poprawienia ........................ 124 Moduł numexpr: przyspieszanie i upraszczanie operacji wewnętrznych ...............................127 Przestroga: weryfikowanie „optymalizacji" (biblioteka scipy) ...................................................129 Podsum ow anie......................................................................................................................................... 131
7.
Kompilowanie do postaci kodu C ...................................................................................133 Jakie wzrosty szybkości są możliwe? ................................................................................................ 134 Porównanie kompilatorów JIT i A O T ................................................................................................ 136 Dlaczego informacje o typie ułatwiają przyspieszenie działania kodu? ..................................136 Użycie kompilatora kodu C .................................................................................................................. 137 Analiza przykładu zbioru Ju lii.............................................................................................................138 Cython ........................................................................................................................................................ 139 Kompilowanie czystego kodu Python za pomocą narzędzia C ython................................139 Użycie adnotacji kompilatora Cython do analizowania bloku kodu .................................141 Dodawanie adnotacji typu .............................................................................................................143
4
|
Spis treści
Shed S k in .................................................................................................................................................... 147 Tworzenie modułu rozszerzenia .................................................................................................. 148 Koszt związany z kopiami pamięci .............................................................................................150 Cython i numpy ....................................................................................................................................... 151 Przetwarzanie równoległe rozwiązania na jednym komputerze z wykorzystaniem interfejsu OpenMP .................................................................................... 152 Numba ........................................................................................................................................................ 154 Pythran ....................................................................................................................................................... 155 PyPy ............................................................................................................................................................ 157 Różnice związane z czyszczeniem pamięci ............................................................................... 158 Uruchamianie interpretera PyPy i instalowanie modułów ...................................................159 Kiedy stosować poszczególne technologie? .....................................................................................160 Inne przyszłe projekty ..................................................................................................................... 162 Uwaga dotycząca układów GPU ................................................................................................. 162 Oczekiwania dotyczące przyszłego projektu kom pilatora....................................................163 Interfejsy funkcji zewnętrznych ...........................................................................................................163 ctypes ....................................................................................................................................................164 c f f i.......................................................................................................................................................... 166 f2py ....................................................................................................................................................... 169 Moduł narzędzia CPython .............................................................................................................171 Podsumowanie ......................................................................................................................................... 174
8. W spółbieżność..................................................................................................................175 Wprowadzenie do programowania asynchronicznego.................................................................176 Przeszukiwacz szeregowy .................................................................................................................... 179 g ev en t.......................................................................................................................................................... 181 tornado ....................................................................................................................................................... 185 A sy n clO ...................................................................................................................................................... 188 Przykład z bazą danych .........................................................................................................................190 Podsumowanie ......................................................................................................................................... 193
9.
Moduł m ultiprocessing....................................................................................................195 Moduł multiprocessing ..........................................................................................................................198 Przybliżenie liczby pi przy użyciu metody Monte Carlo ............................................................ 200 Przybliżanie liczby pi za pomocą procesów i w ątków ................................................................. 201 Zastosowanie obiektów języka Python ...................................................................................... 201 Liczby losowe w systemach przetwarzania równoległego .................................................. 208 Zastosowanie narzędzia numpy ..................................................................................................209 Znajdowanie liczb pierw szych.............................................................................................................211 Kolejki zadań roboczych .................................................................................................................217 Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej ..................... 221 Rozwiązanie z przetwarzaniem szeregowym ..........................................................................225 Rozwiązanie z prostym obiektem Pool ...................................................................................... 225 Rozwiązanie z bardzo prostym obiektem Pool dla mniejszych liczb ............................... 227 Użycie obiektu Manager.Value jako flagi .................................................................................. 228
Spis treści
|
5
Użycie systemu Redis jako flagi ...................................................................................................229 Użycie obiektu RawValue jako fla g i............................................................................................232 Użycie modułu mmap jako flagi ..................................................................................................232 Użycie modułu mmap do odtworzenia flagi ............................................................................233 Współużytkowanie danych narzędzia numpy za pomocą modułu multiprocessing ......... 236 Synchronizowanie dostępu do zmiennych i plików ......................................................................242 Blokowanie plików .......................................................................................................................... 242 Blokowanie obiektu V a lu e ..............................................................................................................245 Podsumowanie ......................................................................................................................................... 248
10.
Klastry i kolejki zadań .................................................................................................... 249 Zalety klastrowania .................................................................................................................................250 Wady klastrowania .................................................................................................................................251 Strata o wartości 462 milionów dolarów na giełdzie Wall Street z powodu kiepskiej strategii aktualizacji klastra .................................................................. 252 24-godzinny przestój usługi Skype w skali globalnej ............................................................253 Typowe projekty klastrowe .................................................................................................................. 254 Metoda rozpoczęcia tworzenia rozwiązania klastrowego
........................................................ 254
Sposoby na uniknięcie kłopotów podczas korzystania z klastrów ........................................... 255 Trzy rozwiązania klastrowe .................................................................................................................257 Użycie modułu Parallel Python dla prostych klastrów lokalnych .....................................257 Użycie modułu IPython Parallel do obsługi badań ................................................................ 259 Użycie systemu NSQ dla niezawodnych klastrów produkcyjnych .......................................... 262 K olejki...................................................................................................................................................263 Publikator/subskrybent ..................................................................................................................264 Rozproszone obliczenia liczb pierwszych ................................................................................. 266 Inne warte uwagi narzędzia klastrow ania........................................................................................268 Podsumowanie ......................................................................................................................................... 269
11. Mniejsze w ykorzystanie pamięci RAM ........................................................................ 271 Obiekty typów podstawowych są kosztowne ................................................................................ 272 Moduł array zużywa mniej pamięci do przechowywania wielu obiektów typu podstawowego...................................................................................................................... 273 Analiza wykorzystania pamięci RAM w kolekcji ..........................................................................276 Bajty i obiekty U nicod e.......................................................................................................................... 277 Efektywne przechowywanie zbiorów tekstowych w pamięci RAM ......................................... 279 Zastosowanie metod dla 8 milionów tokenów .........................................................................280 Wskazówki dotyczące mniejszego wykorzystania pamięci RAM ............................................ 288 Probabilistyczne struktury danych .................................................................................................... 289 Obliczenia o bardzo dużym stopniu przybliżenia z wykorzystaniem jednobajtowego licznika Morrisa ............................................................................................................................. 290 Wartości k-minimum ....................................................................................................................... 291 Filtry B loom a......................................................................................................................................295 Licznik LogLog ..................................................................................................................................299 Praktyczny przykład........................................................................................................................ 303
6
|
Spis treści
12.
Rady specjalistów z b ra n ż y ............................................................................................307 Narzędzie Social Media Analytics (SoMA) firmy AdaptiveL a b .................................................307 Język Python w firmie Adaptive Lab ..........................................................................................308 Projekt narzędzia SoMA .................................................................................................................308 Zastosowana metodologia projektowa ...................................................................................... 309 Serwisowanie systemu SoMA ....................................................................................................... 309 Rada dla inżynierów z branży ...................................................................................................... 310 Technika głębokiego uczenia prezentowana przez firmę RadimRehurek.com .....................310 Strzał w dziesiątkę ........................................................................................................................... 311 Rady dotyczące optym alizacji....................................................................................................... 313 Podsumowanie ..................................................................................................................................315 Uczenie maszynowe o dużej skali gotowe do zastosowań produkcyjnych w firmie Lyst.com ......................................................................315 Rola języka Python w witrynie L y s t............................................................................................316 Projekt klastra ....................................................................................................................................316 Ewolucja kodu w szybko rozwijającej się nowej firmie.......................................................... 316 Budowanie mechanizmu rekomendacji ..................................................................................... 316 Raportowanie i monitorowanie .................................................................................................... 317 Rada ......................................................................................................................................................317 Analiza serwisu społecznościowego o dużej skali w firmieSmesh ........................................... 318 Rola języka Python w firmie Smesh ............................................................................................318 Platform a............................................................................................................................................. 318 Dopasowywanie łańcuchów w czasie rzeczywistym z dużą wydajnością ......................319 Raportowanie, monitorowanie, debugowanie i wdrażanie ................................................. 320 Interpreter PyPy zapewniający powodzenie systemów przetwarzania danych i systemów internetowych..................................................................................................................322 Wymagania wstępne ....................................................................................................................... 322 Baza danych .......................................................................................................................................323 Aplikacja internetow a...................................................................................................................... 323 Mechanizm OCR i tłumaczenie .................................................................................................... 324 Dystrybucja zadań i procesy robocze ..........................................................................................324 Podsumowanie ..................................................................................................................................325 Kolejki zadań w serwisie internetowym Lanyrd.com .................................................................. 325 Rola języka Python w serwisie Lanyrd ...................................................................................... 325 Zapewnianie odpowiedniej wydajności kolejki zadań ..........................................................326 Raportowanie, monitorowanie, debugowanie i wdrażanie ................................................. 326 Rada dla programistów z branży .................................................................................................326
S k o ro w id z........................................................................................................................ 329
Spis treści
|
7
8
|
Spis treści
Przedmowa
Język Python jest łatwy do opanowania. Prawdopodobnie czytasz tę książkę, ponieważ kod działa już popraw nie, ale chcesz, by m iał w iększą w ydajność. Podoba Ci się to, że kod jest łatwy do zmodyfikowania, a ponadto m ożesz szybko korzystać z iteracji przy realizacji swo ich pom ysłów . O gólnie znaną sytuacją, która często pow oduje lam ent, jest trudność pogo dzenia łatwości projektowania z wymaganą szybkością działania kodu. Istnieją rozw iązania tego problemu. Niektórzy używ ają procesów szeregowych, które muszą przebiegać szybciej. Inni zajm ują się problem am i, w przypadku których korzystne byłoby zastosow anie architektur w ielordze niow ych, klastrów lub układów GPU. C zęść osób w ym aga system ów skalow alnych, które bez obniżenia poziom u niezaw odności m ogą przetw arzać więcej lub mniej danych, zależnie od w zględów praktycznych lub dostępnych funduszy. C zęść osób w pew nym m om encie uświadom i sobie, że techniki kodowania, z których korzysta (często zapożyczone z innych języków ), być m oże nie są tak naturalne jak przykłady przedstaw ione przez innych. W książce zostaną omówione w szystkie te zagadnienia. W ram ach praktycznego przewodni ka będziesz m ieć okazję zapoznać się z w ąskim i gardłam i, a także z tw orzeniem szybszych i bardziej skalowalnych rozwiązań. W książce przedstawiono też kilka praw dziw ych historii osób, które od daw na zajm ują się kw estiam i w ydajności. Ci ludzie sporo już dośw iadczyli, a dzięki ich historiom nie będziesz musiał przechodzić przez to samo. Język Python jest odpowiednio przystosowany do szybkiego projektowania, wdrożeń produk cyjnych i systemów skalowalnych. Społeczność związana z tym językiem jest złożona z mnóstwa osób, które w spółpracują w celu zapewnienia jego skalowalności. Dzięki temu będziesz miał czas na skoncentrowanie się na zadaniach, które uznasz za w iększe w yzwanie.
Dla kogo jest przeznaczona ta książka? Prawdopodobnie języka Python używasz w ystarczająco długo, aby zorientow ać się, dlaczego określone elementy są powolne, a także aby poznać takie technologie jak Cython, numpy i PyPy, które są omawiane w książce jako możliwe rozwiązania. Być m oże program owałeś też z w y korzystaniem innych języków , dlatego wiesz, że problem z w ydajnością może zostać rozwią zany przy użyciu więcej niż jednego sposobu.
9
Choć ta książka jest kierowana przede wszystkim do osób zajm ujących się problem am i, do których rozwiązania stosuje się głównie procesory, zajmiemy się także rozwiązaniami opar tym i na pam ięci i transferze danych. Zw ykle z takim i problem am i m ają do czynienia na ukowcy, inżynierowie, eksperci od analizy i pracownicy akademiccy. Przyjrzymy się również problemom, z którymi może się zetknąć projektant aplikacji inter netowych. Dotyczą one przenoszenia danych i użycia kom pilatorów JIT (Just in Time), takich jak PyPy, w celu uzyskania w prosty sposób wzrostu wydajności. Choć pomocna m oże okazać się znajom ość języka C (lub C++ albo być m oże Java), nie jest to w ym aganie w stępne przy lekturze tej książki. N ajpopularniejszy interpreter języka Python, czyli CPython (standardowo udostępniany po wpisaniu polecenia python w w ierszu poleceń), napisano w języku C. W zw iązku z tym m echanizm y przechw ytyw ania kom unikatów oraz biblioteki opierają się na wewnętrznych mechanizmach języka C. W książce omówiono jednak również wiele innych technik, w przypadku których nie jest zakładana znajom ość języka C. Być może dysponujesz specjalistyczną wiedzą na temat procesora, architektury pamięci i magi stral danych. Jednakże i taka wiedza nie jest absolutnie konieczna.
Dla kogo nie jest przeznaczona ta książka? Książkę przewidziano dla programistów używ ających języka Python, którzy mogą się po chwalić jego średnią lub zaaw ansowaną znajomością. Zmotywowani początkujący program i ści również mogą być w stanie poradzić sobie z materiałem w niej zamieszczonym, zalecamy jednak solidne przygotow anie z zakresu języka Python. W książce nie jest omawiana optymalizacja dotycząca systemów przechowywania danych. Jeśli masz do czynienia z w ąskim gardłem występującym w bazie danych SQ L lub NoSQL, ta książka raczej nie będzie pomocna.
Czego się dowiesz? W ciągu wielu lat pracy zarówno w firmach z branży, jak i na uczelniach korzystaliśmy z dużych wolum enów danych, byliśmy konfrontowani z wym aganiam i w rodzaju Chcę szybciej uzyskać odpow iedzi!, a także z potrzebą zapewnienia skalowalnych architektur. Spróbujem y podzielić się zdobytym ciężką pracą doświadczeniem, aby oszczędzić Ci popełniania błędów, jakie sami popełniliśmy. Na początku każdego rozdziału znajduje się lista pytań, na które powinieneś uzyskać odpo wiedź po przeczytaniu treści rozdziału (jeśli tak nie będzie, poinformuj nas o tym, a wprowa dzimy odpowiednie poprawki w następnym wydaniu!). W książce omówiono następujące zagadnienia: • M echanizm y komputera — podstaw ow e informacje na ten tem at pozw olą Ci zoriento wać się, jakie procesy zachodzą w tle. • Listy i krotki — poznasz subtelne różnice dotyczące semantyki i wydajności tych funda mentalnych struktur danych.
10
|
Przedmowa
• Słowniki i zbiory — dowiesz się, jakie są strategie przydzielania pamięci i algorytmy do stępu w przypadku tych istotnych struktur danych. • Iteratory — nauczysz się tworzyć je w sposób bardziej typowy dla języka Python, wyjaśni my także, jak za pomocą iteracji uzyskać dostęp do nieograniczonych strumieni danych. • Metody oparte wyłącznie na kodzie Python — poznasz sposób efektywnego użycia języka Python i jego modułów. • Macierze w przypadku narzędzia numpy — zaprezentujemy sposób wykorzystania w pełni możliwości naszej ulubionej biblioteki numpy. • Kompilacja i obliczenia za pomocą kompilatorów JIT — om ówim y szybsze przetw arza nie przez kompilowanie do postaci kodu m aszynowego przy zapewnieniu, że działa się zgodnie z w ynikam i profilowania. • W spółbieżność — zapoznasz się z metodami efektywnego przemieszczania danych. • Moduł multiprocessing — poznasz różne sposoby użycia wbudowanej biblioteki multiprocessing do przetwarzania równoległego, omówimy wydajne współużytkowanie macierzy narzędzia numpy oraz wady i zalety związane z zastosowaniem komunikacji międzyprocesowej IPC. • Obliczenia klastrowe — nauczysz się przekształcania kodu m odułu multiprocessing tak, aby m ógł zostać uruchomiony w klastrze lokalnym lub zdalnym zarówno w przypadku systemów badawczych, jak i produkcyjnych. • U życie m niejszej ilości pam ięci RAM — poznasz m etody rozw iązyw ania pow ażnych problem ów bez konieczności nabywania komputera o bardzo dużej mocy obliczeniowej. • Porady specjalistów z branży — przeczytasz rady zaw arte w autentycznych historiach osób, które wiele już doświadczyły, dzięki czemu nie będziesz zmuszony do przechodzenia przez to samo.
Język Python 2.7 W ersja 2.7 to najpowszechniej używana wersja języka Python w przypadku obliczeń nauko w ych i inżynieryjnych. W tym segmencie zastosow ań przeważa technologia 64-bitowa, a tak że środow iska uniksow e (często system Linux lub M ac). 64-bitow ość pozw ala adresow ać w iększe ilości pamięci RAM. Systemy uniksowe um ożliwiają tworzenie aplikacji, które mogą być w drażane i konfigurow ane z wykorzystaniem dobrze znanych metod i wzorców. Jeśli jesteś użytkow nikiem systemu W indow s, koniecznie zapnij pasy. W iększość rozw iązań zaprezentow anych w książce zadziała bez żadnych problem ów, niektóre elementy są jednak specyficzne dla systemu operacyjnego, dlatego będziesz m usiał poszukać rozw iązania dla systemu W indows. Najw iększą trudnością, z jaką m oże m ieć do czynienia użytkownik sys temu W indows, jest instalacja m odułów : poszukiwania w serwisach takich jak StackOverflow pow inny zakończyć się uzyskaniem w ym aganych rozw iązań. Jeżeli korzystasz z system u W indows, użycie maszyny wirtualnej (np. VirtualBox) z uruchomioną instalacją systemu Linux może być pomocne w bardziej swobodnym eksperymentowaniu. Użytkownicy systemu Windows powinni zdecydowanie przyjrzeć się wybranemu rozwiązaniu w postaci pakietu, podobnemu do udostępnianych za pośrednictwem dystrybucji Anaconda, Canopy, Python(x,y) lub Sage. Sprawią one także, że praca użytkow ników system ów Linux i M ac będzie znacznie łatwiejsza.
Język Python 2.7
|
11
Przejście na język Python 3 Python 3 to przyszłość języka Python. W szyscy przechodzą na tę wersję. Niemniej jednak ję zyk Python 2.7 będzie w ykorzystyw any przez w iele kolejnych lat (niektóre instalacje nadal używają języka Python 2.4 z roku 2004). Datę wycofania tej wersji języka Python ustalono na rok 2020. Przejście na język Python w w ersji 3.3 lub now szej przyniosło w ystarczającą liczbę proble m ów tw órcom bibliotek, gdyż w iele osób nie spieszy się z przenoszeniem sw ojego kodu (z uzasadnionego powodu). Oznacza to powolny proces adaptacji języka Python 3. W głów nej m ierze jest to w ynikiem złożoności procesu przechodzenia z kom binacji typów danych Unicode i łańcuchowych w skomplikowanych aplikacjach do obecnej w języku Python 3 im plementacji typów danych Unicode i bajtowych. Zazwyczaj gdy w ym agane są wyniki możliwe do odtworzenia na podstaw ie zestawu zaufa nych bibliotek, niewskazane jest bycie zależnym od niesprawdzonej najnowszej technologii. Tw órcy bardzo wydajnego kodu Python będą praw dopodobnie przez kolejne lata korzystać z godnego zaufania języka Python 2.7. W iększość kodu zam ieszczonego w tej książce będzie działać w przypadku języka Python w wersji 3.3 lub nowszej po wprowadzeniu niew ielkich zm ian (najbardziej znacząca m odyfi kacja będzie dotyczyć instrukcji print, którą przekształcono w funkcję). W kilku miejscach ze szczególną uw agą przyjrzym y się ulepszeniom zapew nianym przez język Python w w ersji 3.3 lub nowszej. Jedna z rzeczy, która może zaskoczyć, dotyczy tego, że w języku Python 2.7 znak / oznacza dzielenie liczb całkowitych, w języku Python 3 natom iast znak ten reprezentuje dzielenie liczb zmiennoprzecinkowych. Oczywiście będąc dobrym programistą, dysponujesz dobrze skonstruowanym pakietem testów jednostkowych, które będą testować w ażne ścieżki kodu. Dzięki temu testy jednostkow e ostrzegą Cię w sytuacji, gdy będzie trzeba dokonać po praw ek w kodzie. Począwszy od końca roku 2010, biblioteki scipy i numpy są zgodne z językiem Python 3. Biblioteka matplotl ib jest z nim zgodna od roku 2012, biblioteka scik it-lea rn od roku 2013, a biblioteka NLTK od 2014. Biblioteka D jango uzyskała zgodność w roku 2013. U w agi dotyczące przejścia każdej biblioteki są dostępne w jej repozytoriach i grupach dyskusyjnych. W arto przejrzeć używ ane przez biblioteki procesy, jeśli planuje się m igrację starszego kodu w celu zapewnie nia zgodności z językiem Python 3. Zachęcam y do poeksperym entow ania z językiem Python w w ersji 3.3 lub now szej dla no w ych projektów . Trzeba jednak zachow ać ostrożność w przypadku bibliotek, które zostały przeniesione całkiem niedawno i mają niewielu użytkowników. Identyfikowanie błędów będzie w ich przypadku znacznie trudniejsze. Warto zapewnić zgodność z językiem Python w wersji 3.3 lub nowszej (dowiedz się więcej o importach __future__), ponieważ przyszła aktualizacja będzie wówczas łatwiejsza. Dwa dobre przew odniki to Porting Python 2 Code to Python 3 (https://docs.python.org/3/howto/ pyporting.html) i Porting to Python 3: An in-depth guide (http://python3porting.com/). W przypadku takich dystrybucji jak Anaconda lub Canopy możliwe jest jednoczesne użycie języka Python w w ersjach 2 i 3. Uprości to przenoszenie.
12
|
Przedmowa
Licencja Książka objęta jest licencją Creative Commons Attribution-N onCom m ercial-N oD erivs 3.0 (h ttp :// creativecom m ons.org/licenses/by-nc-nd/3.0/). M ożesz swobodnie wykorzystyw ać tę książkę do celów niekom ercyjnych, w tym do naucza nia o takim charakterze. Licencja zezw ala jedynie na kom pletne reprodukcje. W przypadku częściow ych reprodukcji prosim y skontaktow ać się z w ydaw nictw em . P rosim y u tw orzyć atrybucję w sposób opisany w następnym podrozdziale. Ustaliliśmy, że książka pow inna zostać objęta licencją Creative Commons, aby jej treść mogła być dalej rozpow szechniana na całym świecie. Jeśli taka decyzja okazała się dla Ciebie po mocna, będziem y napraw dę szczęśliw i, jeśli postaw isz nam piwo. Podejrzew am y, że pra cownicy wydaw nictw a też będą z tego zadowoleni.
Sposób tworzenia atrybucji L icencja C reative Com m ons w ym aga atrybucji użycia fragm entu treści książki. A trybucja oznacza po prostu, że należy dołączyć informację, która pozw oli innym osobom na znalezie n ie tej książki. Proponujem y dodanie następu jącego tekstu: M icha G orelick, Ian O zsvald, Python. Program uj szybko i w ydajnie, H elion 2015.
Errata i opinie Zachęcamy do ocenienia książki w publicznych serwisach internetowych. Pomóż innym do w iedzieć się, czy skorzystają na kupnie tej książki! Możesz też napisać do nas na adres e-mail
[email protected]. Szczególnie chętnie dow iem y się o błęd ach w książce, pom yślnych przypad kach użycia, w których książka okazała się pomocna, a także o technikach zapewniających dużą wydajność, jakie powinny zostać przez nas uwzględnione w następnym wydaniu.
Konwencje zastosowane w książce W książce użyto następujących konwencji typograficznych: Kursywa W skazuje now e terminy, adresy URL, adres e-mail, nazw y plików i ich rozszerzenia. Czcionka o s ta łe j szerokości Używana do wyróżnienia w akapitach poleceń, m odułów i elementów programów, takich jak nazwy zmiennych lub funkcji, baz danych, typów danych, zmiennych środowiskowych, instrukcji i słów kluczowych. Listing
W yróżnia fragmenty kodu poza treścią akapitów.
Konwencje zastosowane w książce
|
13
Ta ikona oznacza pytanie lub ćwiczenie.
EJ
T a ikona o zn acza ogóln ą u w agę.
T a ik o n a o z n a cz a ostrzeżen ie.
Użycie przykładowych kodów Dodatkowe materiały (przykłady z kodami, ćwiczenia itp.) są dostępne do pobrania pod ad resem ftp://ftp.helion.pl/przyklady/pytpsw.zip. Książka ma na celu ułatw ienie zrealizowania zadania. Ogólnie rzecz biorąc, jeśli kod przy kładu zam ieszczono w książce, m ożesz go użyć w e w łasnych program ach i dokum entacji. Nie musisz kontaktować się z nami w celu uzyskania zgody, chyba że reprodukujesz znaczną część kodu. Na przykład napisanie programu, który zawiera kilka fragmentów kodu z książki, nie wymaga zgody. Sprzedawanie lub dystrybucja dysku CD-ROM z przykładam i z książek wydawnictwa wymaga zgody. Udzielanie odpowiedzi na pytanie w postaci zacytowania frag mentu z książki wraz z kodem przykładu nie wymaga zgody. Uw zględnienie znacznej części kodu przykładu z książki w dokumentacji w łasnego produktu w ym aga zgody. Jeśli uznasz, że użycie przykładów z kodem wykracza poza dozwolony zakres lub wymienione przypadki konieczności wyrażenia zgody, możesz bez obaw skontaktować się z wydawnictwem.
Podziękowania Podziękowania dla Jake'a Vanderplasa, Briana Grangera, Dana Foremana-M ackeya, Kyrana Dale'a, Johna Montgomery'ego, Jamiego Matthewsa, Calvina Gilesa, Williama Wintera, Christiana Schou Oxviga, Balthazara Rouberola, Matta „snakesa" Reifersona, Patricka Coopera i Michaela Skirpana za bezcenne uwagi i zaangażowanie. Ian dziękuje swojej żonie Emily za pozw ole nie, by zniknął na 10 m iesięcy w celu napisania tej książki (na szczęście jest szalenie w yro zumiała). Micha dziękuje Elaine i reszcie swoich znajom ych oraz rodzinie za wielką cierpli w ość w czasie, gdy uczył się pisać książkę. Również pracownikom wydaw nictw a O 'Reilly — współpraca z nimi była przyjemnością. Osoby zaangażow ane w tworzenie rozdziału 12., „Rady specjalistów z branży", były na tyle uprzejme, by podzielić się swoim czasem i wiedzą zdobytą ciężką pracą. Za pośw ięcony czas i trud dziękujemy następującym osobom: Benowi Jacksonow i, Radim owi Rehurkowi, Sebastjanowi Trebce, Alexowi Kelly'em u, M arkow i Tasicow i i Andrew Godwinowi.
14
|
Przedmowa
_________________ ROZDZIAŁ 1.
Wydajny kod Python
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Jakie są składniki architektury komputerowej? • Jakie są typowe alternatywne architektury komputerowe? • Jak w języku Python przeprow adzana jest abstrakcja bazow ej architektury kom puterow ej? • Jakie są przeszkody na drodze do uzyskania wydajnego kodu Python? • Jakie są różne typy problemów dotyczących wydajności?
Tw orzenie oprogramowania dla komputerów można zobrazow ać jako proces przem ieszcza nia porcji danych i przekształcania ich przy użyciu specjalnych sposobów, aby osiągnąć kon kretny rezultat. Działania te w iążą się jednak z kosztem w postaci czasu. Oznacza to, że two rzenie oprogramowania o dużej wydajności polega na minim alizacji opisanych działań przez redukowanie powodowanego przez nie obciążenia (tj. przez pisanie bardziej wydajnego kodu) lub zm ianę m etody wykonywania działań w celu sprawienia, że każde z nich będzie bardziej konstruktywne (tj. znajdow anie bardziej odpowiedniego algorytmu). Skoncentrujm y się na zredukow aniu obciążenia pow odow anego przez kod, aby bliżej za znajom ić się z samymi urządzeniami, które są wykorzystyw ane do przem ieszczania porcji danych. M oże się w ydaw ać, że będzie to darem ny trud, poniew aż w języku Python w celu ukrycia bezpośrednich interakcji ze sprzętem w dużym zakresie stosuje się abstrakcję. Zrozu mienie najlepszej metody przemieszczania bitów danych w urządzeniach oraz sposobów wy muszania przemieszczania bitów przez abstrakcje w języku Python pozwoli jednak na pisanie w tym języku lepszych programów o dużej wydajności.
Podstawowy system komputerowy O pis bazow ych elem entów tw orzących kom putery m ożna uprościć przez zaklasyfikow anie ich do trzech podstaw ow ych grup: jednostek obliczeniow ych, jednostek pam ięci i połączeń między pierwszymi dwiema grupami. Każda z tych grup ma różne cechy, które umożliwiają
15
jej zrozum ienie. Jednostka obliczeniow a cechuje się liczbą obliczeń, jakie m oże w ykonać w ciągu sekundy. Jednostka pamięci wyróżnia się ilością danych, jakie m oże przechowywać, a także szybkością odczytu i zapisu danych. Z kolei połączenia są określone przez to, jak szybko mogą przem ieszczać dane z jednego miejsca w drugie. Skoro wym ieniliśm y już bloki konstrukcyjne, m ożem y omówić standardową stację roboczą na wielu poziomach zaawansowania. Taka stacja robocza może zawierać na przykład centralną jednostkę obliczeniową CPU (Central Processing Unit) połączoną z pamięcią RAM (Random Access M emory) i dyskiem twardym jako dwiema osobnymi jednostkam i pamięci (każda z nich ce chuje się różnymi pojem nościam i oraz szybkościami odczytu/zapisu) oraz magistralę, która zapew nia połączenia m iędzy w szystkim i tym i kom ponentam i. M ożna to jed n ak bardziej sprecyzować. O każe się, że sam procesor CPU zawiera kilka jednostek pamięci. Są to pamięci podręczne L1, L2, a czasem naw et L3 i L4, które są bardzo szybkie, choć m ają niewielkie po jem ności (pojemność w ynosi od kilku kilobajtów do tuzina m egabajtów). Te dodatkowe jed nostki pam ięci są połączone z procesorem CPU za pom ocą specjalnej m agistrali określanej m ianem m agistrali B SB (B ackside B u s). Co w ięcej, now e architektury kom puterow e oferują zw ykle now e konfiguracje (np. w przypadku procesorów N ehalem firm y Intel m agistralę FSB zastąpiono technologią Intel Q uickPath Interconnect, a ponadto przebudow ano wiele połączeń). W obu przedstaw ionych ogólnych wariantach stacji roboczych nie doceniono zna czenia połączenia sieciowego, które może być bardzo wolne, z potencjalnymi wieloma innymi jednostkami obliczeniowymi i jednostkami pamięci! Aby ułatw ić rozwikłanie tych różnych zawiłości, dokonajmy krótkiego przeglądu wym ienio nych podstaw ow ych bloków.
Jednostki obliczeniowe Jednostka obliczeniow a kom putera w najw iększym stopniu stanow i o jego przydatności. Zapewnia ona możliwość przekształcenia dowolnych odebranych bitów w inne bity lub zmia ny stanu bieżącego procesu. Jednostki CPU to najpowszechniej używane jednostki obliczeniowe. Graficzne jednostki obliczeniowe GPU (Graphics Processing Unit), które pierwotnie były zwykle w ykorzystywane do przyspieszania grafiki komputerowej, a obecnie coraz częściej są stoso wane na potrzeby aplikacji num erycznych, zyskują jednak na popularności. W ynika to z ich naturalnych m ożliw ości przetw arzania rów noległego, które pozw ala na jednoczesne prze prow adzenie w ielu obliczeń. N iezależnie od typu jed nostka obliczeniow a pobiera zestaw bitów (np. reprezentujących liczby) i zwraca kolejny zestaw bitów (np. reprezentujących sumę tych liczb). Oprócz podstawowych operacji arytmetycznych na liczbach całkowitych i rze czywistych oraz operacji bitow ych na liczbach binarnych niektóre jednostki obliczeniowe za pewniają też bardzo specjalistyczne operacje. Przykładem jest operacja FMA (Fused M ultiply Add), która pobiera trzy liczby A, B i C, a następnie zwraca w artość A * B + C. Podstawowe interesujące nas cechy jednostki obliczeniowej to liczba operacji, jaką m oże ona wykonać w jednym cyklu, a także liczba cykli możliw ych do zrealizowania w ciągu sekundy. Pierwsza wartość jest mierzona za pomocą instrukcji przypadających na cykl (IPC — Instructions Per Cycle)1, natom iast druga w artość jest określana przy użyciu szybkości zegara jednostki. Podczas projektow ania now ych jednostek obliczeniow ych tym dwóm param etrom zaw sze
1 N i e n a l e ż y m y l i ć z k o m u n i k a c j ą m i ę d z y p r o c e s o w ą , w p r z y p a d k u kt ó re j u ż y w a n y je s t ta k i s a m s k r ó t I P C (In ter-P ro c ess C o m m u n ic a tio n ). Z a g a d n i e n i e to z o s t a n i e o m ó w i o n e w r o z d z i a l e 9.
16
|Rozdział 1. Wydajny kod Python
towarzyszy rywalizacja. Na przykład procesory z serii Intel Core m ają bardzo wysoką war tość IPC, ale mniejszą szybkość zegara. W przypadku układu Pentium 4 wygląda to odwrotnie. Z kolei jednostki GPU cechują się bardzo w ysoką w artością IPC i szybkością zegara, ale do tyczą ich inne problemy, które zostaną dalej przybliżone. Choć zwiększanie szybkości zegara powoduje prawie natychmiastowe przyspieszenie wszyst kich programów działających na danej jednostce obliczeniowej (ze względu na możliwość wy konania w ciągu sekundy większej liczby obliczeń), wyższa wartość IPC może też w znacznym stopniu w płynąć na przetwarzanie przez zm ianę możliwego poziomu wektoryzacji. W ektoryzacja ma miejsce wtedy, gdy jednostka CPU otrzymuje jednocześnie wiele porcji danych i ma możliw ość działania na nich w szystkich w tym samym czasie. Tego rodzaju instrukcja proce sora określana jest mianem instrukcji SIMD (Single Instruction, M ultiple Data). Generalnie rzecz biorąc, w ciągu minionej dekady jednostki obliczeniow e były dość w olno rozwijane (rysunek 1.1). Szybkości zegara i w artość IPC nie zm ieniały się znacząco z powodu fizycznych ograniczeń związanych z wytwarzaniem coraz mniejszych tranzystorów. W efekcie producenci układów bazowali na innych metodach zw iększania szybkości, w tym na wielowątkowości współbieżnej, bardziej inteligentnym wykonywaniu nieuporządkowanym i ar chitekturach w ielordzeniowych. Wzrost szybkości zegara procesorów z upływem czasu
• •
a
.......................:.......... :.................................... / , , * •• W ..... In iZ 'v •«.** * - —•%. . [...........:........... i........... ;........... ;........... ;••••♦....... j|.S j 1 j -j* ........... i : • ;
-R
x
£n5j 3 IM
» 2 ^ 1A1
: : : :: : : : : : : : : : : : : : :::::::::::::::::::: • • ¿• r U * ;• • • • :• • : • • •: : : • • • ; .
*
:
• 1
^966
i
7
'■y'! Data wprowadzenia procesora na rynek
Rysunek 1.1. Zmiana szybkości zegara procesorów z upływem czasu (dane pochodzą z serwisu CPU DB (http://cpudb.stanford.edu/))
Podstawowy system komputerowy
|
17
Wielowątkowość współbieżna zapewnia systemowi operacyjnemu hosta drugi wirtualny proce sor CPU. Bardziej inteligentna logika sprzętowa próbuje przeplatać dwa wątki instrukcji w jed nostkach wykonawczych jednego procesora CPU. W przypadku powodzenia operacji możliwe jest osiągnięcie wzrostu w ydajności w porównaniu z pojedynczym w ątkiem. W zrost ten wy nosi naw et 30% . Zw ykle spraw dza się to, gdy jednostki robocze w obu w ątkach korzystają z różnych typów jednostki wykonawczej (na przykład jeden wątek wykonuje operacje zmien noprzecinkowe, a drugi wątek realizuje operacje całkowitoliczbowe). W ykonyw anie nieuporządkow ane um ożliw ia kom pilatorow i zidentyfikow anie w ybranych części liniowych sekwencji programu, które nie są zależne od wyniku poprzedniego zadania roboczego. Dzięki temu dwa zadania robocze mogą potencjalnie w ystąpić w dowolnej kolej ności lub jednocześnie. Dopóki w yniki sekwencyjne są prezentow ane w odpowiednim mo mencie, program poprawnie kontynuuje wykonywanie, nawet pomimo tego, że zadania robocze są przetw arzane bez zachowania kolejności określonej program owo. Um ożliwia to w ykona nie niektórych instrukcji, gdy inne mogą być blokowane (podczas oczekiwania na dostęp do pamięci). Dzięki temu możliwe jest lepsze ogólne wykorzystanie dostępnych zasobów. Z punktu w idzenia program isty tw orzącego kod na w yższym poziom ie najw ażniejsza jest wszechobecność architektur wielordzeniowych. Uwzględniają one wiele procesorów w tej sa m ej jednostce. Zw iększa to ogólne m ożliw ości bez zbliżania się do ograniczeń zw iązanych z przyspieszaniem każdej jednostki z osobna. Z tego właśnie powodu trudno obecnie znaleźć jakikolwiek komputer w yposażony w m niej niż dwa rdzenie (w tym przypadku komputer zawiera dwie fizyczne jednostki obliczeniowe, które są ze sobą połączone). Choć pow oduje to zwiększenie łącznej liczby operacji m ożliw ych do wykonania w ciągu sekundy, wprowadza trudności związane z pełnym jednoczesnym wykorzystaniem obu jednostek. Zw ykłe dodanie w iększej liczby rdzeni do procesora nie zaw sze pow oduje skrócenie czasu wykonania programu. W ynika to z czegoś, co określane jest mianem prawa Amdahla. Mówiąc wprost, prawo to głosi, że jeśli program zaprojektow any do działania w wielu rdzeniach za w iera procedury, które w ym agają uruchom ienia tylko w jednym rdzeniu, będzie to w ąskie gardło dla ostatecznego przyspieszenia możliwego do osiągnięcia przez przydzielenie większej liczby rdzeni. Jeśli na przykład m iałaby zostać przeprow adzona ankieta ze stoma osobami, której w ypeł nienie zajęłoby minutę, zadanie to mogłoby zostać ukończone w ciągu 100 minut, gdyby py tania zadawała jedna osoba (czyli osoba ta udałaby się do uczestnika nr 1, zadała mu pytania, poczekała na odpowiedzi, a następnie udała się do uczestnika nr 2). Analogią do takiego wa riantu przeprow adzania ankiety z jedną osobą zadającą pytania i czekającą na odpowiedzi jest proces szeregowy. W przypadku takich procesów operacje są realizowane po jednej naraz. Każda operacja czeka na zakończenie poprzedniej operacji. M ożliwe byłoby jednak przeprow adzenie ankiety w sposób równoległy, gdyby pytania były zadawane przez dwie osoby. Pozwoliłoby to zakończyć cały proces w zaledw ie 50 m inut. Jest to m ożliw e, poniew aż każda osoba zadająca pytania nie m usi niczego w ied zieć o drugiej osobie, która zadaje pytania. W rezultacie zadanie z łatw ością m oże zostać podzielone bez istnienia żadnej zależności między osobami zadającymi pytania. Dodanie większej liczby osób zadających pytania zapewni dodatkowe skrócenie czasu. Będzie tak do momentu zaangażowania stu osób zadających pytania. W tym momencie cały proces za jąłby minutę i byłby ograniczony jedynie przez czas, jaki zajmie uczestnikowi udzielenie odpo wiedzi. Dodanie większej liczby osób, które zadają pytania, w żadnym stopniu nie przyspieszy
18
|Rozdział 1. Wydajny kod Python
dodatkowo procesu, ponieważ nie będą one m iały żadnych zadań do wykonania — w szyst kim uczestnikom są już zadaw ane pytania! Na tym etapie jedyną m etodą skrócenia ogólnego czasu przeprowadzania ankiety jest zredukow anie czasu, jaki zajm uje w ypełnienie jednej an kiety (część problemu związana z procesem szeregowym). Podobnie jest w przypadku proce sorów. W razie potrzeby możliwe jest dodanie większej liczby rdzeni, które mogą zajm ować się różnym i zadaniam i obliczeniow ym i. M ożna to robić do m om entu, w którym pojaw i się w ąskie gardło w postaci konkretnego rdzenia kończącego swoje zadanie. Innym i słowy, wą skie gardło w dow olnym przetw arzaniu rów noległym zaw sze m a postać m niejszych zadań szeregowych, które są rozdzielane. Co w ięcej, pow ażną przeszkodą zw iązaną z wykorzystaniem wielu rdzeni w kodzie Python jest stosowanie przez język Python globalnej blokady interpretera GIL (Global Interpreter Lock). Blokada powoduje, że proces Python m oże naraz uruchom ić tylko jedną instrukcję, niezależ nie od liczby aktualnie używ anych rdzeni. Oznacza to, że naw et pomimo tego, że część kodu Python ma w tym samym czasie dostęp do wielu rdzeni, w danej chwili instrukcja kodu Py thon jest w ykonyw ana tylko przez jeden rdzeń. W przypadku zaprezentow anego w cześniej przykładu ankiety oznaczałoby to, że jeśli naw et zatrudniono by stu ankieterów , w danym m om encie tylko jeden z nich m ógłby zadać pytanie i w ysłuchać odpow iedzi. Pow oduje to utratę wszelkiego rodzaju korzyści w ynikających z zaangażowania wielu ankieterów ! Choć może to wyglądać na sporą przeszkodę, zwłaszcza biorąc pod uwagę to, że obecnym trendem w przetwarzaniu jest stosowanie wielu jednostek obliczeniowych, a nie szybszych jednostek, problemu tego można uniknąć dzięki wykorzystaniu innych standardowych narzędzi biblioteki (np. multiprocessing) lub technologii (np. numexpr, Cython albo rozproszone modele obliczeniowe).
Jednostki pamięci Jednostki pamięci w komputerach są używane do przechowywania bitów. M ogą to być bity reprezentujące zmienne w programie lub piksele obrazu. A zatem abstrakcja jednostki pamięci dotyczy rejestrów na płycie głównej, a także pam ięci RAM i dysku twardego. Podstawową różnicą między wszystkimi tego typu jednostkami pamięci jest szybkość, z jaką są odczytywane lub zapisywane dane. Aby wszystko jeszcze bardziej skomplikować, szybkość odczytu/zapisu w dużym stopniu zależy od sposobu odczytywania danych. Na przykład w iększość jednostek pam ięci działa znacznie lepiej, gdy w czytuje jedną dużą porcję danych zam iast wielu małych porcji (w odniesieniu do tego używane są pojęcia odczy tu sekwencyjnego i danych losowych). Jeśli dane w takich jednostkach pam ięci potraktuje się jak strony w pokaźnej książce, będzie to oznaczać, że większość jednostek pamięci oferuje większą szybkość odczytu/zapisu podczas przetwarzania książki strona po stronie niż w przypadku ciągłego przeskakiwania od jednej losowej strony do kolejnej. Chociaż generalnie dotyczy to w szystkich jednostek pam ięci, skala oddziaływ ania dla po szczególnych typów jednostek jest diam etralnie różna. Oprócz szybkości odczytu/zapisu jednostki pamięci cechują się też opóźnieniem, które można opisać jako czas, jakiego urządzenie potrzebuje na znalezienie używ anych danych. W przy padku obracającego się dysku twardego opóźnienie może być duże, ponieważ dysk fizycznie m usi osiągnąć zadaną prędkość obrotow ą, a głow ica odczytująca m usi przem ieścić się do właściwego położenia. Z kolei w przypadku pamięci RAM opóźnienie m oże być niewielkie, gdyż w całości jest ona urządzeniem typu SS (Solid State). Oto krótki opis różnych jednostek pamięci, które są pow szechnie spotykane w standardowej stacji roboczej (wymieniono w ko lejności rosnącej szybkości odczytu/zapisu):
Podstawowy system komputerowy
|
19
Obracający się dysk twardy Stosowany od dawna magazyn danych zachowywanych nawet po wyłączeniu komputera. Ogólnie rzecz biorąc, oferuje niew ielkie szybkości odczytu/zapisu, ponieważ wymaga fi zycznego uzyskania prędkości obrotowej i przemieszczenia głowicy. W przypadku wzor ców dostępu losow ego spada w ydajność dysków tw ardych, ale m ają one bardzo dużą pojem ność (liczoną w terabajtach). Dysk twardy SSD (Solid State Drive) Urządzenie podobne do obracającego się dysku twardego, które oferuje większe szybkości odczytu/zapisu, ale z mniejszą pojemnością (liczoną w gigabajtach). Pam ięć RAM Używana do przechowywania kodu i danych aplikacji (np. wszystkich wykorzystywanych zmiennych). Choć pam ięć RAM cechuje się krótkim czasem odczytu/zapisu, a ponadto sprawdza się dobrze w przypadku wzorców dostępu losowego, generalnie ma ograniczoną pojemność (liczoną w gigabajtach). Pam ięć podręczna L1/L2 Pamięć oferująca wyjątkowo duże szybkości odczytu/zapisu. Dane kierowane do proce sora muszą po drodze trafić do tej pam ięci. Pam ięć ta ma bardzo niew ielką pojem ność (wyrażaną w kilobajtach). Na rysunku 1.2 przedstawiono graficzną reprezentację różnic m iędzy tymi typami jednostek pamięci, analizując parametry dostępnego aktualnie sprzętu konsumenckiego.
Rysunek 1.2. Wartości parametrów dla różnych typów jednostek pamięci (dane pochodzą z lutego 2014 r.)
20
|
Rozdział 1. Wydajny kod Python
W idoczny w yraźnie trend pokazuje, że szybkości odczytu/zapisu oraz pojem ność są od wrotnie proporcjonalne. Przy zwiększaniu szybkości zmniejsza się pojemność. Z tego powodu w iele systemów stosuje w przypadku pamięci metodę warstwową: na początku dane w cało ści znajdują się na dysku twardym, ich część przenoszona jest do pam ięci RAM, a następnie znacznie mniejszy podzbiór danych trafia do pam ięci podręcznej L1/L2. M etoda warstwowa umożliwia programom utrzymywanie pamięci w różnych miejscach w zależności od wymagań dotyczących czasu dostępu. Przy podejmowaniu próby optymalizacji wzorców pamięci programu po prostu optymalizowane jest to, jakie dane są umieszczane w jakim miejscu, w jaki sposób są one rozmieszczane (w celu zw iększenia liczby odczytów sekwencyjnych), a także ile razy da ne są przem ieszczane między różnymi miejscami. Ponadto m etody takie jak asynchroniczne wejście-wyjście i buforowanie z wywłaszczeniem bez konieczności marnowania dodatkowego czasu procesora pozwalają zapewnić, że dane zawsze będą tam, gdzie są wymagane. Większość takich procesów może odbywać się niezależnie podczas wykonywania innych obliczeń!
Warstwy komunikacji Przyjrzyjmy się jeszcze temu, jak przedstaw ione wcześniej podstaw ow e bloki komunikują się ze sobą. W praw dzie istnieje wiele różnych trybów komunikacji, ale wszystkie są wariantam i tego, co jest określane mianem magistrali. Na przykład magistrala FSB (Frontside Bus) to połączenie m iędzy pam ięcią RAM i pam ięcią podręczną L1/L2. Służy ona do przemieszczania danych gotowych do przekształcenia przez procesor do postaci pozw alającej na rozpoczęcie obliczeń, a także do przesyłania wyników po ich zakończeniu. Istnieją również inne magistrale, takie jak magistrala zewnętrzna pełniąca rolę głównej trasy prowadzącej od urządzeń (np. dyski twarde i karty sieciowe) do procesora i pamięci systemowej. Taka magistrala jest zw ykle wolniejsza od magistrali FSB. Okazuje się, że wiele zalet pamięci podręcznej L1/L2 może być związanych z szybszą magi stralą. M ożliwość kolejkowania w wolnej magistrali (między pamięcią podręczną i procesorem) danych wymaganych do obliczeń w postaci dużych porcji, a następnie udostępnianie ich przy bardzo dużych szybkościach z poziomu magistrali BSB (Backside Bus), umożliwia procesorowi wykonanie większej liczby obliczeń bez czekania przez długi czas. W iele mankam entów zw iązanych z użyciem układu GPU wynika z korzystania z magistrali, z którą jest on połączony. Ponieważ GPU to zazwyczaj urządzenie peryferyjne, komunikuje się za pośrednictwem magistrali PCI, która jest znacznie wolniejsza niż magistrala FSB. W re zultacie pobieranie danych z układu GPU i wysyłanie ich do niego m oże być dość obciążającą operacją. Pojawienie się obliczeń heterogenicznych lub bloków obliczeniowych, które zawierają w magistrali FSB układy CPU i GPU, ma na celu zmniejszenie obciążenia związanego z transfe rem danych oraz zwiększenie możliwości stosowania do obliczeń układu GPU, naw et w sy tuacji, gdy konieczne jest przesłanie dużej ilości danych. Oprócz bloków komunikacji wewnątrz komputera rolę kolejnego takiego bloku m oże pełnić sieć. W porównaniu z wcześniej omówionymi blokami ten blok jest jednak znacznie bardziej elastyczny. Urządzenie sieciowe może zostać połączone z urządzeniem pam ięciowym, takim jak m agazyn danych N AS (N etwork A ttached Storage), lub z innym blokiem obliczeniow ym , tak jak w przypadku w ęzła obliczeniow ego w klastrze. K om unikacja sieciow a je st jednak zw ykle znacznie w olniejsza od innych, w cześniej opisanych typów kom unikacji. M agistrala FSB może przesyłać dziesiątki gigabitów w ciągu sekundy, sieć natom iast jest ograniczona do transferów rzędu kilkudziesięciu megabitów.
Podstawowy system komputerowy
|
21
Jasne jest zatem, że podstaw ow ą zaletą magistrali jest jej szybkość, która określa, ile danych może zostać przemieszczonych w danym czasie. Cecha ta stanowi połączenie dwóch w ielko ści: ilości danych przemieszczanych w ramach jednej operacji transferu (szerokość magistrali) i liczby transferów możliw ych w ciągu sekundy (częstotliwość magistrali). Godne uwagi jest to, że dane przesyłane w jednym transferze zaw sze są sekwencyjne. Porcja danych odczyty wana jest z pamięci i przemieszczana w inne miejsce. Oznacza to, że szybkość magistrali jest rozbijana na te dwie wielkości, ponieważ osobno mogą one m ieć w pływ na różne aspekty związane z obliczeniami. Duża szerokość magistrali może ułatw ić działanie kodu z wektoryzacją (lub dowolnego kodu, który dokonuje sekwencyjnego odczytu z pamięci), umożliwiając przesłanie w szystkich odpowiednich danych w ramach jednej operacji transferu. Z kolei m a gistrala o małej szerokości, lecz bardzo dużej częstotliwości transferów m oże ułatw ić wyko nywanie kodu, który m usi wykonyw ać wiele odczytów z losowych obszarów pam ięci. Inte resujące jest to, że jedna z metod modyfikowania tych cech przez projektantów komputerów polega na wprowadzaniu zmian w fizycznym układzie elementów płyty głównej. Gdy ukła dy zostaną umieszczone bliżej siebie, krótsze będą fizyczne połączenia między nimi. Pozwala to uzyskać większe szybkości transferów. Ponadto sama liczba połączeń określa szerokość ma gistrali (dzięki temu to pojęcie zyskuje realne znaczenie). Ponieważ interfejsy mogą być dostrajane w celu zaoferowania właściwej wydajności na potrzeby konkretnego zastosowania, nie jest zaskoczeniem, że istnieją setki różnych ich typów. Na rysun ku 1.3 (uzyskanym z serwisu Wikimedia Commons (http://commons.wikimedia.org/wiki/Main_Page)) pokazano szybkości transmisji danych dla próbkowania typowych interfejsów. Zauważ, że inform acje te w cale nie dotyczą opóźnienia połączeń, które określa, ile czasu zajmie utworze nie odpowiedzi dla żądania dotyczącego danych (choć opóźnienie w bardzo dużym stopniu zależy od kom putera, istnieją zasadnicze ograniczenia, które są pow iązane z używ anym i interfejsam i).
Łączenie ze sobą podstawowych elementów Zrozumienie, jakie są podstaw ow e komponenty komputerów, nie wystarczy do pełnego za znajomienia się z problemami związanym i z program owaniem pod kątem dużej wydajności. Wzajemna zależność wszystkich tych komponentów oraz sposób ich współpracy w celu roz wiązania problemu w prow adzają dodatkowe poziomy złożoności. W tym podrozdziale zo staną om ów ione uproszczone problem y, które ilustrują, jak działałyby idealne rozw iązania, a także w jaki sposób z problemami radzi sobie język Python. Ostrzeżenie: ten podrozdział może spraw iać wrażenie przygnębiającego — w iększość uwag wydaje się wskazywać, że język Python z założenia nie jest w stanie poradzić sobie z proble mami dotyczącymi wydajności. N ie jest to praw dą z dwóch powodów. Po pierwsze, w przy padku tych wszystkich „elementów wydajnego przetwarzania" nie doceniono bardzo istotnego „czynnika", a m ianow icie program isty. To, czego w kw estii w ydajności standardow o język Python może być pozbawiony, od razu nadrabia dzięki szybkości tworzenia kodu. Po drugie, w książce zostaną przedstaw ione m oduły i idee, które bez problem u m ogą ułatw ić ograni czenie skali wielu opisanych tutaj trudności. Po uwzględnieniu obu tych aspektów będziem y w stanie zachować możliwości szybkiego programowania w języku Python przy jednoczesnym usunięciu wielu ograniczeń dotyczących wydajności.
22
|
Rozdział 1. Wydajny kod Python
Rysunek 1.3. Szybkości połączeń różnych typowych interfejsów (obraz autorstwa Leadbuffalo (http://en.wikipedia.Org/wiki/File:Speeds_of_common_interfaces.svg) [CC BY-SA 3.0])
Porównanie wyidealizowanego przetwarzania z maszyną wirtualną języka Python Aby lepiej zrozumieć szczegóły programowania pod kątem dużej wydajności, przyjrzyjmy się przykładowi prostego kodu, który sprawdza, czy dana liczba to liczba pierwsza: import math def check_prime(number): sqrt_number = math.sqrt(number) number_float = float( num ber) f o r i in xran g e (2 , int(s q rt_n u m b er)+1): i f (number_float / i ) . i s _ i n t e g e r ( ) : re tu rn F a l s e re tu rn True p r i n t "check_prime(10000000) = " , check_prime(10000000) # F a lse p r i n t "check_prime(10000019) = " , check_prime(10000019) # True
Łączenie ze sobą podstawowych elementów
|
23
Przeanalizujm y ten kod za pomocą abstrakcyjnego modelu obliczeniowego, a następnie po rów najm y z tym, co ma m iejsce podczas w ykonyw ania tego kodu przez interpreter języka Python. Podobnie jak w przypadku dowolnej abstrakcji, pominiem y wiele subtelności doty czących zarówno wyidealizow anego komputera, jak i sposobu wykonywania kodu przez in terpreter języka Python. Ogólnie rzecz biorąc, jest to jednak odpowiednie ćwiczenie do wyko nania przed rozwiązaniem problemu: pomyśl o ogólnych elementach algorytmu i zastanów się, jaki będzie najlepszy sposób połączenia ze sobą elementów obliczeniowych w celu znale zienia rozwiązania. Zrozum ienie takiej idealnej sytuacji i uzyskanie w iedzy o tym, co w rze czyw istości ma m iejsce w ew nątrz interpretera języka Python, pozw oli w iteracyjny sposób zbliżyć tworzony kod Python do optymalnego kodu.
W yidealizow ane przetwarzanie Po rozpoczęciu wykonyw ania kodu w pamięci RAM przechowywana jest w artość zmiennej number. W celu obliczenia wartości zmiennych sqrt_number i number_float konieczne jest wysłanie tej wartości do procesora. W idealnej sytuacji możliwe by było jednokrotne wysłanie wartości, które zostałyby zapisane w pamięci podręcznej L1/L2 procesora. Procesor przeprowadziłby obliczenia, a następnie w ysłał w artości z pow rotem do pam ięci RAM w celu ich zapisania. Taki scenariusz jest idealny, ponieważ zminimalizowana zostałaby liczba odczytów wartości zm iennej number z pam ięci RAM, a zam iast tego zdecydow alibyśm y się na opcję odczytów z pamięci podręcznej L1/L2, co jest znacznie szybsze. Co więcej, zminimalizowalibyśmy liczbę transferów danych za pośrednictwem magistrali FSB, wybierając opcję komunikacji przy użyciu szybszej magistrali BSB (łączy różne pamięci podręczne z procesorem ). Taka metoda utrzy mywania danych tam, gdzie są potrzebne, oraz ograniczenie ich przesyłania, odgrywa bardzo ważną rolę w przypadku optymalizacji. Pojęcie „ciężkich danych" (ang. heavy data) odnosi się do tego, że przemieszczanie danych zajm uje czas i zasoby, a tego chcielibyśmy uniknąć. Zamiast w przypadku pętli zawartej w kodzie wysyłać do procesora w danej chwili jedną war tość zm iennej i, bardziej pożądane będzie jednoczesne w ysłanie zm iennej number_float oraz kilku wartości zmiennej i w celu ich sprawdzenia. Jest to możliwe, ponieważ procesor wektoryzuje operacje bez ponoszenia dodatkowego kosztu w postaci czasu. Oznacza to, że w tym samym czasie procesor może wykonywać wiele niezależnych obliczeń. A zatem chcemy wysłać do pamięci podręcznej procesora zmienną number_float, a także taką liczbę wartości zmiennej i, jaką może pomieścić ta pamięć. Dla każdej pary zmiennych number_float i i zostanie wykonane dzielenie i sprawdzenie, czy w ynik jest liczbą całkowitą. Następnie zostanie wysłany z po wrotem sygnał wskazujący, czy dowolna z wartości rzeczywiście okazała się liczbą całkowitą. Jeśli tak, funkcja zostanie zakończona. W przeciw nym razie operacja zostanie pow tórzona. Dzięki temu dla w ielu w artości zm iennej i konieczne jest zw rócenie tylko jednego wyniku, co eliminuje uzależnienie od wolnej magistrali dla każdej w artości. W tym przypadku w yko rzystyw ana jest m ożliw ość w ektoryzacji obliczenia przez procesor lub uruchom ienia jednej instrukcji dla wielu danych w jednym cyklu zegarow ym . Pojęcie wektoryzacji zostało zilustrowane w następującym kodzie: import math def check_prime(number): sqrt_number = math.sqrt(number) number_float = float(num ber) numbers = ran ge (2 , int (sqrt_number)+1) f o r i in x ra n g e (0 , len(numbers ), 5 ) :
24
|
Rozdział 1. Wydajny kod Python
# poniższy w iersz nie zaw iera p op raw n eg o kodu Python r e s u l t = (number_float / n u m b e r s [ i : ( i + 5 ) ] ) . i s _ i n t e g e r ( ) i f any(result): re tu rn Fa ls e re tu rn True
W kodzie określono obliczenia, w przypadku których dzielenie i sprawdzanie liczb całkowi tych realizow ane jest jednocześnie dla zestaw u pięciu w artości zm iennej i . Jeśli dokonano poprawnej wektoryzacji, procesor może wykonać te operacje w jednym kroku, zam iast prze p row adzać osobne obliczenie dla każdej w artości zm iennej i . W idealnej sytuacji operacja any(result) wystąpiłaby w procesorze bez konieczności przesyłania wyników z powrotem do pam ięci RAM. W rozdziale 6. w szerszym zakresie zostanie omówiona w ektoryzacja, sposób jej działania, a także to, kiedy przynosi korzyści w kodzie.
M aszyna w irtualna języka Python Interpreter języka Python wykonuje wiele działań, aby podjąć próbę utworzenia abstrakcji uży wanych bazowych elementów obliczeniowych. Programista wcale nie musi przejmować się przy dzielaniem pamięci tablicom, sposobem zorganizowania tej pamięci lub tym, w jakiej kolejności dane są wysyłane do procesora. Jest to zaleta języka Python, gdyż pozwala skoncentrować się na implementowanych algorytmach. Kosztem tego jest jednak spory spadek wydajności. W ażne jest zdanie sobie sprawy z tego, że w zasadzie interpreter języka Python to tak naprawdę działający zestaw bardzo dobrze zoptym alizow anych instrukcji. Zastosowany zabieg polega jednak na tym, że w tym języku instrukcje te są wykonywane w odpowiedniej kolejności w celu osiągnięcia lepszej wydajności. D obrze w idać to w poniższym przykładzie, w którym funkcja search_fast zadziała szybciej niż funkcja search_slow, nawet pomimo tego, że podczas działania obie mają złożoność obliczeniową O(n). Wynika to po prostu stąd, że pierwsza z funkcji pomija zbędne obliczenia, które wiążą się z tym, że wykonywanie pętli nie zostało wcześniej zakończone. def s e a r c h _ f a s t ( h a y s t a c k , n e ed le ): f o r item in haystack: i f item == need le: re turn True re turn Fa ls e def sea rch _sl ow (hay st ack, n e ed le ): re tu rn_ val ue = Fa ls e f o r item in haystack: i f item == need le: re tu rn_ val ue = True re tu rn re turn_value
Identyfikowanie obszarów kodu o małej wydajności za pomocą profilow ania i znajdowania bardziej efektywnych metod przeprowadzania tych samych obliczeń przypom ina w yszuki w anie tych bezw artościow ych operacji i usuwanie ich. Końcow y rezultat jest identyczny, ale znacznie zmniejszona zostaje liczba obliczeń i transferów danych. Jednym z efektów użycia takiej w arstwy abstrakcji jest to, że w ektoryzacja nie jest od razu uzyskiwana. Zamiast połączenia kilku iteracji w przykładzie kodu sprawdzającego początkową liczbę pierw szą dla w artości zm iennej i zostanie w ykonana jedna iteracja pętli. Gdy jednak przyjrzysz się przykładowi abstrakcji wektoryzacji, stwierdzisz, że nie jest to poprawny kod Python, ponieważ nie jest możliwe dzielenie liczby zmiennoprzecinkowej przy użyciu listy. Zew nętrzne biblioteki, takie jak numpy, okażą się pom ocne w takiej sytuacji, zapewniając moż liwość wykonywania wektoryzowanych operacji matematycznych.
Łączenie ze sobą podstawowych elementów
|
25
Co w ięcej, abstrakcja w języku Python m a negatyw ny w pływ na w szelkie optym alizacje, które bazują na wypełnianiu pamięci podręcznej L1/L2 odpowiednimi danymi na potrzeby następnego obliczenia. W ynika to z wielu czynników , z których najw ażniejszym jest to, że obiekty Python nie są rozmieszczone w pamięci w najbardziej optymalny sposób. Jest to kon sekwencją tego, że język Python to język dokonujący czyszczenia pamięci (jest ona automa tycznie przydzielana i w razie potrzeby uwalniana). Powoduje to fragm entację pam ięci, która może niekorzystnie wpłynąć na transfery do pamięci podręcznych procesora. Ponadto w żad nym momencie nie ma możliwości zmiany układu struktury danych bezpośrednio w pamięci. Oznacza to, że jedna operacja transferu w magistrali może nie zawierać wszystkich odpowied nich informacji na potrzeby obliczeń, naw et pomimo tego, że szerokość magistrali może być wystarczająca dla całych danych. Poza tym fundamentalny problem wynika z typów dynamicznych języka Python, którego kod nie jest kompilowany. Jak wielu programistów używających języka C nauczyło się w ciągu wielu lat, kom pilator jest czasam i sprytniejszy od nas. Podczas kom pilow ania statycznego kodu kompilator może stosować liczne zabiegi w celu zm iany sposobu rozmieszczania elementów, a także sposobu, w jaki procesor będzie uruchamiać określone instrukcje pod kątem zopty m alizowania ich. Kod Python nie jest jednak kompilowany. Co gorsza, zawiera on typy dy namiczne. Oznacza to, że określanie jakichkolwiek ewentualnych możliwości algorytmicznych optymalizacji jest znacząco trudniejsze, ponieważ funkcjonalność kodu m oże być m odyfiko wana podczas jego wykonywania. Istnieje wiele metod zmniejszania skali tego problemu. Podstaw ow ą jest użycie narzędzia C ython, które um ożliw ia kom pilow anie kodu Python, a ponadto pozw ala użytkow nikow i tw orzyć „w skazów ki" dla kom pilatora określające rze czywistą dynamiczność kodu. Ponadto przy próbie równoległego w ykonyw ania kodu w spomniana wcześniej globalna blo kada interpretera GIL m oże spow odow ać spadek w ydajności. Dla przykładu załóżm y, że kod jest modyfikowany w celu użycia wielu rdzeni procesora tak, aby każdy z nich otrzymał porcję liczb z zakresu od 2 do sqrtN. Każdy rdzeń może przeprow adzić obliczenia dla swojej porcji liczb, a następnie po ich zakończeniu rdzenie mogą porównać własne obliczenia. Wygląda to na dobre rozwiązanie, ponieważ choć tracimy możliw ość w czesnego zakończenia w yko nywania pętli, m ożemy zm niejszyć liczbę sprawdzeń, jaką każdy rdzeń m usi w ykonać, po dzieloną przez liczbę używanych rdzeni (oznacza to, że w przypadku Mrdzeni każdy z nich musiałby przeprowadzić sqrtN/M sprawdzeń). Jednak z powodu globalnej blokady interpretera GIL w danej chwili może być używany tylko jeden rdzeń. Oznacza to, że w efekcie zostałby wykonany taki sam kod jak w przypadku wersji kodu z równoległym wykonywaniem, ale bez możliwości wczesnego zakończenia. Problemu tego można uniknąć, zastępując wiele wątków wieloma procesami (z wykorzystaniem modułu multiprocessing) bądź za pom ocą narzędzia Cython lub funkcji zewnętrznych.
Dlaczego warto używać języka Python? Język Python cechuje się w ysokim stopniem ekspresyw ności i jest łatw y do opanow ania. Program iści, którzy zaczynają go używ ać, szybko stw ierdzają, że w krótkim czasie m ogą dzięki niemu osiągnąć całkiem sporo. Za pomocą innych języków zostało napisanych wiele na rzędzi opakowujących biblioteki języka Python, które ułatwiają wywoływanie innych systemów. Na przykład system uczenia maszynowego scikitlearn opakowuje biblioteki LIBLINEAR i LIBSVM
26
|
Rozdział 1. Wydajny kod Python
(obie napisano w języku C), a biblioteka numpy zawiera bibliotekę BLAS oraz inne biblioteki języków C i Fortran. W rezultacie kod Python, który poprawnie wykorzystuje te moduły, może być naprawdę równie szybki jak porównywalny kod C. Język Python jest określany mianem „uwzględniającego baterie", gdyż wbudow ano w niego wiele ważnych i stabilnych bibliotek. To następujące biblioteki: Unicode i bytes Scalone z rdzeniem języka. array W ydajne pod względem w ykorzystywanej pam ięci tablice przeznaczone dla typów pod stawowych. math Podstawowe operacje m atematyczne, w tym kilka prostych funkcji statystycznych. sqlite3 Biblioteka opakowująca powszechnie używany mechanizm magazynowania danych oparty na plikach SQLite3. collections Przeróżne obiekty, w tym obiekt deque, licznik i warianty słownika. Poza obrębem rdzenia języka dostępna jest ogrom na liczba różnych bibliotek. Oto niektóre z nich: numpy Numeryczna biblioteka języka Python (podstawowa biblioteka w przypadku wszystkiego, co ma zw iązek z macierzami). scipy Bardzo duża kolekcja zaufanych bibliotek naukowych, które często opakowują cieszące się dużym uznaniem biblioteki języków C i Fortran. pandas Biblioteka służąca do analizy danych, która przypomina ramki danych języka R lub arkusz kalkulacyjny programu Excel. Biblioteka bazuje na bibliotekach scipy i numpy. scik it-learn Bazująca na bibliotece scipy biblioteka szybko przyjmująca postać domyślnej biblioteki uczenia m aszynowego. biopython Stosowana w bioinform atyce biblioteka, która przypom ina bibliotekę bioperl. tornado Biblioteka, która zapewnia proste powiązania na potrzeby w spółbieżności. Powiązania bazy danych Służą do kom unikacji z niem al w szystkim i bazam i danych, w tym R edis, M ongoD B, HDF5 i SQL. Środowiska do projektowania aplikacji internetowych W ydajne system y służące do tw orzenia w itryn internetow ych, takie jak django, pyramid, fla sk i tornado.
Dlaczego warto używać języka Python?
|
27
OpenCV Powiązania na potrzeby rozpoznawania obrazów. Powiązania interfejsów A PI U łatw iają dostęp do popularnych interfejsów API serw isów internetow ych, takich jak G oogle, Tw itter i Linkedln. Dostosowanie do różnych scenariuszy wdrażania jest m ożliwe dzięki dużej liczbie dostępnych środowisk zarządzanych i powłok. Oto one: • Standardowa dystrybucja dostępna pod adresem http://python.org/. • Bardzo dojrzałe środowiska o dużych m ożliw ościach EPD i Canopy firm y Enthought. • Przeznaczone dla naukowców środowisko Anaconda firmy Continuum. • Przypominające oprogramowanie M atlab środowisko Sage, które zawiera zintegrowane środowisko program istyczne IDE (Integrated Development Environment). • Python(x,y). • IPython, czyli interaktyw na pow łoka języka Python używ ana przez naukow ców i pro jektantów . • Oparty na przeglądarce interfejs pow łoki IPython o nazw ie IPython Notebook, który jest intensywnie wykorzystywany do celów edukacyjnych i pokazowych. • Interaktywna powłoka języka Python BPython. Jedną z głównych zalet języka Python jest to, że pozwala na szybkie prototypowanie koncepcji. Ze względu na bogactwo bibliotek pom ocniczych proste jest sprawdzenie, czy koncepcja jest możliwa do zrealizow ania (nawet jeśli pierwsza im plementacja m oże okazać się dziwna). Aby przyspieszyć procedury m atematyczne, sprawdź bibliotekę numpy. Jeżeli chcesz poeksperym entow ać z uczeniem m aszynow ym , w ypróbuj bibliotekę scik it-lea rn . Jeśli porządkujesz i modyfikujesz dane, dobrą propozycją będzie biblioteka pandas. Ogólnie rzecz biorąc, sensowne jest zadanie sobie następującego pytania: „Jeśli system działa szybciej, czy jako zespół długoterminowo będziem y pracować w olniej?". Choć zaw sze m oż liw e jest uzyskanie w zrostu w ydajności system u, jeśli pośw ięci się na to w ystarczającą ilość czasu przy udziale odpowiedniej liczby osób, może to doprowadzić do uzyskania nieznacz nych i źle zrozum ianych optym alizacji, które ostatecznie spow odują utrudnienia w pracy zespołu. Przykładem może być wprow adzenie narzędzia Cython (omówiono je w rozdziale 7., w pod rozdziale „Cython"). Jest to oparte na kompilatorze rozwiązanie służące do tworzenia adno tacji kodu Python za pomocą typów podobnych do tych wykorzystywanych w języku C. Dzięki temu przekształcony kod może być kompilowany przy użyciu kompilatora języka C. W praw dzie przyrost szybkości może robić w rażenie (często przy stosunkowo niewielkim nakładzie pracy osiągane są szybkości porów nyw alne z szybkością kodu C), ale zwiększy się koszt ob sługi takiego kodu. W szczególności trudniejsza może być obsługa nowego modułu, ponieważ od członków zespołu wymagane będzie określone doświadczenie programistyczne, pozwa lające zrozumieć niektóre zależności, które wystąpiły po zrezygnowaniu z maszyny wirtualnej języka Python i uzyskaniu wzrostu wydajności.
28
|
Rozdział 1. Wydajny kod Python
________________________________ ROZDZIAŁ 2.
Użycie profilowania do znajdowania wąskich gardeł
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Jak można zidentyfikować w kodzie wąskie gardła związane z szybkością i pamięcią RAM? • Jak profilow ane jest wykorzystanie pamięci i procesora? • Jaka głębokość profilowania powinna zostać użyta? • Jak można profilow ać aplikację działającą długoterminowo? • Co się dzieje pod podszew ką w przypadku użycia narzędzia CPython? • Jak zapewnić popraw ność kodu podczas dostrajania wydajności?
Profilowanie umożliwia znalezienie wąskich gardeł. Dzięki temu przy minimalnym nakładzie pracy m ożliw e jest uzyskanie największego praktycznego wzrostu wydajności. Choć można oczekiw ać ogrom nego w zrostu szybkości i zm niejszenia w ykorzystania zasobów przy nie wielkim nakładzie pracy, w praktyce celem jest uzyskanie kodu, który działa „wystarczająco szybko i niezaw odnie", aby spełnić nasze wymagania. Profilow anie pozw oli podjąć najbar dziej pragm atyczne decyzje przy minimalnym wysiłku. Profilowany może być dowolny zasób (nie tylko procesor!), dla którego można dokonać po miaru. W tym rozdziale przyjrzymy się zarówno wykorzystaniu pamięci, jak i czasu procesora. Podobne techniki m ożesz też zastosow ać do pomiaru przepustowości sieci i dyskowych ope racji w ejścia-wyjścia. Jeśli program działa zbyt w olno lub używ ane jest za dużo pam ięci RAM , w skazane będzie popraw ienie w szystkich odpowiedzialnych za to części kodu. Oczywiście m ożesz pom inąć profilow anie i popraw ić to, co, jak wierzysz, m oże stanow ić problem. Trzeba jednak uważać, poniew aż często kończy się to „popraw ieniem " niew łaściw ej rzeczy. Zam iast kierow ać się intuicją, przed dokonaniem zm ian w strukturze kodu lepiej przeprow adź profilow anie ze zdefiniowaną hipotezą.
29
Czasam i pośpiech nie jest w skazany. Profilow anie przed dokonyw aniem zm ian pozw ala szybko zidentyfikow ać wąskie gardła, które trzeba wyelim inować. Później m ożesz usunąć po prostu taką ich liczbę, jaka będzie w ymagana, by osiągnąć żądaną w ydajność. Jeśli pominiesz profilow anie i przejdziesz do optymalizowania, całkiem praw dopodobne jest to, że w dłuż szej perspektyw ie będziesz m usiał w łożyć w to w ięcej pracy. Zaw sze kieruj się w ynikam i profilowania.
Efektywne profilowanie Pierw szym zadaniem w procesie profilow ania jest testow anie reprezentacyjnego systemu w celu określenia, które elementy pracują powoli (lub zużywają zbyt wiele pamięci RAM albo wymuszają za dużo operacji wejścia-wyjścia dysku lub sieci). Profilow anie zw ykle powoduje obciążenie (zazwyczaj ma miejsce spow olnienie od dziesięciu do stu razy). Kod ma nadal być używ any w sposób jak najbardziej zbliżony do tego, jaki w ykorzystuje się w rzeczyw istych warunkach. W yodrębnij przypadek testowy i wyizoluj część systemu, która wymaga spraw dzenia. N ajlepiej byłoby, gdyby ta część została już tak utw orzona, aby znajdow ała się w e w łasnym zestaw ie modułów. Podstawowe techniki przedstaw ione w tym rozdziale jako pierwsze obejmują funkcję „ma giczną" %timeit powłoki IPython, funkcję time.time() i dekorator czasu. Dzięki poznaniu tych metod zrozum iesz działanie instrukcji i funkcji. W dalszej części rozdziału zostanie omówiony m oduł cProfile (podrozdział „Użycie modułu cProfile"). Dowiesz się, w jaki sposób za pom ocą tego wbudowanego narzędzia zidentyfiko w ać w kodzie funkcje, których wykonanie zajm uje najwięcej czasu. Umożliwi Ci to zaznajo mienie się z ogólną prezentacją problemu, dzięki czemu będziesz mógł skupić swoją uwagę na krytycznych funkcjach. Dalej przyjrzym y się narzędziu lin e_p rofiler (podrozdział „Użycie narzędzia line_profiler do pom iarów dotyczących kolejnych wierszy kodu"), które przeprowadzi profilow anie wybra nych funkcji dla kolejnych wierszy kodu. W ynik będzie obejm ować liczbę wyw ołań każdego wiersza i w artość procentow ą czasu poświęconego na każdy w iersz. Dokładnie te informacje są niezbędne do zidentyfikow ania tego, co działa w olno i z jakiego powodu. Gdy będziesz dysponował wynikami działania narzędzia line_p rofiler, uzyskasz informacje wymagane do zastosow ania kompilatora omówionego w rozdziale 7. W rozdziale 6. (przykład 6.8) dowiesz się, jak użyć polecenia perf sta t do przeanalizowania liczby instrukcji, które są ostatecznie wykonywane w procesorze, a także jak efektywnie wyko rzystywać pam ięci podręczne procesora. Pozwala to na dostrajanie operacji macierzowych na zaawansowanym poziomie. Po przeczytaniu tego rozdziału warto przyjrzeć się przykładowi 6.8. Po narzędziu line_p rofiler zostanie zaprezentow ane narzędzie heapy (podrozdział „Inspekcja obiektów w stercie za pomocą narzędzia heapy"), które umożliwia śledzenie wszystkich obiek tów w obrębie pamięci kodu Python. Jest to bardzo przydatne w przypadku identyfikowania dziwnych „przecieków " pamięci. Jeśli pracujesz z długo działającymi systemami, godne za interesowania będzie narzędzie dowser (podrozdział „Użycie narzędzia dowser do generowania aktywnego wykresu dla zmiennych z utw orzonym i instancjam i"). Umożliwia ono analizo wanie aktywnych obiektów w długoterminowym procesie za pośrednictwem interfejsu prze glądarki internetowej.
30
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
A by zilustrować, dlaczego wykorzystanie pamięci RAM jest duże, zostanie omówione narzę dzie memory_profiler (podrozdział „Użycie narzędzia memory_profiler do diagnozowania wy korzystania pam ięci"). Jest ono szczególnie przydatne podczas śledzenia na wykresie z etykie tami wykorzystania pamięci RAM w czasie. Dzięki temu możesz wyjaśnić współpracownikom, dlaczego określone funkcje zużywają więcej pam ięci RAM, niż oczekiwano.
^
N iez a leż n ie od w y b ran ej m e to d y p ro filo w an ia k o d u trzeba p a m ię ta ć o za p ew n ie n iu w n im o d p o w ie d n ie g o z a k r e s u te stó w je d n o s tk o w y c h . T e s ty je d n o s tk o w e u ła tw ia ją p op e łn ian ie p ro s ty ch p o m y łe k , a p o n a d to są p o m o c n e w z a p e w n ie n iu m o ż liw o ści o d tw o r z e n ia w y n ik ó w . R e z y g n u j z n ic h ty lko n a w ła s n e ry zyko . Z a w sz e p ro filu j k o d p rz e d k o m p ilo w a n ie m lu b m o d y f ik o w a n ie m a lg o ry tm ó w . D o o k r e ś le n ia n a jb a rd z ie j e f e k t y w n y c h m e to d p rz y s p ie s z a n ia d z ia ła n ia k o d u n ie z b ę d n y je st d o w ó d .
W dalszej części rozdziału zamieszczono też wprowadzenie do kodu bajtowego Python w ob rębie narzędzia CPython (podrozdział „Użycie modułu dis do sprawdzania kodu bajtowego narzędzia CPython"). Dzięki temu możesz zrozumieć, co się dzieje pod podszewką. W szcze gólności zaznajom ienie się ze sposobem działania maszyny wirtualnej opartej na stosie kodu Python ułatwi Ci zrozum ienie, dlaczego określone style kodowania cechują się m niejszą wy dajnością od innych. Na końcu rozdziału przyjrzym y się sposobom integrowania testów jednostkowych podczas profilowania (podrozdział „Testowanie jednostkowe podczas optymalizacji w celu zachow a nia popraw ności"), aby zachow ać popraw ność kodu podczas dokonywania zmian mających na celu zwiększenie jego wydajności. Rozdział zostanie zakończony om ów ieniem strategii profilow ania (podrozdział „Strategie udanego profilow ania k odu "). D zięki temu m ożliw e będzie niezaw odne profilow anie kodu i gromadzenie właściwych danych na potrzeby testowania określonych hipotez. Dowiesz się, jak skalowanie częstotliwości procesora oraz funkcje takie jak TurboBoost m ogą zafałszować wyniki profilowania, a także jak można je wyłączyć. A by w ykonać wszystkie te kroki, niezbędna jest prosta do analizowania funkcja. W następ nym podrozdziale zostanie przedstaw iony zbiór Julii. Jest to pow iązana z procesorem funk cja, która w trochę większym stopniu korzysta z pamięci RAM. Ponadto przejawia działanie nieliniow e (z tego pow odu nie można z łatwością przew idzieć w yników). Oznacza to, że za miast analizowania w trybie offline, funkcja ta wymaga profilowania podczas działania.
Wprowadzenie do zbioru Julii Zbiór Julii (http://pl.w ikipedia.org/w iki/Zbi% C3% B3r_Julii) to interesujący problem pow iązany z procesorem , od którego w arto zacząć. Z biór ten jest sekw encją fraktalną, która generuje złożony obraz wyjściowy. Nazwa zbioru w ywodzi się od matematyka Gastona Julii. Zamieszczony dalej kod jest trochę dłuższy od wersji, którą m ożesz sam utworzyć. Zawiera on komponent powiązany z procesorem oraz wyjątkowo jawny zbiór wejść. Konfiguracja taka umożliwia profilow anie zarówno wykorzystania procesora, jak i pamięci RAM. Dzięki temu można zrozumieć, jakie części kodu zużywają dwa spośród skromnych zasobów obliczenio wych. Ponieważ taka implementacja jest umyślnie niezoptymalizowana, możemy zidentyfikować
W prowadzenie do zbioru Julii
|
31
operacje wykorzystujące pamięć i wolne instrukcje. W dalszej części rozdziału zostanie popra wiona wolna instrukcja logiczna oraz instrukcja, która w ykorzystuje dużo pam ięci. W roz dziale 7. znacząco skrócony zostanie ogólny czas wykonywania tej funkcji. Przeanalizujem y blok kodu, który w punkcie zespolonym c=-0.62772-0.42193j generuje za rów no w ykres fałszywej skali szarości (rysunek 2.1), jak i w ariant zbioru Julii z czystą skalą szarości (rysunek 2.3). Z biór Ju lii je st tw orzony przez obliczenie każdego piksela obrazu w wyizolowaniu. Jest to „kłopotliwy problem z rów noległością", gdyż między punktami nie są współużytkowane dane.
Rysunek 2.1. Wykres zbioru Julii z fałszywą skalą szarości podkreślającą szczegóły Jeśli zostanie określony inny punkt c, zostanie uzyskany inny obraz. W ybrane położenie za wiera obszary szybkie do obliczenia oraz takie, których obliczenie wymaga więcej czasu. Jest to korzystne pod kątem przykładowej analizy.
32
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Problem jest interesujący, ponieważ każdy piksel jest obliczany przez zastosowanie pętli, która może być wykonywana nieokreśloną liczbę razy. W każdej iteracji sprawdzane jest, czy wartość danej współrzędnej zmierza do nieskończoności, czy wydaje się wstrzymywana przez atraktor. W spółrzędne pow odujące niewiele iteracji m ają ciemny kolor (rysunek 2.1). Z kolei w spół rzędne, które powodują dużą liczbę iteracji, m ają biały kolor. Białe obszary są trudniejsze do obliczenia i w ygenerowanie ich zajm uje więcej czasu. Definiujemy zbiór współrzędnych z, które będą testowane. Obliczana funkcja określa pier w iastek kwadratowy liczby zespolonej z i dodaje punkt c: f ( z ) = z2 + c
Funkcja jest iterowana podczas testowania mającego na celu sprawdzenie, czy warunek zmie rzania wstrzymuje użycie funkcji abs. Jeśli funkcja zmierzania ma wartość False, następuje wyj ście z pętli i zarejestrow anie liczby iteracji w ykonanych dla danej współrzędnej. Jeśli funkcja zm ierzania nigdy nie m a w artości False, pętla je st kończona po liczbie iteracji określonej przez w artość maxiter. W ynik obliczenia liczby zespolonej z zostanie później przekształcony w kolorowy piksel, który reprezentuje położenie tej liczby zespolonej. W pseudokodzie może to w yglądać następująco: f o r z in c o o rd in a te s: f o r i t e r a t i o n in ra n g e (m a x ite r ): # ograniczona liczba iteracji d la punktu i f a b s(z ) < 2 . 0 : # czy w arunek zm ierzania został naruszony? z = z*z + c el se : break # p rzech ow an ie liczby iteracji dla k ażd ej liczby zesp olon ej z i wygenerow anie późn iej wykresu
W celu objaśnienia tej funkcji spróbujmy użyć dwóch w spółrzędnych. N ajpierw użyjem y w spółrzędnej, która zostanie um ieszczona w lewym górnym narożniku wykresu w punkcie -1 .8 -1 .8 j. Zanim będzie możliwe wypróbow anie reguły aktualizacji, ko nieczne jest sprawdzenie warunku abs(z) < 2: z = -1.8-1.8j p r i n t ab s(z) 2.54558441227
Jak w idać, w przypadku lewej górnej w spółrzędnej test funkcji abs(z) da w artość False dla zerowej iteracji. Oznacza to, że nie jest stosowana reguła aktualizacji. W artość zmiennej output dla tej współrzędnej wynosi 0. Przesuńmy się do środka wykresu w punkcie z = 0 + 0j i sprawdźmy kilka iteracji: c = -0.62772-0.42193j z = 0+0j f o r n in r a n g e (9 ): z = z*z + c print " { } : z = {:3 3 }, a b s ( z ) = { : 0 . 2 f } , 0: z= (-0 .6 2 7 7 2 -0 .4 2 1 9 3 j), 1: z= (- 0 .4 1 1 7 1 2 5 2 6 5 + 0 .1 0 7 7 7 7 7 9 9 2 j ), 2: z = ( - 0 . 4 6 9 8 2 8 8 4 9 5 2 3 - 0 . 5 1 0 6 7 6 9 4 0 0 1 8 j) , 3: z= (- 0 .6 67 7 7 1 7 8 9 2 2 2 + 0 .0 5 7 9 3 1 5 1 8 4 1 4 j) , 4: z = ( - 0 . 1 8 5 1 5 6 8 9 8 3 4 5 - 0 . 4 9 9 3 0 0 0 6 7 4 0 7 j) , 5: z = ( - 0 . 8 4 2 7 3 7 4 8 0 3 0 8 - 0 . 2 3 7 0 3 2 2 9 6 3 5 1 j) , 6: z = ( 0 . 0 2 6 3 0 2 1 5 1 2 0 3 -0 .0 2 2 4 1 7 9 9 9 6 4 2 8 j) , 7: z= (- 0 .6 2 7 5 3 0 7 6 3 5 5 - 0 . 4 2 3 1 0 9 2 8 3 2 3 3 j) , 8: z = (- 0.4 12 9 4 6 6 0 6 3 5 6 + 0 .1 0 9 0 9 8 1 8 3 1 4 4 j) ,
c = { } " . f o r m a t ( n , z , a b s ( z ) , c) abs(z)=0.76, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.43, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.69, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.67, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.53, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.88, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.03, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.76, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j) abs(z)=0.43, c=(-0 .6 2 7 7 2 -0 .4 2 1 9 3 j)
W prowadzenie do zbioru Julii
|
33
Jak w idać, każda aktualizacja w spółrzędnej z dla tych kilku pierw szych iteracji pow oduje uzyskanie w artości, dla której w arunek abs(z) < 2 m a w artość True. Po w ykonaniu dla tej w spółrzędnej 300 iteracji w dalszym ciągu test da w artość True. N ie jest m ożliw e określenie liczby iteracji niezbędnych do uzyskania dla warunku w artości False. M oże się to zakończyć nieskończoną sekwencją. Klauzula zatrzymania z maksymalną liczbą iteracji (wartość maxiter) spowoduje zakończenie potencjalnie nieskończonej iteracji. Na rysunku 2.2 pokazano 50 pierw szych iteracji powyższej sekwencji. Dla punktu 0+0j (linia ciągła ze znacznikami w postaci okręgów) sekwencja w ydaje się pow tarzać co ósmą iterację. Każda sekwencja siedmiu obliczeń ma niewielkie odchylenie względem poprzedniej sekwencji. Nie jest możliwe stwierdzenie, czy ten punkt będzie bez końca iterow any w obrębie warunku granicznego przez długi czas, czy być może zaledw ie przez kilka kolejnych iteracji. Linia kre skowana odcięcie prezentuje granicę w punkcie +2. Dwa przykłady rozwijania funkcji abs(z) przy użyciu punktu c = -0,62772-0,42193j 2,0
.......................................................... Odcięcie
j 1,5
z = Oj 1,0
>
» ■« z = (-0,82+0jł
0,5
0,0
0
10
20
30
40
50
Liczba iteracji
Rysunek 2.2. Dwa przykłady współrzędnych rozwijane dla zbioru Julii W przypadku punktu -0.82+0j (linia kreskowana ze znacznikami w postaci rombów) widać, że po dziewiątej aktualizacji w ynik bezw zględny przekroczył linię odcięcia +2, dlatego nastą piło zatrzymanie aktualizowania tej wartości.
Obliczanie pełnego zbioru Julii W tym podrozdziale zostanie omówiony kod, który generuje zbiór Julii. Kod będzie analizowany w rozdziale na różne sposoby. Jak widać w przykładzie 2.1, na początku modułu importowany jest moduł time pierwszej metody profilowania i definiowanych jest kilka stałych współrzędnych. Przykład 2.1. Definiowanie stałych globalnych dla przestrzeni współrzędnych G en erator zbioru Ju lii bez op cjon aln eg o rysow ania obrazów na bazie biblioteki P IL ..... import time # obszar przestrzeni zesp olon ej d o p rzeanalizow ania x1, x2 , y1, y2 = - 1 . 8 , 1 . 8 , - 1 . 8 , 1.8 c _ r e a l , c_imag = - 0 . 6 2 7 7 2 , - . 4 2 1 9 3
34
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Aby w ygenerować wykres, tworzone są dwie listy danych wejściow ych. Pierwsza lista to zs (współrzędne zespolone z), a druga to cs (zespolony w arunek początkowy). Żadna z list nie zm ienia się. Listę cs m ożna zoptym alizow ać do postaci pojedynczej w artości c jako stałej. Powodem utworzenia dwóch list wejściowych jest chęć uzyskania rozsądnie prezentujących się danych, które będą używane podczas profilowania wykorzystania pamięci RAM w dalszej części rozdziału. A by utw orzyć listy zs i cs, konieczne jest określenie w spółrzędnych dla każdego punktu z. W przykładzie 2.2 te w spółrzędne są tworzone przy użyciu zm iennych xcoord i ycoord, a po nadto określono zmienne x_step i y_step. Szczegółow ość takiej konfiguracji przydaje się przy przenoszeniu kodu do innych narzędzi (np. do narzędzi numpy) i środowisk opartych na ko dzie Python, poniew aż ułatwia zdefiniow anie w szystkiego w bardzo przejrzysty sposób na potrzeby debugowania. Przykład 2.2. Definiowanie list współrzędnych jako wejść przykładowej funkcji obliczeniowej def calc_p ure_python(desired_width, m a x _ i t e r a t i o n s ) : Tworzenie listy w spółrzędnych zespolonych (zs) i param etrów zespolonych (cs), budow anie zbioru Ju lii i wyświetlanie danych..... x_ st ep = ( f l o a t ( x 2 - x1) / f l o a t ( d e s ir e d _ w i d t h ) ) y_s te p = ( f l o a t ( y 1 - y2) / f l o a t ( d e s ir e d _ w i d t h ) ) x = [] y = [] ycoord = y2 while ycoord > y1: y.append(ycoord) ycoord += y_step xcoord = x1 while xcoord < x2: x.append(xcoord) xcoord += x_step # Utwórz listę współrzędnych i w arunek początkow y d la każd ej kom órki # Zauważ, że w arunek początkow y to stała, która z łatw ością m oże zostać usunięta # Stała służy do symulowania rzeczywistego scenariusza z kilkom a w ejściam i # przekazanym i przykładow ej fu n kcji zs = [] cs = [] f o r ycoord in y: f o r xcoord in x: zs.append(complex(xcoord, y c o o rd )) cs.app en d(c om plex(c _re al, c_imag)) p r i n t "Długość dla x : " , le n (x ) p r i n t "Łączna l i c z b a elementów:", l e n ( z s ) s t a r t _ t i m e = t i m e . ti m e () output = c a l c u la t e _ z _ s e r i a l _ p u r e p y t h o n ( m a x _ i te r a ti o n s , z s , cs) end_time = t i m e . ti m e () se c s = end_time - s t a r t _ t i m e p r i n t "D ziałan ie f u n k cji " + ca lcu la te_ z_se rial_pure python .f un c_ n am e + " tr w a ł o " , s e c s , "s" # Suma ta je s t oczekiw ana dla siatki 1000^2 z 300 iteracjam i # Przechwytywane s ą drobne błędy, które m og ą s ię p ojaw ić # p o d cza s przetw arzania ustalonego zbioru w ejść a s s e r t sum(output) == 33219980
Po utworzeniu list zs i cs zwracane są informacje o ich wielkości, a ponadto obliczana jest li sta output za pom ocą funkcji calculate_z_serial_purepython. Na końcu sum ow ane są dane zmiennej output i instrukcji assert, które są zgodne z oczekiwaną w artością wyjściową. Jeden z autorów posłużył się tutaj tym kodem, aby zapewnić, że w książce nie pojawi się kod za wierający błędy.
Obliczanie pełnego zbioru Julii
|
35
Ponieważ kod jest deterministyczny, m ożemy sprawdzić, czy funkcja działa zgodnie z ocze kiwaniami, sumując w szystkie obliczone wartości. Przydaje się to jako kontrola poprawności. W przypadku wprowadzania zmian w kodzie numerycznym bardzo wskazane jest sprawdze nie, czy nie został uszkodzony algorytm. W idealnej sytuacji zostałyby użyte testy jednostkowe, a ponadto sprawdzona więcej niż jedna konfiguracja powiązana z problemem. W przykładzie 2.3 definiow ana jest następnie funkcja calculate_z_serial_purepython, która rozszerza omówiony wcześniej algorytm. Na początku definiowana jest też lista output o takiej samej długości jak w przypadku list zs i cs. M ożesz również zastanaw iać się, dlaczego za m iast funkcji xrange używ ana jest funkcja range (tak w łaśnie jest w podrozdziale „Użycie na rzędzia m em ory_profiler do diagnozow ania w ykorzystania pam ięci"). W ten sposób można pokazać, jak rozrzutna potrafi być funkcja range! Przykład 2.3. Funkcja obliczeniowa powiązana z procesorem def c a lcu la t e _ z _ s e r i a l _ p u r e p y t h o n (m a x i te r , z s , c s ) : O bliczanie listy output przy użyciu reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while abs( z) < 2 and n < m ax iter : z = z * z + c n += 1 o u t p u t [i ] = n re tu rn output
W przykładzie 2.4 w yw oływ ana jest funkcja obliczeniow a. Przez opakow anie jej za pomocą kodu k o n tro ln eg o main w przypadku niektórych m etod profilowania m ożemy bezpiecz nie zaim portow ać m oduł bez rozpoczynania obliczeń. Zauważ, że nie jest tutaj prezentowana metoda używana do generowania wykresu danych wyjściowych. Przykład 2.4. Funkcja main kodu if
name == " main ": # O bliczanie zbioru Ju lii za p o m o c ą czystego rozw iązania op artego na języku Python # z wykorzystaniem w artości domyślnych rozsądnych d la laptopa cal c_pure_python(desired_wi dth=1000, m ax_i teratio ns= 300 )
Po uruchomieniu kodu zostaną uzyskane dane wyjściowe opisujące złożoność problemu: # uruchom ienie pow yższego kodu d aje następujący wynik: Długość dla x: 1000 Łączna l i c z b a elementów: 1000000 D zia łan ie f u n k cji c alc u la t e _ z _ s e r i a l _ p u re p y t h o n trwało 12.3479790688 s
W przypadku w ykresu fałszyw ej skali szarości (rysunek 2.1) zm iany kolorów o w ysokim kontraście pozwoliły nam zorientow ać się, gdzie koszt funkcji zmieniał się wolno lub szybko. Na rysunku 2.3 widoczna jest liniowa mapa kolorów: kolor czarny można szybko wygene rować, a generowanie koloru białego jest kosztowne. Uwidocznienie dwóch reprezentacji tych samych danych pozwala zauważyć, że przy odwzo rowaniu liniowym traconych jest wiele szczegółów. Czasem podczas analizowania kosztu funkcji przydatne może być uw zględnienie różnych reprezentacji.
36
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Rysunek 2.3. Przykład wykresu zbioru Julii używający czystej skali szarości
Proste metody pomiaru czasu — instrukcja print i dekorator Po wykonaniu kodu z przykładu 2.4 uzyskano dane wyjściow e wygenerowane przez kilka instrukcji print. Na laptopie jednego z autorów wykonanie tego kodu przy użyciu narzędzia CPython 2.7 zajęło około 12 sekund. Warto zauważyć, że czas wykonania zaw sze będzie tro chę inny. Podczas pomiaru czasu wykonywania kodu musisz zaobserwować standardową zmienność, ponieważ w przeciwnym razie możesz niepoprawnie przypisać wzrost wydajności w kodzie po prostu losowej zmienności czasu wykonywania. W czasie działania kodu kom puter będzie realizow ać inne zadania, takie jak uzyskiwanie do stępu do sieci, dysku lub pamięci RAM. Czynniki te mogą pow odow ać zm iany czasu w yko nywania programu. Laptop jednego z autorów to Dell E6420 z procesorem Intel Core I7-2720QM (2,20 GHz, pamięć podręczna 6 M B i poczw órny rdzeń) oraz pam ięcią RAM 8 GB i systemem Ubuntu 13.10. W funkcji calc_pure_python (przykład 2.2) znajduje się kilka instrukcji print. Jest to najprostsza m etoda pom iaru czasu w ykonyw ania porcji kodu w obrębie funkcji. Jest to bardzo proste rozwiązanie, które pomimo szybkości i braku przejrzystości może okazać się bardzo pomocne w początkowej fazie analizowania porcji kodu. U życie instrukcji print je st pow szechne podczas d ebugow ania i profilow ania kodu. Choć m etoda ta szybko staje się trudna w obsłudze, przyd aje się w przypadku krótkich analiz.
Proste metody pomiaru czasu — instrukcja print i dekorator
|
37
Spróbuj uporządkow ać wyniki po zakończeniu analiz, ponieważ w przeciwnym razie stan dardowe wyjście stdout nie będzie przejrzyste. M etodą zapew niającą trochę w iększą czytelność jest użycie dekoratora. W tym przypadku pow yżej interesującej nas funkcji dodajem y jeden w iersz kodu. D ekorator m oże być bardzo prosty i m oże replikow ać jedynie efekt działania instrukcji print. Później można go bardziej rozbudować. W przykładzie 2.5 definiow ana jest now a funkcja timefn, która pobiera funkcję w ew nętrzną measure_time jako argument. Funkcja ta pobiera argumenty *args (zmienna liczba argumentów pozycyjnych) i **kwargs (zmienna liczba argumentów klucz/wartość), a ponadto przekazuje je funkcji fn w celu w ykonania. W ram ach w ykonyw ania funkcji fn przechw ytyw ana jest funkcja time.time(), a następnie za pom ocą instrukcji print wyśw ietlane są w yniki wraz z na zw ą funkcji (kod fn.func_name). Choć obciążenie wynikające z zastosowania tego dekoratora jest niewielkie, w przypadku wywołania funkcji fn miliony razy może ono stać się zauważalne. Aby ujawnić nazwę funkcji i notkę dokumentacyjną (ang. docstring) elementowi wywołującemu funkcji z dekoratorem, używa się kodu @wraps(fn) (w przeciwnym razie widoczna byłaby na zwa funkcji i notka dokumentacyjna dla dekoratora, a nie dekorowanej przez niego funkcji). Przykład 2.5. Definiowanie dekoratora do automatyzowania pomiarów czasu from f u n c to o l s import wraps def t i m e f n ( f n ) : @wraps(fn) def m easu re _tim e( *a rgs , **kw arg s): t1 = t i m e . ti m e () r e s u l t = f n ( * a r g s , **kwargs) t2 = t i m e . ti m e () p r i n t ("@timefn: d z i a ł a n i e f u n k c ji " + fn.func_name + " trwało " + s t r ( t 2 - t1 ) + " s " ) r e tu r n r e s u l t re tu rn measure_time @timefn def c a lcu la t e _ z _ s e r i a l _ p u r e p y t h o n (m a x i te r , z s , c s ) :
Po uruchom ieniu tej w ersji (zachow ano instrukcje print z w cześniejszego kodu) w idać, że czas w ykonyw ania w ersji z dekoratorem jest napraw dę niew iele krótszy niż w przypadku w ywołania z funkcji calc_pure_python. W ynika to z obciążenia związanego z wywoływaniem funkcji (różnica jest znikoma): Długość elementu x: 1000 Łączna l i c z b a elementów: 1000000 @timefn: d z i a ł a n i e f u n k c ji c a l c u la t e _ z _ s e r i a l _ p u re p y t h o n trwało 12.2218790054 s D zia łan ie f u n k cji c alc u la t e _ z _ s e r i a l _ p u re p y t h o n trwało 12. 2219250043 s
D o d a n i e i n f o r m a c ji o p r o f i l o w a n i u n a p e w n o s p o w o l n i k o d . N i e k t ó r e o p c je p r o f i lo w a n i a z a w i e r a ją m n ó s t w o i n f o r m a c ji i p o w o d u j ą z n a c z n y s p a d e k szy b k o ś c i . K o n i e c z n e b ę d z ie
i
w y p ra co w a n ie k o m p r o m isu p o m ię d z y szczegółow ością p rofilow ania i szybkością.
Użycie modułu tim eit to kolejny sposób przeprow adzenia zwykłego pomiaru szybkości wy konywania funkcji powiązanej z procesorem. Ta metoda jest zw ykle wykorzystywana przy określaniu czasu dla różnych typów prostych wyrażeń podczas eksperym entowania ze spo sobami rozwiązania problemu.
38
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Z au w aż, że m o d u ł tim e it ty m c z a so w o w y łącza p ro g ra m czyszczący pam ięć. M o ż e o n m i e ć w p ły w n a u z y s k a n ą s z y b k o ś ć w p r z y p a d k u r z e c z y w is ty c h o p era cji, jeśli p r o g r a m b y łb y n o r m a ln ie p rz e z n ie w y w o ły w a n y . P o m o c z t y m z w ią z a n a je st d o s t ę p n a w d o k u m e n t a c j i j ę z y k a P y t h o n (h tt p s ://d o c s .p y t h o n .o r g /2 /lib r a r y /tim e it .h tm l).
Z poziomu wiersza poleceń możesz uruchom ić m oduł tim eit w następujący sposób: $ python -m ti m e i t -n 5 - r 5 - s "import j u l i a 1 " "j u li a1.c alc _p u re _p yth on (d es i red_width=1000, max _ it erati o ns= 300)"
Zauw aż, że w ram ach kroku konfiguracyjnego m usisz zaim portow ać m oduł przy użyciu opcji -s, ponieważ funkcja calc_pure_python znajduje się wewnątrz tego modułu. M oduł timeit zawiera kilka praktycznych w artości domyślnych w przypadku krótkich sekcji kodu, ale na potrzeby dłużej działających funkcji sensowne może być określenie liczby wykonania pętli, której w yniki są uśredniane dla każdego testu (-n 5), oraz liczby pow tórzeń (-r 5). Jako od powiedź zwracany jest w ynik wykonania w szystkich powtórzeń. Domyślnie uruchom ienie dla tej funkcji modułu tim eit bez określania opcji -n i -r spowoduje wykonanie 10 razy pętli z pięcioma powtórzeniami. Całość operacji zajmie 6 minut. Nadpisanie wartości domyślnych może m ieć sens, jeśli wyniki mają zostać uzyskane trochę szybciej. Interesują nas wyłącznie w yniki dla najlepszego przypadku, ponieważ przeciętny i najgorszy przypadek to praw dopodobnie rezultat ingerencji innych procesów . W ybór najlepszego spo śród pięciu powtórzeń dla pięciu średnich w yników powinien dać nam dość pew ny wynik: 5 loop s, b est o f 5: 13.1 sec per loop
Spróbuj kilkakrotnie uruchomić test porównawczy, aby sprawdzić, czy uzyskiwane są zmienne wyniki. W celu ustalenia najkrótszego czasu przy stabilnym wyniku może być wymagana większa liczba powtórzeń. Ponieważ nie ma „właściwej" konfiguracji, jeśli występuje duża roz bieżność w yników pom iaru czasu, zw iększaj liczbę pow tórzeń do m om entu uzyskania sta bilnego wyniku końcowego. Uzyskane dane pokazują, że ogólny koszt wywołania funkcji calc_pure_python to 13,1 sekundy (dla najlepszego przypadku), pojedyncze wywołania funkcji calculate_z_serial_purepython zaj mują natomiast 12,2 sekundy, co zostało zmierzone przez dekorator @timefn. Różnica to głównie czas, jaki zajęło utworzenie list zs i cs. W obrębie pow łoki IPython w ten sam sposób można użyć funkcji „m agicznej" %timeit. Jeśli tw orzysz kod interaktyw nie w tej pow łoce, a funkcje znajdują się w lokalnej przestrzeni nazw (prawdopodobnie z pow odu użycia funkcji %run), możesz zastosow ać następujący kod: %timeit calc_pure_p ython(desi red_wi dth=1000, m ax_ite ra t ions=300)
W arto rozw ażyć zm ienność obciążenia w ystępującego w przypadku zw ykłego kom putera. Aktyw nych jest wiele zadań w tle (np. usługa Dropbox, proces tworzenia kopii zapasowej), które w losowy sposób mogą w pływ ać na zasoby dyskowe i procesor. Skrypty na stronach internetowych mogą też w yw oływ ać nieprzewidyw alne wykorzystanie zasobów. Na rysun ku 2.4 pokazano pojedynczy procesor obciążony w 100% przez część w cześniej w ykonanych kroków pomiaru czasu. Inne rdzenie procesora tego samego komputera są nieznacznie ob ciążone przez inne zadania.
Proste metody pomiaru czasu — instrukcja print i dekorator
|
39
Rysunek 2.4. Narzędzie System Monitor w systemie Ubuntu, które prezentuje zmienne wykorzystanie procesora podczas pomiaru czasu dla przykładowej funkcji Sporadycznie narzędzie System Monitor pokazuje dla komputera szczytową aktywność. Warto obserwować dane tego narzędzia, aby upew nić się, że nic innego nie oddziałuje na krytyczne zasoby (procesor, dysk, sieć).
Prosty pomiar czasu za pomocą polecenia time systemu Unix Na chwilę wyjdziemy trochę poza język Python, aby opisać użycie standardowego narzędzia dostępnego w systemach uniksowych. Następujące polecenie zarejestruje różne w idoki czasu wykonywania programu, pomijając strukturę w ew nętrzną kodu: $ /u s r / b i n /t i m e -p python ju lia 1 _ n o p i l.p y Długość dla x: 1000 Łączna l i c z b a elementów: 1000000 D zia łan ie fu n k c j i c a l c u la t e _ z _ s e r i a l _ p u re p y t h o n trwało 12. 7298331261 s re al 13.46 user 13.40 sys 0 .0 4
Zauważ, że zam iast time podano dokładniejszą postać /usr/bin/time, aby skorzystać z syste mowego narzędzia time, a nie z jego prostszej (i mniej przydatnej) wersji wbudowanej w po włokę. Jeśli spróbujesz użyć polecenia time --verbose i zostanie wygenerowany błąd, prawdo podobnie masz do czynienia z poleceniem time wbudow anym w powłokę, a nie z poleceniem systemowym. Użycie flagi przenośności -p pow oduje uzyskanie następujących trzech w yników : • re a l. Rejestruje aktualny czas lub czas, który upłynął. • user. Rejestruje czas, jaki procesor poświęcił na realizowanie zadania poza funkcjami jądra. • sys. Rejestruje czas, jaki upłynął w funkcjach na poziom ie jądra. Uw zględnienie wyników user i sys pozwala zorientow ać się, ile czasu minęło w procesorze. Różnica między tymi wynikami i wynikiem real daje możliwość określenia czasu, jaki upłynął podczas oczekiwania na operację wejścia-wyjścia. Różnica m oże też wskazywać, że system jest zajęty przetwarzaniem innych zadań, które zakłócają pomiary.
40
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Narzędzie time jest przydatne, ponieważ nie jest pow iązane z językiem Python. Uwzględnia ono czas w ym agany do uruchom ienia program u w ykonyw alnego python, który m oże być znaczny, jeśli urucham ianych jest w iele now ych procesów (zam iast jednego długotrw ałego procesu). Jeśli często używasz krótko działających skryptów, w przypadku których czas uru chamiania stanowi znaczącą część ogólnego czasu działania, narzędzie time m oże okazać się bardziej przydatną m etodą pomiaru. Aby uzyskać jeszcze więcej danych wyjściowych, można dodać flagę --verbose: $ / u s r / b in /t im e -- ve rb os e python j u l ia 1 _ n o p i l.p y Długość dla x: 1000 Łączna l i c z b a elementów: 1000000 D ziałan ie f u n k c ji c alc u la t e _ z _ s e r i a l _ p u re p y t h o n trwało 12.3145110607 s Command being timed: "python j u l i a 1 _ n o p i l . p y " User time (seco n ds) : 13.46 System time (s e co n ds) : 0.05 Percen t o f CPU t h i s jo b got: 99% Elapsed (wall c lo c k ) time (h:mm:ss or m : s s ) : 0 : 1 3 . 5 3 Average shared t e x t s i z e ( k b y te s ): 0 Average unshared data s i z e ( k b y te s ): 0 Average st a c k s i z e ( k b y te s ): 0 Average t o t a l s i z e ( k b y te s ): 0 Maximum re s i d e n t s e t s i z e ( k b y te s ): 131952 Average re s i d e n t s e t s i z e ( k b y te s ): 0 Major (r e q u i ri n g I/O) page f a u l t s : 0 Minor (r ec la im in g a frame) page f a u l t s : 58974 Voluntary co ntex t sw it ch es: 3 Involuntary c on text sw it ch es: 26 Swaps: 0 F i l e system inp uts : 0 F i l e system out put s: 1968 Socket messages s e n t : 0 Socket messages re c e iv e d : 0 S i g n a l s d e l iv e r e d : 0 Page s i z e ( b y t e s ) : 4096 Ex it s t a t u s : 0
W tym przypadku praw dopodobnie najbardziej przydatny w skaźnik to Major (requiring I/O) page fa u lts, który określa, czy system operacyjny m usi załadow ać z dysku strony danych, ponieważ nie ma ich już w pamięci RAM. Spow oduje to spadek szybkości. Ponieważ w przykładzie wymagania kodu i danych są niewielkie, nie w ystępują błędy stro nicowania. W przypadku procesu powiązanego z pam ięcią lub kilku program ów, które uży wają zmiennej i dużej ilości pamięci RAM, może to um ożliw ić zidentyfikow anie programu spowalnianego przez operacje dostępu do dysku na poziomie systemu operacyjnego, ponie waż części programu zostały przeniesione z pamięci RAM na dysk.
Użycie modułu cProfile M oduł cP ro file to w budow ane narzędzie do profilow ania w bibliotece standardow ej. Jest ono podłączane do maszyny wirtualnej w m odule cP rofile w celu pomiaru czasu, jaki zaj muje uruchomienie każdej napotkanej funkcji. Choć powoduje to znaczne obciążenie, pozwala uzyskać odpowiednio w iększą ilość informacji. Czasami dodatkowe informacje mogą dopro wadzić do zaskakujących wniosków dotyczących kodu.
Użycie modułu cProfile
|
41
M oduł cProfile to jedno z trzech narzędzi profilujących w bibliotece standardowej. Dwa po zostałe narzędzia to hotshot i profile. Narzędzie hotshot ma postać eksperym entalnego kodu, a narzędzie p rofile to oryginalne narzędzie profilujące bazujące w yłącznie na kodzie Python. M oduł cP ro file zaw iera ten sam interfejs co narzędzie p ro file, a ponadto jest obsługiw any i pełni rolę domyślnego narzędzia profilującego. Jeśli ciekawi Cię historia tych bibliotek, zaj rzyj na stronę Armina Rigo (https://mail.python.org/pipermail/python-dev/2005-), który w 2005 roku wystosował prośbę o dołączenie modułu cProfil e do biblioteki standardowej. Dobrą praktyką w czasie profilowania jest utworzenie hipotezy dotyczącej szybkości części kodu przed rozpoczęciem profilowania go. Jeden z autorów woli wydrukować rozpatrywany frag ment kodu i dołączyć do niego adnotacje. W cześniejsze zdefiniowanie hipotezy oznacza, że możesz stwierdzić, jak bardzo się mylisz (i nadal będziesz!), a ponadto m ożesz popraw ić in tuicję odnośnie do konkretnych stylów tworzenia kodu.
m
N ig d y n ie n a le ż y u n ik a ć p ro filo w an ia z p o w o d u p rz e cz u c ia (o strz eg a m y Cię, p o n ie w a ż z o s ta n ie to ź l e p r z e z C i e b i e z r o z u m i a n e ! ) . Z d e c y d o w a n i e w a r t o s f o r m u ł o w a ć h i
p o te z ę p rz e d ro z p o c z ę c ie m p ro filo w an ia, a b y u ła tw ić sobie o p a n o w a n ie um iejętn o ści w y k r y w a n ia w ko d zie m o ż liw y c h d ziałań zm n ie jsz a ją cy ch szybk ość. P o n a d to z a w s z e
n a le ż y p o p r z e ć d o w o d e m p o d e jm o w a n e decy zje.
Zaw sze kieruj się w ynikam i uzyskanymi przez pomiar i zaczynaj od podstawowego profilo wania, aby upew nić się, że dotyczy ono właściwego obszaru. Nie ma nic bardziej upokarza jącego niż zręczne optym alizow anie sekcji kodu tylko po to, aby uśw iadom ić sobie (wiele godzin lub dni później), że pom inięto najwolniejszą część procesu i w rzeczywistości w ogóle nie zajęto się zasadniczym problemem. A zatem jaka jest hipoteza w omawianym przykładzie? W iemy, że funkcja calculate_z_serial_ purepython będzie prawdopodobnie najwolniejszym elementem kodu. W tej funkcji przepro wadzanych jest wiele operacji usuwania odniesień i tworzenia licznych odwołań do podsta wowych operatorów arytmetycznych i funkcji abs. Okażą się one raczej tymi, które zużywają sporo zasobów procesora. M oduł cP ro file zostanie użyty do uruchom ienia w ariantu kodu. C hoć dane w yjściow e są skromne, pom agają zidentyfikow ać miejsce do dalszej analizy. Flaga -s cumulative nakazuje modułowi cProfile sortowanie według łącznego czasu, jaki upłynął w obrębie każdej funkcji. Pozwala to nam uzyskać w gląd w najwolniejsze części sekcji kodu. Dane wyjściowe modułu są wyświetlane na ekranie bezpośrednio po standardowych wynikach polecenia print: $ python -m c P r o f i l e - s cumulative j u l ia 1 _ n o p i l.p y 36221992 fu n c tio n c a l l s in 19.6 64 seconds Ordered by: cumulative time n c a l l s to t ti m e p e r c a l l cumtime p e r ca l l 1 0.0 3 4 0.0 3 4 19.664 19.664 1 0. 843 0.8 43 19.630 19.630 ( c a l c_pure_python) 1 14.121 14.121 18.627 18.627 j u l i a 1 _ n o p i l . p y : 9 (c alc u l a te _ z _ s e ri a l _ p u r e p y t h o n ) 34219980 4. 487 0. 000 4.487 0.0 0 0 {abs} 2002000 0.1 50 0.0 0 0 0.150 0.0 0 0 {method 'append' o f ' l i s t ' o b j e c t s } 1 0.0 19 0.0 1 9 0.019 0.0 1 9
42
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
1 2 4 1
0.0 1 0 0.0 0 0 0.0 0 0 0.0 0 0
0.010 0.000 0.000 0.000
0.010 0.0 0 0 0.0 0 0 0.0 0 0
0.0 1 0 0.000 0.000 0.000
{sum} {t im e .t im e} {len } {method ' d i s a b l e ' of '_ l s p r o f .P r o f i l e r ' obje cts}
Sortowanie według łącznego czasu pozw ala zorientow ać się, w jakim miejscu upływa więk szość czasu w ykonyw ania. U zyskany w ynik pokazuje, że 36 221 992 w yw ołania funkcji w ystąpiły w czasie wynoszącym zaledw ie ponad 19 sekund (tym razem uwzględnia to ob ciążenia zw iązane z użyciem m odułu cP ro file). W cześniej w ykonanie kodu zajęło około 13 sekund. Po prostu dodano obciążenie wynoszące 5 sekund podczas pom iaru czasu w ykona nia każdej funkcji. M ożna zauważyć, że punkt wejścia do kodu skryptu ju lia1_cprofile.py w w ierszu 1. zajmuje łącznie 19 sekund. A jest to jedynie odw ołanie main do funkcji calc_pure_python. W kolumnie ncalls jest wartość 1, która wskazuje, że wiersz ten jest wykonywany tylko raz. W obrębie funkcji calc_pure_python wywołanie funkcji calculate_z_serial_purepython zajmuje 18,6 sekundy. Obie funkcje są w yw oływ ane tylko raz. M ożna wywnioskować, że w przybli żeniu jedna sekunda jest poświęcana na w iersze kodu w obrębie funkcji calc_pure_python, niezależnie od w yw ołania funkcji calculate_z_serial_purepython, która intensyw nie korzysta z procesora. N ie można jednak stwierdzić przy użyciu m odułu cProfile, jakie wiersze zajmują czas w obrębie funkcji. Czas pośw ięcany na w iersze kodu (bez wywoływ ania innych funkcji) wewnątrz funkcji calculate_z_serial_purepython w ynosi 14,1 sekundy. Funkcja tworzy 34 219 980 wyw ołań funkcji abs, co w sumie zajmuje 4,4 sekundy, uwzględniając kilka innych wywołań, które nie zajmują wiele czasu. A co z w yw ołaniem {abs}? Jego w iersz służy do pom iaru czasu poszczególnych w yw ołań funkcji abs w obrębie funkcji calculate_z_serial_purepython. Choć czas przypadający na poje dyncze wywołanie jest nieistotny (został zarejestrowany jako 0,000 sekundy), łączny czas dla 34 219 980 wyw ołań to 4,4 sekundy. N ie możemy z góry przewidzieć, ile dokładnie zostanie wykonanych wyw ołań funkcji abs, ponieważ funkcja zbioru Julii cechuje się nieprzewidy walną dynam iką (z tego w łaśnie pow odu jest to tak godne zainteresowania zagadnienie). W najlepszym razie możemy określić, że funkcja będzie wywoływana co najmniej 1 000 000 razy, gdy obliczenia są przeprow adzane dla iloczynu 1000*1000 pikseli. Co najwyżej, funkcja zostanie wywołana 300 000 000 razy, jeśli obliczenia dotyczą 1 000 000 pikseli przy maksy malnej liczbie iteracji wynoszącej 300. Oznacza to, że 34 miliony wywołań to w przybliżeniu 10% najgorszego wariantu. Jeśli przyjrzym y się oryginalnemu obrazowi skali szarości (rysunek 2.3) i wyobrazimy sobie, że jego białe części zostały ściśnięte w narożniku, m ożem y oszacow ać, że kosztow ny biały obszar stanowi mniej więcej 10% reszty obrazu. W następnym wierszu profilowanych danych wyjściowych ({method 'append' of 'l i s t ' objects}) podano informację o utworzeniu 2 002 000 pozycji listy. D l a c z e g o j e s t 2 0 0 2 0 0 0 p o z y c j i ? P r z e d d a l s z ą l e k t u r ą z a s t a n ó w się, il e j e s t t w o r z o n y c h p o z y c j i li st y.
Użycie modułu cProfile
|
43
Tworzenie takiej liczby pozycji ma miejsce w funkcji calc_pure_python na etapie konfigurowania. Listy zs i cs będą zaw ierać po 1000*1000 pozycji. Listy te są budow ane przy użyciu listy 1000 współrzędnych x i 1000 współrzędnych y. Łącznie do dodania jest 2 002 000 wywołań. G odne uw agi jest to, że te dane w yjściow e m odułu cP ro file nie są porządkow ane przez funkcje nadrzędne. M oduł podsum ow uje koszt w szystkich funkcji w w ykonyw anym bloku kodu. Stw ierdzenie, co m a m iejsce w poszczególnych w ierszach kodu, jest bardzo trudne w przypadku modułu cProfile, ponieważ uzyskiw ane są jedynie informacje profilow ania dla samych wyw ołań funkcji, a nie dla każdego wiersza w obrębie funkcji. W ew nątrz funkcji calculate_z_serial_purepython m ożna teraz przeprow adzić obliczenie dla {ab s} i {range}. W sumie te dwie funkcje powodują koszt w ynoszący w przybliżeniu 4,5 se kundy. W iemy, że łączny koszt funkcji calculate_z_serial_purepython to 18,6 sekundy. Ostatni w iersz danych wyjściowych profilowania odnosi się do narzędzia lsprof. Jest to ory ginalna nazwa narzędzia, które rozwinęło się do postaci modułu cProfile. Nazwa może zostać zignorowana. Aby uzyskać w iększą kontrolę nad wynikami modułu cProfile, m ożesz utw orzyć plik staty styk, a następnie dokonać jego analizy za pomocą języka Python: $ python -m c P r o f i l e -o p r o f i l e . s t a t s j u l i a 1 . p y
Po załadowaniu w następujący sposób pliku w interpreterze języka Python zostanie uzyskany taki sam jak wcześniej raport czasu łącznego: In [ 1 ] : In [ 2 ] : In [ 3 ] : O u t[ 3]: In [ 4 ] : Tue Jan
import p s t a t s p = p s ta ts .S ta ts ("p ro file .s ta ts ") p.sort_stats("cum ulative") < p s t a t s . S t a t s i n s ta n ce at 0x177dcf8> p .p rin t_stats() 7 2 1 : 0 0 : 5 6 2014 p ro file .sta ts 36221992 fu n c tio n c a l l s in 19.983 seconds Ordered by: cumulative time n c a l l s to t ti m e p e r c a l l cumtime p e r c a l l f i l e n a m e :l i n e n o ( f u n c t i o n ) 1 0. 033 0. 033 19.983 19.983 j u l i a l nopil.py:1(
) 1 0.8 4 6 0. 846 19.950 19. 95 0 j u l i a l n o p i l. p y : 2 3 (c a l c pure python) 1 13.585 13.585 18.944 18.9 44 j u l i a l n o p i l . p y : 9 ( c a l c u l a t e z s e r i a l purepython) 34219980 5.3 4 0 0.0 00 5.340 0 . 0 0 0 {abs} 2002000 0.1 50 0.0 00 0.150 0 . 0 0 0 {method 'append' o f ' l i s t ' o b j e c t s } 1 0.0 19 0.0 1 9 0.019 0 . 0 1 9 {range} 1 0.0 10 0.0 1 0 0.010 0 . 0 1 0 {sum} 2 0.0 00 0. 000 0.000 0 . 0 0 0 {t im e .t im e} 4 0.0 00 0. 000 0.000 0 . 0 0 0 {len } 1 0.0 00 0. 000 0.000 0 . 0 0 0 {method ' d i s a b l e ' o f '_ l s p r o f .P r o f i l e r ' o b je cts}
W celu śledzenia tego, jakie funkcje są profilowane, możesz wyświetlić informacje o elemencie w yw ołującym . Na dw óch poniższych listingach m ożesz zobaczyć, że funkcja calculate_z_ serial_purepython ma największy koszt, a ponadto wywoływana jest z jednego miejsca. Jeśli ta funkcja zostałaby wywołana z wielu miejsc, listingi te m ogłyby być pom ocne przy zawężaniu położeń najbardziej kosztownych funkcji nadrzędnych: In [ 5 ] : p . p r i n t _ c a l l e r s ( ) Ordered by: cumulative time Function
44
|
was c a l l e d b y . . . n calls
t o t ti m e
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
cumtime
ju lia 1_n o p il.p y :1 (< m o d u le> ) ju l ia 1 _ n o p i l .p y :2 3 ( c a l c _ p u r e _ p y t h o n )
<<-
846 19.950 1 0 ju l ia 1 _ n o p i .py:1() 13.585 18 .9 44 ju l ia 1 _ n o p i l . p y : 9 ( c a l c u l a t e _ z _ s e r i a l _ p u r e p y t h o n ) <1 ju l ia 1 _ n o p i .p y:23 (calc_pure_python) <- 34219980 .340 5.3 4 0 {abs} ju l ia 1 _ n o p i .p y :9 ( c a l c u l a t e _ _se ria l_pur epy thon) <- 2002000 0 150 0. 150 {method 'append' o f ' l i s t ' o b j e c t s } ju l ia 1 _ n o p i .p y:23 (calc_pure_python) 1 0 019 0.0 1 9 {range} ju l ia 1 _ n o p i .p y :9 ( c a l c u l a t e _ _ser ia l_pur epy thon) 1 0 010 0.0 10 {sum} ju l i a 1 _ n o p i l .p y:23 (calc_pure_python) { t i m e .t im e} < 2 0 000 0.0 00 ju l ia 1 _ n o p i .p y:23 (calc_pure_python) { len } < 2 0 000 0. 0 00 ju l ia 1 _ n o p i .p y :9 ( c a l c u l a t e _ _ser ia l_pur epy thon) 2 0 000 0.0 00 ju l ia 1 _ n o p i .p y:23 (calc_pure_python) {method 'd i s a b l e ' o f ' _ l s p r o f . P r o f i l e r ' o b j e c t s }
Aby pokazać, jakie funkcje wyw ołują inne funkcje, pow yższe dane wyjściowe można przed stawić w następujący sposób: In [ 6 ] : p . p r i n t _ c a l l e e s ( ) Ordered by: cumulative time Function
c a lle d ... n c a l l s t o t ti m e cumtime -> 1 0.846 19. 95 0 ju lia 1_ n opil.p y:1 ( ) j u l i a 1 _ n o p i l . p y : 23 (calc_pure_python) -> 1 13.585 18. 94 4 ju l ia 1 _ n o p i l .p y :2 3 ( c a l c _ p u r e _ p y t h o n ) j u l i a 1 _ n o p i l.p y :9 (c alc u l a te _ z _ s e ri a l _ p u r e p y t h o n ) 2 0.000 0.0 0 0 {len } 2002000 0.150 0 .1 5 0 {method 'append' of ' l i s t ' ob jects} 1 0.010 0.0 1 0 {sum} 2 0.000 0.0 0 0 {t im e .t im e} ju l ia 1 _ n o p i l . p y : 9 ( c a l c u l a t e _ z _ s e r i a l _ p u r e p y t h o n ) -> 34219980 5.340 5.3 40 {abs} 2 0.000 0.0 0 0 {len } 1 0.019 0.019 {range} {abs} -> {method 'append' o f ' l i s t ' o b j e c t s } -> {range} -> {sum} -> {t i m e .t im e} -> {l en } -> {method 'd i s a b l e ' o f ' _ l s p r o f . P r o f i l e r ' o b j e c t s } ->
Użycie modułu cProfile
|
45
Ze względu na to, że moduł cProfile udostępnia sporo informacji, w celu wyświetlenia ich bez intensywnego korzystania z funkcji zawijania wierszy konieczny będzie szeroki ekran. Ponie waż jednak moduł jest wbudowany, stanowi wygodne narzędzie do szybkiego identyfikowania wąskich gardeł. Takie narzędzia jak line_profiler, heapy i memory_profiler, omówione w dalszej części rozdziału, ułatwią przejście do konkretnych wierszy, na które należy zwrócić uwagę.
Użycie narzędzia runsnake do wizualizacji danych wyjściowych modułu cProfile runsnake to narzędzie do wizualizacji statystyk profilowania utworzonych przez moduł cProfile. Pozwala ono na szybkie zorientow anie się tylko przez przyjrzenie się wygenerowanemu dia gramowi, jakie funkcje są najbardziej kosztowne. Użyj narzędzia runsnake do ogólnego zaznajomienia się z plikiem statystyk m odułu cProfile, zwłaszcza wtedy, gdy analizowany jest nowy i duży kod bazowy. Pozwoli to wstępnie określić obszary, którymi należy się zająć. Narzędzie daje też możliwość ujawnienia obszarów, których kosztowności nie byłbyś w innym przypadku świadomy. M oże to pomóc Ci zidentyfikow ać „szybkie w ygrane", którymi należy się zająć. Narzędzie może też zostać użyte podczas omawiania w zespole programistów mało wydaj nych obszarów kodu, ponieważ ułatwia analizowanie wyników. Aby zainstalow ać narzędzie runsnake, w ykonaj polecenie pip in stall runsnake. Zauważ, że wymaga ono pakietu wxPython, którego instalacja m oże być utrudniona w narzędziu virtualenv. Jeden z autorów zdecydował się więcej niż raz na zainstalow anie tego pakietu globalnie, zam iast podejm ow ać próby uruchamiania go w narzędziu virtualenv, tylko po to, aby dokonać analizy pliku profilowania. Rysunek 2.5 przedstawia wykres wcześniejszych danych modułu cProfi le. Inspekcja w formie wizualnej powinna ułatw ić szybkie zrozum ienie, że wykonanie funkcji calculate_z_serial_ purepython zajm uje w iększość czasu, a także tego, że jedynie część tego czasu wynika z wy w oływania innych funkcji (spośród nich jedyną znaczącą jest funkcja abs). M ożesz zauważyć, że znikom a ilość czasu zw iązana jest z podprogram em konfiguracji, gdyż zdecydow ana w iększość czasu wykonywania odnosi się do podprogram u obliczeniowego. W przypadku narzędzia runsnake m ożesz kliknąć funkcje i przejść do złożonych wyw ołań za gnieżdżonych. Narzędzie to okaże się bezcenne podczas omawiania w zespole przyczyn po w olnego wykonywania w segmencie kodu.
Użycie narzędzia line_profiler do pomiarów dotyczących kolejnych wierszy kodu W opinii jednego z autorów narzędzie line_p rofiler Roberta Kerna oferuje największe m oż liwości identyfikowania w kodzie Python przyczyny problem ów pow iązanych z procesorem. Działanie narzędzia polega na profilowaniu wiersz po wierszu poszczególnych funkcji. Ozna cza to, że należy zacząć od modułu cProfile i skorzystać z ogólnego widoku, który pozwoli określić, jakie funkcje m ają być profilow ane za pom ocą narzędzia line_profiler.
46
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Rysunek 2.5. Wizualizacja pliku profilowania modułu cProfile przy użyciu narzędzia runsnake W arto podczas modyfikowania kodu wyśw ietlać wersje danych wyjściowych tego narzędzia i tw orzyć dla nich adnotacje, poniew aż dzięki temu uzyskuje się zapis zm ian (pom yślnych lub nie), do których można szybko wrócić. Nie polegaj na pamięci przy wprowadzaniu zmian w poszczególnych wierszach. Aby zainstalow ać narzędzie line_p rofiler, wykonaj polecenie pip in stall line_profiler. Dekorator (@profile) służy do oznaczania wybranej funkcji. Skrypt kernprof.py jest używany do wykonywania kodu. Rejestrowany jest czas pracy procesora oraz inne statystyki dla każ dego wiersza wybranej funkcji.
^ r f’
W y m ó g m o d y fik o w a n ia k o d u ź ró d ło w e g o sta n o w i d ro b n y kłopot, g d y ż d o d an ie d e k o ra to r a s p o w o d u je ro z b icie te s tó w je d n o s tk o w y c h , c h y b a ż e z o s ta n ie u t w o r z o n y fik c y jn y d e k o ra to r (w ięcej in fo rm a c ji z a w ie ra p u n k t „ D e k o r a to r @ p ro file b ez o p e ra cji").
Argum ent - l umożliwia profilow anie wiersz po wierszu, a argum ent -v pow oduje zwrócenie szczegółowych danych wyjściowych. Bez argumentu -v zostaną uzyskane dane wyjściowe w po staci pliku .lprof, które m ożesz później poddać analizie przy użyciu m odułu lin e_p rofiler. Przykład 2.6 prezentuje pełne uruchom ienie funkcji powiązanej z procesorem . Przykład 2.6. Uruchomienie skryptu kernprof z danymi wyjściowymi dotyczącymi poszczególnych wierszy dla funkcji z dekoratorem w celu zarejestrowania czasu wykonywania przez procesor każdego wiersza kodu $ kernprof.py - l -v j u l i a 1 _ l i n e p r o f i l e r . p y Wrote p r o f i l e r e s u l t s to j u l i a 1 _ l i n e p r o f i l e r . p y . l p r o f Timer u n i t : 1e-06 s
Użycie narzędzia line_profiler do pomiarów dotyczących kolejnych wierszy kodu
|
47
F ile: ju lia1_lin ep ro file r.p y Function: c a l c u la t e _ z _ s e r i a l _ p u re p y t h o n a t l i n e 9 Total time: 100.81 s Line # Hits Per Hit % Time Line Contents @profi l e def c a l c u l a t e z s e r i a l
0 0 0 0 0 36 32 27 0 0.
0 8 8 8 8 2 6 2 9 0
O
68 70.0 0 .8 0 .8 0 .8 0 .8 1.1 1.0 0 .8 0 .9 4.0
N
1 1000001 1000000 1000000 1000000 34219980 33219980 33219980 1000000 1
II
12 13 14 15 16 17 18 19 20 21
N
11
purepython(maxiter, zs, c s ): ..... O bliczanie listy output przy użyciu reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while a bs( z) < 2 and n < m axite r: N
9 10
n += 1 ou t p u t [i ] = n re turn output
Zastosow anie skryptu kernprof.py pow oduje znaczne w ydłużenie czasu działania. W tym przykładzie w ykonanie funkcji calculate_z_serial_purepython zajm uje około 100 sekund. Jest to spory skok w porów naniu z 13 i 19 sekundam i odpow iednio w przypadku użycia pro stych instrukcji print i modułu cProfile. Korzyścią jest to, że możliwe jest przeanalizowanie w iersz po w ierszu tego, jak upływa czas w obrębie funkcji. K olum na % Time je st najbardziej pom ocna. Jak w idać, 36% czasu zajm uje testow anie pętli while. Nie w iem y jednak, czy pierwsza instrukcja (abs(z) < 2) zajm uje więcej czasu niż druga (n < maxiter). W ew nątrz pętli w idać, że aktualizacja liczby z rów nież zajm uje sporo czasu. N aw et instrukcja n += 1 kosztuje w iele czasu! M echanizm dynamicznego wyszukiw ania ję zyka Python działa w przypadku każdej pętli, naw et pom im o tego, że dla każdej zm iennej w każdej pętli używ ane są takie sam e typy. W tym przypadku kom pilow anie i specjalizacja typów (rozdział 7.) zapewnią ogromną poprawę. Tworzenie listy output i aktualizacje w wier szu 20. zajmują stosunkowo mało czasu w porównaniu z pętlą while. Oczywistą m etodą dalszego analizowania instrukcji pętli while jest jej rozbicie. Choć w spo łeczności związanej z językiem Python pojaw iła się dyskusja dotycząca pom ysłu ponownego tworzenia plików .pyc przy użyciu bardziej szczegółowych inform acji na potrzeby wieloczęściowych instrukcji w postaci jednego wiersza, nieznane są nam żadne narzędzia produkcyjne, które oferują bardziej dokładną analizę niż narzędzie line_profiler. W przykładzie 2.7 dokonano rozbicia na kilka instrukcji logiki pętli while. Taka dodatkow a złożoność zw iększy czas działania funkcji, poniew aż będzie w ięcej w ierszy kodu do w yko nania. M oże to jednak być pom ocne w zrozum ieniu kosztów zw iązanych z w ykonyw aniem tej części kodu. Z a n im p r z y jr z y s z s ię k o d o w i, c z y m y ś l i s z , ż e w t e n s p o s ó b d o w i e m y s i ę , j a k i j e s t c z a s w y k o n y w a n ia fu n d a m e n ta ln y c h operacji? C z y in n e cz y n n ik i m o g ą s k o m p lik o w a ć analizę?
48
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Przykład 2.7. Rozbicie złożonej instrukcji pętli while na poszczególne instrukcje w celu zarejestrowania czasu wykonania dla każdej części oryginalnych segmentów kodu $ kernprof.py - l -v j u l i a 1 _ l i n e p r o f i l e r 2 . p y Wrote p r o f i l e r e s u l t s to j u l i a 1 _ l i n e p r o f i l e r 2 . p y . l p r o f Timer u n i t : 1e-06 s F ile : ju lia 1 _ lin e p r o file r2 .p y Function: c alc u la t e _ z _ s e r i a l _ p u re p y t h o n at l i n e 9 Total tim e: 184. 73 9 s Line # Hits Per Hit % Time Line Contents 9 10
@profi l e def c a l c u l a t e z s e r i a l
11 12 13 14 15 16 17 18 19 20
1 1000001 1000000 1000000 1000000 34219980 34219980 34219980 34219980
683 1.0 0.8 0.8 0.9 0.8 0.8 1.0 0.8 0.8
0.0 0.4 0.4 0.5 0.4 14.9 19.0 15.5 15.1
21 22 23 24 25 26
33219980 33219980
1.0 0.9
17.5 15.3
1000000 1000000 1
0.8 0.9 5.0
0.4 0.5 0.0
purepython(maxiter, zs, cs): ..... Obliczanie listy output przy użyciu reguły aktualizacji zbioru Ju lii..... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while True: not y e t escaped = ab s( z ) < 2 i t e r a t i o n s l e f t = n < m ax ite r i f not y e t escaped and i t e r a t i o n s l e f t : z = z * z + c n += 1 else: break o u t p u t [i ] = n re tu rn output
W ykonanie tej w ersji kodu zajm uje 184 sekundy, w przypadku poprzedniej w ersji było to natom iast 100 sekund. Inne czynniki utrudniły analizę. W tym przypadku kod jest spow al niany przez dodatkow e instrukcje, które m uszą zostać w ykonane 34 219 980 razy. Jeśli nie zostałby użyty skrypt kernprof.py do analizowania wiersz po wierszu efektu tej zmiany, z po wodu braku niezbędnego dowodu być może zostałyby wyciągnięte inne wnioski na temat przy czyny spowolnienia. Na tym etapie sensowne jest cofnięcie się o krok, do wcześniejszej metody opartej na m odule tim eit, aby sprawdzić czas wykonywania poszczególnych wyrażeń: >>> z = 0+0j # punkt w środku obrazu >>> %timeit ab s( z) < 2 # testow ane w o b rę b ie p o w łok i IPython 10000000 loo ps , bes t of 3: 119 ns per loop >>> n = 1 >>> m axite r = 300 >>> %timeit n < m axi ter 10000000 loo ps , bes t of 3: 77 ns per loop
Na podstaw ie tej prostej analizy można stwierdzić, że test logiki dla n jest praw ie dwukrotnie szybszy niż wywołanie funkcji abs. Ponieważ w artości dla wyrażeń języka Python są okre ślane zarów no od lewej do prawej strony, jak i oportunistycznie, sensowne jest umieszczenie najszybszego testu po lewej stronie równania. W przypadku jednego testu z każdej grupy 301 testów wykonyw anych dla każdej w spółrzędnej test warunku n < maxiter da w artość False. Oznacza to, że interpreter języka Python nie będzie wymagał określenia wartości drugiej strony operatora and.
Użycie narzędzia line_profiler do pomiarów dotyczących kolejnych w ierszy kodu
|
49
Do momentu określenia dla niego wartości nie wiemy, czy warunek abs(z) < 2 da wartość False. W cześniejsze obserwacje dla tego obszaru przestrzeni zespolonej sugerują, że jest to w artość True dla około 10% czasu w przypadku w szystkich 300 iteracji. Aby dobrze zrozum ieć złożo ność dotyczącą czasu dla tej części kodu, sensow ne byłoby kontynuow anie analizy num e rycznej. W tej sytuacji zależy nam jednak na prostym sprawdzeniu, czy możliwe jest szybkie zwiększenie szybkości kodu. M ożem y sformułować now ą hipotezę, która brzmi: „Zmieniając kolejność operatorów w in strukcji while, osiągniemy solidne przyspieszenie". Choć hipotezę tę można sprawdzić za po m ocą skryptu kernprof.py, dodatkow e obciążenie zw iązane z profilow aniem w ten sposób m oże spow odow ać zbyt dużo zam ieszania. M ożem y w ięc użyć w cześniejszej w ersji kodu, urucham iając test, który porów nuje instrukcję while abs(z) < 2 and n < maxiter z instrukcją while n < maxiter and abs(z) < 2:. W ynik to dość trwały w zrost szybkości w ynoszący około 0,4 sekundy. Oczywiście nie robi on w ielkiego w rażenia, a ponadto w bardzo dużym stopniu pow iązany jest z rozpatryw anym problemem; zastosow anie odpowiedniejszej metody do rozwiązania tego problemu (np. sko rzystanie z narzędzia Cython lub PyPy w sposób opisany w rozdziale 7.) dałoby w iększe w zrosty szybkości. M ożem y być pewni otrzymanego wyniku, ponieważ: • Określono hipotezę prostą do sprawdzenia. • Zm odyfikowano kod w taki sposób, aby była sprawdzana wyłącznie hipoteza (nigdy nie sprawdzaj dwóch rzeczy jednocześnie!). • Uzyskano wystarczający dowód na poparcie wyciągniętego wniosku. Aby w szystko było kompletne, m ożemy uruchom ić skrypt kernprof.py dla dwóch głównych funkcji z uwzględnieniem optymalizacji w celu potwierdzenia, że dysponujemy pełnym ob razem ogólnej złożoności kodu. Po zamianie miejscami w wierszu 17. dwóch składników testu pętli while w przykładzie 2.8 w idać skromne skrócenie czasu wykonywania z 36,1% do 35,9% (taki w ynik utrzymywał się dla pow tarzanych uruchomień). Przykład 2.8. Zmiana miejsca występowania instrukcji pętli while w kodzie liczb zespolonych w celu nieznacznego przyspieszenia testu $ kernprof.py - l -v j u l i a 1 _ l i n e p r o f i l e r 3 . p y Wrote p r o f i l e r e s u l t s to j u l i a 1 _ l i n e p r o f i l e r 3 . p y . l p r o f Timer u n i t : 1e-06 s F ile : ju lia 1 _ lin e p r o file r3 .p y Function: c alc u la t e _ z _ s e r i a l _ p u re p y t h o n a t l i n e 9 Total time: 99.709 7 s Line # Hits Per Hit % Time Line Contents @profi l e def c a l c u l a t e z s e r i a l
50
|
68 31.0 0.8 0.8 0.9 0 .8
0 0 0 0 0
0 8 8 9 8
II
1 1000001 1000000 1000000 1000000
(/>
12 13 14 15 16
N
11
purepython(maxiter z s, cs) ..... O bliczanie listy outputprzy użyciu reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 N
9 10
c = cs[i]
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
17 18 19 20 21
34219980 33219980 33219980 1000000 1
1.0 1.0 0 .8 0 .9 5.0
9 0 9 9 0
while n < m axite r and ab s( z) < 2: z= z * z + c n += 1 o u t p u t [i ] = n re turn output
Zgodnie z oczekiwaniami na podstaw ie danych wyjściowych kodu z przykładu 2.9 widać, że wykonyw anie funkcji calculate_z_serial_purepython zajm uje w iększość czasu (97%) działania jej funkcji nadrzędnej. Dla porównania kroki związane z tworzeniem listy zajm ują znikomą ilość czasu. Przykład 2.9. Testowanie wiersz po wierszu czasu wykonywania programu konfiguracyjnego F ile : ju lia 1 _ lin e p r o file r3 .p y Function: calc_pure_python a t l i n e 24 Total tim e: 195. 21 8 s Line # Hits Per Hit % Time Line Contents 24 25
@ pro fi le def c a l c pure python(draw output, de sired width, max i t e r a t i o n s ) :
44 45 46 47 48
1 1 1001 1001000 1000000
1.0 1.0 1.1 1.1 1.5
0.0 0.0 0.0 0.5 0.8
49
1000000
1.6
0.8
50 51 52 53 54
1 1 1 1
5 1 .0 11.0 6.0 191031307.0
55 56 57
1 1 1
4.0 2.0 5 8 .0
0.0 0.0 0.0
1
9 7 99.0
0.0
0.0 0.0 0.0 97 9
zs = [] cs = [] f o r ycoord in y: f o r xcoord in x: zs.append( complex(xcoord, ycoord )) cs.append( complex(c r e a l , c imag)) p r i n t "Długość dla x : " , le n (x ) p r i n t "Łączna l i c z b a elementów:", l e n (z s s t a r t time = t i m e . ti m e () output = c a l c u l a t e z s e r i a l purepython (max i t e r a t i o n s , z s , cs) end time = ti m e . ti m e () s ec s = end time - s t a r t time p r i n t D zia łan ie f u n k c ji c a l c u l a t e z sser er ia i l_purepy thon .fun c name + " tr w a ł o " , s e c s , "s"
58 59
# sum a ta je s t oczekiw ana d la siatki 1000^2... a s s e r t sum(output) == 33219980
Użycie narzędzia memory_profiler do diagnozowania wykorzystania pamięci Tak jak narzędzie lin e_p rofiler Roberta Kerna dokonuje pomiaru wykorzystania procesora, tak m oduł memory_profiler Fabiana Pedregosa i Philippe'a Gervaisa m ierzy w ykorzystanie pam ięci dla kolejnych wierszy kodu. Zrozum ienie cech kodu związanych z w ykorzystaniem pam ięci umożliwia zadanie sobie następujących dwóch pytań: • Czy możliwe jest użycie mniejszej ilości pamięci RAM przez modyfikację funkcji pod kątem bardziej efektywnego działania? • Czy możliwe jest za pom ocą buforowania użycie większej ilości pamięci RAM i zyskanie cykli procesora?
Użycie narzędzia memory_profiler do diagnozow ania wykorzystania pamięci
|
51
M oduł memory_profiler działa w sposób bardzo podobny do narzędzia line_p rofiler, ale za pewnia znacznie mniejszą w ydajność. Po zainstalowaniu pakietu psutil (opcjonalny, lecz za lecany) moduł memory_profiler będzie działać szybciej. Profilow anie pamięci bez trudu może sprawić, że kod będzie wykonywany od 10 do 100 razy wolniej. W praktyce m oduł ten bę dzie używany sporadycznie, a narzędzie lin e_p rofiler (do profilowania wykorzystania pro cesora) częściej. M oduł memory_profiler zainstaluj za pomocą polecenia pip in stall memory_profiler (opcjonal nie użyj polecenia pip in stall psutil). Jak w spomniano, im plementacja modułu memory_profiler nie zapewnia takiej wydajności jak im plementacja narzędzia line_profiler. A zatem sensowne m oże być wykonyw anie testów za pom ocą modułu dla mniejszego problemu. Testy te zostaną zakończone w rozsądnym czasie. C ałonocne uruchom ienia m ogą m ieć sens w przypadku spraw dzania popraw ności, ale do diagnozowania problemów i określania hipotez dotyczących rozwiązań niezbędne będą szyb kie i rozsądne iteracje. W kodzie z przykładu 2.10 używana jest siatka 1000x1000. W przypadku laptopa jednego z autorów zgromadzenie statystyk zajęło około 1,5 godziny. Przykład 2.10. Wyniki modułu memory_profiler uzyskane dla obu głównych funkcji, które uwidaczniają nieoczekiwane użycie pamięci w funkcji calculate_z_serial_purepython $ python -m memory_profiler julia1_memoryprofiler .py Line #
Mem usage
Increment
Line Contents
9 10
8 9 .934 MiB
0. 00 0 MiB
97 .5 66 M B 130.215 M B 130.215 M B 130.215 M B 130.215 M B 130.215 M B 130.215 M B 130.215 M B 130.215 M B 122.582 M B Mem usage
7.63 3 M B 32 .6 48 M B 0.000 M B 0.000 M B 0.000 M B 0.000 M B 0.000 M B 0.000 M B 0.000 M B -7.633 M B Increment
@ profi le def c a l c u l a t e z s e r i a l purepython(maxiter, zs, cs): ..... O bliczanie listy output przy użyciu... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while n < m axite r and a bs( z) < 2: z = z * z + c n += 1 ou t p u t [i ] = n re tu rn output Line Contents
11 12 13 14 15 16 17 18 19 20 21 Line # 24 25
26 27 28 29 30 31 32 33 34 35 36 37 38
52
|
10.574 MiB - 1 1 2 .0 0 8 MiB
10.574 10.574 10.574 10.574 10.574 10.574 10.574 10.574 10.574 10.582 10.582 10.582
MB MB MB MB MB MB MB MB MB MB MB MB
0.000 0. 000 0. 000 0. 0 00 0. 0 00 0. 0 00 0. 0 00 0. 0 00 0. 0 00 0.0 0 8 0. 0 00 0. 0 00
MB MB MB MB MB MB MB MB MB MB MB MB
@profi l e def calc_pure_python(draw_output, desired _width, m ax_iterations): ..... Tworzenie listy liczb zespolonych. x st ep = ( f l o a t ( x 2 - x1) / . . . y st ep = ( f l o a t ( y 1 - y2) / . . . x = [] y = [] ycoord = y2 while ycoord > y1: y.append(ycoord) ycoord += y step xcoord = x1 while xcoord < x2: x.append(xcoord) xcoord += x step
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
44 45 46 47 48 49 50 51 52 53 54 55
10.582 10.582 8 9 .926 8 9 .926 8 9 .926 8 9 .926
MB MB MB MB MB MB
0.0 0 0 0.0 0 0 79. 344 0.0 0 0 0.0 0 0 0.0 0 0
MB MB MB MB MB MB
8 9 .934 M B 8 9 .934 M B 8 9 .934 M B
0.0 0 8 M B 0.0 0 0 M B 0.0 0 0 M B
122.582 MiB
32 .6 48 MiB
^
zs = [] cs = [] f o r ycoord in y: f o r xcoord in x: zs.append(complex(xcoord, ycoord )) cs.append(complex(c r e a l , c imag)) p r i n t “Długość dla x : 11, le n (x ) p r i n t "Łączna l i c z b a elementów:“ , l e n ( z s ) s t a r t time = ti m e . ti m e () output = c a l c u l a t e z s e r i a l . . . end time = t i m e . ti m e ()
W y m ó g m o d y fik o w a n ia k o d u ź ró d ło w e g o sta n o w i d ro b n y k łop ot. P o d o b n ie ja k w p rz y p a d k u n a rz ęd z ia l i n e _ p r o f i l e r , d e k o ra to r (@ p rofile) słu ż y d o oz n a cz en ia w y b ra n e j fu nk cji. D e k o r a to r s p o w o d u je ro z b icie te s tó w je d n o s tk o w y c h , c h y b a ż e z o stanie u t w o r z o n y fik cy jn y d e k o ra to r (w ięcej in fo rm a c ji z a w ie r a p u n k t „ D e k o r a to r @ p rofile b ez o p era cji").
Gdy zajmujesz się przydzielaniem pamięci, musisz m ieć świadom ość tego, że sytuacja nie jest tak klarowna jak w przypadku wykorzystania procesora. Ogólnie rzecz biorąc, bardziej efek tywne jest nadm ierne przypisanie pamięci do procesu w puli lokalnej, która m oże być uży w ana w dogodnym m om encie, poniew aż operacje przydziału pam ięci zajm ują stosunkow o dużo czasu. Ponadto czyszczenie pam ięci nie następuje od razu, dlatego obiekty m ogą być niedostępne, ale w dalszym ciągu przez jakiś czas mogą znajdow ać się w puli procesu czysz czenia pamięci. Efekt tego jest taki, że trudno w pełni zrozumieć, co dzieje się z wykorzystaniem i zwalnianiem pam ięci w obrębie programu Python. W ynika to stąd, że w iersz kodu m oże nie przydzielić możliwej do określenia ilości pamięci, jaką ustalono poza obrębem procesu. Obserwowanie ogól nego trendu dla zestaw u w ierszy praw dopodobnie doprow adzi do lepszych w niosków niż m onitorowanie zachowania tylko jednego wiersza. Przyjrzyjmy się danym wyjściowym modułu memory_profiler z przykładu 2.10. W ewnątrz funkcji calculate_z_serial_purepython w w ierszu 12. w idać, że przydzielenie 1 000 000 ele m entów pow oduje dodanie do procesu około 7 M B pam ięci RAM 1. N ie oznacza to, że lista output na pewno ma wielkość w ynoszącą 7 M B, ale jedynie to, że proces zwiększył wielkość w przybliżeniu o 7 M B w czasie wewnętrznej alokacji listy. W w ierszu 13. widać, że w obrę bie pętli proces powiększył się w przybliżeniu o kolejne 32 MB. Można to przypisać w yw oła niu funkcji range (śledzenie pam ięci RAM zostało obszerniej omówione w przykładzie 11.1; różnica w wielkościach 7 M B i 32 M B wynika z zawartości dwóch list). W procesie nadrzęd nym w wierszu 46. widoczne jest, że alokacja list zs i cs powoduje wykorzystanie około 79 MB. I tym razem godne uwagi jest to, że niekoniecznie jest to rzeczywista wielkość tablic, ale je dynie w ielkość, o jaką pow iększył się proces po utworzeniu tych list.
1 M o d u ł memory_profiler m i e r z y w y k o r z y s ta n i e p a m i ę c i z g o d n i e z je d n o s tk ą M i B (m eb ib ajt) org aniza c ji In t e r n a ti o n a l E l e c t r o te c h n ic a l C o m m i s s i o n . J e d n o s t k a ta o d p o w i a d a w a r to ś c i 2 20 b a jt ó w . R ó ż n i się t r o c h ę o d p o w s z e c h n i e js z e j, l e c z te ż b a r d z i e j n i e je d n o z n a c z n e j je d n o s t k i M B ( m e g a b a jt m a d w i e o g ó l n i e a k c e p t o w a n e de fi nic je !) . J e d e n M i B o d p o w i a d a 1 , 0 4 8 5 7 6 (lub w p r z y b l i ż e n i u 1,05) M B . Je ś li n i e b ę d z i e m o w y o b a r d z o sp e c y fi c z n y c h w ie lk o ściac h , n a p o t r z e b y o m ó w i e n i a b ę d z i e m y p o s ł u g i w a ć się j e d n o s t k ą M iB .
Użycie narzędzia memory_profiler do diagnozow ania wykorzystania pamięci
|
53
Inna m etoda w izualizacji zm iany w ykorzystania pam ięci polega na próbkow aniu w czasie i prezentowaniu wyniku na wykresie. Narzędzie memory_profiler oferuje program o nazwie mprof, który po pierwsze, służy do próbkowania wykorzystania pamięci, a po drugie, do wi zualizacji próbek. Ponieważ próbkowanie odbywa się w czasie, a nie w oparciu o wiersze kodu, ma znikom y w pływ na działanie kodu. Rysunek 2.6 utw orzono za pom ocą polecenia mprof run julia1_memoryprofiler.py. Tworzy ono plik statystyk, który następnie jest poddawany w izualizacji przy użyciu polecenia mprof plot. Dwie przykładowe funkcje są zawarte w nawiasach kwadratowych. Dzięki temu widoczne jest, w jakim momencie w czasie są one aktywowane, a ponadto możliwe jest obserwowanie wzrostu wykorzystania pam ięci RAM w trakcie ich działania. W obrębie funkcji calculate_z_serial_ purepython widoczny jest ciągły w zrost wykorzystania pamięci RAM podczas jej wykonywania. Jest to spowodowane przez wszystkie niewielkie obiekty (typów int i float), które są tworzone.
Rysunek 2.6. Raport narzędzia memory_profiler wygenerowany przy użyciu programu mprof O prócz obserw ow ania zachow ania na poziom ie funkcji m ożesz dodać etykiety za pom ocą menedżera kontekstów. Fragment kodu z przykładu 2.11 służy do w ygenerowania wykresu z rysunku 2.7. W idoczna jest etykieta create_output_list, która pojaw ia się chwilę po funkcji calculate_z_serial_purepython, pow odując przydzielenie procesow i w iększej ilości pam ięci RAM . Dalej następuje w strzym anie na sekundę. Funkcja tim e.sleep(1) to sztuczny dodatek, którego zadaniem jest ułatw ienie zrozum ienia w ykresu. Po etykiecie create_range_of_zs widoczny jest duży i szybki wzrost wykorzystania pamięci RAM. W zmodyfikowanym kodzie z przykładu 2.11 etykieta ta będzie widoczna podczas tworzenia li sty iterations. Zamiast funkcji xrange użyto funkcji range. Na diagramie powinno być wyraźnie widoczne, że instancja dużej listy 1 000 000 elementów jest tworzona tylko na potrzeby generowa nia indeksu. Ponadto jest to nieefektywne rozwiązanie, które nie będzie skalowane w przypadku list o większym rozmiarze (zabraknie pamięci RAM!). Sama alokacja pamięci użyta w celu utrzy mania tej listy zajmie niewielką ilość czasu, co w przypadku tej funkcji nie da żadnych korzyści.
54
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Rysunek 2.7. Raport narzędzia memory_profiler z etykietami wygenerowanymi przy użyciu programu mprof Przykład 2.11. Użycie menedżera kontekstów do dodania etykiet do wykresu programu mprof @ p r o file def c a lcu la t e _ z _ s e r i a l _ p u r e p y t h o n (m a x i te r , z s , c s ) : O bliczanie listy outputprzy użyciu reguły aktualizacji zbioru J u lii..... with p r o f i le .t im e s ta m p ( " c r e a te _ o u tp u t_ l i s t " ) : output = [0] * l e n ( z s ) tim e.sleep (l) with p r o f i l e .t i m e s t a m p ( " c r e a t e _ r a n g e _ o f _ z s " ) : ite ra tio n s = ran ge(len(zs)) with p r o f i l e . t i m e s t a m p ( " c a l c u l a t e _ o u t p u t " ) : f o r i in i t e r a t i o n s : n = 0 z = zs[i] c = cs[i] while n < m axite r and ab s( z) < 2: z = z * z + c n += 1 o u tp u t[i ] = n re tu rn output
W j ę z y k u P y t h o n 3 z m i a n i e u l e g a d z i a ł a n i e f u n k c j i range. D z i a ł a o n a p o d o b n i e d o f u n k c j i xra nge z j ę z y k a P y t h o n 2. W j ę z y k u P y t h o n 3 f u n k c j a x ra n ge z o s t a ł a w y c o f a n a , a n a r z ę d z i e k o n w e r s j i 2 t o 3 z a j m u j e si ę tą z m i a n ą a u t o m a t y c z n i e .
* W bloku etykiety calculate_output, którego działanie obejm uje w iększość wykresu, widoczny jest bardzo pow olny liniowy w zrost wykorzystania pam ięci RAM. Będzie to efektem użycia w szystkich liczb tym czasow ych w pętlach w ew nętrznych. Zastosow anie etykiet napraw dę pomaga zrozum ieć, gdzie dokładnie jest w ykorzystywana pamięć.
Użycie narzędzia memory_profiler do diagnozow ania wykorzystania pamięci
|
55
Na koniec możemy zm ienić wywołanie funkcji range w wywołanie funkcji xrange. Na rysun ku 2.8 w idać odpowiedni spadek wykorzystania pam ięci RAM w pętli wewnętrznej.
Rysunek 2.8. Raport narzędzia memory_profiler prezentujący efekt zmiany funkcji range na funkcję xrange Aby dokonać pomiaru ilości pamięci RAM używanej przez kilka instrukcji, można skorzystać z funkcji „m agicznej" %memit pow łoki IPython, która działa tak jak funkcja %timeit. W roz dziale 11. przyjrzym y się użyciu funkcji %memit do pom iaru ilości pam ięci w ykorzystyw anej przez listy, a ponadto omówimy różne m etody bardziej efektywnego użycia pam ięci RAM.
Inspekcja obiektów w stercie za pomocą narzędzia heapy Projekt Guppy oferuje narzędzie do inspekcji sterty o nazw ie heapy, które pozwala sprawdzić num er i w ielkość każdego obiektu w stercie interpretera języka Python. W gląd w interpreter i zrozum ienie tego, co znajduje się w pamięci, okazuje się wyjątkowo przydatne w przypad ku rzadkich, lecz trudnych sesji debugowania, gdy napraw dę niezbędne jest określenie liczby używanych obiektów, a także tego, czy są usuwane z pamięci w odpowiednim momencie. Jeśli masz do czynienia z kłopotliwym „przeciekiem " pam ięci (prawdopodobnie spowodowanym odwołaniami do obiektów, które pozostają ukryte w złożonym systemie), narzędzie heapy jest tym, które pozwoli dotrzeć do źródła problemu. Jeśli dokonujesz przeglądu kodu w celu stw ierdzenia, czy generuje przew idyw aną liczbę obiektów, narzędzie to bardzo Ci się przyda. W yniki mogą być zaskakujące i m ogą zapewnić now e możliwości optymalizacji. Aby skorzystać z narzędzia heapy, zainstaluj pakiet guppy za pomocą polecenia pip install guppy.
56
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Listing kodu z przykładu 2.12 to nieznacznie zm odyfikow ana w ersja kodu zbioru Julii. O biekt sterty hpy dołączono do funkcji calc_pure_python. Z ostanie w yśw ietlony stan sterty w trzech m iejscach. Przykład 2.12. Użycie narzędzia heapy do sprawdzenia, jak liczba obiektów zmienia się podczas działania kodu def calc_pure_python(draw_output, desired_widt h, m a x _ i t e r a t i o n s ) : while xcoord < x2: x.append(xcoord) xcoord += x_step from guppy import hpy; hp = hpy() p r i n t "Stan narzędzia heapy po utworzeniu l i s t y i x l i c z b zmiennoprzecinkowych" h = hp.heap() print h p rin t zs = [] cs = [] f o r ycoord in y: f o r xcoord in x: zs.append(complex(xcoord, y c o o rd )) cs.app en d(c om plex(c _re al, c_imag)) p r i n t "Stan narzędzia heapy po utworzeniu l i s t zs i cs przy użyciu l i c z b zespolonych" h = hp.heap() print h p rin t p r i n t "Długość dla x : " , le n (x ) p r i n t "Łączna l i c z b a elementów:", l e n ( z s ) s t a r t _ t i m e = t i m e . ti m e () output = c a l c u la t e _ z _ s e r i a l _ p u r e p y t h o n ( m a x _ i te r a ti o n s , z s , cs) end_time = t i m e . ti m e () se c s = end_time - s t a r t _ t i m e p r i n t D zia łan ie fu n k c j i ca lcu la te_ z_se rial_pure python .f un c_ n am e + " t r w a ł o " , s e c s , "s" p rin t p r i n t "Stan narzędzia heapy po wywołaniu f u n k cji c alc u la t e _ z _ se r i a l _ p u re p y t h o n " h = hp.heap() print h p rin t
Dane w yjściow e z przykładu 2.13 pokazują, że w ykorzystanie pam ięci staje się bardziej in teresujące po utw orzeniu list zs i cs. Z pow odu 2 000 000 obiektów complex zużyw ających 64 000 000 bajtów wykorzystanie pamięci w zrosło w przybliżeniu o 80 M B. Liczby zespolone reprezentują w iększość wykorzystania pam ięci na tym etapie. Jeśli miałoby zostać zoptym a lizow ane zużycie pam ięci w kodzie tego program u, uzyskany w ynik ujaw niłby w szystko, ponieważ pozwala stwierdzić zarówno, ile obiektów jest przechowywanych, jak i jaka jest ich ogólna wielkość. Przykład 2.13. Sprawdzanie danych wyjściowych narzędzia heapy w celu stwierdzenia, jak się zwiększa liczba obiektów na każdym głównym etapie wykonywania kodu
cn cn
$ python julia1_guppy.py Stan narzędzia heapy po utworzeniu l i s t y P a r t i t i o n of a s e t o f 27293 o b j e c t s . Total Index Count % Size % Cumulative 0 10960 40 1050376 31 1050376 1 5768 21 465016 14 1515392 2 1 210856 6 1726248 3 72 0 206784 6 1933032 4 1592 6 203776 6 2136808 5 313 1 201304 6 2338112 6 1557 6 186840 5 2524952
i x l i c z b zmiennoprzecinkowych s i z e = 3416032 b ytes. % Kind ( c l a s s / d i c t o f c l a s s 31 s t r 44 tuple 51 d i c t o f type 57 d i c t o f module 63 types.CodeType 68 d i c t (no owner) 74 fu nc tio n
Inspekcja obiektów w stercie za pomocą narzędzia heapy
|
57
7 199 1 177008 5 2701960 79 type 8 124 0 135328 4 2837288 83 d i e t o f c l a s s 9 1045 4 83600 2 2920888 86 builtin .wrapp er_descri ptor <91 more rows. Type e . g . '_.more' to view.> Stan narzęd zia heapy po utworzeniu l i s t zs i es przy użyciu l i c z b zespolonych P a r t i t i o n of a s e t o f 2027301 o b j e c t s . Total s i z e = 83671256 b ytes. % Cumulative % Kind ( c l a s s / d i e t Index Count % Size 0 2000000 99 64000000 76 64000000 76 complex 1 185 0 16295368 19 80295368 96 l i s t 2 10962 1 1050504 1 81345872 97 s t r 3 5767 0 464952 1 81810824 98 tu ple 4 199 0 210856 0 82021680 98 d i e t o f type 5 72 0 206784 0 82228464 98 d i e t o f module 6 1592 0 203776 0 82432240 99 types.CodeType 7 319 0 202984 0 82635224 99 d i e t (no owner) 8 1556 0 186720 0 82821944 99 fu ncti on 9 199 0 177008 0 82998952 99 type <92 more rows. Type e . g . '_. more ' to view.> Długość dla x: 1000 Łączna l i c z b a elementów: 1000000 D zia łan ie f u n k cji c alc u la t e _ z _ s e r i a l _ p u re p y t h o n trwało 13.2436609268 s Stan narzęd zia heapy po wywołaniu fu n k c j i c alc u la t e _ z _ s e r i a l _ p u re p y t h o n P a r t i t i o n of a s e t o f 2127696 o b j e c t s . Total s i z e = 94207376 b ytes. % % Cumulative % Kind ( c l a s s / d i c t Index Count Si z e 0 2000000 94 64000000 68 64000000 68 complex 1 186 0 24421904 26 88421904 94 l i s t 2 100965 5 2423160 3 90845064 96 int 3 10962 1 1050504 1 91895568 98 s t r 4 5767 0 464952 0 92360520 98 tuple 5 199 0 210856 0 92571376 98 d i e t o f type 6 72 0 206784 0 92778160 98 d i e t of module 7 1592 0 203776 0 92981936 99 types.CodeType 8 319 0 202984 0 93184920 99 d i c t (no owner) 9 1556 0 186720 0 93371640 99 fu nc tio n <92 more rows. Type e . g . '_. more ' to view.>
W trzeciej sekcji po obliczeniu wyniku dla zbioru Julii zostały użyte 94 MB. Oprócz liczb ze spolonych w pam ięci znajduje się obecnie duża kolekcja liczb całkowitych oraz więcej pozycji przechow yw anych na listach. Ponieważ funkcja hpy.setrelheap() może posłużyć do utw orzenia punktu kontrolnego konfi guracji pamięci, kolejne wywołania funkcji hpy.heap() spowodują wygenerowanie delty przy użyciu tego punktu. Dzięki temu możesz uniknąć sprawdzania wewnętrznych mechanizmów interpretera języka Python i wcześniejszej konfiguracji pamięci przed analizowanym miejscem programu.
Użycie narzędzia dowser do generowania aktywnego wykresu dla zmiennych z utworzonymi instancjami Narzędzie dowser Roberta Brewera podłącza się do przestrzeni nazw działającego kodu i za pewnia w przeglądarce internetowej za pośrednictwem interfejsu CherryPy w czasie rzeczy wistym w idok zm iennych z utw orzonym i instancjami. Każdy śledzony obiekt ma powiązany wykres przebiegu w czasie, dlatego m ożesz sprawdzić, czy zw iększają się liczby określonych obiektów. Przydaje się to przy debugowaniu długotrwałych procesów.
58
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Jeśli dla długotrwałego procesu oczekujesz wystąpienia innego zachow ania pamięci zależnie od działań podjętych w aplikacji (np. w przypadku serwera W W W możesz w ysłać dane lub spowodować uruchom ienie złożonych zapytań), może to zostać potw ierdzone interaktywnie. Odpowiedni przykład zaprezentowano na rysunku 2.9.
builtin
.list
Min: 885 Cur: 1117 Max: 1160 T R A C E builtin
.method_descriptor
Min: 718 Cur: 722 Max: 722 T R A C E builtin
.set
Min: 181 Cur: 183 Max: 186 T R A C E
Rysunek 2.9. Kilka wykresów przebiegu w czasie wyświetlonych za pośrednictwem interfejsu CherryPy przy użyciu narzędzia dowser Aby można było skorzystać z narzędzia dowser, do kodu zbioru Julii zostanie dodana w ygod na funkcja (zaprezentow ana w przykładzie 2.14), która m oże uruchom ić serw er interfejsu CherryPy. Przykład 2.14. Funkcja pomocnicza służąca do uruchomienia narzędzia dowser w aplikacji def launch_memory_usage_server(port=8080): import cherrypy import dowser c h e rry p y .tre e .m o u n t(dowser. R o o t ( ) ) c h e r r y p y . c o n fi g . u p d a te ({ ' environm en t': 'embedded' , ' s e r v e r . s o c k e t _ p o r t ' : port }) che rry p y .en g ine .start()
Przed rozpoczęciem intensywnych obliczeń zostanie uruchomiony serwer interfejsu CherryPy (przykład 2.15). Po zakończeniu obliczeń konsola może pozostać otwarta dzięki funkcji time.sleep. W tym przypadku proces interfejsu CherryPy nadal działa, co pozwala kontynuować analizę stanu przestrzeni nazw. Przykład 2.15. Wywoływanie w aplikacji narzędzia dowser w odpowiednim momencie. Powoduje to uruchomienie serwera WWW f o r xcoord in x: zs. append(complex(xcoord, y c oord )) cs.app en d(c om plex(c _re al, c_imag)) launch_memory_usage_server() output = c a l c u la t e _ z _ s e r i a l _ p u r e p y t h o n ( m a x _ i te r a ti o n s , z s , cs) p r i n t " O c z e k iw a n ie .. ." while True: tim e.sleep (l)
Użycie narzędzia dowser do generowania aktywnego wykresu dla zmiennych z utworzonymi instancjami |
59
Odnośniki TRACE w idoczne na rysunku 2.9 pozwalają wyśw ietlić zaw artość każdego obiektu l i s t (rysunek 2.10). M ożliwe jest też dokładniejsze sprawdzenie każdego obiektu li s t . Choć przypomina to użycie interaktywnego debugera w środowisku IDE, możliwe jest do zreali zowania na wdrożonym serwerze bez użycia tego środowiska.
36722880 list list of len 1000: [-1.8. -1.7964. -1.7928. -1.7892. -1.7855999999999999. -1.7819999999999998. -1.778... 36395056 list list of len 1000: [1.8. 1.7964. 1.7928. 1.7892. 1.7855999999999999. 1.7819999999999998. 1.7783999999. 36722016 list list of len
: [(-1.8+1.8j). (1.7964+1.8j). (-1.7928+1.8j). (-1.7892+1.8j). (-1.7855999999999...
36722952 list list of len 1000000: [(-0.62772-0.42193j). (-0.62772-0.42193j). (-0.62772-0.42193j). (-0.62772-0.421...
Rysunek 2.10. Milion pozycji na liście po zastosowaniu narzędzia dowser
^
Preferujemy wyodrębnianie bloków kodu, które mogą być profilowane w kontrolo wanych warunkach. Czasem jest to jednak niepraktyczne. W niektórych sytuacjach będzie po prostu wymagana prostsza diagnostyka. Obserwowanie śledzenia w czasie rzeczywistym działającego procesu może stanowić kompromis dla uzyskania nie zbędnego dowodu bez uciekania się do zaawansowanej inżynierii oprogramowania.
Użycie modułu dis do sprawdzania kodu bajtowego narzędzia CPython Do tej pory dokonano przeglądu różnych metod pomiaru czasu wykonywania kodu Python (w przypadku wykorzystania procesora i pamięci RAM). Nie zajęliśm y się jednak jeszcze ba zowym kodem bajtowym używanym przez m aszynę w irtualną. Zrozum ienie tego, co dzieje się pod podszew ką, ułatw i zbudow anie m odelu pam ięciow ego tego, co odbyw a się w po wolnych funkcjach. Ponadto okaże się pom ocne podczas kom pilowania kodu. Zajm ijm y się zatem kodem bajtowym. M oduł dis umożliwia spraw dzanie bazowego kodu bajtow ego, który jest urucham iany w ob rębie m aszyny w irtualnej narzędzia C Python opartej na stosie. Z rozu m ienie tego, co się dzieje w m aszynie w irtualnej urucham iającej kod Python w yższego poziom u, pozw oli zo rientować się, dlaczego niektóre style tworzenia kodu zapewniają w iększą szybkość niż inne. Ułatw i to rów nież użycie narzędzia takiego jak Cython, które „w ychodzi" poza kod Python i generuje kod C. M oduł dis jest w budowany. Po przekazaniu do niego kodu lub m odułu wyświetli on wyniki dezasemblacji. W przykładzie 2.16 przeprowadzana jest dezasemblacja pętli zewnętrznej funk cji powiązanej z procesorem. Należy spróbować dezasemblacji jednej z własnych funkcji, a następnie podjąć próbę dokładnego prześledzenia tego, jaka jest zgodność kodu poddanego dezasemblacji z danymi wyjściowymi po tej operacji. Czy możesz dopasować przedstawione poniżej dane wyjściowe modułu dis do oryginalnej funkcji?
60
| Rozdział 2.
Użycie profilowania do znajdowania wąskich gardeł
Przykład 2.16. Użycie wbudowanego modułu dis do zrozumienia bazowej maszyny wirtualnej opartej na stosie, która wykonuje kod Python In [ 1 ] : import dis In [ 2 ] : import ju lia 1_n o p il In [ 3 ] : d i s . d i s ( j u l i a 1 _ n o p i l . c a l c u l a t e _ _ s e r i al_purepython) 11 0 LOAD CONST 1 (0) 3 BUILD LIST 1 6 LOAD GLOBAL 0 (len ) 9 LOAD FAST 1 (zs) 12 CALL FUNCTION 1 15 BINARY MULTIPLY 16 STORE_FAST 3 (output) 19 SETUP_LOOP 123 (to 145) 12 22 LOAD GLOBAL 1 (range) 25 LOAD GLOBAL 0 (len ) 28 LOAD FAST 1 (zs) 31 CALL FUNCTION 1 34 CALL FUNCTION 1 37 GET ITER 38 FOR ITER 103 (to 144) 41 STORE_FAST 4 (i) 13 44 LOAD_CONST 1 (0) 47 STORE_FAST 5 (n) # ... # W celu utrzymania zw ięzłości zostanie obcięta reszta p ę tli wewnętrznej! # ... 19 >> 131 LOAD_FAST 5 (n) 134 LOAD_FAST 3 (output) 137 LOAD_FAST 4 (i) 140 STORE_SUBSCR 141 JUMP_ABSOLUTE 38 >> 144 POP_BLOCK 20 >> 145 LOAD_FAST 3 (output) 148 RETURN_VALUE
M im o sw ojej zw ięzłości dane w yjściow e są dość zrozum iałe. Pierw sza kolum na zaw iera num ery w ierszy pow iązane z oryginalnym plikiem . W drugiej kolum nie znajduje się kilka symboli >>. Reprezentują one miejsca docelowe dla punktów skoku gdzieś w kodzie. Trzecia kolumna zawiera adres operacji wraz z nazwą operacji. Czwarta kolumna przechowuje pa rametry operacji. W piątej kolumnie znajdują się adnotacje ułatw iające dopasowanie kodu bajtow ego do oryginalnych parametrów kodu Python. A by dopasow ać kod bajtow y do odpow iedniego kodu Python, cofnij się do przykładu 2.3. Kod bajtow y umieszcza najpierw na stosie stałą w artość 0, a następnie tworzy listę jednoelem entową. Dalej kod przeszukuje przestrzenie nazw w celu znalezienia funkcji len, umieszcza ją w stosie, ponow nie przeszukuje przestrzenie nazw, aby znaleźć listę zs, po czym wstawia ją do stosu. W wierszu 12. kod bajtowy wywołuje funkcję len ze stosu, która używa odwołania do listy zs w stosie, a następnie dla dwóch ostatnich argumentów (długość listy zs i lista jednoelem entow a) stosuje mnożenie binarne i w ynik przechowuje na liście output. Jest to pierw szy w iersz funkcji języka Python, jaką się teraz zajm owaliśm y. Prześledź następny blok kodu bajtowego, aby zrozum ieć działanie drugiego wiersza kodu Python (pętla zewnętrzna for). P u n k t y s k o k u (>>) d o p a s o w u j ą i n s t r u k c je , t a k i e j a k JUMP_ABSOLUTE i POP_JUMP_IF_FALSE. P rzeanalizu j w ła sn ą fu nk cję p o d d a n ą d e z a sem b la cji i d op asu j p u n k t y sk o k u d o in strukcji skoku.
Użycie modułu dis do sprawdzania kodu bajtowego narzędzia CPython
|
61
Po wprowadzeniu do kodu bajtow ego możemy zadać następujące pytanie: „Jak na kod baj towy i czas w ykonyw ania wpływa jaw ne tworzenie funkcji w porównaniu z użyciem w tym samym celu funkcji w budow anych?".
Różne metody, różna złożoność P o w in n a is t n ie ć je d n a , i n a jle p ie j ty lk o je d n a , o c z y w is ta m e to d a z r e a liz o w a n ia c z eg o ś . C h o ć p o c z ą tk o w o m e to d a ta m o ż e n ie b y ć o c z y w is ta , o ile n ie j e s t e ś H o le n d r e m ... — T im P eters, T he Z en o f P y th on
Istnieją różne m etody w yrażania pom ysłów za pom ocą języka Python. C hoć generalnie pow inno być jasne, jaka opcja jest najbardziej rozsądna, jeśli m asz doświadczenie głównie ze starszą w ersją języka Python lub innego języka programowania, m ożesz m ieć na myśli inne rozwiązania. Niektóre z tych metod mogą zapewniać mniejszą w ydajność od innych. W przypadku większości kodu praw dopodobnie bardziej zależy Ci na jego czytelności niż szybkości, aby zespół program istów m ógł efektyw nie tw orzyć kod bez dłuższego zastana w iania się nad w ydajnym , lecz zagm atw anym kodem . Czasam i jed nak w ym agana będzie w ydajność (bez utraty czytelności kodu). W tym przypadku niezbędne może być testowanie szybkości. Przyjrzyj się dwóm fragmentom kodu z przykładu 2.17. Choć oba realizują to samo zadanie, pierwszy z nich wygeneruje mnóstwo dodatkowego kodu bajtowego Python, co spowoduje większe obciążenie. P r z y k ł a d 2 .1 7 . N a i w n y i b a r d z i e j e f e k t y w n y s p o s ó b r o z w i ą z a n i a t e g o s a m e g o p r o b l e m u d o ty c z ą c e g o s u m o w a n ia def fn _ex pres si v e( u pper = 1000000): to t a l = 0 f o r n in xran ge (upper): t o t a l += n re tu rn t o t a l def fn _ te rse ( u p p e r = 1000000): re tu rn sum(xrange(upper)) p r i n t "Funkcje zwracają ten sam wy nik :" , f n _ e x p r e s s i v e ( ) == f n _ t e r s e ( ) Funkcje zwr acają ten sam wynik: True
Obie funkcje obliczają sumę dla zakresu liczb całkowitych. Prosta zasada (m usi jednak zostać poparta użyciem profilowania!) głosi, że w iększa liczba w ierszy kodu bajtow ego będzie wy konywana wolniej niż mniejsza liczba odpowiednich wierszy kodu bajtowego, który korzysta z wbudowanych funkcji. W przykładzie 2.18 użyto funkcji „magicznej" %timeit powłoki IPython do pomiaru najlepszego czasu wykonywania na podstawie zestawu uruchomień. P r z y k ł a d 2 .1 8 . U ż y c ie f u n k c j i % t im e it d o t e s t o w a n ia h i p o t e z y o k r e ś la ją c e j, ż e u ż y c ie f u n k c j i w b u d o w a n y c h p o w i n n o z a p e w n i ć w ię k s z ą s z y b k o ś ć n i ż w p r z y p a d k u n a p is a n ia w ła s n y c h f u n k c j i %timeit fn _ e x p r e s s i v e () 10 loops , b e s t o f 3: 42 ms per loop 100 loops , b est o f 3: 12.3 ms per loop %timeit f n _ t e r s e ( )
62
| Rozdział 2.
Użycie profilowania do znajdowania wąskich gardeł
Jeśli do sprawdzenia kodu dla każdej funkcji zostałby użyty moduł dis (przykład 2.19), oka załoby się, że m aszyna w irtualna ma do w ykonania 17 w ierszy dla bardziej rozbudow anej funkcji i tylko 6 wierszy w przypadku bardzo czytelnej, lecz bardziej zwięzłej drugiej funkcji. Przykład 2.19. Użycie modułu dis do wyświetlenia liczby instrukcji kodu bajtowego objętych dwiema przykładowymi funkcjami import dis p r i n t fn_expressive.func_name d is.dis(fn_exp ressive) fn _exp r essiv e 2 0 LOAD CONST 3 STORE FAST 3 6 SETUP LOOP 9 LOAD GLOBAL 12 LOAD FAST 15 CALL FUNCTION 18 GET ITER >> 19 FOR ITER 22 STORE FAST 4 25 LOAD FAST 28 LOAD FAST 31 INPLACE ADD 32 STORE FAST 35 JUMP ABSOLUTE >> 38 POP BLOCK 5 >> 39 LOAD FAST 42 RETURN VALUE p r i n t fn t e r s e . f u n c name d is .d is (fn terse) fn t e r s e 8 0 LOAD GLOBAL 3 LOAD GLOBAL 6 LOAD FAST 9 CALL FUNCTION 12 CALL FUNCTION 15 RETURN VALUE
1 1 30 0 0 1
(0) (to ta l) (t o 39) (xrange) (upper)
16 2 1 2
(t o 38) (n) (to tal) (n)
1 (total) 19 1 (to tal)
0 (sum) 1 (xrange) 0 (upper) 1 1
Różnica między dwoma blokami kodu jest ewidentna. Wewnątrz funkcji fn_expressive() utrzy mywane są dwie zmienne lokalne, a ponadto w ykonywana jest iteracja dla listy przy użyciu instrukcji for. Pętla for będzie sprawdzana w celu stwierdzenia, czy w yjątek StopIteration w ystąpił w każdej pętli. Każda iteracja stosuje funkcję t o t a l. add , która sprawdzi typ dru giej zmiennej (n) w każdej iteracji. W szystkie te sprawdzenia pow odują niew ielkie obciążenie pod kątem wydajności. W obrębie funkcji fn_terse() wywoływana jest zoptym alizowana funkcja wyrażeń listowych języka C, która potrafi w ygenerow ać w ynik końcow y bez tw orzenia pośrednich obiektów Python. Choć jest to znacznie szybsze, każda iteracja w dalszym ciągu musi dokonywać spraw dzenia typów dodawanych razem obiektów (w rozdziale 4. przyjrzymy się metodom ustalania typu, dzięki czemu nie ma potrzeby sprawdzania go w każdej iteracji). Jak wcześniej w spomniano, konieczne jest profilow anie kodu. Jeśli będzie się polegać jedynie na heurystyce, bez w ątpienia w pew nym m om encie zostanie utw orzony w olniejszy kod. Zdecydow anie w arto dow iedzieć się, czy język Python oferuje krótszą, a jednocześnie nadal zrozum iałą metodę rozwiązania problemu. Jeśli tak będzie, bardziej praw dopodobne jest to, że kod okaże się bardziej czytelny dla innego programisty, czyli prawdopodobnie będzie dzia łać szybciej.
Użycie modułu dis do sprawdzania kodu bajtowego narzędzia CPython
|
63
Testowanie jednostkowe podczas optymalizacji w celu zachowania poprawności Jeśli jeszcze nie stosujesz dla kodu testowania jednostkowego, praw dopodobnie w dłuższej perspektyw ie w płynie to niekorzystnie na Tw oją produktywność. Jeden z autorów (rumieni się) w stydzi się w spom nieć, że raz spędził cały dzień na optym alizow aniu sw ojego kodu przy wyłączonych testach jednostkowych, dlatego że były niewygodne, tylko po to, aby od kryć, że wynikow e znaczne przyspieszenie było spowodowane rozbiciem części ulepszanego przez niego algorytmu. Ani razu nie m usisz popełniać tego błędu. Oprócz testowania jednostkowego należy też pow ażnie rozw ażyć użycie skryptu coverage.py. Umożliwia on stwierdzenie, jakie wiersze kodu są sprawdzane przez testy, a ponadto identy fikuje sekcje bez pokrycia. Skrypt pozwala szybko określić, czy testujesz kod, który zostanie zoptymalizowany. Dzięki temu w szelkie pomyłki, jakie m ogą pojaw ić się w podczas procesu optymalizacji, zostaną szybko wychwycone.
Dekorator @profile bez operacji Testy jednostkow e nie powiodą się z wygenerowanym wyjątkiem NameError, jeśli w kodzie używany jest dekorator @profile z narzędzia lin e_p rofiler lub memory_profiler. Powodem jest to, że środow isko testów jednostkow ych nie w prow adzi dekoratora @profile do lokalnej przestrzeni nazw. Problem ten rozwiązuje przedstaw iony tutaj dekorator bez operacji. Naj prościej, należy go dodać do testowanego bloku kodu i usunąć po zakończeniu testów. Dzięki dekoratorowi bez operacji m ożesz urucham iać testy bez modyfikowania testowanego kodu. Oznacza to, że m ożesz w ykonyw ać testy po każdej profilowanej optymalizacji. Dzięki temu nigdy nie zostaniesz zaskoczony niepoprawnym krokiem optymalizacji. Załóżm y, że istnieje tryw ialny m oduł ex.py zaprezentow any w przykład zie 2.20. M oduł zaw iera test (dla narzędzia nosetests) i funkcję, która była profilowana za pomocą narzędzia lin e_p rofiler lub memory_profil er. Przykład 2.20. Prosta funkcja i przypadek testowy, w którym ma zostać użyty dekorator @profile # ex.py import u n i t t e s t @profi l e def some_fn(nbr): re tu rn nbr * 2 c la s s T estC a se(u n ittest.T estC a se): def t e s t ( s e l f ) : r e s u l t = some_fn(2) s e l f . a s s e r t E q u a l s ( r e s u l t , 4)
Jeśli dla kodu modułu ex.py zostanie uruchomione narzędzie nosetests, zostanie w ygenero wany w yjątek NameError: $ n o se te sts ex.py E ERROR: F a i l u r e : NameError (name ' p r o f i l e ' NameError: name ' p r o f i l e ' Ran 1 t e s t in 0 . 001s FAILED (e rr or s= 1)
64
|
i s not defined)
i s not defined
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
Rozw iązanie polega na dodaniu dekoratora bez operacji na początku modułu kodu ex.py (dekorator m ożesz usunąć po zakończeniu profilowania). Jeśli dekorator @profile nie zostanie znaleziony w jednej z przestrzeni nazw (ponieważ nie jest używane narzędzie lin e_p rofiler lub memory_profiler), zostanie dodana nowo utw orzona wersja dekoratora bez operacji. Jeśli narzędzie line_p rofiler lub memory_profiler wprowadziło do przestrzeni nazw now ą funkcję, ta wersja dekoratora zostanie zignorowana. W przypadku narzędzia line_p rofiler możemy dodać kod z przykładu 2.21. Przykład 2.21. Poprawka dla narzędzia line_profiler dodająca dekorator @profile bez operacji do przestrzeni nazw podczas testowania jednostkowego # w przypadku narzędzia line_ p rofiler i f ' bu iltin ' not in d i r ( ) or not h a s a t t r ( def p r o f i l e ( f u n c ) : def i n n e r ( * a r g s , **k warg s): re tu rn f u n c ( * a r g s , **kwargs) r e tu r n inner
builtin
,
'p r o file '):
F u n k c ja b u iltin test jest przeznaczona dla narzędzia nosetests, a test h asattr służy do identyfikowania faktu wprowadzenia dekoratora @profile do przestrzeni nazw. Tym razem m ożemy pom yślnie uruchom ić narzędzie nosetests dla przykładowego kodu: $ kernprof.py -v - l ex.py Line # Hits Time 11 12 13 1 $ n o s e t e s t s ex.py
Per %%HTMLit
3
3.0
% Time
100.0
Line Contents
@ profi le def some_fn(nbr): re tu rn nbr * 2
Ran 1 t e s t in 0. 00 0s
W przypadku narzędzia memory_profiler używamy kodu z przykładu 2.22. Przykład 2.22. Poprawka dla narzędzia memory_profiler dodająca dekorator @profile bez operacji do przestrzeni nazw podczas testowania jednostkowego # dla narzędzia m em ory_profiler i f ' p r o f i l e ' not in d i r ( ) : def p r o f i l e ( f u n c ) : def i n n e r ( * a r g s , **k warg s): re tu rn f u n c ( * a r g s , **kwargs) r e tu r n inner
M ożna oczekiwać wygenerowania następujących danych wyjściowych: python -m memory_profiler ex.py Line #
Mem usage
11 10.8 09 MiB 12 13 10.8 09 MiB $ n o s e t e s t s ex.py
Increment
Line Contents
0.0 0 0 Mi B
@ p r o fi le de f some_fn(nbr): re tu rn nbr * 2
0.0 0 0 MiB
Ran 1 t e s t in 0.0 0 0
C hoć zrezygnow anie z użycia tych dekoratorów pozw oli zyskać kilka m inut, po straceniu w ielu godzin na przeprow adzenie niewłaściwej optymalizacji, która powoduje, że kod prze staje działać, postanowisz uwzględnić dekoratory w przepływ ie pracy.
Testowanie jednostkowe podczas optymalizacji w celu zachowania poprawności
| 65
Strategie udanego profilowania kodu Profilowanie wymaga trochę czasu i koncentracji. Oddzielenie sekcji przeznaczonej do testo wania od głównego segmentu kodu pozwoli zwiększyć szanse na zrozumienie kodu. Aby za chować poprawność, m ożesz następnie w ykonać dla kodu test jednostkowy, a ponadto prze kazać do niego dane w ygenerow ane w rzeczyw istych w arunkach w celu zidentyfikow ania niewydajnych instrukcji. Pamiętaj o w yłączeniu wszelkich akceleratorów bazujących na BlOS-ie, ponieważ spowodują one tylko niejasność uzyskanych wyników. W przypadku laptopa jednego z autorów funkcja Intel TurboBoost może tymczasowo przyspieszyć działanie procesora ponad jego normalną, m aksym alną szybkość, jeśli jest wystarczająco chłodny. Oznacza to, że procesor w takim sta nie może uruchom ić ten sam blok kodu szybciej, niż wtedy, gdy układ jest nagrzany. System operacyjny m oże też kontrolow ać szybkość zegara. Laptop zasilany akum ulatorow o praw dopodobnie będzie bardziej agresyw nie kontrolow ać szybkość procesora niż laptop podłą czony do zasilania sieciow ego. Aby utw orzyć bardziej stabilną konfigurację do testów po równawczych, wykonaj następujące czynności: • W yłącz w BlO S-ie funkcję TurboBoost. • Wyłącz funkcję systemu operacyjnego, która nadpisuje funkcję SpeedStep (jeśli masz moż liwość zarządzania BIOS-em, znajdziesz w nim tę funkcję). • Używaj wyłącznie zasilania sieciowego (nigdy akumulatorowego). • Podczas eksperymentowania wyłącz narzędzia działające w tle, takie jak narzędzia two rzące kopie zapasow e i Dropbox. • W ielokrotnie przeprowadzaj eksperymenty, aby uzyskać stabilny pomiar. • Jeśli to możliwe, przejdź na poziom uruchamiania 1 (system Unix), aby nie działały żadne inne zadania. • By mieć całkowitą pewność wyników, zrestartuj komputer i ponownie przeprowadź eks perymenty. Spróbuj określić hipotezę dla oczekiwanego działania kodu, a następnie potwierdź (lub obal!) jej popraw ność przy użyciu wyników kroku profilowania. Choć opcje w yboru nie zm ienią się (decyzje m ożesz pod jąć tylko na podstaw ie w yników profilow ania), zw iększy się poziom intuicyjnego rozum ienia kodu, co przyniesie pozytyw ne efekty w przyszłych projektach, poniew aż z w iększym praw dopodobieństw em podejm iesz decyzje zapew niające w iększą w ydajność. O czyw iście decyzje te będą w eryfikow ane w trakcie działań z w ykorzystaniem profilowania. Nie żałuj czasu na przygotowania. Jeśli spróbujesz przetestow ać kod pod kątem wydajności głęboko wewnątrz większego projektu bez oddzielenia od niego tego kodu, prawdopodobnie doświadczysz efektów ubocznych, które zaprzepaszczą cel starań. W przypadku wprowadza nia bardziej szczegółowych zmian trudniejsze będzie użycie testu jednostkowego dla większego projektu. M oże to dodatkowo komplikować działania. Efekty uboczne m ogą obejm ować inne w ątki i procesy w pływ ające na w ykorzystanie procesora i pam ięci oraz na funkcjonow anie sieci i dysków. Spowoduje to, że wyniki nie będą do końca wiarygodne.
66
| Rozdział 2.
Użycie profilowania do znajdowania wąskich gardeł
W przypadku serwerów W W W skorzystaj z narzędzi dowser i dozer. Um ożliw iają one wizu alizację w czasie rzeczywistym działania obiektów w przestrzeni nazw. Jeśli to m ożliwe, zde cydowanie rozważ oddzielenie kodu przeznaczonego do przetestowania od głównej aplikacji internetowej, ponieważ znacznie przyspieszy to profilowanie. Upewnij się, że testy jednostkowe sprawdzają wszystkie ścieżki kodu w analizowanym kodzie. W szystko, co nie jest testowane, ale jest używ ane w testach porównawczych, m oże spowo dować subtelne błędy, które spowolnią działania. Użyj skryptu coverage.py, aby potwierdzić, że testy obejmują wszystkie ścieżki kodu. U trudniony m oże być test jednostkow y skom plikow anej sekcji kodu, który generuje dużo numerycznych danych wyjściowych. Nie obawiaj się kierowania danych wyjściowych do pliku tekstowego wyników w celu przetworzenia go przez narzędzie d iff lub użycia obiektu pickled. W przypadku problem ów z optymalizacją num eryczną jeden z autorów lubi tworzyć długie pliki tekstowe z liczbami zmiennoprzecinkowymi i korzystać z narzędzia d iff. Mniejsze błędy zaokrąglania pojawiają się natychmiast, nawet jeśli występują rzadko w danych wyjściowych. Jeśli kodu m ogą dotyczyć problem y z zaokrąglaniem liczb z pow odu niew ielkich zm ian, lepszym rozwiązaniem jest użycie dużej ilości danych wyjściowych, które m ogą zostać użyte przed porównaniem i po nim. Przyczyną błędów zaokrąglania jest różnica w precyzji liczb zm iennopozycyjnych, jaka w ystępuje m iędzy rejestram i procesora i głów ną pam ięcią. Uru chom ienie kodu z wykorzystaniem innej ścieżki kodu może spowodować subtelne błędy za okrąglania, które później m ogą w yw ołać niejasności. Lepiej m ieć tego św iadom ość od razu, gdy takie błędy się pojawią. Oczywiście podczas profilowania i optymalizowania sensowne jest użycie narzędzia do kon trolowania kodu źródłowego. Stosowanie rozgałęzień nie jest kosztowne, a zapewnia spokój.
Podsumowanie Po zaznajomieniu się z technikami profilowania masz do dyspozycji wszystkie narzędzia, jakie są niezbędne do identyfikowania w kodzie wąskich gardeł związanych z w ykorzystaniem procesora i pam ięci RAM. W następnym rozdziale dowiesz się, jak w języku Python im ple m entow ane są najbardziej typow e kontenery. Dzięki temu będziesz w stanie podejm ow ać rozsądne decyzje dotyczące reprezentowania w iększych kolekcji danych.
Podsumowanie
|
67
68
|
Rozdział 2. Użycie profilowania do znajdowania wąskich gardeł
ROZDZIAŁ 3.
Listy i krotki
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Kiedy przydają się listy i krotki? • Jaka jest złożoność wyszukiwania w przypadku listy/krotki? • Jak ta złożoność jest osiągana? • Jakie są różnice między listami i krotkami? • Na czym polega dołączanie do listy? • Kiedy należy używ ać list i krotek?
Jedną z najw ażniejszych rzeczy zw iązanych z pisaniem w ydajnych program ów jest zrozu m ienie tego, co zapewniają używ ane struktury danych. O kazuje się, że program owanie pod kątem wydajności w dużej m ierze sprowadza się do określenia, jakie pytania próbujesz zadać w odniesieniu do danych. W ybór struktury danych pozwala na szybkie udzielenie odpowiedzi na te pytania. W rozdziale będzie mowa o rodzajach pytań, na jakie listy i krotki mogą szybko zapewnić odpowiedzi. Zostanie też wyjaśnione, w jaki sposób się to odbywa. Listy i krotki to klasa struktur danych nazywanych tablicami. Tablica to po prostu zwykła lista danych z pewnym wewnętrznym uporządkowaniem . W iedza a priori o uporządkowaniu ma duże znaczenie: gdy wie się, że dane w tablicy znajdują się w konkretnym położeniu, można je pobrać w czasie O(1)! Ponadto tablice m ogą być im plem entow ane na w iele sposobów . W ytycza to inną linię podziału m iędzy listam i i krotkam i: listy są tablicam i dynam icznym i, krotki natom iast to tablice statyczne. R ozszerzm y trochę pow yższe inform acje. Pam ięć system ow ą kom putera m ożna traktow ać jako serię ponum erow anych pojem ników , z których każdy m oże przechow yw ać num er. Numery mogą służyć do reprezentowania dowolnych używanych zmiennych (liczb całkowi tych, liczb zmiennopozycyjnych, łańcuchów lub innych struktur danych), ponieważ są one prostym i odwołaniami do położenia danych w pam ięci1.
1 W k o m p u te r a c h 64- b ito w ych p a m i ę ć o w ielkośc i 12 k B za p e w n i a 725 p o jem n ik ó w , a 52 G B p am ięc i ud ost ę pnia 3 25 0 000 000 p oje m nikó w !
69
Gdy w ym agane jest utw orzenie tablicy (czyli listy lub krotki), najpierw m usisz przydzielić blok pamięci systemowej (każda sekcja takiego bloku będzie używana jako w skaźnik do rze czyw istych danych w yrażany za pom ocą liczby całkow itej). W iąże się to z użyciem jądra, podprocesu system u operacyjnego i zażądaniem zastosow ania N ciągłych pojem ników . Na rysunku 3.1 pokazano przykład struktury pamięci systemowej dla tablicy (w tym przypadku jest to lista) o wielkości wynoszącej 6 . Zauważ, że w języku Python listy przechow ują też in formację o swojej wielkości, dlatego z sześciu przydzielonych bloków tylko pięć nadaje się do użycia. Pierwszy element określa długość tablicy.
Rysunek 3.1. Przykład struktury pamięci systemowej dla tablicy o wielkości równej 6 Aby w yszukać dow olny w ybrany elem ent listy, m usisz po prostu w iedzieć, jaki to ma być element, a ponadto pamiętać, od jakiego pojemnika rozpoczynają się dane. Ponieważ wszystkie dane zajmują tyle samo miejsca (czyli jeden „pojemnik" lub, dokładniej ujmując, jeden wskaź nik do rzeczywistych danych wyrażany za pomocą liczby całkowitej), do wykonania obliczeń nie są w ymagane żadne informacje o typie przechow yw anych danych. J e ś l i b y ł o b y w i a d o m o , g d z i e w p a m i ę c i r o z p o c z y n a s i ę l i s ta N e l e m e n t ó w , w j a k i s p o s ó b m o ż n a b y b y ł o z n a l e ź ć d o w o l n y e l e m e n t n a l iście ?
Jeśli na przykład konieczne byłoby pobranie pierw szego elementu tablicy, po prostu należa łoby przejść do pierwszego pojemnika w sekwencji Mi odczytać znajdującą się w nim wartość. Jeśli z kolei niezbędny byłby piąty elem ent tablicy, należałoby przejść do pojemnika na pozy cji M+5 i odczytać jego zawartość. Ogólnie rzecz biorąc, w celu pobrania elementu i z tablicy należy przejść do pojemnika M+i. Oznacza to, że dzięki przechowywaniu danych w kolejnych pojemnikach i dysponowaniu wiedzą na tem at uporządkowania danych m ożliw e jest zloka lizowanie danych przy użyciu informacji o tym, jaki pojem nik ma zostać sprawdzony w jed nym kroku lub w czasie O(1) (niezależnie od wielkości tablicy; przykład 3.1). Przykład 3.1. Określanie czasu dla wyszukiwań list o różnej wielkości >>> %%timeit l = range(10) l [5] 10000000 loo ps, b est o f 3: 7 5 .5 ns per loop >>> >>> %%timeit l = ran ge (10000000) . . . : l [100000] 10000000 loo ps, b est o f 3: 7 6 .3 ns per loop
70
|
Rozdział 3. Listy i krotki
Co będzie w przypadku tablicy o nieznanym uporządkowaniu, z której ma zostać pobrany konkretny element? Jeśli porządkow anie byłoby znane, można byłoby po prostu w yszukać tę konkretną w artość. Jednakże w tym przypadku konieczna jest operacja search. N ajprostsze rozwiązanie tego problemu jest określane mianem wyszukiw ania liniowego, w którym prze prowadzana jest iteracja każdego elementu tablicy, a ponadto sprawdzane jest, czy wartość jest żądaną (przykład 3.2). Przykład 3.2. Wyszukiwanie liniowe listy def l i n e a r _ s e a r c h ( n e e d l e , a r r a y ) : f o r i , item in enum er ate(a rra y): i f item == need le: re tu rn i re tu rn -1
W najgorszym przypadku algorytm ten ma wydajność O(n). Ma to miejsce przy szukaniu ele mentu, którego nie ma w tablicy. Aby stwierdzić, że szukany element nie znajduje się w tablicy, m usimy najpierw sprawdzić to przy użyciu każdego z pozostałych elementów. Ostatecznie zostanie osiągnięta końcowa instrukcja return -1 . Okazuje się, że algorytm ten to właśnie al gorytm używ ający funkcji list.in d e x (). Jedyną m etodą zw iększenia szybkości jest zrozum ienie, jak dane są um ieszczane w pam ięci, lub uporządkow anie przechow yw anych pojem ników danych. Problem ten, przez pominięcie oryginalnego uporządkowania danych i określenie innego bardziej nietypowego, rozwiązują w czasie O(1) tabele mieszające, które są fundamentalną strukturą danych obsługującą słow niki i zbiory. Ewentualnie, jeśli dane są sortowane w taki sposób, że każdy element jest większy (lub mniejszy) od swojego sąsiada po lewej (lub prawej) stronie, m ożna zastosow ać specjali styczne algorytmy wyszukiwania, które m ogą skrócić czas wyszukiw ania do czasu O(log n). Choć w przypadku wcześniej opisanych w yszukiw ań o stałym czasie m oże się to w ydać krok niem ożliw y do w ykonania, czasem jest to najlepsza opcja (zw łaszcza dlatego, że algorytm y w yszukiw ania są bardziej elastyczne i um ożliw iają definiow anie w yszukiw ań przy użyciu kreatywnych sposobów). D l a n a s t ę p u j ą c y c h d a n y c h n a p i s z a l g o r y t m , k t ó r y z n a j d u j e i n d e k s w a r t o ś c i 61: [9,
18,
18,
1 9, 2 9 , 4 2 , 5 6 , 6 1 , 8 8 , 95]
Ja k m o ż e s z p rz y s p ie s z y ć tę operację, g d y z n a n y jest s p o s ó b u p o r z ą d k o w a n ia d an y c h ?
Wskazówka:
J e ś li p o d z i e l i s z ta b l i c ę n a p ó ł , s t w i e r d z i s z , ż e w s z y s t k i e w a r t o ś c i p o le w e j
stron ie s ą m n i e js z e o d n a jm n i e js z e g o e l e m e n t u z b i o r u p o p r a w e j stronie. S k orz ystaj z tego!
Bardziej efektywne wyszukiwanie Jak w cześniej wspomniano, możliwe jest uzyskanie większej wydajności wyszukiwania, gdy najpierw dane zostaną tak posortowane, że w szystkie elem enty po lewej stronie konkretnego elementu będą mniejsze (lub większe) od niego. Porównanie jest dokonywane za pomocą funkcji „m ag iczny ch " eq i l t obiektu, a w przypadku używania obiektów niestandar dowych operacja ta może być definiowana przez użytkownika. Bez fu n k c ji
eq
i
11
o b ie k t n ie s ta n d a r d o w y b ę d z ie p o ró w n y w a n y je d y n ie
z o b ie k ta m i tego s a m e g o ty pu, a p o ró w n a n ie b ę d z ie w y k o n y w a n e z w y k o rz y s ta n ie m lo k o w a n ia instancji w p am ięci.
i
Bardziej efektywne wyszukiwanie
|
71
Dwa niezbęd ne składniki to algorytm sortow ania i algorytm w yszukiw ania. Listy języka Python m ają w budow any algorytm sortowania, który korzysta z algorytmu Timsort. Algo rytm ten umożliwia sortowanie listy w czasie O(n) w najlepszym przypadku. W najgorszym przypadku jest to czas O(n log n). Taką wydajność algorytm osiąga przez wykorzystanie wielu typów algorytmów sortowania i użycie heurystyki, aby określić, jaki algorytm dla konkret nych danych sprawdzi się najlepiej (dokładniej rzecz biorąc, jest to kombinacja algorytmów sortowania ze wstawianiem i scalaniem). Po poddaniu listy sortowaniu można znaleźć żądany elem ent przy użyciu wyszukiwania bi narnego (przykład 3.3), które ma średnią złożoność O(log n). Taka w ydajność jest osiągana przez spraw dzenie najpierw środka listy i porów nanie w ybranej w artości z żądaną. Jeśli w artość środkowego elementu jest m niejsza od żądanej w artości, rozpatryw ana jest prawa połowa listy, po czym kontynuowane jest dzielenie listy na pół do momentu znalezienia warto ści lub stwierdzenia, że wartość na pewno nie w ystępuje na posortowanej liście. W rezultacie nie jest w ym agane odczytanie w szystkich wartości na liście, co było niezbędne w przypadku wyszukiwania liniowego. Odczytywany jest jedynie niewielki podzbiór wartości. Przykład 3.3. Efektywne wyszukiwanie posortowanej listy — wyszukiwanie binarne def b in a r y _ se a r c h (n e e d l e , h aysta c k): imin, imax = 0, len (h ayst ack ) while True: i f imin >= imax: r e tu r n -1 midpoint = (imin + imax) // 2 i f haystack[midpoint] > needle: imax = midpoint e l i f haystack[midpoint] < needle: imin = midpoint+1 else: r e tu r n midpoint
Metoda ta umożliwia znalezienie elementów na liście bez uciekania się do potencjalnie złożonego rozwiązania słownikowego. Szczególnie w przypadku przetwarzania listy danych, która sama w sobie jest posortowana, w celu znalezienia obiektu na liście i uzyskania złożoności wyszukiwania O(log n) bardziej efektywne będzie po prostu wyszukiwanie binarne zam iast przekształcania danych w słownik, a następnie wykonywania dla niego wyszukiwania. Choć wyszukiw anie w słowniku zajmuje czas O(1), przekształcenie w słownik zajmuje czas O(n), a ograniczenie słowni ka w postaci braku możliwości występowania powtarzających się kluczy może być niepożądane. Ponadto moduł bisect znacznie upraszcza ten proces, oferując proste metody dodawania ele mentów do listy podczas sortowania jej. M oduł zapewnia też znajdow anie elementów przy użyciu m ocno zoptymalizowanego wyszukiw ania binarnego. W tym celu m oduł udostępnia alternatywne funkcje, które dodają elem ent do popraw nie posortowanej listy. W przypadku zaw sze sortow anej listy z łatw ością m ożna znaleźć szukane elem enty (odpow iednie przy kłady są dostępne w dokum entacji modułu bisect (https://docs.python.org/2/library/bisect.htm l)). Dodatkowo m oduł ten pozwala bardzo szybko znaleźć elem ent najbliższy szukanemu (przy kład 3.4). M oże to być bardzo przydatne przy porównywaniu dwóch zbiorów danych, które są do siebie podobne, lecz nie są identyczne. Przykład 3.4. Znajdowanie bliskich wartości na liście za pomocą modułu bisect import b i s e c t import random def f i n d _ c l o s e s t ( h a y s t a c k , n eed le ): # fu n kcja bisect.bisect_left zw róci p ierw szą w artość w tablicy haystack, # która je s t w iększa o d w artości needle
72
|
Rozdział 3. Listy i krotki
i = b i s e c t . b i s e c t _ l e f t ( h a y s t a c k , needle) i f i == l e n ( h a y s t a c k ) : re tu rn i - 1 e l i f h a y s ta c k [ i ] == needle: re tu rn i e l i f i > 0: j = i - 1 # pon iew aż wiadom o, że w artość je s t w iększa niż w artość needle (i odw rotnie w przypadku # w artości w j), nie je s t konieczne użycie w tym m iejscu w artości bezwzględnych i f h a y s ta c k [ i ] - needle > needle - h a y s t a c k [ j ] : re tu rn j re tu rn i important_numbers = [] f o r i in x ra n g e (1 0 ): new_number = ra ndom.randin t(0, 1000) b i s e c t. i n s o r t(i m p o r t a n t _ n u m b e r s , new_number) # lista important_numbers będzie ju ż uporządkow ana, p on iew aż w staw iono now e elementy # za p o m o c ą fu n kcji bisect.insort p r i n t important_numbers c l o s e s t _ i n d e x = fi n d_close st (i m p ort an t_ n um b er s, -250) p r i n t "N a jb li ż s z a wartość dla -2 5 0 : " , important_numbers[closest_index] c l o s e s t _ i n d e x = fi n d_close st (i m p ort an t_ n um b er s, 500) p r i n t "N a jb li ż s z a wartość dla 500: " , important_numbers[closest_index] c l o s e s t _ i n d e x = fi n d_close st (i m p ort an t_ n um b er s, 1100) p r i n t "N a jb li ż s z a wartość dla 1100: " , im po rtant_nu mbers[closest_index]
Ogólnie rzecz biorąc, ma to zw iązek z fundamentalną regułą pisania efektywnego kodu: wy bierz właściw ą strukturę danych i trzymaj się jej! Choć dla konkretnych operacji m ogą istnieć bardziej efektywne struktury danych, koszt przekształcenia w nie może zniw eczyć jakikol wiek w zrost wydajności.
Porównanie list i krotek Czym się różnią listy i krotki, jeśli korzystają z tej samej bazowej struktury danych? Oto pod sumowanie głównych różnic: 1. Listy to tablice dynamiczne. M ogą się zm ieniać i m ożliwa jest zmiana ich w ielkości (zmia na liczby przechow yw anych elementów). 2. Krotki są tablicami statycznymi. Krotki nie m ogą się zm ieniać, a zaw arte w nich dane nie mogą zostać zm odyfikowane po utworzeniu krotki. 3 . Krotki są buforow ane przez środow isko uruchom ieniow e interpretera języka Python. Oznacza to, że nie jest w ymagana komunikacja z jądrem w celu zarezerw ow ania pamięci każdorazowo, gdy ma zostać użyta. W ym ienione różnice określają ideową odmienność obu struktur: krotki służą do opisywania wielu właściwości jednej rzeczy, która nie ulega zmianie, listy natom iast m ogą być używane do przechow yw ania kolekcji danych dotyczących różnych obiektów. Na przykład składniki numeru telefonu idealnie nadają się do tego, by zastosować krotkę: nie będą się zmieniać, a jeśli to nastąpi, powstała kombinacja będzie reprezentow ać nowy obiekt lub inny num er telefonu. Podobnie współczynniki wielomianu są odpowiednie dla krotki, ponieważ inne w spółczyn niki reprezentują inny wielomian. Z kolei imiona osób czytających aktualnie tę książkę lepiej kw alifikują się do użycia listy. C hoć takie dane n ieustannie się zm ien iają zarów no pod w zględem zaw artości, jak i wielkości, zaw sze reprezentują to samo.
Porównanie list i krotek
|
73
Godne uwagi jest to, że listy i krotki mogą korzystać z m ieszanych typów. Jak się okaże, m o że to pow odow ać całkiem spore obciążenie i ograniczać potencjalne optymalizacje. Obciąże nie to może zostać wyeliminowane, jeśli zostanie wymuszone, aby w szystkie dane były tego samego typu. W rozdziale 6 . będzie mowa o zmniejszaniu za pom ocą narzędzia numpy zarów no ilości używ anej pam ięci, jak i obciążenia zw iązanego z obliczeniam i. Inne pakiety, takie jak b l i s t i array, rów nież m ogą zredukow ać takie obciążenia w przypadku innych sytuacji niemających zw iązku z obliczeniami numerycznymi. Naw iązuje to do głównej kwestii, jaka pojawia się w przypadku programowania pod kątem wydajności i zostanie omówiona w dal szych rozdziałach. Chodzi m ianow icie o to, że podstaw ow y kod jest znacznie wolniejszy niż kod napisany specjalnie w celu rozwiązania konkretnego problemu. Poza tym niezmienność krotki, w przeciwieństwie do listy, której wielkość może być modyfiko wana, pozwala uzyskać bardzo prostą strukturę danych. Oznacza to, że podczas przechowywa nia krotek w pamięci pamięć nie jest nadmiernie obciążana, a operacje na krotkach są dość przej rzyste. Jak się dowiesz, zmienność list wiąże się z większym wykorzystaniem pamięci niezbędnej do ich przechowywania, a ponadto z dodatkowymi obliczeniami wymaganymi do obsługi list. C z y d l a p o d a n y c h p o n i ż e j p r z y k ł a d o w y c h z b i o r ó w d a n y c h u ż y ł b y ś k r o t k i , c z y listy ? U z a sa d n ij dlaczego. 1 . P ie rw sz e 2 0 liczb p ierw sz y ch . 2 . N a z w y ję z y k ó w p ro g ram o w an ia. 3 . W iek, w a g a i w zro st osoby. 4 . D z ie ń i m iejsce u ro d z e n ia oso b y. 5 . W y n ik k o n k retn eg o zakład u. 6 . W y n ik i k o lejn y ch serii z a k ła d ó w . R o zw iązan ie: 1 . K r o tk a , p o n i e w a ż d a n e s ą s t a t y c z n e i n i e b ę d ą s i ę z m i e n i a ć . 2 . L is ta , p o n i e w a ż z b i ó r d a n y c h n i e u s t a n n i e s i ę z w i ę k s z a . 3 . L is ta , p o n i e w a ż w a r t o ś c i b ę d ą w y m a g a ć z a k t u a l i z o w a n i a . 4 . K r o tk a , p o n i e w a ż i n f o r m a c j e s ą s t a t y c z n e i n i e b ę d ą s i ę z m i e n i a ć . 5 . K r o tk a , p o n i e w a ż d a n e s ą s t a t y c z n e . 6 . L is ta , p o n i e w a ż b ę d z i e o b s t a w i a n y c h w i ę c e j z a k ł a d ó w ( o k a z u j e się, ż e m o ż l i w e b y ło b y u ż y c ie listy krotek , b o c h o ć n ie b ę d ą się z m ie n ia ć m e ta d a n e k a ż d e g o p o szczegó ln ego zakład u, p rz y o b sta w ia n iu kolejny ch z a k ła d ó w ko n iecz n e będ zie d o d a w a n ie d alszy ch m etad an ych ).
Listy jako tablice dynamiczne Po utw orzeniu listy w razie potrzeby możesz swobodnie zm ienić jej zawartość: >>> >>> >>> [5 ,
numbers = [5 , 8 , 1, 3 , 2, 6] numbers[2] = 2*numbers[0] # O numbers 8 , 10, 3, 2 , 6]
O Jak zostało w cześniej opisane, czas takiej operacji wynosi O(1) , ponieważ od razu można znaleźć dane przechow yw ane w zerowym i drugim elemencie.
74
|
Rozdział 3. Listy i krotki
Dodatkowo możesz dołączyć do listy nowe dane i zw iększyć jej wielkość: >>> 6 >>> >>> [5 , >>> 7
len(numbers) numbers.append(42) numbers 8 , 10, 3 , 2 , 6 , 42] len(numbers)
Jest to możliwe, ponieważ tablice dynamiczne obsługują operację resize, która zwiększa po jem ność tablicy. Gdy po raz pierw szy do listy o wielkości Nzostanie dołączony element, in terpreter języka Python musi utw orzyć now ą listę, która jest wystarczająco duża, aby prze chow yw ać pierw otną liczbę N elem entów , a oprócz tego d ołączone dane. Z am iast jed nak przydzielać N+1 elementów, przydziela się w rzeczywistości Melementów, gdzie M > N. Ma to na celu zapewnienie dodatkowego miejsca dla przyszłych operacji dołączania. Dane ze starej listy są kopiow ane do now ej listy, po czym stara lista jest usuw ana. Idea tego jest taka, że jedna operacja dołączania stanowi prawdopodobnie początek wielu takich operacji. Żądając dodatkowego miejsca, możemy zmniejszyć liczbę koniecznych wystąpień operacji przydzielania, a tym samym całkowitą liczbę niezbędnych kopii w pamięci. Jest to dość istotne, gdyż kopie w pamięci pow odują spore obciążenie, zw łaszcza gdy zaczyna się zw iększać w ielkość listy. Na rysunku 3.2 pokazano, jak takie nadm ierne przydzielanie przebiega w przypadku inter pretera języka Python 2.7. Przykład 3.5 zawiera równanie określające ten w zrost wielkości. Nadmierne przydzielanie dla list
w«# ..............................
]............................. ].........
i
3c
le e e
£
.5
£11
tl
----6
‘ 2906
'
*
: ■ •1606
■
-
6666
-
* 6666
Wielkość listy
Rysunek 3.2. Wykres prezentujący, ile dodatkowych elementów przydzielanych jest do listy o konkretnej wielkości
Listyjako tablice dynamiczne
|
75
Przykład 3.5. Równanie przydziału dla listy w przypadku interpretera języka Python 2 7 M = (N >> 3) + (N < 9 ? 3 : 6) N 0 1-4 5 -8 9 -1 6 17-25 26-35 36-4 6 ... 991-1120 M 0 4 8 16 25 35 46 ... 1120
Przy dołączaniu danych jest używ ane dodatkowe miejsce i zwiększana rzeczywista w ielkość Nlisty. W rezultacie wielkość Nzwiększa się podczas dołączania nowych danych do momentu w ystąpienia w arunku N == M. Gdy to nastąpi, nie będzie żadnego dodatkow ego m iejsca na w stawienie nowych danych, dlatego konieczne będzie utw orzenie nowej listy, która zajmie więcej dodatkowego miejsca. Nowa lista zawiera dodatkow ą rezerwę określoną przez rów nanie z przykładu 3.5. W to miejsce kopiowane są stare dane. Na rysunku 3.3 dokonano wizualizacji opisanej sekwencji zdarzeń. Na rysunku prześledzono różne operacje w ykonyw ane dla listy l w przykładzie 3.6.
Rysunek 3.3. Przykład sposobu zmiany listy w przypadku wielu operacji dołączania Przykład 3.6. Zmiana wielkości listy l = [ 1 , 2] f o r i in ran ge (3 , 7 ) : l.append ( i )
76
|
Rozdział 3. Listy i krotki
T a k a op eracja d o d a tk o w e g o p rz y d z ia łu m a m iejsce p rz y p ie r w s z y m uż y ciu op eratora append. W p r z y p a d k u b e z p o ś r e d n i e g o u t w o r z e n i a listy, j a k w p o p r z e d n i m p r z y k ł a d z i e , p rz y d z ie la n a jest ty lko w y m a g a n a liczba elem en tó w .
i C hoć przew ażnie ilość dodatkow ej rezerw y, która jest przyd zielana, jest dość m ała, m oże ulec zwiększeniu. Efekt ten stanie się szczególnie dobrze widoczny przy utrzymywaniu wielu małych list lub w przypadku używ ania w yjątkowo dużej listy. Jeśli przechowywany jest mi lion list, z których każda zaw iera 10 elem entów , m ożem y przyjąć, że zostan ie u żyta ilość pam ięci mieszcząca 10 milionów elementów. W rzeczywistości jednak, gdyby do utworzenia listy został użyty operator append, mogłoby zostać przydzielonych m aksym alnie 16 milionów elementów. Podobnie dla dużej listy liczącej 100 milionów elementów w rzeczywistości zo stałoby przydzielonych 112 500 007 elementów!
Krotki w roli tablic statycznych Krotki są niezmienne. Oznacza to, że inaczej niż w przypadku listy, po utworzeniu krotki nie można jej m odyfikować ani zm ieniać jej wielkości: >>> t = ( 1 , 2 , 3 , 4 ) >>> t [ 0 ] = 5 Traceback (most r e c e n t c a l l l a s t ) : F i l e " < st d in > ", l i n e 1, in TypeError: 't u p l e ' o b j e c t does not support item assignment
Choć nie m ożna zm ieniać w ielkości krotek, m ożliw e jest połączenie ze sobą dw óch krotek i utw orzenie now ej krotki. O peracja ta przypom ina operację resize w ykonyw aną dla list, z tym że nie ma możliwości przydziału żadnego dodatkowego miejsca dla wynikowej krotki: >>> >>> >>> (1,
t1 t2 t1 2,
= = + 3,
(1 ,2 ,3 ,4 ) (5 ,6 ,7 ,8 ) t2 4 , 5, 6, 7, 8)
Jeśli porów nam y to z operacją append stosowaną dla list, stwierdzimy, że przydział w przy padku krotki odbywa się w czasie O(n), szybkość list natom iast wynosi O(1). W ynika to z te go, że operacje przydziału/kopiow ania m uszą m ieć m iejsce każdorazow o przy dodaw aniu nowych danych do krotki. Dla porównania w przypadku list takie operacje są wykonyw ane tylko po w yczerpaniu dodatkowej rezerwy. W rezultacie nie w ystępuje żadna wewnętrzna operacja podobna do operacji operatora append. Sumowanie dwóch krotek zaw sze pow oduje zwrócenie nowej krotki umieszczonej w nowym miejscu w pamięci. Zrezygnowanie z utrzymywania dodatkowej rezerwy na potrzeby zmiany wielkości zapewnia korzyść w postaci mniejszego zużycia zasobów. Lista licząca 100 m ilionów elementów, która została utw orzona za pom ocą operacji operatora append, w rzeczyw istości zajm uje pam ięć m ieszczącą 112 500 007 elem entów . Z kolei krotka przechow ująca taką sam ą ilość danych zużyje tylko ilość pam ięci, jaka m ieści dokładnie 100 m ilionów elem entów . Pow oduje to, że w przypadku danych statycznych krotki zużywają mniej pamięci i są preferowane. Co w ięcej, jeśli naw et lista zostanie utw orzona bez operatora append (stąd też nie pojaw i się dodatkowa rezerwa w prowadzana przez ten operator), w dalszym ciągu będzie ona zajmo w ać w pam ięci w ięcej m iejsca niż krotka z tymi sam ym i danym i. W ynika to stąd, że w celu
Krotki w roli tablic statycznych
|
77
efektywnej zm iany swojej wielkości listy muszą śledzić więcej informacji o swoim bieżącym stanie. Choć te dodatkow e inform acje zajm ują niew iele m iejsca (odpow iednik jednego do datkow ego elem entu), sum arycznie m ogą okazać się pokaźne, gdy używ anych jest kilka m ilionów list. Inną korzyścią w ynikającą ze statyczności krotek jest proces, który interpreter języka Python realizuje w tle. M ow a m ianow icie o buforow aniu zasobów . W przypadku tego języka ma miejsce czyszczenie pamięci. Oznacza to, że gdy zmienna nie jest już używana, interpreter ję zyka Python zwalnia pam ięć zajm owaną przez nią, zw racając ją system owi operacyjnemu do wykorzystania w innych aplikacjach (lub przez inne zmienne). Jednak w przypadku krotek liczących od 1 do 20 elementów, gdy nie są one już używane, zajm owane przez nie miejsce nie jest od razu zw racane systemowi, lecz jest zachow ywane na potrzeby przyszłego w yko rzystania. Oznacza to, że gdy w przyszłości niezbędna okaże się nowa krotka o takiej w ielko ści, nie będzie trzeba komunikować się z systemem operacyjnym w celu znalezienia obszaru w pamięci, w którym zostaną um ieszczone dane, ponieważ będzie już istnieć rezerwa wolnej pamięci. Choć może się to w ydać niewielką korzyścią, jest to jedna ze znakomitych cech krotek. Można je tworzyć z łatw ością i szybko, gdyż um ożliw iają one uniknięcie konieczności komunikowa nia się z systemem operacyjnym, co może zająć program owi dość sporo czasu. W przykła dzie 3.7 pokazano, że tworzenie instancji listy m oże być 5,1 razy wolniejsze niż w przypadku krotki. Jeśli operacja jest w ykonywana w obrębie szybkiej pętli, w artość ta m oże dość szybko się powiększyć! Przykład 3.7. Porównanie czasu tworzenia instancji list i krotek >>> %timeit l = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] 1000000 loop s, bes t of 3: 285 ns per loop >>> %timeit t = ( 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ) 10000000 loo ps, b est o f 3: 5 5 .7 ns per loop
Podsumowanie Listy i krotki to szybkie i mało obciążające obiekty, które są używane, gdy dane są już w e wnętrznie uporządkowane. Takie uporządkowanie umożliwia uniknięcie problemu z wyszu kiwaniem w przypadku tych struktur. Jeśli uporządkow anie jest znane w cześniej, czas wy szukiwań w ynosi O(1). Dzięki temu eliminuje się kosztowne czasowo wyszukiw anie liniowe z czasem O(n) . Choć możliwa jest zmiana wielkości list, konieczne jest właściw e zrozum ienie tego, jaka jest skala nadmiernego przydziału, aby m ieć pewność, że zbiór danych nadal może zm ieścić się w pamięci. Z kolei krotki m ogą być szybko tworzone bez towarzyszącego listom dodatkowego obciążenia. Kosztem jest jednak brak możliwości ich modyfikowania. W pod rozdziale „Czy listy języka Python są wystarczająco dobre?" omówiono, w jaki sposób wstęp nie przydzielać listy w celu częściowego zmniejszenia obciążenia związanego z częstym dołą czaniem danych do list języka Python. Ponadto przeanalizowane zostały niektóre inne metody optymalizacji, które mogą być pomocne w radzeniu sobie z tymi problemami. W następnym rozdziale zajmiemy się właściwościam i obliczeniowym i słowników, które przy dodatkow ym obciążeniu pozw alają rozw iązać problem y z w yszukiw aniem w przypadku nieuporządkowanych danych.
78
|
Rozdział 3. Listy i krotki
___________ ROZDZIAŁ 4.
Słowniki i zbiory
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Kiedy przydają się słowniki i zbiory? • Pod jakim w zględem słowniki i zbiory są jednakowe? • Jakie jest obciążenie pow odow ane zastosowaniem słownika? • Jak można zoptym alizow ać wydajność słownika? • W jaki sposób w języku Python używane są słowniki do śledzenia przestrzeni nazw?
Zbiory i słowniki to idealne struktury danych, używ ane dla danych bez wewnętrznego upo rządkow ania, ale udostępniające unikalny obiekt um ożliw iający odw oływ anie się do nich (obiekt odwołania to zwykle łańcuch, ale może to być obiekt dowolnego typu pozwalającego na zastosowanie mieszania). Taki obiekt nazywany jest „kluczem ", a dane to „wartość". Słowniki i zbiory są niemal identyczne, z tym wyjątkiem, że zbiory w rzeczywistości nie zawierają war tości. Zbiór to po prostu kolekcja unikalnych kluczy. Jak wskazuje nazwa, zbiory bardzo przy dają się do realizowania operacji na zbiorach.
^
T y p u m o ż liw ia ją c y m ie s z a n ie (a n g . h a sh a b le ) t o t y p i m p l e m e n t u j ą c y f u n k c j ę „ m a g i c z n ą " hash
oraz f u n k c j ę
eq
lu b
cmp
. W szystkie w b u d o w a n e ty p y ję zyka P yth o n
i m p l e m e n t u j ą j u ż te fu n k c j e , a w s z y s t k i e k l a s y u ż y t k o w n i k a m a j ą w a r t o ś c i d o m y ś l n e . W i ę c e j i n f o r m a c ji z a w i e r a p u n k t „ F u n k c j e m i e s z a n i a i e n t r o p i a " .
W poprzednim rozdziale wspomniano, że w przypadku list/krotek bez wewnętrznego upo rządkowania (z wykorzystaniem operacji wyszukiwania) jesteśm y ograniczeni do czasu wy szukiw ania w ynoszącego co najw yżej O(log n). Słow niki i zbiory zapew niają w yszukiw ania z czasem O(n) bazujące na arbitralnym indeksie. Ponadto, podobnie do list/krotek, słowniki i zbiory zapewniają czas wstawiania wynoszący O(l)1. Jak się okaże w podrozdziale „Jak działają 1 Ja k w s p o m n i a n o w p u n k c i e „Fu n k cje m ie sz a n ia i e n tr o p ia", słow n iki i z b io ry są w d u ż y m stop niu za le ż n e od f u n k cji m ie s z a n ia . Je śli fu n k cja m i e s z a n i a d la k o n k r e t n e g o t y p u d a n y c h n ie z a p e w n i a c z a s u O ( 1 ) , d o w o l n y s ł o w n i k l u b z b ió r, k t ó r y z a w i e r a t e n t y p , n ie b ę d z ie j u ż g w a r a n t o w a ć c z a su O ( 1 ) .
79
słowniki i zbiory?", taka szybkość jest osiągana przez użycie w roli bazowej struktury danych tabeli mieszającej z adresowaniem otwartym. Z użyciem słow ników i zbiorów zw iązany jest jednak koszt. Przede w szystkim zajm ują one więcej miejsca w pamięci. Ponadto, choć złożoność operacji wstawiania/wyszukiwania wy nosi O(1), rzeczyw ista szybkość zależy w dużej m ierze od używ anej funkcji m ieszania. Jeśli określanie w artości przez tę funkcję jest pow olne, podobnie będzie rów nież w przypadku dowolnych operacji wykonyw anych na słownikach lub zbiorach. Przyjrzyjmy się przykładowi. Załóżmy, że przechow yw ane m ają być informacje kontaktowe dla każdej osoby z książki telefonicznej. Inform acje te m ają być przechow yw ane w postaci, która w przyszłości ułatw i udzielenie odpow iedzi na następujące pytanie: „Jaki jest num er telefonu Jana N ow aka?". W przypadku list num ery telefonów i nazw iska byłyby przecho w yw ane kolejno, a w celu znalezienia w ymaganego numeru byłaby przeszukiwana cała lista (przykład 4.1). Przykład 4.1. Wyszukiwanie w książce telefonicznej za pomocą listy def find_phonenumber(phonebook, name): f o r n, p in phonebook: i f n == name: r e tu r n p r e tu r n None phonebook = [ (" Ja n Nowak", " 5 5 5 - 5 5 5 - 5 5 5 5 " ) , (" A lb e rt E i n s t e i n " , " 2 1 2 - 5 5 5 - 5 5 5 5 " ) , p r i n t "Numer te l e f o n u Jana Nowaka t o " , find_phonenumber(phonebook, "Jan Nowak")
M o ż l i w e j e s t r ó w n i e ż u ż y c i e s o r t o w a n i a l i s t y i m o d u ł u bi s e c t d o u z y s k a n i a w y d a j n o ś c i O(lo g n ) .
i Użycie słownika zapewnia jednak uzyskanie po prostu „indeksu" w postaci nazw isk i „war tości" jako num erów telefonów (przykład 4.2). Umożliwia to wyszukiw anie niezbędnej war tości i pobranie bezpośredniego odwołania do niej zam iast konieczności odczytywania każdej w artości ze zbioru danych. Przykład 4.2. Wyszukiwanie w książce telefonicznej za pomocą słownika phonebook = { "J a n Nowak": "5 5 5 - 5 5 5 -5 5 5 5 " , "A lb er t E i n s te i n " : " 2 1 2 - 5 5 5 - 5 5 5 5 " , } p r i n t "Numer te l e f o n u Jana Nowaka t o " , phonebook["Jan Nowak"]
W przypadku pokaźnych książek telefonicznych dość znaczna jest różnica między czasem O(1) wyszukiwania w słowniku i czasem O(n) (lub w najlepszym razie czasem O(log n) , gdy zostanie użyty moduł bisect) wyszukiwania liniowego w obrębie listy. U t w ó r z skryp t, k tó ry m ie r z y w y d a jn o ś ć d la m e t o d y op artej n a liście i m o d u le b i s e c t ora z z a s to s o w a n ia s ło w n ik a d o z n a jd o w a n ia n u m e r u w k sią ż c e telefon iczn ej. Ja k w y g lą d a s k a la p o m ia r u c z a su p r z y z w ię k s z a ją c e j się w ie lk o ś c i k sią ż k i telefon iczn ej?
80
|
Rozdzia ł 4. S łowniki i zbiory
Z kolei aby udzielić odpow iedzi na następujące pytanie: „Ile w m ojej książce telefonicznej znajduje się unikalnych im io n ?", m ożna skorzystać ze zbiorów . Jak w spom niano, zbiór to prosta kolekcja unikalnych kluczy. W łaśnie ta właściw ość zostanie wymuszona dla przykła dowych danych, inaczej niż w przypadku m etody opartej na liście, w przypadku której taka właściwość musi być wymuszana niezależnie od struktury danych przez porównanie każdego imienia ze wszystkimi pozostałymi. Zostało to zilustrowane w przykładzie 4.3. Przykład 4.3. Znajdowanie unikalnych imion za pomocą list i zbiorów def l i st_unique_names(phonebook): unique_names = [] f o r name, phonenumber in phonebook: fi r s t_ n a m e , last_name = n a m e .s p l i t ( " " , 1) f o r unique in unique_names: i f unique == firs t_ n am e : break else: uni que_names.append(first_name) re tu rn len(unique_names) def set_unique_names(phonebook): unique_names = s e t ( ) f o r name, phonenumber in phonebook: fi r s t_ n a m e , last_name = n a m e .s p l i t ( " " , 1) unique_names.add(first_name) re tu rn len(unique_names) phonebook = [ (" Ja n Nowak", " 5 5 5 - 5 5 5 - 5 5 5 5 " ) , (" A lb e rt E i n s t e i n " , " 2 1 2 - 5 5 5 - 5 5 5 5 " ) , (" Ja n Kowalski", " 2 0 2 - 5 5 5 - 5 5 5 5 " ) , (" A lb e rt R ut he rfor d", " 6 4 7 - 5 5 5 - 5 5 5 5 " ) , ("Edyta B arska", " 3 0 1 - 5 5 5 - 5 5 5 5 " ) ,
# O # 0
# © # O
] p r i n t "L iczba unikalnych imion przy zastosowaniu metody o p a rt e j na z b i o r z e : " , set_unique_names(phonebook) p r i n t "L iczba unikalnych imion przy zastosowaniu metody o p a r t e j na l i ś c i e : " , list_unique_names(phonebook)
O © K onieczne jest spraw dzenie w szystkich pozycji w książce telefonicznej, dlatego czas działania używanej pętli wynosi O(n).
0 W tym przypadku niezbędne jest porów nanie bieżącego im ienia ze w szystkim i już na potkanym i imionami. Jeśli imię okaże się nowym unikalnym imieniem, zostanie dodane do listy unikalnych imion. Dalej kontynuowane jest przetwarzanie listy. Krok ten jest wy konywany dla każdej pozycji w książce telefonicznej.
O Zam iast iterowania w przypadku metody opartej na zbiorze w szystkich napotkanych już unikalnych imion można po prostu dodać bieżące imię do zbioru unikalnych imion. Ponie waż zbiory zapewniają unikalność kluczy, które zawierają, próba dodania elementu znaj dującego się już w zbiorze spow oduje, że elem ent ten po prostu nie zostanie dodany. Co w ięcej, czas takiej operacji w ynosi O(1). Pętla w ew nętrzna algorytm u listy dokonuje iteracji elem entów listy unique_names, która na początku jest pusta, a następnie pow iększa się. W najgorszym przypadku, gdy w szystkie imiona będą unikalne, lista osiągnie w ielkość książki telefonicznej. M oże to być postrzegane jako wykonywanie wyszukiwania liniowego każdego imienia w książce telefonicznej dla listy, która cały czas się powiększa. A zatem cały algorytm jest w ykonyw any w czasie O(n log n), ponieważ pętla zewnętrzna generuje czas O(n), pętla wewnętrzna natomiast jest wykonywana w czasie O(log n).
Jak działają słowniki i zbiory?
|
81
Z kolei algorytm zbioru nie m a pętli w ew nętrznej. set.add to proces z czasem O(1 ), który kończy się po wykonaniu ustalonej liczby operacji, niezależnie od tego, jak duża jest książka telefoniczna (związanych jest z tym kilka drobnych zastrzeżeń, które zostaną przedstawione podczas omawiania implementowania słowników i zbiorów). Oznacza to, że jedynym zmien nym elementem złożoności tego algorytmu jest pętla wykonywana dla książki telefonicznej. Dzięki temu czas działania algorytmu wynosi O(n). Przy pomiarze czasu dla tych dwóch algorytmów używanych dla książki telefonicznej liczącej 10 000 pozycji i 7422 unikalnych imion widoczne jest, jak znaczna może być różnica między czasami O(n) i O(n log n): >>> %timeit list_unique_names(large_phonebook) 1 loops, b est o f 3: 2 .5 6 s per loop >>> %timeit set_unique_names(large_phonebook) 100 loops, b est o f 3: 9 .5 7 ms per loop
Innymi słowy, algorytm zbioru zapewnił 267-krotne przyspieszenie! Ponadto przy zw iększa niu się książki telefonicznej w zrasta przyrost szybkości (dla książki telefonicznej liczącej 100 tysięcy pozycji i 15 574 unikalne imiona uzyskuje się 557-krotne przyspieszenie).
Jak działają słowniki i zbiory? Słowniki i zbiory używ ają tabel mieszających do osiągnięcia czasu O(1) dla swoich operacji wy szukiw ania i w staw iania. Taka w ydajność jest w ynikiem bardzo m ądrego w ykorzystania funkcji m ieszania w celu przekształcenia dowolnego klucza (np. łańcucha lub obiektu) w in deks listy. Funkcja m ieszania i lista m ogą być później stosowane do szybkiego określenia bez wyszukiwania, gdzie znajduje się dowolna w ybrana porcja danych. Przekształcanie klucza danych w coś, co może być używ ane podobnie jak indeks listy, pozwala uzyskać w ydajność taką samą jak w przypadku listy. Ponadto zam iast konieczności odwoływania się do danych przy użyciu indeksu liczbow ego, który sam im plikuje określone uporządkow anie danych, możemy odwołać się do danych za pom ocą dowolnego klucza.
Wstawianie i pobieranie Aby od podstaw utw orzyć tabelę m ieszającą, zaczynam y od przydzielonej pamięci, podobnie jak w przypadku tablic. Jeśli do tablicy m ają zostać w staw ione dane, po prostu znajdujem y najm niejszy nieużyw any pojem nik i wstawiamy do niego dane (w razie potrzeby zmieniając w ielkość pojem nika). W przypadku tabel m ieszających konieczne jest najpierw określenie um iejscowienia danych w ciągłym obszarze pamięci. U m iejscow ienie danych jest zależne od dw óch w łaściw ości w staw ianych danych: w artości klucza poddanej mieszaniu oraz tego, jak w artość porów nuje się z innymi obiektami. Wynika to stąd, że po wstawieniu danych klucz jest najpierw poddawany mieszaniu i m askowaniu, aby został przekształcony w efektywny indeks w tablicy2. M aska zapewnia, że w artość mie szania, która może być w artością dowolnej liczby całkowitej, mieści się w przydzielonej licz bie pojem ników . A zatem jeśli przydzielono 8 bloków pamięci, a w artość m ieszania to 28975,
2 M aska to licz b a b inarna, k t ó ra o b c in a w a r to ść li cz b o w ą. O z n a c z a to, ż e 0 b 1 1 11101 & 0b111 = 0b101 = 5 re p re z en tu je o per ację m a s k o w a n i a (0b 11 1) liczby 0b 111110 1. O p e r a c ja t a k a m o ż e też b y ć tr a k t o w a n a ja k o po biera n ie określ one j liczb y n ajm niej z n a c z ą c y c h cyfr liczby.
82
|
Rozdział 4. Słowniki i zbiory
pod uwagę brany jest pojemnik o indeksie 28975 & 0b111 = 7. Jeśli jednak słownik powiększył się i wymaga 512 bloków pamięci, maska przyjmie postać 0b111111111 (w tym przypadku pod uwa gę będzie brany pojem nik o indeksie 28975 & 0b11111111). Konieczne jest teraz sprawdzenie, czy taki pojem nik nie jest już używany. Jeśli okaże się pusty, można w staw ić klucz i wartość do takiego bloku pamięci. Klucz jest przechowywany, dzięki czemu m ożliw e jest zapewnie nie pobrania popraw nej w artości w operacjach w yszukiw ania. Jeśli pojem nik jest używ any, a jego w artość jest równa w artości, która ma zostać wstawiona (porównanie realizowane jest za pom ocą w budow anego narzędzia cmp), para złożona z klucza i w artości znajduje się już w tabeli mieszającej, dlatego możliwe jest zw rócenie pary. Jeśli jednak wartości nie są zgodne, niezbędne będzie znalezienie nowego miejsca, w którym zostaną umieszczone dane. W celu znalezienia nowego indeksu oblicza się go za pom ocą prostej funkcji liniowej. Jest to metoda nazywana sondowaniem. Mechanizm sondowania języka Python korzysta z bitów o wyż szej pozycji oryginalnej wartości m ieszania (jak wspomniano, dla tabeli o długości 8 dla po czątkow ego indeksu rozpatryw ano jedynie 3 ostatnie bity w artości m ieszania, korzystając z w artości m aski mask = 0b111 = bin (8 - 1)). Użycie bitów o wyższej pozycji zapewnia każdej wartości mieszania inną kolejność następnych możliw ych w artości m ieszania, co jest pom oc ne w uniknięciu przyszłych kolizji. W ybierając algorytm do generow ania now ego indeksu, masz dużą swobodę działania. Dość w ażne jest jednak to, że schem at sprawdza każdy m oż liwy indeks w celu równom iernego rozmieszczania danych w tabeli. O tym, jak dobrze dane są rozmieszczane w tabeli mieszającej, decyduje w spółczynnik obciążenia, który jest pow ią zany z entropią funkcji m ieszania. Pseudokod z przykładu 4.4 ilustruje obliczenie indeksów wartości mieszania używ anych w narzędziu CPython 2.7. Przykład 4.4. Sekwencja wyszukiwania w słowniku def index_sequence(key, mask=0b111, PERTURB_SHIFT=5): perturb = hash(key) # O i = perturb & mask y ield i while True: i = ( ( i << 2) + i + perturb + 1) perturb >>= PERTURB_SHIFT y i e l d i & mask
0
Funkcja hash zw raca liczbę całkow itą, sam kod C w narzędziu C Python korzysta nato m iast z liczby całkow itej bez znaku. Z tego pow odu ten pseudokod nie pow iela w 100% działania w narzędziu CPython. Jest to jednak dobre przybliżenie.
Takie sondowanie stanowi modyfikację naiwnej m etody sondowania liniowego. W przypadku sondow ania liniow ego po prostu uzyskiw ane są w artości dla i = (5 * i + 1) & mask, gdzie 1 jest inicjowane jako w artość m ieszania klucza, a w artość 5 nie ma znaczenia w niniejszym om ów ieniu3. G odne uw agi jest to, że sondow anie liniow e dotyczy jedynie kilku ostatnich bajtów w artości m ieszania, a reszta jest pom ijana (czyli dla słow nika z 8 elem entam i pod uwagę będą brane tylko 3 ostatnie bity, ponieważ w tym przypadku maska to 0x 111). Ozna cza to, że jeśli m ieszanie dwóch elementów daje te same trzy ostatnie cyfry binarne, nie tylko nie w ystąpi kolizja, ale też identyczna będzie kolejność sondowanych indeksów. W celu roz w iązania tego problemu w e wprowadzającym zam ieszanie schemacie używ anym w języku Python uwzględniana zacznie być w iększa liczba bitów z wartości m ieszania elementów.
3 W a r t o ś ć 5 w y n i k a z w ł a ś c i w o ś c i g e n e r a to r a k o n g r u e n c ji l in io w ej L C G (L in ear C on g ru en tia l G en era to r), k t ó r y u ż y w a n y je st w p r z y p a d k u g e n e r o w a n i a licz b l o so w y ch .
Jak działają słowniki i zbiory?
|
83
Podobna procedura ma miejsce podczas przeprowadzania w yszukiw ań dotyczących kon kretnego klucza. Dany klucz jest przekształcany w indeks, który jest sprawdzany. Jeśli klucz w tym indeksie jest zgodny (jak wcześniej wspomniano, oryginalny klucz jest też przechowy wany podczas wykonywania operacji wstawiania), możliwe jest zwrócenie tej wartości. W prze ciwnym razie dalej tworzone są nowe indeksy przy użyciu tego samego schematu do momentu znalezienia danych lub natrafienia na pusty pojemnik. W drugim przypadku można wywnio skować, że dane nie istnieją w tabeli. Na rysunku 4.1 zilustrowano proces dodawania danych do tabeli mieszającej. W przykładzie zdecydow ano się na utw orzenie funkcji m ieszania, która po prostu używ a pierw szej litery z wprow adzonych danych. Jest to osiągane przez zastosow anie funkcji ord języka Python dla pierwszej litery podanych danych w celu uzyskania liczbowej reprezentacji tej litery (jak wspo mniano, funkcje mieszania muszą zwracać liczby całkowite). Jak się okaże w punkcie „Funkcje mieszania i entropia", język Python zapewnia funkcje mieszania dla większości swoich typów. Dzięki temu, z wyjątkiem ekstremalnych sytuacji, nie będzie trzeba samodzielnie dbać o taką funkcję.
Rysunek 4.1. Wynikowa tabela mieszająca po wykonaniu operacji wstawiania z kolizjami W stawienie klucza Barcelona pow oduje kolizję. Nowy indeks jest obliczany za pom ocą sche matu z przykładu 4.4. Słow nik ten m oże też zostać utw orzony w języku Python za pom ocą kodu z przykładu 4.5. Przykład 4.5. Niestandardowa funkcja mieszania class C ity (s tr ): def hash ( s e l f ) : r e tu r n o r d ( s e l f [ 0 ] ) # Tworzony je s t słow nik, w którym d o m iast przypisyw ane s ą dow olne w artości data = { City("R zym "): 4 , C ity (" San F r a n c i s c o " ) : 3, City("Nowy J o r k " ) : 5, C i t y ( " B a r c e l o n a " ) : 2, }
84
|
Rozdział 4. Słowniki i zbiory
W tym przypadku klucze Barcelona i Rzym powodują kolizję wartości mieszania (na rysunku 4.1 pokazano w ynik takiej operacji w stawiania). Coś takiego ma miejsce, ponieważ dla słownika z czterema elementami używana jest wartość maski 0b111. W rezultacie klucz Barcelona podejmie próbę użycia indeksu ord("B") & 0b111 = 66 & 0b111 = 0b1000010 & 0b111 = 0b010 = 2. Podobnie klucz Rzym spróbuje zastosować indeks ord("R") & 0b111 = 82 & 0b111 = 0b1010010 & 0b111 = 0b010 = 2. P rz e a n a liz u j p o n iż s z e p r o b le m y . O to o m ó w ie n ie k olizji w a r to ś c i m ie sz a n ia : 1.
Znajdowanie elementu.
J a k b ę d z i e w y g l ą d a ć w y s z u k i w a n i e d l a k l u c z a Jo h a n n e s
b u r g w p r z y p a d k u u ż y c i a s ł o w n i k a u t w o r z o n e g o w p r z y k ł a d z i e 4 .5 ? J a k i e i n d e k s y zostan ą spraw dzone? 2.
Usuwanie elementu .
Ja k b ęd z ie o b słu g iw a n e u s u w a n ie k lu cza R zy m w p rz y
p a d k u u ż y c ia s ło w n ik a u t w o r z o n e g o w p r z y k ła d z ie 4.5? Ja k b ę d ą o b s łu g iw a n e k o l e j n e w y s z u k i w a n i a d l a k l u c z y R z y m i B a r c e lo n a ? 3.
Kolizje wartości mieszania .
Biorąc p od u w a g ę słow n ik u tw o rzo n y w p rz y k ła
d z ie 4 .5 , ilu m o ż e s z s p o d z ie w a ć się k o liz ji w a r t o ś c i m i e s z a n ia d la 5 0 0 m ia s t d o d a n y c h d o ta b e li m ie s z a ją c e j, k t ó r y c h n a z w y r o z p o c z y n a ją się d u ż ą literą? C o b ęd z ie w p rz y p a d k u 1000 m iast? C z y p rz y ch o d z i C i n a m y ś l sp o s ó b z m n ie j s z e n i a l i c z b y k o l i z ji ? D la 5 0 0 m ia st b ę d ą istn ieć w p rz y b liż e n iu 4 7 4 ele m e n ty sło w n ika, k tó re k o lid o w a ły z p o p r z e d n ią w a r to ś c ią (5 0 0 - 2 6 ), g d y z k a ż d ą w a rto ś c ią m ie s z a n ia p o w ią z a n y c h je st 5 0 0 : 2 6 = 1 9 ,2 m i a s t a . W p r z y p a d k u 1 0 0 0 m i a s t k o l i z j a w y s t ę p o w a ł a b y d l a 9 7 4 e l e m e n tó w , a z k a ż d ą w a rto ścią m ie s z a n ia p o w ią z a n y c h b y ło b y 1000:26 = 3 8 ,4 m iasta. W y n i k a to s t ą d , ż e w a r t o ś ć m i e s z a n i a p o p r o s t u b a z u j e n a w a r t o ś c i l i c z b o w e j p i e r w s z e j l i te r y , k t ó r a m o ż e b y ć j e d n ą z l i t e r o d A d o Z . P o w o d u j e to , ż e d o z w o l o n y c h j e s t t y l k o 2 6 n i e z a l e ż n y c h w a r t o ś c i m i e s z a n i a . O z n a c z a to , ż e w y s z u k i w a n i e w ta k iej ta b e li w y m a g a ło b y aż 38 kolejn y ch operacji w y sz u k iw a n ia w celu z n ale zien ia p o p ra w n ej w a r t o ś c i . A b y to p o p r a w i ć , k o n i e c z n e je s t z w i ę k s z e n i e l i c z b y m o ż l i w y c h w a r t o ś c i m i e s z a n i a p r z e z u w z g l ę d n i e n i e w tej w a r t o ś c i i n n y c h a s p e k t ó w z w i ą z a n y c h z m i a s t a m i . D o m y ś ln a fu n k cja m ie s z a n ia u ż y w a n a d la ła ń cu ch a ro z p a tru je k a ż d y z n a k , ab y z m a k s y m a liz o w a ć liczbę m o ż liw y c h w artości. D o k ła d n iejsz e o b jaśn ien ie z a m ie sz cz o n o w p u n k cie „Fu nk cje m iesz a n ia i en tro p ia".
Usuwanie Po usunięciu w artości z tabeli m ieszającej nie m ożna po prostu zapisać w artości NULL w da nym pojemniku pamięci. W ynika to z tego, że wartości NULL zostały użyte jako wartość po m ocnicza podczas sondow ania pod kątem kolizji w artości m ieszania. W efekcie konieczne jest zapisanie specjalnej wartości, która wskazuje, że pojemnik jest pusty. Przy zajmowaniu się kolizją wartości mieszania w dalszym ciągu mogą za tym pojemnikiem w ystępować wartości, które należy uwzględnić. W takich pustych miejscach możliwy jest później zapis. Po zmianie wielkości tabeli mieszającej są one usuwane.
Zmiana wielkości W staw ienie w iększej liczby elem entów do tabeli m ieszającej pow oduje, że trzeba zm ienić wielkość tej tabeli, by można było uwzględnić nowe elementy. Może się okazać, że tabela wy pełniona nie więcej niż w dwóch trzecich swojej pojemności będzie się cechować optymalnym
Jak dzia łają słowniki i zbiory?
|
85
wykorzystaniem miejsca, a jednocześnie nadal będzie m ieć odpowiedni lim it spodziewanej liczby kolizji. A zatem po osiągnięciu punktu krytycznego tabela zw iększy się. Aby tak się stało, przydzielana jest w iększa tabela (czyli w pam ięci rezerwowanych jest więcej pojem ni ków), maska jest modyfikowana w celu dopasowania do nowej tabeli, a wszystkie elementy starej tabeli są ponow nie w stawiane do nowej tabeli. W ymaga to ponow nego obliczenia in deksów, poniew aż zm odyfikow ana m aska zm ieni w ynikow y indeks. W rezultacie zm iana wielkości dużych tabel m ieszania może być dość kosztowną operacją! Ze względu jednak na to, że taka operacja zm iany w ielkości jest w ykonyw ana tylko w tedy, gdy tabela jest zbyt m ała, w przeciw ieństw ie do każdej operacji w staw iania, zam ortyzow any koszt w staw iania nadal wynosi O(1). Domyślnie najm niejsza w ielkość słownika lub zbioru wynosi 8 (oznacza to, że jeśli przecho w yw ane są tylko trzy w artości, interpreter języka Python w dalszym ciągu przydzieli 8 ele mentów). Przy zmianie wielkości słownika/zbioru liczba pojem ników zwiększa się cztero krotnie do momentu osiągnięcia 50 000 elementów, a później przyrost wielkości jest dwukrotny. W związku z tym możliwe są następujące wielkości: 8 , 32 , 128, 512, 2048, 81 92 , 32768, 131072, 262144,
...
G odne uw agi jest to, że zm iana w ielkości m oże m ieć na celu zm niejszenie lub zw iększenie tabeli m ieszającej. Oznacza to, że jeśli zostanie usunięta w ystarczająca liczba elem entów ta beli m ieszającej, jej wielkość może zostać przeskalowana w dół. Zmiana wielkości ma jednak miejsce tylko podczas operacji wstawiania.
Funkcje mieszania i entropia O biekty w języku Python przew ażnie m ogą być poddaw ane m ieszaniu, poniew aż są już z nim i pow iązane w budow ane fu n k c je hash i cmp . W przypadku typów liczbow ych (int i flo at) w artość m ieszania jest po prostu oparta na wartości bitowej liczby, którą one re prezentują. Krotki i łańcuchy m ają w artość m ieszania, która bazuje na ich zawartości. Z kolei listy nie obsługują m ieszania, ponieważ ich w artości m ogą się zmieniać. Jako że w artości listy mogą się zmieniać, a tym samym wartość mieszania, która reprezentuje listę, m oże to zmienić względne umiejscowienie danego klucza w tabeli mieszającej4. Klasy definiowane przez użytkownika również są w yposażone w dom yślne funkcje miesza nia i porów nyw ania. D om yślna fu n k c ja hash po prostu zw raca um iejscow ienie obiektu w pamięci podane przez w budow aną funkcję id. Podobnie o p era to r cmp porów nuje w ar tość liczbową umiejscowienia obiektu w pamięci. Jest to przew ażnie akceptow alne, poniew aż dwie instancje klasy są generalnie różne i nie powinny kolidować ze sobą w tabeli mieszającej. Jednakże w niektórych sytuacjach w skazane będzie użycie obiektów set lub dict do ujednoznacznienia elementów. Przyjrzyj się następu jącej definicji klasy: class P o in t(o b ject): def in it (s e lf , x, y): s e l f . x , s e l f . y = x, y
4 W ięc ej i n f o rm a c ji n a t e n t e m a t d o s t ę p n y c h je st p o d a d r esem : h ttp s://w iki.p y th o n .org /m oin /D iction ary K ey s.
86
|
Rozdział 4. Słowniki i zbiory
Jeśli dla wielu obiektów Point zostałyby utworzone instancje z tymi samymi wartościami x i y, w szystkie byłyby niezależnym i obiektam i w pam ięci, czyli znajdow ałyby się w niej w róż nych miejscach. Spowodowałoby to zapewnienie im wszystkim różnych wartości mieszania. Oznacza to, że umieszczenie wszystkich tych obiektów w obiekcie set sprawiłoby, że miałyby one osobne pozycje: >>> p1 = P o i n t ( 1 , 1 ) >>> p2 = P o i n t ( 1 , 1 ) >>> s e t ( [ p 1 , p2]) s e t ( [ < main . P o in t at 0x1099bfc90>, < >>> P o i n t ( 1 , 1 ) in s e t ( [ p 1 , p2]) False
main
.P o i n t a t 0x1099bfbd0>])
Aby temu zaradzić, możesz utworzyć niestandardową funkcję mieszania, która bazuje na rze czywistej zawartości obiektu, a nie na jego umiejscowieniu w pamięci. Funkcja mieszania może być dowolna, dopóki niezm iennie dla jednego obiektu zapewnia taki sam w ynik (pojawiają się też kwestie związane z entropią funkcji mieszania, które zostaną omówione w dalszej części rozdziału). Następująca ponowna definicja klasy Point da oczekiwane wyniki: cla ss P o in t(o b ject): def in it ( s e l f , x, y ) : s e l f . x , s e l f . y = x, y def hash ( s e l f ) : re tu rn h a s h ( ( s e l f . x , s e l f . y ) ) def eq ( s e l f , o t h e r ) : re tu rn s e l f . x == o t h e r . x and s e l f . y == o th e r .y
Pozwala to utw orzyć pozycje w zbiorze lub słowniku indeksowane za pom ocą właściwości obiektu Point, a nie adresu pamięci obiektu, dla którego utworzono instancję: >>> p1 = P o i n t ( 1 , 1 ) >>> p2 = P o i n t ( 1 , 1 ) >>> s e t ( [ p 1 , p2]) s e t ( [ < main . P o in t at 0x109b95910>]) >>> P o i n t ( 1 , 1 ) in s e t ( [ p 1 , p2]) True
Jak wspom niano w e w cześniejszej uwadze poświęconej kolizji w artości m ieszania, niestan dardowa funkcja m ieszania pow inna być uw ażnie w ybrana, aby rów nom iernie rozprow a dzała w artości m ieszania w celu uniknięcia kolizji. W ystępow anie w ielu kolizji spow oduje zm niejszenie wydajności tabeli mieszającej. Jeśli w iększość kluczy pow oduje kolizje, koniecz ne będzie ciągłe „sondow anie" innych wartości. W efekcie znalezienie żądanego klucza może w ym agać przejścia potencjalnie dużej części słow nika. W najgorszym razie, gdy w szystkie klucze w słowniku będą ze sobą kolidować, w ydajność w yszukiw ań w słowniku będzie wy nosić O(n). Będzie to oznaczać taką samą wydajność jak w przypadku przeszukiwania listy. Jeśli w słowniku przechow yw anych jest 5000 wartości, a ponadto niezbędne jest utworzenie funkcji m ieszania dla obiektu, który ma zostać użyty jako klucz, słownik będzie przechow y w any w tabeli mieszającej o wielkości 32 768. Oznacza to, że tylko 15 ostatnich bitów w arto ści m ieszania używ anych jest do tworzenia indeksu (dla tabeli mieszającej o takiej wielkości m aska to bin(32758-1) = 0b111111111111111). Pojęcie określające „stopień rozłożenia używ anej funkcji m ieszania" nazyw ane jest entropią funkcji m ieszania. Oto definicja entropii: S = - £
p ( i ) - log(p ( i ) )
Jak działają słowniki i zbiory?
|
87
Gdzie: p(i) to prawdopodobieństwo tego, że funkcja m ieszania zapewni wartość m ieszania i . Entropia jest maksymalizowana, gdy każda w artość m ieszania ma równe praw dopodobień stwo wybrania. Funkcja m ieszania, która m aksym alizuje entropię, nosi nazwę idealnej funkcji m ieszania, ponieważ gwarantuje minim alną liczbę kolizji. W przypadku nieskończenie dużego słow nika idealna jest funkcja m ieszania używ ana dla liczb całkow itych. W ynika to stąd, że w artość m ieszania dla liczby całkow itej to po prostu liczba całkowita! Dla takiego słownika w artość maski jest nieskończona, dlatego rozpatrywa ne są w szystkie bity wartości m ieszania. Oznacza to, że dla danych dwóch dowolnych liczb można zagwarantować, że ich w artości m ieszania nie będą jednakowe. Jeśli jednak słownik stałby się skończony, taka gwarancja nie byłaby dłużej możliwa. Na przy kład w przypadku słownika z czterema elementami używana maska to 0b111. A zatem wartość mieszania dla liczby 5 wynosi 5 & 0b111 = 5, a wartość mieszania dla liczby 501 to 501 & 0b111 = 5. Oznacza to, że ich pozycje będą kolidować.
^
W celu z n a le z ie n ia m a s k i d la s ło w n ik a z d o w o ln ą licz b ą Ne le m e n tó w n a jp ie r w z n a j d o w a n a je s t m in im a ln a licz b a p o je m n ik ó w , ja k ie s ło w n ik m u s i m ieć, a b y n a d a l p o z o s t a w a ł z a p e ł n i o n y w d w ó c h t r z e c i c h (N * 5 / 3). P ó ź n i e j o k r e ś l a n a j e s t n a j m n i e j s z a w i e l k o ś ć s ł o w n i k a , j a k a p o z w o l i p o m i e ś c i ć tę l i c z b ę e l e m e n t ó w (8; 3 2 ; 1 2 8 ; 5 1 2 ; 2 0 4 8 itd .) , o r a z z n a j d o w a n a j e s t l i c z b a b i t ó w n i e z b ę d n a d o p r z e c h o w a n i a t a k i e j l i c z b y e le m e n tó w . Je śli n a p r z y k ła d Nw y n o s i 1039, m u s i istn ie ć co n a jm n ie j 1731 p o je m n i k ó w . O z n a c z a to, ż e w y m a g a n y j e s t s ł o w n i k z 2 0 4 8 p o j e m n i k a m i . A z a t e m m a s k a to b i n ( 2 0 4 8 - 1) = 0b11 111111111.
Przy korzystaniu ze skończonego słow nika nie istnieje jedna, najlepsza funkcja m ieszania, którą możesz zastosować. Jednakże wcześniejsze ustalenie, jaki będzie używany zakres war tości, a także jak duży będzie słownik, ułatwia dokonanie dobrego wyboru. Jeśli na przykład przechowuje się wszystkie 676 kombinacji dwóch małych liter jako klucze w słowniku (aa, ab, ac itd.), odpowiednią funkcją mieszania będzie funkcja zaprezentow ana w przykładzie 4.6. Przykład 4.6. Optymalna funkcja mieszania twoletter_hash def tw o l e tt e r _ h a s h (k e y ): o ffset = o rd ('a ') k1, k2 = key r e tu r n (ord(k2) - o f f s e t ) + 26 * (ord(k1) - o f f s e t )
W przypadku m aski 0b1111111111 (słownik liczący 676 w artości będzie utrzym yw any w tabeli mieszającej o długości 2048 z maską bin(2048-1) = 0b11111111111) funkcja nie pow oduje żad nych kolizji w artości mieszania dla żadnej kombinacji dwóch małych liter. W przykładzie 4.7 bardzo w yraźnie przedstawiono konsekwencje użycia złej funkcji miesza nia dla klasy definiowanej przez użytkownika. W tym przypadku kosztem zastosowania takiej funkcji (okazuje się, że jest to najgorsza z możliw ych funkcja mieszania!) jest 21,8 razy wol niejsze wyszukiwanie. Przykład 4.7. Pomiar czasu dla dobrych i złych funkcji mieszania import s t r i n g import ti m e i t c l a s s BadHash(str): def hash ( s e l f ) : re tu rn 42
88
|
Rozdział 4. Słowniki i zbiory
c l a s s GoodHash(str): def _ _ h a s h _ _ ( s e l f ) : J e s t to nieznacznie zoptym alizow ana w ersja fu n kcji tw oletter_hash r e tu r n o r d ( s e l f [ 1 ] ) + 26 * o r d ( s e l f [ 0 ] ) - 2619 badd ict = s e t ( ) gooddict = s e t ( ) f o r i in s t r i n g . a s c i i _ l o w e r c a s e : f o r j in s t r i n g . a s c i i _ l o w e r c a s e : key = i + j badd ict.add(BadHash(key)) gooddict.add(GoodHash(key)) badtime = t i m e i t . r e p e a t ( "key in b a d d i c t " , setup = "from main import b a d d i c t , BadHash; key = B a d H a sh ('z z ') " , re pe at = 3, number = 1000000, goodtime = t i m e i t . r e p e a t ( "key in good dict" , setup = "from main re pe at = 3, number = 1000000, ) p r i n t "Minimalny p r i n t "Minimalny # Wyniki: # M inimalny czas # M inimalny czas
import good dict, GoodHash; key = GoodHash('zz')"
czas wyszukiwania dla słownika b add ict : czas wyszukiwania dla słownika good dict:
mi n(badtime) min(goodtime)
wyszukiwania d la słow nika baddict: 16,3375990391 wyszukiwania d la słow nika g ood d ict: 0,748275995255
1. W y k a ż , ż e d l a n i e s k o ń c z o n e g o s ł o w n i k a (czyli d l a n ie s k o ń c z o n e j m a s k i ) u ż y c i e w a r t o ś c i l i c z b y c a ł k o w i t e j j a k o j e g o w a r t o ś c i m i e s z a n i a n i e s p o w o d u j e ż a d n y c h koli zji. 2. W y k a ż , ż e f u n k c j a m i e s z a n i a p o d a n a w p r z y k ł a d z i e 4 . 6 n a d a j e s i ę i d e a l n i e d l a tab e li m ie sz a ją c e j o w ie lk o ś c i 1024. D la c z e g o n ie je s t o n a id e a ln a d la m n ie js z y c h tab el m iesz a ją cych ?
Słowniki i przestrzenie nazw W yszukiw anie w słowniku jest szybkie. Niemniej jednak niepotrzebne wykonanie takiej ope racji spow oduje spow olnienie kodu, tak jak wszelkie nieistotne wiersze. Jednym z obszarów, w których się to objawia, jest zarządzanie przestrzenią nazw w języku Python. W tym obszarze intensywnie wykorzystuje się słowniki do wykonywania operacji wyszukiwania. Każdorazowo, gdy zmienna, funkcja lub m oduł są wyw oływ ane w kodzie Python, używana jest hierarchia, która określa, gdzie interpreter tego języka pow inien szukać tych obiektów. N ajpierw interpreter szuka w obrębie tablicy lo ca ls(), która zawiera elementy dla w szystkich zmiennych lokalnych. Interpreter języka Python bardzo stara się zapewnić szybkie wyszukiwanie zmiennych lokalnych. Jest to jedyna część procesu, która nie wymaga wyszukiwania w słow niku. Jeśli obiekt nie istnieje w tej tablicy, przeszukiwany jest następnie słownik globals(). Jeśli tutaj też obiekt nie zostanie znaleziony, zostanie przeszukany obiekt __builtin__ . Godne uwagi jest to, że choć tablice lo cals() i globals() to jaw ne słowniki, a bu iltin to z technicznego punktu widzenia obiekt modułu, w przypadku przeszukiwania tego obiektu pod kątem danej właściwości po prostu ma miejsce w yszukiw anie słow nikow e w obrębie jego odwzorowania tablicy lo cals() (dotyczy to w szystkich obiektów modułu i obiektów klasy!).
S łowniki i przestrzenie nazw
|
89
Aby stało się to bardziej zrozumiałe, przyjrzyjmy się prostemu przykładowi w ywoływania funkcji zdefiniowanych w różnych zasięgach (przykład 4.8). Za pom ocą m odułu dis m ożem y dokonać dezasemblacji funkcji (przykład 4.9), aby lepiej zrozum ieć przebieg operacji w yszu kiwania w przestrzeniach nazw. Przykład 4.8. Wyszukiwanie w przestrzeniach nazw import math from math import sin def t e s t 1 ( x ) : > > > %timeit test1(123456) 1000000 loops, best o f 3: 381 ns p e r loop r e tu r n m ath .s in (x ) def t e s t 2 ( x ) : > > > %timeit test2(123456) 1000000 loops, best o f 3: 311 ns p e r loop r e tu r n s i n ( x ) def t e s t 3 ( x , s i n = m a t h . s i n ) : > > > %timeit test3(123456) 1000000 loops, best o f 3: 3 0 6 ns p e r loop r e tu r n s i n ( x )
Przykład 4.9. Wyszukiwanie w przestrzeniach nazw po dokonanej dezasemblacji >>> d i s . d i s ( t e s t l ) 9 0 LOAD GLOBAL 3 LOAD ATTR 6 LOAD FAST 9 CALL FUNCTION 12 RETURN_VALUE >>> di s . d i s ( t e s t 2 ) 15 0 LOAD GLOBAL 3 LOAD FAST 6 CALL FUNCTION 9 RETURN_VALUE >>> di s . d i s ( t e s t 3 ) 21 0 LOAD FAST 3 LOAD FAST 6 CALL FUNCTION 9 RETURN VALUE
0 (math) 1 (s i n ) 0 (x) 1
# Wyszukiwanie w słowniku # Wyszukiwanie w słowniku # Wyszukiwanie lokalne
0 (s i n ) 0 (x) 1
# Wyszukiwanie w słowniku # Wyszukiwanie lokalne
1 (s i n ) 0 (x) 1
# Wyszukiwanie lokalne # Wyszukiwanie lokalne
Pierwsza funkcja te s tl tworzy wywołanie funkcji sin przez jaw ne użycie biblioteki math. Jest to też w id oczne w w ygenerow anym kodzie bajtow ym : najpierw m usi zostać załadow ane odwołanie do modułu math, a następnie dla tego modułu przeprow adzane jest wyszukiw anie atrybutów do momentu uzyskania odwołania do funkcji sin. Odbywa się to za pomocą dwóch operacji wyszukiw ania w słowniku. Pierwsza z nich znajduje m oduł math, a druga funkcję sin w module. Z kolei funkcja t e s t 2 jaw nie im portu je funkcję sin z m odułu math, a następn ie funkcja jest bezpośrednio dostępna w obrębie globalnej przestrzeni nazw. Oznacza to, że m ożesz uniknąć wyszukiw ania modułu math i kolejnego wyszukiw ania atrybutów. Niemniej jednak nadal ko nieczne jest znalezienie funkcji sin w globalnej przestrzeni nazw. Jest to jeszcze jeden powód, dla którego należy w yraźnie określić, jakie funkcje są im portowane z modułu.
90
|
Rozdział 4. Słowniki i zbiory
Takie podejście nie tylko zw iększa czytelność kodu, gdyż program ista w ie dokładnie, jakie funkcje są w ym agane ze źródeł zewnętrznych, ale też przyspiesza w ykonyw anie kodu! Funkcja te s t 3 definiuje funkcję sin jako argument słowa kluczowego z wartością domyślną, która jest odwołaniem do funkcji sin w obrębie modułu math. Choć w dalszym ciągu konieczne jest znalezienie odwołania do tej funkcji w module, trzeba to zrobić tylko wtedy, gdy funkcja te s t3 zostanie zdefiniowana jako pierwsza. Później odwołanie do funkcji sin jest przechowy wane w definicji funkcji jako zmienna lokalna w postaci domyślnego argumentu słowa klu czow ego. Jak w cześniej w spom niano, do znalezienia zm iennych lokalnych nie trzeba prze prow adzać wyszukiwania w słowniku. Zm ienne te są przechow yw ane w bardzo niewielkiej tablicy, która cechuje się bardzo krótkimi czasami wyszukiwania. Z tego powodu znajdowanie funkcji przebiega dość szybko! C hoć takie efekty są interesującym w ynikiem sposobu zarząd zania przestrzeniam i nazw w języku Python, funkcja test3 zdecydowanie nie jest odpowiednia dla tego języka. Okazuje się na szczęście, że takie dodatkow e wyszukiw ania w słowniku zaczynają pow odow ać spadek wydajności tylko wtedy, gdy są bardzo często wywoływane (tzn. w najbardziej wewnętrznym bloku bardzo szybkiej pętli, tak jak w przykładzie zbioru Julii). M ając to na uwadze, bardziej przejrzystym rozwiązaniem byłoby ustaw ienie przed uruchomieniem pętli zmiennej lokalnej z odw ołaniem globalnym . Choć w dalszym ciągu konieczne będzie w yszukiw anie globalne każdorazowo przy wywołaniu funkcji, wszystkie wywołania jej w pętli będą przebiegać szybciej. Przemawia za tym to, że naw et minutow e spowolnienia w kodzie m ogą zostać spotęgowane, jeśli kod jest urucham iany m iliony razy. N aw et jeśli wyszukiw anie w słowniku zajm uje zale dwie kilkaset nanosekund, w przypadku wykonywania miliony razy pętli dla tej operacji czas szybko może się wydłużyć. Jak w idać w przykładzie 4.10, przyspieszenie 9,4% uzyskiwane jest już tylko przez samo utworzenie funkcji sin jako lokalnej względem intensywnej oblicze niowo pętli, która ją w ywołuje. Przykład 4.10. Efekty powolnych wyszukiwań w przestrzeniach nazw w pętlach from math import si n def t i g h t _ l o o p _ s l o w ( i t e r a t i o n s ) : > > > %timeit tight_loop_slow (10000000) 1 loops, best o f 3: 2.21 s p e r loop result = 0 f o r i in x r a n g e ( i t e r a t i o n s ) : # to w yw ołanie fu n kcji sin wymaga w yszukiwania g lobaln eg o r e s u l t += s i n ( i ) def t i g h t _ l o o p _ f a s t ( i t e r a t i o n s ) : > > > %timeit tight_loop_fast(10000000) 1 loops, best o f 3: 2.02 s p e r loop result = 0 l o c a l _ s i n = si n f o r i in x r a n g e ( i t e r a t i o n s ) : # to w yw ołanie fu n kcji local_sin wymaga wyszukiwania lokaln ego r e s u l t += lo c a l s i n ( i )
Słowniki i przestrzenie nazw
|
91
Podsumowanie Słow niki i zbiory zapew niają znakom ity sposób przechow yw ania danych, które m ogą być indeksowane w edług klucza. M etoda użycia takiego klucza z wykorzystaniem funkcji mie szania może w dużym stopniu wpłynąć na wynikową wydajność struktury danych. Co więcej, zaznajom ienie się ze sposobem działania słow ników pozwala lepiej zrozum ieć nie tylko to, jak zorganizow ać dane, ale też jak uporządkow ać kod. W ynika to stąd, że słow niki stanow ią nieodłączną część wewnętrznych funkcji języka Python. W następnym rozdziale zostaną omówione generatory, które um ożliw iają zapewnienie da nych kodowi przy większej kontroli uporządkowania i bez konieczności uprzedniego prze chowywania w pamięci pełnych zbiorów danych. Pozwala to uniknąć wielu m ożliwych prze szkód, które m ogą w ystąpić podczas używania dowolnej z wewnętrznych struktur danych języka Python.
92
|
Rozdział 4. Słowniki i zbiory
__________________ ROZDZIAŁ 5.
Iteratory i generatory
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • W jaki sposób generatory oszczędzają pamięć? • W jakich sytuacjach najlepiej skorzystać z generatora? • W jaki sposób użyć narzędzia iterto o ls do tworzenia złożonych przepływ ów pracy generatora? • Kiedy wartościow anie leniwe jest korzystne, a kiedy nie?
Gdy w iele osób m ających doświadczenie z innym językiem zaczyna się uczyć języka Python, zaskakuje je różnica w zapisie pętli for. Oznacza to, że zam iast zapisu: # Inne języ ki f o r (i = 0 ; i
w języku Python w ystępuje w niej wprow adzenie do nowej funkcji o nazw ie range lub xrange: # Jęz y k Python f o r i in ran ge (N ): do_work(i)
Te dw ie funkcje zapew niają w gląd w paradygm at program ow ania oparty na użyciu gene ratorów. Abyś m ógł w pełni zrozum ieć generatory, utwórzmy najpierw proste im plementacje funkcji range i xrange: def r a n g e ( s t a r t , s t o p , s tep = 1): numbers = [] while s t a r t < st op : num bers.append(start) s t a r t += step return numbers def x r a n g e ( s t a r t , stop , st e p = 1 ): while s t a r t < st op : yield s ta rt # O s t a r t += step f o r i in r a n g e ( 1 ,1 0 0 0 0 ) : pass f o r i in x r a n g e (1 ,1 0 0 0 0 ) : pass
93
O Funkcja yield zw róci wiele wartości zam iast jednej. Powoduje to przekształcenie tej zw y czajnie w yglądającej funkcji w generator, który może być wielokrotnie odpytywany o na stępną dostępną wartość. Pierwszą rzeczą godną uwagi jest to, że im plementacja funkcji range m usi uwzględniać wcze śniejsze utw orzenie listy w szystkich liczb z zakresu. A zatem jeśli zakres obejm uje w artości od 1 do 10 000, funkcja wykona 10 000 operacji dołączania do listy numbers (jak wspomniano w rozdziale 3., związane jest z tym dodatkowe obciążenie), a następnie zw róci ją. Z kolei ge nerator ma możliwość „zwracania" wielu wartości. Każdorazowo, gdy w kodzie zostanie na potkana funkcja yield, w yemituje ona swoją wartość. Po zażądaniu kolejnej w artości funkcja wznowi działanie (zachowując swój poprzedni stan) i w yemituje now ą wartość. Gdy funkcja osiągnie swój koniec, zostanie zgłoszony wyjątek StopIteration wskazujący, że dany generator nie zaw iera więcej w artości. W rezultacie naw et pom im o tego, że obie funkcje m uszą osta tecznie w ykonać taką samą liczbę obliczeń, w ersja poprzedniej pętli z funkcją range zużywa 10 razy więcej pamięci (lub N razy więcej w przypadku zakresu od 1 do N). M ając ten kod na uw adze, m ożna dokonać dekom pozycji pętli for, w których używ ane są im plem entacje funkcji range i xrange. W języku Python pętle for w ym agają, aby iteracja była obsługiwana przez obiekt, dla którego wykonywana jest pętla. Oznacza to, że musi być m oż liwe utworzenie iteratora poza obiektem, dla którego ma być stosowana pętla. W celu utwo rzenia iteratora przy użyciu niemal każdego obiektu m ożesz po prostu skorzystać z wbudo wanej funkcji ite r języka Python. W przypadku list, krotek, słowników i zbiorów funkcja ta zwraca iterator dla elementów lub kluczy w obiekcie. Dla bardziej złożonych obiektów funkcja ite r zwraca w ynik w łaściw o ści ite r obiektu. Ponieważ funkcja xrange zwraca już iterator, wywołanie dla niego funkcji ite r jest trywialną operacją, która pow oduje po prostu zw róce nie oryginalnego obiektu (stąd też type(xrange(1 , 10)) == type(iter(xrange( 1 , 10) ))). Ponieważ jednak funkcja range zwraca listę, konieczne jest utw orzenie nowego obiektu, czyli iteratora listy, który przeprowadzi iterację dla w szystkich wartości na liście. Po utworzeniu iteratora wywoływana jest dla niego funkcja next(), która pobiera nowe wartości do momentu zgło szenia wyjątku StopIteration. Zapewnia to odpowiedni obraz pętli for poddanych rozkładowi, co ilustruje przykład 5.1. Przykład 5.1. Pętla w języku Python poddana rozkładowi # P ętla w języku Python f o r i in o b j e c t : do_work(i) # O dpow iada temu o b je ct_ ite ra to r = ite r(o b je c t) while True: try: i = o b je c t_ ite r a to r.n e x t() do_work(i) except S t o p I t e r a t i o n : break
W kodzie pętli for widać, że w ykonyw ane jest dodatkow e działanie polegające na w yw oła niu funkcji ite r podczas używania funkcji range zam iast funkcji xrange. W przypadku stoso w ania funkcji xrange tw orzony jest generator, który bez trudu przekształcany jest w iterator (to już je st iterator!). Gdy jed n ak używ ana jest funkcja range, konieczne je st przyd zielenie nowej listy i wcześniejsze obliczenie jej wartości, a następnie nadal w ymagane jest utworzenie iteratora!
94
|
Rozdział 5. Iteratory i generatory
Co ważniejsze, wcześniejsze obliczanie listy funkcji range wymaga przydzielenia wystarczają cego miejsca dla pełnego zbioru danych i ustawienia dla każdego elementu poprawnej warto ści, nawet pomimo tego, że zawsze potrzebna jest jednocześnie tylko jedna wartość. Powoduje to też, że przydzielanie listy staje się nieprzydatne. Okazuje się, że może naw et uniemożliwić uruchom ienie pętli, poniew aż funkcja range m oże próbow ać przydzielić w ięcej pam ięci, niż jest dostępne (funkcja range(100,000,000) utw orzyłaby listę o w ielkości w ynoszącej 3,1 GB!). Po pom iarze czasu będzie to bardzo w yraźnie widoczne: def t e s t _ r a n g e ( ) : > > > %timeit test_range() 1 loops, best o f 3: 446 ms p e r loop f o r i in ra n g e (1 , 10000000): pass def t e s t _ x r a n g e ( ) : > > > %timeit test_xrange() 1 loops, best o f 3: 2 7 6 ms p e r loop f o r i in xran g e (1 , 10000000): pass
Choć może to w yglądać na problem dosyć prosty do rozwiązania (po prostu należy zastąpić w szystkie w yw ołania funkcji range w yw ołaniam i funkcji xrange), w rzeczyw istości jest on znacznie pow ażniejszy. Załóżm y, że istnieje długa lista liczb, w przypadku której chcem y określić, ile spośród nich jest podzielnych przez 3. M oże to w yglądać następująco: d i v i s i b l e _ b y _ t h r e e = l e n ( [ n f o r n in list_of_num bers i f n % 3 == 0])
Towarzyszy temu jednak taki sam problem jak funkcji range. Ponieważ tworzone jest wyrażenie listowe, wcześniej generowana jest lista liczb podzielnych przez 3 tylko w celu wykonania dla niej obliczeń. Jeśli taka lista liczb będzie dość duża, m oże to oznaczać przydzielenie, niem al bez żadnego powodu, dużej ilości pamięci, potencjalnie większej, niż jest dostępna. Jak wcześniej wspomniano, wyrażenie listowe możesz utworzyć za pomocą instrukcji o postaci [ for in i f ]. Spowoduje to utworzenie listy wszystkich elementów . Alternatywnie możliwe jest zastosowanie podobnej składni do utworzenia generatora elementów zamiast listy. Składnia ma postać: ( for in i f ). Przy skorzystaniu z tej subtelnej różnicy między wyrażeniem listowym i wyrażeniem gene ratora możliwe jest optymalizowanie wcześniej utworzonego kodu dla listy divisible_by_three. Generatory nie mają jednak właściwości length. W efekcie trzeba się wykazać trochę większym sprytem: d i v i s i b l e _ b y _ t h r e e = sum((1 f o r n in list_of_num bers i f n % 3 == 0))
W tym przypadku generator em ituje w artość 1 każdorazowo po napotkaniu liczby podzielnej przez 3, a w przeciwnym razie nic się nie dzieje. Przy sumowaniu w szystkich elementów w tym generatorze właściwie realizowane jest to samo co w wersji z wyrażeniem listowym. W ydajność dwóch wersji tego kodu jest praw ie identyczna, ale wpływ wersji z generatorem na pam ięć jest znacznie m niejszy niż wersji z wyrażeniem listowym. Co więcej, m ożliwe jest proste przekształcenie wersji z wyrażeniem listowym w generator, ponieważ wszystko, co jest istotne dla każdego elementu listy, sprowadza się do jej bieżącej w artości. Liczba jest podzielna przez 3 albo nie. N ie ma znaczenia to, gdzie jest um iejscowiona na liście, ani to, jakie
Iteratory dla szeregów nieskończonych
|
95
są poprzednie/następne w artości. Choć możliwe jest również przekształcenie w generatory bardziej złożonych funkcji, zależnie od ich powiązania ze stanem, m oże to okazać się trudne do zrealizowania.
Iteratory dla szeregów nieskończonych Ponieważ niezbędne jest jedynie przechow yw anie określonej wersji stanu i em itowanie tylko bieżącej wartości, generatory idealnie nadają się do zastosowania dla szeregów nieskończo nych. Znakomitym przykładem jest szereg Fibonacciego, który jest szeregiem nieskończonym z dwiema zmiennymi stanu (dwie ostatnie liczby Fibonacciego): def f i b o n a c c i ( ) : i , j = 0, 1 while True: yield j i, j = j,
i + j
W kodzie widać, że choć j jest em itowaną wartością, śledzone jest również i, ponieważ prze chowuje stan szeregu Fibonacciego. Informacja o stanie niezbędna do obliczeń jest dość istot na dla generatorów, gdyż przekształcana jest w informację o faktycznym wykorzystaniu pa mięci przez obiekt. Oznacza to tyle, że jeśli używana jest funkcja, która korzysta intensywnie ze stanu i zwraca znikom ą ilość danych, lepszym rozwiązaniem może być w stępne przetw a rzanie listy danych przez tę funkcję niż stosowanie generatora dla listy. Powodem, dla którego generatory nie są wykorzystywane w takim stopniu, w jakim mogłyby, jest to, że wiele zawartej w nich logiki może być hermetyzowane w używanym kodzie z logiką. Oznacza to, że generatory służą w rzeczywistości do organizowania kodu i zapewniania bar dziej inteligentnych pętli. Na przykład na wiele sposobów można udzielić odpowiedzi na na stępujące pytanie: „Ile liczb Fibonacciego poniżej wartości 5000 jest nieparzystych?". def f i b o n a c c i _ n a i v e ( ) : i , j = 0, 1 count = 0 while j <= 5000: i f j % 2: count += 1 i, j = j, i + j r e tu r n count def f i b o n a c c i _ t r a n s f o r m ( ) : count = 0 f o r f in f i b o n a c c i ( ) : i f f > 5000: break i f f % 2: count += 1 r e tu r n count from i t e r t o o l s import i s l i c e def f i b o n a c c i _ s u c c i n c t ( ) : is_odd = lambda x : x % 2 f i r s t _ 5 0 0 0 = i s l i c e ( f i b o n a c c i ( ) , 0 , 5000) r e tu r n sum(1 f o r x in f i r s t _ 5 0 0 0 i f is _o dd (x ))
Choć w szystkie te m etody m ają podobne działanie (każda pow oduje identyczne obciążenie pam ięci i zapew nia taką sam ą w ydajność), funkcja fibonacci_transform zyskuje pod w ielo ma w zględam i. Przede w szystkim funkcja ta jest znacznie bardziej szczegółow a niż funkcja fibonacci_su ccinct. O znacza to, że inny program ista będzie m ógł ją bez trudu debugow ać
96
|
Rozdział 5. Iteratory i generatory
i zrozum ieć. Kw estia ta stanow i głów nie ostrzeżenie, które w arto w ziąć pod uw agę przed lekturą następnego podrozdziału. Zostaną w nim om ówione typowe przepływy pracy w yko rzystujące m oduł iterto o ls. Choć znacznie upraszcza on wiele działań z użyciem iteratorów, m oże też szybko spraw ić, że kod Python stanie się w yjątkow o nietypow y dla tego języka. Dla odmiany funkcja fibonacci_naive realizuje wiele działań jednocześnie, co powoduje ukrycie rzeczywiście w ykonyw anych przez nią obliczeń! W przypadku funkcji generatora oczywiste jest, że iterowane są liczby Fibonacciego, nie jesteśmy więc szczególnie obciążani rzeczywistymi obliczeniam i. Funkcja fibonacci_transform m oże być bardziej uogólniona. N azw ę tej funkcji można zm ienić na num_odd_under_5000. Funkcja ta może pobierać generator przy użyciu argu m entu, a tym samym przetw arzać dowolne szeregi. Ostatnią zaletą funkcji fibonacci_transform jest obsługiwanie przez nią występujących w obli czeniach dwóch faz: generowania i przekształcania danych. Funkcja ta w bardzo przejrzysty sposób w ykonuje transformację danych, funkcja fibonacci natom iast generuje je. To w yraźne rozgraniczenie zapewnia dodatkową przejrzystość i funkcjonalność: m ożliw e jest zastosow a n ie funkcji transformacji dla nowego zbioru danych lub do w ykonania wielu transformacji istniejących danych. Ten paradygm at zaw sze był istotny podczas tworzenia złożonych pro gramów. Generatory ułatwiają to wyraźnie, ponieważ stają się odpowiedzialne za tworzenie danych. Zw ykłe funkcje odpowiadają za w ykonyw anie działań na wygenerowanych danych.
Wartościowanie leniwe generatora Jak wcześniej wspomniano, korzyści związane z pamięcią w przypadku generatora uzyskiwane są przez zajmowanie się wyłącznie bieżącymi żądanymi wartościami. W dowolnym momencie obliczeń z wykorzystaniem generatora używana jest tylko bieżąca wartość, a ponadto nie można odwoływać się do żadnych innych elementów w sekwencji (działające w ten sposób algorytmy są ogólnie nazywane „jednoprzejściowymi" lub online). Choć czasem może to powodować, że genera tory będą trudniejsze do zastosowania, istnieje wiele modułów i funkcji, które mogą w tym pomóc. Podstawowa godna zainteresowania biblioteka nosi nazwę itertool s. Stanowi ona część biblioteki standardowej. Oprócz wielu innych przydatnych funkcji zapewnia w ersje z generatorem wbu dowanych funkcji języka Python, takich jak map, reduce, fi lte r i zip (w module itertools wersje te m ają nazwy imap, ireduce, i f i l t e r i izip). Na szczególną uwagę zasługują następujące funkcje: is lic e Umożliwia podział potencjalnie nieskończonego generatora. chain Łączy w łańcuch wiele generatorów. takewhile Dodaje warunek, który pow oduje zakończenie działania generatora. cycle Przez ciągłe powtarzanie skończonego generatora powoduje, że staje się on nieskończony. Utw órzm y przykład użycia generatorów do analizowania dużego zbioru danych. Załóżm y, że dla istniejących danych przeprowadzono procedurę analizy. Przez ostatnie 20 lat w ciągu sekundy analizowano jedną porcję danych. Oznacza to 631 152 000 punktów danych! Dane są zapisyw ane w pliku po jednym w ierszu na sekundę. Do pamięci n ie można załadow ać całe go zbioru danych. Aby przeprow adzić proste w ykryw anie niepraw idłow ości, m ożna użyć generatorów, w ogóle n ie przydzielając żadnych list!
W artościowanie leniwe generatora
|
97
Problem jest następujący: dla podanego pliku danych z wierszami w postaci „znacznik czasu, w artość" m ają zostać znalezione dni z w artością trzech odchyleń standardow ych (3 sigma) od średniej dla tego dnia. N ajpierw napiszem y kod, który w czyta p lik w iersz po w ierszu, a następnie zw róci w artość każdego w iersza jako obiekt języka Python. Zostanie rów nież utworzony generator read_fake_data do generowania fałszywych danych, za pom ocą których mogą być testowane algorytmy. Funkcja ta wymaga jeszcze argumentu filename, aby korzy stać z tej samej sygnatury funkcji co w przypadku funkcji read_data. Zostanie to jednak po prostu zignorowane. Te dwie funkcje (przykład 5.2) napraw dę są poddawane procesowi le niwego wartościowania. Następny wiersz w pliku jest wczytywany lub generowane są fałszywe dane tylko wtedy, gdy zostanie wywołana właściwość next() generatora. Przykład 5.2. Leniwe wczytywanie danych from random import nor m alvari ate, rand from i t e r t o o l s import count def re a d _ d a t a (file n a m e ): with open(filenam e) as fd: f o r l i n e in fd: data = l i n e . s t r i p ( ) . s p l i t ( ' , ' ) y i e l d m ap(in t, data) def re ad _ fa k e _ d a t a (file n a m e ): f o r i in c o u n t ( ) : sigma = rand() * 10 y i e l d ( i , n o rm a l v a ri a t e (0 , sigma))
M ożliwe jest teraz zastosowanie funkcji groupby w m odule iterto o ls do grupowania znaczni ków czasu występujących w tym samym dniu (przykład 5.3). Działanie tej funkcji polega na pobraniu sekwencji elementów i klucza używanego do grupowania tych elementów. Wynikiem jest generator tw orzący krotki, których elem enty są kluczem grupy, a także generator dla elementów w grupie. W roli funkcji klucza zostanie utw orzona funkcja lambda, która zwraca obiekt date. Obiekty te są równorzędne, gdy w ystępują tego samego dnia, co pow oduje gru powanie ich w edług dnia. Taka funkcja klucza może przyjąć dowolną postać. Dane m ogą być grupowane według godziny, roku lub wybranej właściwości zawartej w rzeczywistych danych. Jedynym ograniczeniem jest to, że grupy będą tw orzone tylko dla sekw encyjnych danych. A zatem jeśli istniałyby dane w ejściow e A A A A B B A A i grupa groupby grupow ana w edług litery, zostałyby uzyskane trzy grupy (A, [A, A, A, A]), (B, [B, B]) i (A, [A, A]). Przykład 5.3. Grupowanie danych from dat etime import date from i t e r t o o l s import groupby def d a y _ g r o u p e r ( i te r a b le ) : key = lambda (timestamp, value) : date.fromtimestamp(timestamp) re tu r n g r o u p b y ( it e r a b l e , key)
Aby teraz przeprow adzić rzeczywiste wykrywanie nieprawidłowości, w ykonam y iterację dla w artości dnia, śledząc w artość średnią i m aksym alną. Średnia będzie obliczana za pom ocą średniej online i algorytmu odchylenia standardowego1. M aksimum jest utrzymywane, ponie waż stanow i najlepszego kandydata, by w ykazać niepraw idłow ości. Jeśli m aksim um wynosi
1 U ż y w a n y jest algo ry tm o n l n e K n u t h a średniej. U m o żliw ia o n obliczenie średniej i pi er w sz eg o m o m e n t u (w ty m pr zy padk u odch ylenia s tandardowego) z a p o m o c ą pojedynczej z mien nej tymczasowej. M ożliw e jest też obliczanie ko l e jn y c h m o m e n t ó w p rz e z n ie z n a c z n e z m o d y f i k o w a n i e r ó w n a ń i d o d a n i e w ię ksz ej l icz b y z m i e n n y c h stanu (po jednej n a moment). Więcej informacji dostępnych jest p od adresem http://www.johndcook.com/standard_deviation.html.
98
|
Rozdział 5. Iteratory i generatory
więcej niż 3 sigma przekraczające średnią, zostanie zwrócony obiekt date reprezentujący dzień, który właśnie poddano analizie. W przeciwnym razie zostanie zwrócona wartość False z myślą o przyszłych zastosowaniach. Równie dobrze jednak działanie funkcji mogłoby po prostu zostać zakończone (z niejaw nie zw róconą w artością None). W artości te są zw racane, gdyż funkcja check_anomaly jest definiow ana jako filtr danych. Zw raca ona w artość True dla danych, które pow inny zostać zachowane, oraz w artość False w przypadku danych do pominięcia. Umoż liw ia to filtrow anie oryginalnego zbioru danych i pozostaw ianie tylko tych dni, które są zgodne z określonym warunkiem. Funkcję check_anomaly zaprezentowano w przykładzie 5.4. Przykład 5.4. Wykrywanie nieprawidłowości przy użyciu generatora import math def check_anomaly((day, day _ data)) : # Z najdow ana je s t średnia, odchylen ie standardow e i w artość m aksym alna d la dnia # Użycie algorytmu jed n op rzejściow eg o średniej/odchylenia standardow ego po zw a la jed y n ie # raz wczytać dane dla dnia n = 0 mean = 0 M2 = 0 max_value = None f o r timestamp, value in day_data: n += 1 d e l t a = value - mean mean = mean + delta/n M2 += d e l t a * ( v a l u e - mean) max_value = max(max_value, value) var iance = M2/(n - 1) st an d ar d_d ev ia tion = m a th .s q r t(v a r i a n c e ) # W tym m iejscu faktyczn ie spraw dzane jest, czy dane d la dnia w ykazują niepraw idłow ość # J e ś l i tak jest, zw racana je s t w artość dnia; w przeciwnym razie zostanie zw rócona w artość F a lse i f max_value > mean + 3 * st and ar d_d ev ia tion : re tu rn day re tu rn F a l s e
Elem entem zw iązanym z tą funkcją, który m oże w ydać się dziw ny, jest dodatkow y zestaw nawiasów okrągłych w definicji parametrów. Nie jest to żadna literówka, lecz w ynik użycia tej funkcji, która pobiera dane w ejściow e z generatora groupby. Jak w spom niano, generator ten zwraca krotki, które stają się parametram i właśnie funkcji check_anomaly. W efekcie w celu właściwego wyodrębnienia klucza i danych grupy konieczne jest rozszerzenie krotek. Ponie waż używ any jest iterator if i l t e r , innym sposobem poradzenia sobie z tym bez konieczności rozszerzania krotek wewnątrz definicji funkcji jest zdefiniowanie iteratora is ta r filte r . Iterator wykonuje działania podobne do działań iteratora istarmap w przypadku iteratora imap (więcej informacji zawiera dokumentacja narzędzia itertools). Na koniec m ożem y połączyć w łańcuch generatory, aby uzyskać dni z danymi w ykazującym i nieprawidłowości (przykład 5.5). Przykład 5.5. Połączenie generatorów w łańcuch from i t e r t o o l s import i f i l t e r , imap data = re ad_ da ta (d ata_filen am e) data_day = day_grouper(data) anomalous_dates = i f i l t e r ( N o n e , imap(check_anomaly, data_day)) # O fi rs t_ an o m alou s_d ate , firs t_ an om al ou s_ data = anom alo us_ da te s.nex t() p r i n t "Pierwsza data z nieprawidłowościami: " , first_an om al ou s_ date
W artościowanie leniwe generatora
|
99
O Iterator i f il t e r usunie wszystkie elementy, które nie są zgodne z danym filtrem. Domyślnie iterator (ma to miejsce w momencie przekazania wartości None do pierw szego parametru) odfiltruje wszystkie elementy, dla których zostanie określona wartość False. Tym sposobem nie będą uwzględniane żadne dni, w przypadku których funkcja check_anomaly nie stwierdzi nieprawidłowości. Metoda ta w bardzo prosty sposób pozwala uzyskać listę dni wykazujących nieprawidłowości bez konieczności ładowania całego zbioru danych. Godne uwagi jest to, że powyższy kod w rze czywistości nie przeprowadza żadnego obliczenia. Konfiguruje jedynie potok w celu wykonania obliczenia. Plik w ogóle nie zostanie wczytany do momentu użycia funkcji anomalous_dates.next() lub zastosowania iteracji dla generatora anomalous_dates. Okazuje się, że analiza jest przepro w adzana dopiero po zażądaniu now ej w artości z generatora anomalous_dates. A zatem jeśli pełny zbiór danych zaw iera pięć niepraw id łow ych dat, ale w kodzie pobierana je st jedna data, po czym następuje zatrzymanie w celu zażądania nowych wartości, plik zostanie wczy tany tylko do miejsca, w którym wystąpią dane dla danego dnia. Jest to określane mianem wartościowania leniwego. W jego przypadku w ykonyw ane są wyłącznie jaw nie zażądane obli czenia. Jeśli istnieje w arunek w czesnego zakończenia, może to znacząco skrócić ogólny czas działania kodu. Inna subtelność związana z organizowaniem analizy w ten sposób umożliwia wykonywanie bez trudu bardziej wymagających obliczeń bez konieczności przebudowywania dużych porcji kodu. Aby na przykład uzyskać ruchome okno dla jednego dnia, zamiast używać grupowania według dni, można utworzyć nową funkcję day_grouper: from datetime import datetime def rolling_window_grouper(data, window_size=3600): window = t u p l e ( i s l i c e ( d a t a , 0 , window_size)) while True: curr en t_ d ate tim e = datetime.fromtimestamp(window[0][0]) y i e l d (c u r ren t_ d a te ti m e, window) window = window[1:] + ( d a t a . n e x t ( ) , )
Zastępujem y teraz po prostu w yw ołanie funkcji day_grouper z przykładu 5.5 w yw ołaniem funkcji rolling_window_grouper i uzyskujemy żądany wynik. W przypadku tej wersji widoczne jest rów nież bardzo w yraźnie zapew nienie pamięci przez tę i poprzednią m etodę. Jako stan ta metoda będzie przechow yw ać tylko dane odpowiadające oknu (w obu przypadkach jest to jeden dzień lub 3600 punktów danych). By to zmienić, należy wielokrotnie otworzyć plik i użyć różnych deskryptorów cyklu życia do w skazania dokładnie tych danych, które m ają zostać użyte (lub skorzystać z modułu linecache). Jest to jednak w ym agane tylko wtedy, gdy przy kładowy podzbiór zbioru danych nadal nie mieści się w pamięci. Końcowa uwaga: w funkcji rol ling_window_grouper wykonywanych jest wiele operacji pop i append dla listy window. M ożliwe jest zoptymalizowanie tego w znacznym stopniu przez zastosowa nie obiektu deque w m odule collections. Obiekt ten zapewnia operacje dołączania i usuwania o czasie 0 ( 1), które są w ykonyw ane dla elementów znajdujących się na początku lub końcu listy (zwykłe listy cechują się czasem 0 ( 1) dla operacji dołączania i usuwania wykonywanych dla końca listy oraz czasem 0 (n) dla tych samych operacji dotyczących początku listy). Za po mocą obiektu deque możliwe jest dołączenie nowych danych po prawej stronie (lub na końcu) listy. M etoda deque.popleft() pozwala usunąć dane po lewej stronie (lub na początku) listy bez konieczności przydzielania dodatkowego miejsca lub wykonywania czasochłonnych ope racji o czasie 0 (n).
100
|
Rozdział 5. Iteratory i generatory
Podsumowanie Z definiow anie algorytm u znajd ującego niepraw id łow ości za pom ocą iteratorów pozw ala przetw arzać znacznie w ięcej danych, niż m oże zm ieścić się w pam ięci. Co w ięcej, m ożliw e jest wykonanie tego w krótszym czasie niż w przypadku zastosow ania list, ponieważ elimi nowane są wszystkie kosztowne operacje append. Ze względu na to, że iteratory są typem podstawowym języka Python, zaw sze należy uży w ać ich przy próbie zmniejszenia poziomu wykorzystania pamięci przez aplikację. Korzyści w postaci wartościowania leniwego wyników sprawiają, że przetw arzane są tylko niezbędne dane, a ponadto oszczędzana jest pamięć, ponieważ nie są w niej przechow yw ane wcześniej sze w yniki, dopóki nie zostanie to jaw nie zażądane. W rozdziale 11. będzie m ow a o innych m etodach m ożliw ych do w ykorzystania w przypadku bardziej specyficznych problem ów . Ponadto w rozdziale tym zostaną zaprezentow ane now e sposoby analizowania trudności, gdy pojawi się kłopot z pam ięcią RAM. Inną korzyścią z rozwiązywania problem ów za pomocą iteratorów jest przygotowanie kodu do użycia z w ykorzystaniem wielu procesorów lub komputerów (więcej o tym w rozdziałach 9. i 10.). Jak wspomniano w podrozdziale „Iteratory dla szeregów nieskończonych", podczas stosowania iteratorów zaw sze trzeba pam iętać o różnych stanach w ym aganych do działania algorytmu. Po stwierdzeniu, w jaki sposób ma zostać przygotowany stan niezbędny do uru chomienia algorytmu, nie będzie mieć znaczenia to, gdzie on działa. Tego rodzaju paradygmat można zauważyć na przykład w przypadku modułów multiprocessing i ipython, które do uru chamiania zadań równoległych używają funkcji podobnej do funkcji map.
Podsumowanie
|
101
102
j
Rozdział 5. Iteratory i generatory
_______________________________________ ROZDZIAŁ 6.
Obliczenia macierzowe i wektorowe
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Jakie są wąskie gardła w obliczeniach wektorowych? • Jakich n arzęd zi m ożna u żyć do określen ia, ja k efek ty w n ie p ro ceso r w ykonuje obliczen ia? • Dlaczego narzędzie numpy lepiej radzi sobie z obliczeniami numerycznymi niż czysty kod Python? • Czym są liczniki cache-misses i page-faults? • Jak można śledzić przydziały pam ięci w utworzonym kodzie?
Niezależnie od tego, jaki problem ma zostać rozwiązany za pom ocą komputera, w pewnym momencie zetkniesz się z obliczeniami w ektorow ym i. Stanowią one integralną część tego, jak komputer działa, a także w jaki sposób próbuje skrócić czas działania program ów aż do po ziomu kondensatorów . Kom puter potrafi jedynie przetw arzać liczby. Oznacza to, że uzyska nie informacji o sposobie jednoczesnego w ykonyw ania kilku takich obliczeń pozw oli przy spieszyć program. W tym rozdziale spróbujemy objaśnić część złożoności tego problem u. W tym celu skoncen trujemy się na dość prostym zadaniu matematycznym, rozwiązując równanie dyfuzji i anali zując to, co dzieje się na poziom ie procesora. Zrozumienie, jak różny kod Python w pływ a na działanie procesora i jak efektyw nie badać ten w pływ , pozw oli Ci opanow ać um iejętność analizowania również innych problem ów . Najpierw zaprezentujem y problem i przedstaw im y szybkie rozwiązanie bazujące na czystym kodzie Python. Po zidentyfikow aniu problem ów dotyczących pam ięci i podjęciu próby ich rozwiązania za pomocą czystego kodu Python skorzystam y z narzędzia numpy i zastanowim y się, jak i dlaczego pow oduje ono przyspieszenie wykonania kodu. Później zaczniem y wpro w adzać zmiany w algorytmie i przygotowywać kod pod kątem rozwiązania problemu. Dzięki usunięciu części ogólnych elementów używanych bibliotek możliwe będzie dodatkowe zwięk szenie szybkości. Na końcu zostanie zastosowanych kilka dodatkowych modułów, które uła twią tego rodzaju przetwarzanie w praktyce. Jednocześnie przypomnimy ostrzeżenie dotyczące optymalizowania przed profilowaniem.
103
Wprowadzenie do problemu
^
N in ie jsz y p o d ro z d z ia ł m a z a z a d a n ie p o m ó c C i w le p s z y m z ro z u m ie n iu ró w n ań , k tó re b ę d ą ro z w ią z y w a n e w rozd ziale. P rz e d lektu rą re sz ty ro zd ziału a b so lu tn ie n ie m u s is z o p a n o w a ć treści tego p o d ro z d z ia łu . Jeśli z a m ie rz a s z go p o m in ą ć , p a m ięta j o p r z e a n a l i z o w a n i u a l g o r y t m u z p r z y k ł a d ó w 6.1 i 6.2, b y z r o z u m i e ć k o d , k t ó r y b ę d z i e op tym alizow any . Je ś li j e d n a k p o s t a n o w i s z p r z e c z y t a ć t e n p o d r o z d z i a ł , a n a w e t b ę d z i e s z o c z e k i w a ć d o d a t k o w e g o o b ja ś n i e n i a , p r z e c z y t a j r o z d z i a ł 17. tr zec iej e d y c ji k s i ą ż k i N u m e r ic a l R ec ip e s a u to rs tw a W illia m a P re ssa i in n y c h ( w y d a w n ic tw o C a m b r id g e U n iv e r s ity Press).
Aby objaśnić przedstawione w rozdziale obliczenia macierzowe i wektorowe, wielokrotnie zo stanie użyty przykład dyfuzji płynów. Dyfuzja to jeden z mechanizm ów pow odujących ruch płynów i podejm ujących próby jednakow ego ich zmieszania. W podrozdziale zostaną przybliżone pojęcia matem atyczne zw iązane z równaniem dyfuzji. Choć m oże w ydać się to skom plikow ane, nie m a pow odu do obaw! A by całe zagadnienie stało się bardziej zrozum iałe, szybko je uprościm y. O panow anie w podstaw ow ym zakresie końcowego równania, które będzie rozwiązywane, okaże się przydatne w trakcie lektury roz działu, ale nie jest niezbędne. W kolejnych rozdziałach skupim y się przede w szystkim na różnych wariantach kodu, a nie na samym równaniu. Jeśli zrozum iesz równanie, będzie Ci po prostu łatwiej zapoznać się ze sposobam i optym alizow ania kodu. Tak jest w w iększości przypadków. Zrozum ienie tego, na czym kod bazuje, oraz niuansów algorytmu pozwoli Ci dokładniej poznać m ożliw e metody optymalizacji. Prostym przykładem dyfuzji jest rozprzestrzenianie się barw nika w w odzie. Jeśli w wodzie o tem peraturze pokojowej umieścisz kilka kropli barwnika, będzie on pow oli przemieszczać się aż do momentu, gdy całkowicie wymiesza się z wodą. Ponieważ w oda nie jest mieszana ani nie jest na tyle ciepła, aby w yw ołać prądy konw ekcyjne, dyfuzja będzie podstaw ow ym procesem , który spow oduje w ym ieszanie się dw óch płynów . W przypadku num erycznego rozwiązywania równań związanych z procesem dyfuzji wybieram y żądany w arunek począt kowy, a ponadto mamy możliw ość rozwijania go w m iarę upływu czasu w celu stwierdzenia, jaką postać przyjmie na późniejszym etapie (rysunek 6 .2 ). Gdy już to wiadomo, najważniejszą do naszych celów informacją na temat dyfuzji jest jej de finicja. Rów nanie dyfuzji, definiowane jako częściowe równanie różnicowe o jednym wym ia rze (1W), jest zapisyw ane w następującej postaci: 3 t \ „ 32 t v — u ( x , t ) = D -------- u ( x , t ) dt K ’ dx2 V ’
W powyższym równaniu u to w ektor reprezentujący m asy poddawane dyfuzji. Na przykład m oże istnieć w ektor o w artościach 0 (w przypadku samej w ody), 1 (dla sam ego barw nika) oraz wartościach pośrednich odpowiadających w ymieszaniu w ody i barwnika. Ogólnie rzecz biorąc, będzie to macierz dw uwym iarowa (2W) lub trójwym iarowa (3W), która reprezentuje rzeczywistą powierzchnię lub objętość płynu. Tym sposobem wektor u może być macierzą 3W, która reprezentuje płyn w szklance. Zamiast wyznaczenia drugiej pochodnej w kierunku x ko nieczne byłoby w yznaczenie jej dla w szystkich osi. Ponadto sym bol D w rów naniu to w iel kość fizyczna, która reprezentuje sym ulow ane w łaściw ości płynu. Duża w artość w ielkości D
104
|
Rozdzia ł 6. Obliczenia macierzowe i wektorowe
odzwierciedla płyn, który z dużą łatwością może podlegać dyfuzji. Choć dla uproszczenia na potrzeby używ anego kodu zostanie ustaw ione D o w artości 1, w dalszym ciągu w ielkość ta będzie uwzględniana w obliczeniach. R ó w n a n i e d y f u z j i j e s t te ż n a z y w a n e r ó w n a n ie m p r z e w o d n ic tw a c ie p ln e g o . W t y m p r z y p a d k u u re p reze n tu je te m p era tu rę o b szaru , a D o pisuje stop ie ń p rz e w o d n ic tw a ciepl n e g o m a te ria łu . R o z w ią z a n ie r ó w n a n ia p o z w a la s tw ierd z ić, ja k ie je st p r z e w o d n ic tw o c i e p l n e . U m o ż l i w i a n i e t y l k o o k r e ś l e n i e , j a k k i l k a k r o p l i b a r w n i k a p r z e m i e s z c z a si ę w w o d zie, ale i sp raw d ze n ie, ja k a jest d y fu z ja w rad iatorze c ie p ła g e n e ro w a n e g o p rzez procesor.
Dla rów nania dyfuzji, które jest ciągłe w czasie i przestrzeni, dokonam y aproksym acji przy użyciu objętości i czasów dyskretnych. W tym celu zostanie użyta metoda Eulera. Polega ona po prostu na pobraniu pochodnej i zapisaniu jej jako różnicy w następujący sposób: d dt
i( x ,t ) :
u ( x ,t + d t) + u ( x, t) dt
W pow yższym rów naniu dt oznacza stałą liczbę reprezentującą krok czasow y lub rozw ią zanie w czasie, dla którego równanie ma zostać rozwiązane. M ożna to przyrów nać do liczby klatek na sekundę filmu, który próbujesz stworzyć. Gdy liczba klatek będzie w zrastać (lub dt zm niejszać się), uzyskamy bardziej przejrzysty obraz tego, co się dzieje. Okazuje się, że gdy dt zmierza do zera, aproksymacja Eulera staje się dokładna (zauważ jednak, że ta dokładność może być osiągnięta w yłącznie teoretycznie, ponieważ kom puter cechuje się tylko skończoną precyzją, a ponadto błędy numeryczne szybko wpłyną znacząco na wyniki). Możliwe jest zatem zmodyfikowanie tego równania w celu stwierdzenia, jakie jest u(x, t+dt) dla danego u (x ,t). Oznacza to, że możemy zacząć od pewnego stanu początkowego u(x, 0) (reprezentuje szklankę w ody w m omencie umieszczenia w niej kropli barwnika) i w ykorzystać opisane mechanizmy do „rozwinięcia" tego stanu, aby sprawdzić, jak będzie się prezentować w przyszłych momen tach czasu (u(x,dt)). Tego typu problem jest określany mianem problemu wartości początkowej lub problemu Cauchy'ego. Stosując podobny zabieg dla pochodnej osi x z wykorzystaniem aproksymacji różnic skoń czonych, uzyskujemy następujące równanie końcowe: . . . . u ( x + d x , t) + u (x + d x , t) + 2 ■u ( x , t ) -----------— u ( x , t + d t) = u ( x , t ) + dt * D * — ----------- -----------W tym przypadku, podobnie do tego, jak dt reprezentuje liczbę klatek na sekundę, dx repre zentuje rozdzielczość obrazów. Mniejsza wartość dx oznacza mniejszy obszar reprezentowany przez każdą komórkę w macierzy. Dla uproszczenia po prostu zostanie ustaw ione Do w arto ści 1 oraz dx o wartości 1. Te dwie w artości stają się bardzo istotne podczas przeprowadzania właściw ych symulacji fizycznych. Ponieważ jednak równanie dyfuzji jest tu rozwiązywane tylko do celów demonstracyjnych, w tym przypadku nie mają one żadnego znaczenia. Korzystając z przedstawionego równania, można rozwiązać praw ie każdy problem dotyczą cy dyfuzji. Istnieją jednak pew ne kw estie z nim zw iązane. Przede w szystkim , jak wcześniej w spom niano, indeks przestrzenny w u (tj. param etr x) będzie reprezentow any jako indeksy w macierzy. Co się stanie przy próbie znalezienia w artości w x-dx, gdy x znajduje się na po czątku macierzy? Problem ten nazywany jest warunkiem brzegowym. M ogą istnieć trwałe warunki
W prowadzenie do problemu
| 105
brzegow e z następującą informacją: „każda wartość spoza granic używanej macierzy zostanie ustawiona na 0 (lub dowolną inną w artość)". Alternatywnie m ogą istnieć okresowe warunki brzegowe informujące o tym, że wartości będą „przekręcane". Oznacza to, że jeśli jeden z wy m iarów m acierzy m a długość N, w artość w tym w ym iarze w indeksie -1 jest taka sama jak w w ym iarze N-1, a w artość w w ym iarze Njest identyczna z w artością w indeksie 0. Inaczej mówiąc, przy próbie uzyskania dostępu do wartości w indeksie i zostanie otrzymana wartość w indeksie (i%N). Inną kw estią jest to, jak będzie przechow yw anych w iele składników czasu w ektora u. Dla każdej w artości czasu, dla której są przeprow adzane obliczenia, m oże istnieć jedna macierz. W ydaje się, że jako minimum będą wymagane dwie macierze: po jednej dla bieżącego stanu płynu i jego następnego stanu. Jak się okaże, w przypadku tej konkretnej kwestii pojawiają się czynniki mające bardzo duży wpływ na wydajność. A zatem jak w praktyce wygląda rozw iązyw anie tego problem u? Przykład 6.1 zawiera pseudokod, który prezentuje sposób użycia do tego przedstawionego równania. Przykład 6.1. Pseudokod dla dyfuzji jednowymiarowej # Tworzenie w arunków początkow ych u = v e c t o r o f len gth N f o r i in range (N ): u = 0 i f t h e r e i s water, 1 i f t h e r e i s dye # R ozw ijanie warunków początkow ych D= 1 t = 0 dt = 0.0 00 1 while True: p r i n t “Bieżący c zas: %f" % t unew = v e c t o r o f s i z e N # A ktualizowanie kroku d la każd ej kom órki f o r i in range(N): unew[i] = u [ i ] + D * dt * (u[(i+1)%N] + u[(i-1)%N] - 2 * u [ i ] ) # Przen iesienie zaktualizow anego rozw iązania d o w ektora u u = unew v i s u a l i z e(u )
Powyższy kod pobiera warunki początkowe dla barwnika w wodzie i informuje, jak układ ten będzie wyglądał z biegiem czasu przy interwale wynoszącym 0,0001 sekundy. W yniki dzia łania kodu pokazano na rysunku 6 .1 , na którym zaprezentowano przemieszczanie się w mia rę upływu czasu bardzo skoncentrowanej kropli barwnika (reprezentowana na rysunku przez najwyżej um ieszczoną funkcję w kształcie kapelusza). Na wykresach można zaobserwować, jak z czasem barw nik staje się dobrze wymieszany, i m oment, gdy wszędzie w ystępuje jego podobne stężenie. Na potrzeby rozdziału zostanie uzyskane rozwiązanie dwuwym iarowej wersji poprzedniego równania. Oznacza to jedynie, że zam iast w ektora (lub inaczej mówiąc, m acierzy z jednym indeksem) będzie przetwarzana macierz dw uwym iarowa. Jedyna zmiana w równaniu (a tym samym w poniższym kodzie) w iąże się z koniecznością pobrania drugiej pochodnej dla kie runku y. Oznacza to po prostu, że pierwotne równanie, które zostało użyte, przyjm ie obecnie następującą postać: 9 u t( x , y , t \ — ) =D
106
|
& u
91 u t( * , y, t \\ (x , y , t \ )+— )
t
Rozdział 6. Obliczenia macierzowe i wektorowe
Rysunek 6.1. Przykład dyfuzji jednowymiarowej To num eryczne równanie dyfuzji w wersji dwuwymiarowej wyrażane jest w postaci pseudokodu z przykładu 6.2. W tym celu można użyć tych samych m etod co wcześniej. Przykład 6.2. Algorytm do obliczania dyfuzji dwuwymiarowej f o r i in ran ge (N ): f o r j in range(M): u n e w [ i ] [ j ] = u [ i ] [ j ] + dt * ( (u[(i+1)%N] [ j ] + u [ ( i - 1 ) % N ] [ j ] ( u [ i ] [ ( j +1)%M] + u [ j ] [ ( j- 1 ) % M ]
- 2 * u [i][j]) - 2 * u [i][j])
\ + \ # dA2 u / dxA2 \ # dA2 u / dyA2
)
M ożna teraz zebrać to wszystko i utw orzyć w języku Python pełny kod dyfuzji dwuwym ia rowej, który posłuży jako baza do testów porów nawczych opisanych w dalszej części książki. Choć kod wygląda na bardziej skomplikowany, w yniki przypom inają te uzyskane dla kodu dyfuzji jednowymiarowej (zaprezentowano je na rysunku 6 .2 ). Aby poszerzyć w iedzę zw iązaną z tem atam i poruszanym i w tym podrozdziale, zajrzyj na stronę serwisu Wikipedia poświęconą równaniu dyfuzji (http://en.wikipedia.org/wiki/Diffusion_equation), a także przeczytaj rozdział 7. książki N um erical methods fo r complex systems autorstw a S. V. Gurevicha (http://pauli.uni-muenster.de/tp/fileadmin/lehre/NumMethoden/WS0910/ScriptPDE/Heat.pdf).
Czy listy języka Python są wystarczająco dobre? Użyjm y pseudokodu z przykładu 6.1 i tak go zm odyfikujm y, aby łatw iej było analizow ać w ydajność jego działania. Pierw szym krokiem jest utw orzenie funkcji przetw arzania, która pobiera macierz i zwraca jej stan po przetwarzaniu. Prezentuje to przykład 6.3.
Czy listy języka Python są wystarczająco dobre?
|
107
Rysunek 6.2. Przykład dyfuzji dla dwóch zbiorów warunków początkowych Przykład 6.3. Dyfuzja dwuwymiarowa przy użyciu czystego kodu Python grid_shape = (1024, 1024) def e v o l v e ( g r i d , d t , D=1.0): xmax, ymax = grid_shape new_grid = [ [ 0 . 0 , ] * ymax f o r x in xrange(xmax)] f o r i in xrange(xmax): f o r j in xrange(ymax): grid_xx = g r i d [ ( i + 1)%xmax][j] +g r i d [ ( i - 1 ) % x m a x ] [ j] - 2 . 0 * g r i d [ i ] [ j ] grid_yy = g r i d [ i ] [ ( j + 1)%ymax] +g r id [ i ] [ ( j- 1 ) % y m a x ] - 2 . 0 * g r i d [ i ] [ j ] n e w _ g r i d [ i ] [ j] = g r i d [ i ] [ j ] + D* (grid_xx + grid_yy) * dt r e tu r n new_grid
Z a m i a s t w s t ę p n i e p r z y d z i e l a ć l istę new_grid, m o ż n a u t w o r z y ć j ą w p ę t l i f o r z a p o m o c ą m e t o d append. C h o ć t e n w a r i a n t b y ł b y z n a c z n i e s z y b s z y o d p i e r w s z e g o z p o d a n y c h , w n i o s k i w d a l s z y m c ią g u b y ły b y w ła śc iw e . Z d e c y d o w a liś m y się n a w s tę p n e p rzy ^
d z i e l e n i e li sty , p o n i e w a ż j e s t b a r d z i e j o b r a z o w a .
Zmienna globalna grid_shape określa w ielkość obszaru, który będzie symulowany. Jak w yja śniono w podrozdziale „W prowadzenie do problem u", używane są okresowe w arunki brze gowe (z tego w łaśnie powodu dla indeksów stosowana jest operacja m odulo). Aby faktycznie użyć tego kodu, konieczne jest zainicjow anie siatki i w yw ołanie dla niej funkcji evolve. Kod z przykładu 6.4 to bardzo ogólna procedura inicjowania, która będzie wielokrotnie wykorzy stywana w rozdziale (parametry w ydajnościow e tego kodu nie będą analizowane, ponieważ w ym aga on tylko jednokrotnego uruchom ienia w przeciw ieństw ie do w ielokrotnie w yw o ływanej funkcji evolve).
108
|
Rozdział 6. Obliczenia macierzowe i wektorowe
Przykład 6.4. Inicjalizacja dyfuzji dwuwymiarowej przy użyciu czystego kodu Python def ru n_e xperim en t( nu m _it era tions) : # Ustawianie w arunków początkow ych O xmax, ymax = grid_shape gr id = [ [ 0 . 0 , ] * ymax f o r x in xrange(xmax)] block_low = in t (g r i d _ s h a p e [ 0 ] * .4) block_high = in t (g r i d _ s h a p e [ 0 ] * .5 ) f o r i in xrange(block_low, bloc k_high): f o r j in xr ange(block_low, bloc k_hi gh): g r i d [ i ] [ j ] = 0.0 05 # Rozw ijanie warunków początkow ych s t a r t = ti m e . ti m e () f o r i in r a n g e (n u m _ ite r a ti o n s ): gr id = e v o l v e ( g r i d , 0 . 1 ) re tu rn t i m e . ti m e () - s t a r t
O Używane tutaj warunki początkowe są takie same jak w przykładzie z kwadratami (rysunek 62). W artości liczby dt i elementów siatki zostały tak dobrane, aby były wystarczająco małe w celu zapewnienia stabilności algorytmu. W trzeciej edycji książki Numerical Recipes (http://www.nr.com/) autorstwa Williama Pressa i innych można znaleźć obszerniejsze omówienie właściwości zbież ności tego algorytmu.
Problemy z przesadną alokacją U żyw ając narzędzia lin e_p ro filer dla funkcji przetw arzania czystego kodu Python, można rozpocząć analizowanie tego, co wpływa na m ożliw y w olny czas działania. Po przyjrzeniu się danym wyjściowym tego narzędzia do profilowania (przykład 6.5) stwierdzimy, że w ięk szość czasu działania funkcji zajm uje obliczenie pochodnej i aktualizowanie siatki1. W łaśnie tego oczekiwaliśmy, gdyż jest to problem powiązany wyłącznie z procesorem. Jakikolwiek czas poświęcony na coś innego niż rozwiązywanie tego problemu oznacza oczywiście, że pojawia się potrzeba zastosowania optymalizacji. Przykład 6.5. Profilowanie dyfuzji dwuwymiarowej przy użyciu czystego kodu Python $ k ernprof.py -l v diffusion_python.py Wrote p r o f i l e r e s u l t s to d i f fu s i o n _ p y t h o n .p y .l p r o f Timer u n i t : 1e-06 s F i l e : diffu sion_p yt ho n.p y Function: evolve a t l i n e 8 Total time: 16.1 39 8 s Line # Hits Time Per Hit % Time Line Contents 8 9 10 11 12 13 14 15 16 17
10 2626570 5130 2626560 2621440 2621440 2621440 10
39 2159628 4167 2126592 4259164 4196964 3393273 10
3.9 0.8 0.8 0.8 1.6 1.6 1.3 1.0
0.0 13.4 0.0 13.2 2 6 .4 2 6 .0 2 1 .0 0.0
@ profi le def e v o l v e ( g r i d , d t , D=1 . 0 ) : xmax, ymax = gr id shape # O new gr id = . . . f o r i in xrange(xmax): # 0 f o r j in xrange(ymax): gr id xx = . . . grid_yy = . . . n e w _ g r i d [ i ] [ j] = . . . r e tu r n gr id # ©
1 S ą t o d a n e w y j ś c i o w e k o d u z p r z y k ł a d u 6.3, k t ó r e z o s t a ł y p r z y c i ę t e d o m a r g i n e s ó w st ro n y . J a k w c z e ś n i e j w s p o m n i a n o , a b y m o ż l i w e b y ł o p r o f i l o w a n i e p r z e z s k r y p t ke rn pr of.p y, k o n i e c z n e je s t z a s t o s o w a n i e d la f u n k c ji d e k o r a t o r a @ p r o fi l e (w ięc ej in fo rm ac ji z a w i e r a p o d r o z d z i a ł „ U ż y c i e n a r z ę d z i a l in e _p ro fi ler d o p o m i a r ó w d o t y c z ą c y c h k o l e jn y c h w i e r s z y k o d u " ) .
Czy listy języka Python są wystarczająco dobre?
|
109
0
Instrukcja zajm uje tak wiele czasu dla jednego trafienia z pow odu konieczności pobrania zm iennej grid_shape z lokalnej przestrzeni nazw (więcej inform acji zaw iera podrozdział „Słowniki i przestrzenie nazw ").
© Z tym w ierszem pow iązanych jest 5130 trafień. Oznacza to, że dla przetw arzanej siatki xmax ma wartość 512. Wynika to z wykonania 512 operacji sprawdzania dla każdej wartości w zakresie funkcji xrange oraz jednej takiej operacji dotyczącej warunku zakończenia pętli. W szystko to zostało pow tórzone 10 razy. © Z tym w ierszem pow iązanych je st 10 trafień. O znacza to, że funkcja była profilow ana w ram ach 10 uruchom ień. Dane wyjściow e pokazują też jednak, że 20% czasu zajęło przydzielanie listy new_grid. Jest to m arnotrawstwo, ponieważ właściwości tej listy nie zm ieniają się. Niezależnie od tego, jakie w artości są w ysyłane do funkcji evolve, lista new_grid zaw sze będzie m ieć taką sam ą postać 1 w ielkość, a ponadto będzie zaw ierać jednakow e w artości. Prosta optymalizacja polegałaby na jednorazowym przydzieleniu tej listy i wykorzystaniu jej ponownie. Tego rodzaju optyma lizacja przypomina przemieszczenie powtarzającego się kodu poza obręb szybkiej pętli: from math import sin def lo op_s lo w (nu m _i ter at io n s) : > > > %timeit loop_slow (int(1e4)) 100 loops, best o f 3: 2.6 7 ms p e r loop result = 0 f o r i in x ra n g e (n u m _ ite r a ti o n s): r e s u l t += i * sin (n u m _ i te r a ti o n s ) # O r e tu r n r e s u l t def lo o p _ f a s t ( n u m _ i t e r a t i o n s ) : > > > %timeit loop_fast(int(1e4)) 1000 loops, best o f 3: 1.38 ms p e r loop result = 0 f a c t o r = sin (n u m _ i te r a ti o n s ) f o r i in x ra n g e (n u m _ ite r a ti o n s): r e s u l t += i re tu rn r e s u l t * f a c t o r
O W artość funkcji sin(num_iterations) nie zmienia wydajności pętli, dlatego nie jest za każdym razem obliczana. Jak ilustruje to przykład 6 .6, m ożliw a jest do przeprow adzenia transform acja podobna do użytej w kodzie dyfuzji. W tym przypadku instancja listy new_grid z przykładu 6.4 zostanie utworzona i wysłana do funkcji evolve. Funkcja zadziała tak samo jak w cześniej, czyli wczyta listę grid i zapisze ją w liście new_grid. Następnie można po prostu zam ienić listę new_grid na listę grid i w znowić kontynuowanie działań. Przykład 6.6. Dyfuzja dwuwymiarowa przy użyciu czystego kodu Python po zmniejszeniu liczby alokacji pamięci def e v o l v e ( g r i d , d t , out, D=1.0): xmax, ymax = grid_shape f o r i in xrange(xmax): f o r j in xrange(ymax): grid_xx = g r i d [ ( i + 1)%xmax][j] + g r i d [ ( i - 1 ) % x m a x ] [ j] - 2 . 0 * g r i d [ i ] [ j ] grid_yy = g r i d [ i ] [ ( j + 1)%ymax] + g r i d [ i ] [ ( j- 1 ) % y m a x ] - 2 . 0 * g r i d [ i ] [ j ] o u t [ i ] [ j ] = g r i d [ i ] [ j ] + D * (grid_xx + grid_yy) * dt def ru n_e xpe rim en t(nu m _ite ra tions ):
110
|
Rozdział 6. Obliczenia macierzowe i wektorowe
# Ustawianie w arunków początkow ych xmax,ymax = grid_shape n ex t_ g rid = [ [ 0 . 0 , ] * ymax f o r x in xrange(xmax)] gr id = [ [ 0 . 0 , ] * ymax f o r x in xrange(xmax)] block_low = in t (g r i d _ s h a p e [ 0 ] * .4) block_high = in t (g r i d _ s h a p e [ 0 ] * .5 ) f o r i in xrange(block_low, bloc k_high): f o r j in xr ange(block_low, bloc k_hi gh): g r i d [ i ] [ j ] = 0.0 05 s t a r t = ti m e . ti m e () f o r i in r a n g e (n u m _ ite r a ti o n s ): e v o l v e ( g r i d , 0 . 1 , n ext_g rid) g r i d , n ex t_ g rid = n e x t_ g ri d , grid re tu rn t i m e . ti m e () - s t a r t
Na podstaw ie profilow ania w ierszy zm odyfikow anej w ersji kodu z przykładu 6.7 2 m ożna stwierdzić, że ta niewielka zmiana zapewniła przyspieszenie w ynoszące 21%. Prowadzi to do wniosku podobnego do tego, który wynikał z omówienia operacji dołączania wykonywanych dla list (punkt „Listy jako tablice dynam iczne" z rozdziału 3.), a mianowicie, że alokacje pa mięci oznaczają koszt. Każdorazowo, gdy żądana jest pam ięć w celu zapisania zmiennej lub listy, interpreter języka Python musi poświęcić czas na komunikację z systemem operacyj nym w celu przydzielenia nowego miejsca w pam ięci, a następnie na wykonanie iteracji dla nowo przydzielonego obszaru, co jest niezbędne do zainicjowania go przy użyciu danej warto ści. Gdy tylko jest to możliwe, ponow ne wykorzystanie już przydzielonego miejsca zapewni w zrost wydajności. Trzeba jednak zachow ać ostrożność przy wprowadzaniu takich zmian. Choć przyspieszenie może być znaczące, jak zaw sze należy przeprow adzić profilowanie, aby upewnić się, że osiągamy oczekiwane wyniki, a nie tylko wprowadzamy nieporządek w kodzie podstawowym. Przykład 6.7. Profilowanie wierszy kodu Python dyfuzji po zmniejszeniu liczby alokacji pamięci $ kernprof.py -l v diffusion_python_memory.py Wrote p r o f i l e r e s u l t s to diffusion_python_memory.py.lprof Timer u n i t : 1e-06 s F i l e : diffusion_python_memory.py Function: evolve a t l i n e 8 Total time: 13.3 20 9 s Line # Hits Time Per Hit % Time Line Contents 8 9 10 11 12 13 14 15
10 5130 2626560 2621440 2621440 2621440
15 3853 1942976 4059998 4038560 3275454
1.5 0 .8 0.7 1.5 1.5 1.2
0.0 0.0 14.6 30 .5 30 .3 2 4 .6
@ profi le def e v o l v e ( g r i d , d t , out, D=1. xmax, ymax = gr id shape f o r i in xrange(xmax): f o r j in xrange(ymax): grid_xx = . . . grid_yy = . . . o u t[i][j] = ...
Fragmentacja pamięci Z kodem Python utw orzonym w przykładzie 6.6 w dalszym ciągu zw iązany jest problem , który nieodłącznie towarzyszy użyciu języka Python do wykonywania tego rodzaju operacji na w ektorach. Rzecz w tym, że język ten nie obsługuje w e w łasnym zakresie w ektoryzacji. Wynika to z dwóch powodów: listy języka Python przechowują wskaźniki do samych danych, 2 P r o f i l o w a n y tutaj k o d to k o d z p r z y k ł a d u 6.6, p rz y c i ę ty d o m a r g i n e s ó w stron y.
Fragmentacja pamięci
|
111
a ponadto kod bajtowy Python nie jest zoptymalizowany pod kątem wektoryzacji. Oznacza to, że pętle for nie mogą „przew idzieć", kiedy użycie wektoryzacji byłoby korzystne. To, że listy języka Python przechow ują wskaźniki, oznacza, że zam iast utrzym yw ać same inte resujące nas dane, listy zawierają położenia określające, gdzie dane m ogą zostać znalezione. W większości zastosowań jest to dobre, ponieważ umożliwia przechowywanie w obrębie listy danych dowolnego żądanego typu. Gdy jednak pojaw iają się operacje wektorowe i m acie rzowe, jest to przyczyną dużego spadku wydajności. Zm niejszenie w ydajności ma m iejsce, poniew aż przy każdorazow ym pobieraniu elem entu z macierzy grid konieczne jest wykonyw anie wielu wyszukiwań. Na przykład kod grid[5][2] w ym aga przeprow adzenia najpierw w yszukiw ania na liście grid indeksu 5. Pow oduje to zwrócenie wskaźnika określającego miejsce przechow yw ania danych w tym położeniu. Dalej wymagane jest kolejne wyszukiw anie na liście elementu o indeksie 2 dotyczące zwróconego obiektu. Po uzyskaniu tego odwołania określone zostanie miejsce przechow yw ania rzeczywi stych danych. O bciążenie zw iązane z jednym takim w yszukiw aniem nie jest duże i w w iększości sytuacji m oże zostać zignorow ane. Jeśli jednak żądane dane zostałyby um ieszczone w jednym cią głym bloku w pamięci, możliwe byłoby przemieszczenie wszystkich danych w jednej operacji, a nie w dw óch dla każdego elem entu. Jest to jedna z głów nych kw estii zw iązanych z fragmentacją danych: gdy taka sytuacja występuje, konieczne jest przem ieszczanie każdej porcji danych osobno, a nie w postaci jednego bloku. Oznacza to, że generowane jest większe obcią żenie związane z transferami z pamięci. Ponadto procesor jest zmuszony do czekania podczas przesyłania danych. Okaże się, jak jest to ważne, podczas analizowania licznika cache-misses za pom ocą narzędzia perf. Problem z przekazywaniem właściwych danych do procesora (gdy ich wymaga) jest związany z tak zwanym wąskim gardłem Von Neumanna. Odnosi się do faktu, że istnieje ograniczona przepustow ość m iędzy pam ięcią i procesorem, co jest spowodowane warstwową architektu rą pam ięci w ykorzystyw aną w now oczesnych komputerach. Jeśli możliwe byłoby przemiesz czanie danych nieskończenie szybko, obecność jakiejkolwiek pamięci podręcznej byłaby zbęd na, ponieważ procesor byłby w stanie natychm iast pobierać dowolne w ym agane dane. Byłby to stan, w którym nie istniałoby wąskie gardło. Ponieważ dane nie mogą być przem ieszczane nieskończenie szybko, konieczne jest wstępne pobieranie ich z pamięci RAM i przechowywanie w mniejszych, lecz szybszych pamięciach podręcznych procesora, aby, przy odrobinie szczęścia, procesor mógł znaleźć potrzebną porcję danych w miejscu, z którego dane m ogą być szybko w czytane. Choć jest to skrajnie wyideali zowany sposób postrzegania architektury, w dalszym ciągu w idocznych jest kilka związa nych z nią problem ów . Jak stw ierdzić, jakie dane będą potrzebne w przyszłości? Procesor spraw dza się dobrze w przypadku m echanizm ów nazyw anych przew idyw aniem rozgałęzień i potokowaniem . M echanizm y te podejm ują jeszcze podczas przetw arzania bieżącej instrukcji próbę przewidzenia następnej instrukcji i załadow ania do pam ięci podręcznej odpowiednich porcji danych z pamięci. Niemniej jednak do zminim alizowania wpływu wąskiego gardła na wydajność najważniejsza jest wiedza na temat sposobu przydzielania pam ięci i przeprow a dzania obliczeń dla danych. Dość trudne m oże być sprawdzenie, jak dobrze zaw artość pam ięci jest przenoszona do pro cesora. Jednakże w systemie Linux narzędzie perf może być używ ane do uzyskania zadzi wiającej ilości informacji o tym, jak procesor radzi sobie z działającym programem. Narzędzie
112
|
Rozdział 6. Obliczenia macierzowe i wektorowe
to można uruchom ić na przykład dla czystego kodu Python z przykładu 6 .6, aby sprawdzić, jak efektywnie procesor w ykonuje kod. W yniki zaprezentowano w przykładzie 6 .8 . Zauważ, że dane wyjściow e w tym i dalszych przykładach zastosow ania narzędzia perf zostały przy cięte do m arginesów strony. Usunięte dane zaw ierały w ariancje dla każdego pomiaru wska zujące, w jakim stopniu wartości zmieniły się w ciągu wielu testów porównawczych. Przydaje się to do stwierdzenia, jak bardzo mierzona wartość jest zależna od rzeczywistych parametrów wydajnościow ych programu w porównaniu z innymi właściwościami systemu (np. innymi działającymi programami, które korzystają z zasobów systemowych). Przykład 6.8. Liczniki wydajności dla dyfuzji dwuwymiarowej przy użyciu czystego kodu Python po zmniejszeniu liczby alokacji pamięci $ p e rf s t a t -e c y c le s ,s ta lle d -c y c le s -f r o n te n d ,s ta lle d -c y c le s -b a c k e n d ,in s tr u c tio n s ,\ c a ch e -re fe re n c e s ,c a c h e -m is s e s ,b ra n c h e s ,b ra n c h -m is s e s ,ta s k -c lo c k ,fa u lts ,\ m in o r-fa u lts ,c s ,m ig ra tio n s - r 3 python diffusion_python_memory.py Performance co un ter s t a t s f o r 'python diffusion_python_memory.py' (3 ru ns ): 3 2 9 , 1 5 5 ,3 5 9 , 0 1 5 c y c l e s # 3.4 7 7 GHz 76,800,457,550 s ta lle d -c y c le s-fro n te n d # 23.33% fr ontend c y c l e s i d l e 46,556,100,820 stalled -cycles-b ackend # 14.14% backend c y c l e s i d l e 5 9 8 , 1 3 5 ,1 1 1 , 0 0 9 i n s t r u c t i o n s # 1.82 insns per c y c l e # 0 .1 3 s t a l l e d c y c l e s per insn 3 5 , 4 9 7 ,1 9 6 c a c h e - r e f e r e n c e s # 0.3 7 5 M/sec 1 0 , 7 1 6 ,9 7 2 cach e- misse s # 30 .1 91 % of a l l cache r e f s 1 3 3 ,8 8 1 ,2 4 1 , 2 5 4 branches # 1414.067 M/sec 2 , 8 9 1 , 0 9 3 , 3 8 4 branch-misses # 2.16% o f a l l branches 94678.127621 t a s k - c l o c k # 0 . 9 9 9 CPUs u t i l i z e d 5.439 page-faults # 0.0 5 7 K/sec 5 . 4 3 9 m i n o r - f a u lt s # 0.0 5 7 K/sec 125 c o n t e x t -s w i t c h e s # 0.0 01 K/sec 6 CPU-migrations # 0 . 0 0 0 K/sec 94.749 389 12 1 seconds time elapsed
Narzędzie perf Poświęćmy chwilę na zrozum ienie różnych liczników wydajności zapewnianych przez na rzędzie perf oraz ich zw iązku z przykładowym kodem. Licznik task-clock informuje o liczbie cykli zegarowych, jakie zajm uje w ykonyw ane zadanie. Jest to coś innego niż całkowity czas działania, ponieważ jeśli działanie programu zajęło 1 sekundę, ale zostały użyte dwa proce sory, licznik task-clock będzie m iał w artość 1000 (przew ażnie jest ona w yrażana w m ilise kundach). W wygodny dla nas sposób narzędzie perf przeprowadza obliczenia i obok tego licznika podaje inform ację o liczbie w ykorzystanych procesorów . W pow yższych danych wyjściowych w artość licznika nie w ynosi dokładnie 1, ponieważ wystąpiły okresy, w których proces korzystał z innych podsystem ów w celu wykonania instrukcji (na przykład podczas przydzielania pamięci). Liczniki context-switches i CPU-migrations informują o sposobie wstrzymania pracy programu w celu poczekania na zakończenie operacji jądra (np. operacji wejścia-wyjścia), umożliwienia działania innym aplikacjom lub przekazania w ykonyw ania do innego rdzenia procesora. Gdy wystąpi przełączenie kontekstu, wykonywanie programu jest wstrzymywane, a inny pro gram m oże rozpocząć działanie. Jest to bardzo czasochłonne zadanie, które pow inno zostać w ja k najw iększym stopniu zm inim alizow ane. N ie m am y jed n ak zbyt dużej kontroli nad momentem wystąpienia tego zdarzenia. Jądro decyduje o tym, kiedy program y m ogą zostać przełączone. Możliwe są jednak działania, które zapobiegną przemieszczaniu naszego programu
Fragmentacja pamięci
|
113
przez jądro. Ogólnie rzecz biorąc, w m omencie wykonyw ania operacji wejścia-wyjścia (np. odczytu z pam ięci, dysku lub sieci) jądro w strzym uje program . Jak się okaże w kolejnych rozdziałach, m ożliw e jest użycie procedur asynchronicznych w celu zapew nienia, że pro gram w dalszym ciągu będzie korzystał z procesora naw et podczas oczekiwania na operacje wejścia-wyjścia. Pozwala to zachow ać działanie program u bez przełączania kontekstu. Po nadto istnieje m ożliw ość ustawienia dla programu wartości za pom ocą narzędzia nice, aby określić dla programu priorytet i uniemożliwić jądru użycie dla niego przełączania kontekstu. W artość licznika CPU-migrations jest rejestrowana po zatrzym aniu program u i w znowieniu go w innym procesorze niż ten, który był używ any w cześniej, aby w szystkie procesory m iały taki sam poziom wykorzystania. M oże to być postrzegane jako szczególnie niewłaściwe prze łączanie kontekstu, ponieważ nie tylko program jest tymczasowo w strzym ywany, ale też tra cone są wszelkie dane znajdujące się w pamięci podręcznej L1 (jak wspomniano, każdy proces zawiera w łasną tego typu pamięć). Licznik page-fault stanowi część now oczesnego schematu przydzielania pam ięci w systemie Unix. Po przydzieleniu pam ięci jądro nie robi w iele, z w yjątkiem przekazania program ow i odwołania do pamięci. Później jednak w m omencie pierwszego użycia pam ięci system opera cyjny zgłasza niewielkie przerwanie błędu stronicowania, które powoduje wstrzymanie dzia łającego program u i odpow iednie przydzielenie pam ięci. Jest to nazyw ane system em przy dzielania leniwego. Choć w porównaniu z wcześniejszym i systemami alokacji pamięci metoda ta jest dość zoptymalizowana, niew ielkie błędy stronicowania stanowią kosztowną operację, ponieważ większość operacji realizowanych jest poza zasięgiem działającego programu. Istnieje również główny błąd stronicowania, który w ystępuje w m om encie zażądania przez program danych z urządzenia (dysku, urządzenia sieciowego itp.), z którego nie został jeszcze wyko nany odczyt. Tego rodzaju operacje są jeszcze bardziej kosztowne, gdyż nie tylko pow odują przerw anie program u, ale również uwzględniają odczyt z dowolnego urządzenia, na którym znajdują się dane. Tego typu błąd stronicowania nie ma zw ykle wpływu na funkcjonowanie pow iązania z procesorem . M oże jed n ak być źródłem problem u w przypadku dow olnego programu, który w ykonuje operacje odczytu/zapisu z wykorzystaniem dysku lub sieci. Gdy odwołamy się do danych znajdujących się w pamięci, pokonają one drogę przez różne w arstwy pamięci (omówienie tego zagadnienia zawiera podrozdział „W arstwy kom unikacji" w rozdziale 1.). Każdorazowo przy odwołaniu do danych zawartych w pamięci podręcznej zwiększa się w artość licznika cache-references. Jeśli takich danych nie będzie jeszcze w pa mięci podręcznej i w ym agane będzie pobranie ich z pam ięci RAM, zmieni się w artość liczni ka cache-miss. Nie dojdzie do tego, jeżeli odczytywane są dane w cześniej wczytyw ane (takie dane nadal są w pam ięci podręcznej) lub zlokalizow ane w pobliżu danych niedaw no uży w anych (dane są w ysyłane z pam ięci RAM do pam ięci podręcznej w porcjach). Chybienia w pamięci podręcznej mogą być źródłem spowolnienia w przypadku działania powiązanego z procesorem , ponieważ w takiej sytuacji nie tylko konieczne jest oczekiwanie na pobranie danych z pamięci RAM, ale też przerywany jest przepływ potoku wykonywania (więcej o tym wkrótce). W dalszej części rozdziału zostanie omówiona metoda zredukowania tego efektu przez optymalizację układu danych w pamięci. Licznik instructions informuje o liczbie instrukcji wydawanych procesorowi przez kod. Ze wzglę du na potokowanie jednocześnie może działać kilka takich instrukcji. W łaśnie o tym informuje adnotacja insns per cycle. W celu lepszej obsługi potokowania liczniki stalled-cycles-frontend i stalled-cycles-backend przekazują informacje o liczbie cykli, przez jakie program oczekiwał na w ypełnienie przodu lub tyłu potoku. M oże to w ystąpić z pow odu chybienia w pam ięci
114
|
Rozdział 6. Obliczenia macierzowe i wektorowe
podręcznej, niepopraw nie przewidzianego rozgałęzienia lub konfliktu zasobów. Przód poto ku odpowiada za pobieranie następnej instrukcji z pamięci i dekodowanie jej do postaci po prawnej operacji, tył potoku jest natom iast odpowiedzialny za samo uruchomienie operacji. W przypadku potokowania procesor jest w stanie wykonyw ać bieżącą operację podczas po bierania i przygotowywania następnej operacji. Licznik branches określa m om ent działania kodu, w którym zm ienia się przepływ wykony wania. Pomyśl o instrukcjach if..th e n . Zależnie od wyniku wykonania instrukcji warunkowej będzie wykonywana jedna lub druga sekcja kodu. Zasadniczo jest to rozgałęzienie w w yko nywaniu kodu — następna instrukcja w program ie m oże być jedną z dwóch. Aby to zopty malizować, zwłaszcza w odniesieniu do potoku, procesor próbuje odgadnąć, w jakim kierunku podąży rozgałęzienie, oraz w cześniej załadow ać odpow iednie instrukcje. Gdy rezultat tego przew idyw ania okaże się niepopraw ny, zostaną uzyskane w artości liczników stalled -cycles i branch-miss. Chybienia rozgałęzień m ogą być dość zaw iłe i pow odow ać w iele dziw nych w yników (na przykład niektóre pętle będą działać znacznie szybciej dla list sortowanych niż dla list niesortowanych, po prostu dlatego, że w pierwszym przypadku będzie mniej chybień rozgałęzień).
^
A b y z a z n a jo m i ć się z b a rd z ie j s z c z e g ó ł o w y m o b ja ś n ie n ie m tego, co się d z ie je n a p o z io m ie p ro c e s o r a w p r z y p a d k u ró ż n y c h licz n ik ó w w y d a jn o śc i, zajrz yj d o z n a k o m i t e g o p r z e w o d n i k a C o m p u t e r A r c h it e c t u r e T u to r ia l a u t o r s t w a G u r p u r a M . P r a b h u (h ttp ://w w w .c s .ia s ta te .e d u /~ p r a b h u /T u to r ia l/tit le .h tm l) . O m ó w i o n o w n i m p r o b l e m y n a b a rd z o n is k im p o z io m ie, d zięki c z e m u m o ż n a d o b rz e z ro z u m ie ć, co m a m iejsc e p o d p o d sz ew k ą p o d cz as d ziałan ia kodu.
Podejmowanie decyzji z wykorzystaniem danych wyjściowych narzędzia perf Liczniki wydajności przedstawione w przykładzie 6.8 pozwalają stwierdzić, że w czasie działa nia kodu procesor odwoływał się 35 497 196 razy do pamięci podręcznej L1/L2. Spośród tych odw ołań 10 716 972 (lub 30,191% ) stanow iły żądania danych, których nie było w pam ięci w momencie żądania, i w ym agały pobrania. Ponadto można zauważyć, że w każdym cyklu procesora m ożliw e jest w ykonanie średnio 1,82 instrukcji. Pozw ala to określić całkow ity w zrost szybkości w ynikający ze stosow ania potokow ania, w ykonyw ania nieuporządkow a nego i hiperw ątkow ości (lub dowolnej innej funkcji procesora, która umożliwia uruchom ie nie więcej niż jednej instrukcji w ciągu cyklu procesora). Fragmentacja zwiększa liczbę transferów z pamięci do procesora. Ponadto, ze względu na to, że w momencie zażądania obliczenia w pamięci podręcznej procesora nie ma gotowych wielu porcji danych, nie będzie m ożliw a w ektoryzacja obliczeń. Jak w spom niano w podrozdziale „W arstwy kom unikacji" w rozdziale 1., w ektoryzacja obliczeń (lub spowodowanie jednocze snego wykonywania przez procesor wielu obliczeń) może w ystąpić tylko w przypadku wypeł nienia pamięci podręcznej procesora wszystkimi odpowiednimi danymi. Ponieważ magistrala może przesyłać w yłącznie ciągłe obszary pamięci, będzie to m ożliwe tylko w tedy, gdy dane siatki będą sekwencyjnie przechow yw ane w pamięci RAM. Ze względu na to, że lista prze chowuje wskaźniki do danych, a nie rzeczywiste dane, faktyczne w artości w siatce są poroz rzucane po całej pamięci, przez co nie mogą być w szystkie od razu skopiowane.
Fragmentacja pamięci
|
115
Skalę tego problemu można zmniejszyć przez zastosowanie modułu array zamiast list. Obiekty tego modułu przechowują dane w pamięci sekwencyjnie, dlatego wycinek obiektu array w rze czywistości reprezentuje ciągły obszar w pamięci. Nie rozwiązuje to jednak całkowicie pro blemu. Choć dane są przechowywane w pamięci sekwencyjnie, interpreter języka Python w dal szym ciągu nie ma informacji o sposobie wektoryzowania pętli. Pożądane jest, aby każda pętla, która w danym m omencie w ykonuje operację arytmetyczną dla jednego elementu tablicy, ko rzystała z porcji danych. Jak jednak wcześniej wspomniano, interpreter języka Python jest po zbaw iony m ożliw ości takiej optym alizacji kodu bajtow ego (po części z pow odu wyjątkowo dynamicznej natury języka).
^
D la cz e g o s e k w e n c y jn e p rz e c h o w y w a n ie ż ą d a n y c h d a n y c h w p a m ię c i n ie z a p e w n ia a u t o m a ty c z n ie w e k to r y z a c ji? P o p r z y jr z e n iu się k o d o w i m a s z y n o w e m u u r u c h a m i a n e m u p r z e z p r o c e s o r z o b a c z y s z , ż e o p e r a c j e w e k t o r y z o w a n e (n p . m n o ż e n i e d w ó c h ta b l i c ) k o r z y s t a j ą z i n n e j c z ę ś c i p r o c e s o r a , a t a k ż e o d m i e n n y c h i n s t r u k c j i n i ż o p e r a c j e n i e w e k t o r y z o w a n e . A b y i n t e r p r e t e r j ę z y k a P y t h o n u ż y ł t y c h s p e c j a l n y c h i n s t r u k c ji , n ie z b ę d n y je s t m o d u ł s t w o r z o n y w t y m celu. W k r ó t c e z o s ta n ie w y ja śn io n e , ja k n a r z ę d z i e numpy z a p e w n i a d o s t ę p d o t y c h w y s p e c j a l i z o w a n y c h i n s t r u k c ji .
Co więcej, z powodu szczegółów implementacji, użycie typu array podczas tworzenia list da nych, które wymagają przeprowadzenia iteracji, w rzeczywistości przebiega wolniej niż zwykłe utw orzenie listy. W ynika to z tego, że obiekt array zaw iera bardzo niskopoziom ow ą repre zentację przechow yw anych liczb, którą przed zwróceniem użytkownikowi trzeba przekształ cić w w ersję zgodną z językiem Python. To dodatkow e obciążenie w ystępuje każdorazow o podczas indeksowania typu array. Taka decyzja związana z im plem entacją spowodowała, że obiekt array stał się mniej odpowiedni do operacji matematycznych, a bardziej do efektywnego przechowywania w pamięci danych o stałym typie.
Wprowadzenie do narzędzia numpy Aby poradzić sobie z fragm entacją stw ierdzoną za pom ocą narzędzia perf, konieczne jest znalezienie pakietu, który potrafi efektywnie w ektoryzować operacje. Na szczęście narzędzie numpy oferuje wszystkie potrzebne funkcje. Przechowuje ono dane w ciągłych porcjach w pa mięci i obsługuje operacje w ektoryzowane w ykonywane na danych. W efekcie wszelkie obli czenia arytm etyczne w ykonyw ane na tablicach narzędzia numpy korzystają z porcji danych bez konieczności jaw nego stosow ania pętli dla każdego elem entu. Taka m etoda nie tylko znacznie ułatwia operacje arytmetyczne na macierzach, ale też skraca czas ich w ykonywania. Przyjrzyjmy się przykładowi: from a rr ay import arr ay import numpy def n o r m _ s q u a r e _ l is t (v e c to r ) : > > > vector = range(1000000) > > > %timeit norm _square_list(vector_list) 1000 loops, best o f 3: 1.16 ms p e r loop norm = 0 f o r v in v e c t o r : norm += v*v r e tu r n norm def norm _s qua re _lis t_ compr eh en si on (v ec tor): > > > vector = range(1000000)
116
|
Rozdzia ł 6. Obliczenia macierzowe i wektorowe
> > > %timeit norm _square_list_com prehension(vector_list) 1000 loops, best o f 3: 913 p s p e r loop re tu rn sum([v*v f o r v in v e c t o r ] ) def norm_squared_generator_comprehension(vector): > > > vector = range(1000000) > > > % tim eitnorm _square_generator_com prehension(vector_list) 1000 loops, best o f 3: 7 4 7 p s p e r loop re tu rn sum(v*v f o r v in v e ctor) def norm _sq ua re _ar ra y(vector ): > > > vector = array('l', range(1000000)) > > > %timeit n orm _square_array(vector_array) 1000 loops, best o f 3: 1.44 ms p e r loop norm = 0 f o r v in v e c t o r : norm += v*v re tu rn norm def norm_square_numpy(vector): > > > vector = num py.arange(1000000) > > > %timeit norm_square_numpy(vector_numpy) 10000 loops, best o f 3: 30.9 p s p e r loop re tu rn numpy.sum(vector * v e ct o r) # O def norm_square_numpy_dot(vector): > > > vector = num py.arange(1000000) > > > %timeit norm_square_numpy_dot(vector_numpy) 10000 loops, best o f 3: 21.8 p s p e r loop re tu rn numpy.dot(vector, v e ct o r) # ©
O Wiersz tworzy dwie niejawne pętle dla obiektu vector (jedna służy do mnożenia, a druga do sumowania). Choć pętle te przypominają pętle użyte w funkcji norm_square_l ist_comprehension, są wykonywane z wykorzystaniem zoptymalizowanego kodu numerycznego narzędzia numpy. © Jest to preferowana metoda tworzenia norm w ektora w narzędziu numpy za pom ocą wektoryzowanej operacji numpy.dot. W celu zilustrowania tego udostępniono mniej efektywny kod funkcji norm_square_numpy. Prostszy kod narzędzia numpy działa 37,54 razy szybciej niż kod funkcji norm_square_list oraz 29,5 razy szybciej od „zoptym alizow an ego" w yrażenia listow ego języka Python. Różnica w szybkości między czystą metodą w ykonyw ania pętli i m etodą wyrażenia listowego uwi dacznia korzyść w ynikającą z dodatkow ych obliczeń realizow anych w tle zam iast jaw nego wykonywania ich w kodzie Python. Przeprowadzanie obliczeń za pom ocą już wbudowanych mechanizm ów języka Python pozwala uzyskać szybkość wykonywania rodzimego kodu C, na którym kod Python bazuje. Po części to samo rozum owanie umożliwia uzasadnienie tak znacznego przyspieszenia w kodzie narzędzia numpy. Z am iast u żyw ać bardzo uogólnionej struktury listy, korzystam y z precyzyjnie dostrojonego obiektu utw orzonego specjalnie na potrzeby przetwarzania tablic liczb. Oprócz uproszczonych i wyspecjalizow anych mechanizm ów obiekt numpy zapewnia również lokalizację w pamięci oraz wektoryzowane operacje. Jest to niezmiernie ważne podczas prze prowadzania obliczeń numerycznych. Procesor jest wyjątkowo szybki. Przeważnie najlepszym
Fragmentacja pamięci
|
117
sposobem szybkiego zoptymalizowania kodu będzie po prostu przekazanie procesorowi żą danych przez niego danych w krótszym czasie. U ruchom ienie każdej z zaprezentow anych w cześniej funkcji za pom ocą narzędzia perf pokazuje, że funkcje z obiektem array i czyste funkcje Python w ym agają w przybliżeniu 1x10n instru kcji, w ersja funkcji narzędzia numpy wykorzystuje natom iast mniej więcej 3x10 9 instrukcji. Ponadto w przypadku funkcji z obiek tem array i czystych funkcji Python w ystąpiło w przybliżeniu 80% chybień w pam ięci pod ręcznej, a dla funkcji narzędzia numpy w artość ta wyniosła około 55%. W kodzie funkcji norm_square_numpy podczas wykonywania operacji vector * vector występuje niejawna pętla, którą zajm ie się narzędzie numpy. Pętla ta jest tą samą pętlą, która jaw nie zo stała utworzona w innych przykładach: dla w szystkich elem entów obiektu vector wykony wana jest pętla, a ponadto każdy z nich jest mnożony przez samego siebie. Ponieważ jednak jest to realizowane nie w obrębie kodu Python, ale za pomocą odpowiednio poinstruowanego narzędzia numpy, narzędzie to może w ykorzystać wszystkie żądane optym alizacje. W tle uży wa bardzo zoptymalizowanego kodu C, który został stworzony specjalnie z m yślą o skorzy staniu z dowolnej wektoryzacji obsługiwanej przez procesor. Ponadto tablice narzędzia numpy są reprezentow ane w pam ięci sekwencyjnie jako niskopoziom ow e typy num eryczne. Dzięki temu m ają one takie same w ym agania dotyczące miejsca jak obiekty array (z modułu array). Dodatkową korzyścią jest to, że problem można ponow nie sform ułow ać przy użyciu iloczy nu skalarnego obsługiw anego przez narzędzie numpy. Pow oduje to, że do obliczenia żądanej w artości używana jest jedna operacja zam iast uzyskiwania najpierw iloczynu dwóch wekto rów, a następnie ich sum ow ania. Jak w idać na rysunku 6.3, pod w zględem czasu działania funkcja norm_square_numpy_dot pozostawia daleko w tyle wszystkie pozostałe. W ynika to z jej specjalizacji, a także z braku konieczności przechow yw ania w artości pośredniej operacji vector * vector, co miało miejsce w przypadku funkcji norm_square_numpy.
Rysunek 6.3. Czas działania różnych funkcji potęgowanych przy użyciu normy dla wektorów o różnej długości
118
|
Rozdział 6. Obliczenia macierzowe i wektorowe
Zastosowanie narzędzia numpy w przypadku problemu dotyczącego dyfuzji W ykorzystując zdobyte informacje o narzędziu numpy, z łatwością można przystosow ać czysty kod Python do obsługi wektoryzacji. Jedyną nową funkcją,która wymaga zaprezentowania, jest funkcja roll narzędzia numpy. Choć zapewnia ona to samo co użyty wcześniej zabieg indek sowania z wykorzystaniem operacji modulo, dotyczy to całej tablicy narzędzia numpy. Zasad niczo funkcja dokonuje wektoryzacji następującej operacji ponownego indeksowania: >>> import numpy as np >>> n p . r o l l ( [ 1 , 2 , 3 , 4 ] , 1) a r r a y ( [ 4 , 1, 2 , 3 ]) >>> n p . r o l l ( [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] ] , a r r a y ( [ [ 3 , 1, 2 ] , [6, 4 , 5 ] ] )
1, axis=1)
Funkcja ro ll tw orzy now ą tablicę narzędzia numpy, co m oże zostać potraktow ane zarów no jako jej zaleta, jak i w ada. W adą jest pośw ięcenie czasu na przydzielenie now ego miejsca, które musi zostać następnie w ypełnione odpowiednimi danymi. Z kolei po utworzeniu takiej nowej obróconej tablicy m ożliw e będzie dość szybkie wektoryzowanie operacji dotyczących tablicy. Ponadto nie wystąpią chybienia w pamięci podręcznej procesora. Może to mieć znaczny w pływ na szybkość rzeczywistego obliczenia, które m usi zostać przeprowadzone dla siatki. W dalszej części rozdziału metoda ta zostanie zmodyfikowana w taki sposób, że ta sama ko rzyść zostanie uzyskana bez potrzeby ciągłego przydzielania większej ilości pamięci. D zięki tej dodatkow ej funkcji m ożem y zm odyfikow ać kod Python dyfuzji z przykładu 6 .6, korzystając z prostszych i wektoryzowanych tablic narzędzia numpy. Dodatkowo do osobnej funkcji wydzielane jest obliczenie pochodnych grid_xx i grid_yy. Przykład 6.9 prezentuje po czątkową w ersję kodu dyfuzji bazującego na narzędziu numpy. P r z y k ł a d 6 .9 . P o c z ą t k o w a w e r s ja k o d u d y f u z j i b a z u ją c e g o n a n a r z ę d z iu n u m p y import numpy as np grid_shape = (10 24 , 1024) def l a p l a c i a n ( g r i d ) : re tu rn n p . r o l l ( g r i d , +1, 0) + n p . r o l l ( g r i d , - 1 , 0) + \ n p . r o l l ( g r i d , +1, 1) + n p . r o l l ( g r i d , - 1 , 1) - 4 * grid def e v o l v e ( g r i d , d t , D=1): re tu rn gr id + dt * D * l a p l a c i a n ( g r i d ) def run_ exp er im en t(nu m _ite ra tion s) : gr id = np.zero s( g ri d_s h ape) block_low = in t (g r i d _ s h a p e [ 0 ] * .4) block_h igh = in t (g r i d _ s h a p e [ 0 ] * .5 ) grid [b lo c k _lo w :b lo c k _h ig h , block _low:block_high ] = 0.0 05 s t a r t = ti m e . ti m e () f o r i in r a n g e (n u m _ ite r a ti o n s ): gr id = e v o l v e ( g r i d , 0 . 1 ) re tu rn t i m e . ti m e () - s t a r t
Od razu widać, że pow yższy kod jest znacznie krótszy. Zw ykle jest to dobry w skaźnik wy dajności. W iele znacznych popraw ek jest dokonyw anych poza obrębem interpretera języka Python, a praw dopodobnie także wewnątrz modułu, który został stworzony specjalnie pod kątem w ydajności i rozw iązania konkretnego problem u (niem niej jednak zaw sze pow inno to być spraw dzane!). Jednym z przyjętych tutaj założeń jest to, że narzędzie numpy korzysta z lepszego sposobu zarządzania pam ięcią, aby szybciej przekazać procesorow i w ym agane
Zastosowanie narzędzia numpy w przypadku problemu dotyczącego dyfuzji
|
119
przez niego dane. Ponieważ jednak to, czy faktycznie tak będzie, czy nie, zależy od samej im plementacji narzędzia numpy, przeprow adźm y profilow anie kodu, aby stwierdzić, czy przyjęta hipoteza jest poprawna. Przykład 6.10 prezentuje wyniki. Przykład 6.10. Liczniki wydajności dla dwuwymiarowej dyfuzji z wykorzystaniem narzędzia numpy $ p e r f s t a t -e c y c l e s , s t a l l e d - c y c l e s - f r o n t e n d , s t a l l e d - c y c l e s - b a c k e n d , i n s t r u c t i o n s , \ c a c h e -r e f e r e n c e s ,c a c h e -m is s e s , b r a n c h e s ,b r a n c h -m is s e s , ta s k -c lo c k ,f a u l ts ,\ m in o r - f a u l t s ,c s ,m i g r a t i o n s - r 3 python diffusion_numpy.py Performance co un ter s t a t s f o r 'python diffusion_numpy.py' (3 ru ns): 1 0 , 1 9 4 ,8 1 1 , 7 1 8 c y c l e s # 3.3 3 2 GHz 4,4 3 5 ,8 5 0 ,4 1 9 s ta lle d -c y c le s-fro n te n d # 43.51% frontend c y c l e s i d l e 2,055,861,567 stalled -cycles-backend # 20.17% backend c y c l e s i d l e 1 5 , 1 6 5 ,1 5 1 , 8 4 4 i n s t r u c t i o n s # 1. 49 insns per c y c l e # 0 . 2 9 s t a l l e d c y c l e s per insn 3 4 6 , 7 9 8 ,3 1 1 c a c h e - r e f e r e n c e s # 113. 362 M/sec 519,7 93 cach e- misse s # 0 . 1 5 0 % o f a l l cache r e f s 3 , 5 0 6 , 8 8 7 , 9 2 7 branches # 1146. 33 4 M/sec 3 , 6 8 1 ,4 4 1 branch-misses # 0.10% o f a l l branches 3059.2 198 62 t a s k - c l o c k # 0 . 9 9 9 CPUs u t i l i z e d 751.7 07 p a g e - f a u l t s # 0 . 2 4 6 M/sec 751.7 07 m i n o r - f a u lt s # 0.2 4 6 M/sec 8 c o n t e x t -s w i t c h e s # 0.0 0 3 K/sec 1 CPU-migrations # 0 . 0 0 0 K/sec 3.061 883 21 8 seconds time elapsed
W yniki pokazują, że prosta zm iana dokonana za pom ocą narzędzia numpy dała 40-krotne przyspieszenie w porównaniu z im plementacją czystego kodu Python ze zm niejszoną liczbą przydziałów pamięci (przykład 6 .8 ). Jak zostało to osiągnięte? Przede wszystkim zawdzięczamy to wektoryzacji zapewnianej przez to narzędzie. Choć ba zująca na nim wersja kodu wydaje się wykonywać mniej instrukcji w ciągu cyklu, każda z tych instrukcji realizuje znacznie więcej pracy. Oznacza to, że jedna wektoryzowana instrukcja może mnożyć cztery (lub więcej) liczby w tablicy, zamiast w ymagać czterech niezależnych instrukcji mnożenia. Generalnie umożliwia to wykorzystanie mniejszej łącznej liczby instrukcji niezbęd nych do rozwiązania tego samego problemu. Istnieje też kilka innych czynników m ających w pływ na w ersję kodu narzędzia numpy, która wymaga mniejszej całkowitej liczby instrukcji służących do rozwiązania problemu dotyczą cego dyfuzji. Jeden z nich ma zw iązek z pełnym interfejsem API języka Python, który jest do stępny podczas wykonywania czystego kodu Python, lecz niekoniecznie w przypadku wersji kodu narzędzia numpy (na przykład siatki z czystym kodem Python m ogą być dołączane do takiego kodu, ale nie do kodu narzędzia numpy). Nawet pomim o tego, że ta (lub inna) funk cjonalność nie jest jaw nie używana, pojawia się obciążenie związane z udostępnianiem sys tem u, w którym m oże ona być dostępna. Poniew aż narzędzie numpy m oże przyjm ow ać, że przechow yw ane dane zaw sze będą liczbami, wszystko, co dotyczy tablic, może być optyma lizowane pod kątem operacji wykonyw anych dla liczb. Przy omawianiu kompilatora Cython (zajrzyj do podrozdziału „Cython") w dalszym ciągu będziemy eliminować funkcje niezbędne do poprawienia wydajności. W tym przypadku możliwe jest naw et usunięcie sprawdzania powiązań list w celu przyspieszenia wyszukiw ania w ich obrębie. Liczba instrukcji zw ykle nie m usi być skorelowana z wydajnością. Program z m niejszą liczbą instrukcji może nie wykonywać ich efektywnie lub mogą one okazać się wolne. W idoczne jest jednak, że oprócz zredukowania liczby instrukcji wersja kodu narzędzia numpy znacznie zmniej szyła też nieefektywność w postaci chybień w pamięci podręcznej (0,15% chybień zamiast 30,2%).
120
|
Rozdział 6. Obliczenia macierzowe i wektorowe
Jak wyjaśniono w podrozdziale „Fragmentacja pam ięci", chybienia w pamięci podręcznej spo w alniają obliczenia, ponieważ procesor musi czekać na pobranie danych z wolniejszej pamięci, a nie od razu m ieć do nich dostęp w swojej pamięci podręcznej. Okazuje się, że fragmentacja pam ięci jest na tyle dominującym czynnikiem wpływającym na w ydajność, że w przypadku wyłączenia wektoryzacji w narzędziu numpy3 i pozostawienia reszty bez zm ian nadal zauw a żalny będzie w zrost szybkości w porównaniu z wersją czystego kodu Python (przykład 6.11). P r z y k ł a d 6 .1 1 . L i c z n i k i w y d a j n o ś c i d la d w u w y m i a r o w e j d y f u z j i z w y k o r z y s t a n i e m n a r z ę d z ia n u m p y b e z w e k to r y z a c ji $ p e r f s t a t -e c y c l e s , s t a l l e d - c y c l e s - f r o n t e n d , s t a l l e d - c y c l e s - b a c k e n d , i n s t r u c t i o n s , \ c a ch e -r e f e r e n c e s ,c a c h e -m is s e s , b r a n c h e s ,b r a n c h -m is s e s , ta s k -c lo c k ,f a u l ts ,\ m in o r - f a u l t s ,c s ,m i g r a t i o n s - r 3 python diffusion_numpy.py Performance co un ter s t a t s f o r 'python diffusion_numpy.py' (3 ru ns ): 48,923,515,604 cycles # 3.4 1 3 GHz 2 4 , 9 0 1 ,9 7 9 , 5 0 1 s t a l l e d - c y c l e s - f r o n t e n d # 50.90% fr ontend c y c l e s i d l e 6 ,5 8 5 ,982,510 stalled -cycles-backend # 13.46% backend c y c l e s i d l e 5 3 , 2 0 8 ,7 5 6 , 1 1 7 i n s t r u c t i o n s # 1.09 insns per c y c l e # 0 .4 7 s t a l l e d c y c l e s per insn 8 3 , 4 3 6 ,6 6 5 c a c h e - r e f e r e n c e s # 5.8 2 1 M/sec 1 , 2 1 1 ,2 2 9 cach e- misse s # 1.45 2 % o f a l l cache r e f s 4 , 4 2 8 , 2 2 5 , 1 1 1 branches # 3 08.926 M/sec 3 , 7 1 6 , 7 8 9 branch-misses # 0.08% o f a l l branches 14334.244888 t a s k - c l o c k # 0 . 9 9 9 CPUs u t i l i z e d 751.1 85 p a g e - f a u l t s # 0.0 5 2 M/sec 751.1 85 m i n o r - f a u lt s # 0.0 5 2 M/sec 24 c o n t e x t -s w i t c h e s # 0.0 0 2 K/sec 5 CPU-migrations # 0 . 0 0 0 K/sec 14.345794896 seconds time elapsed
W yniki pokazują, że dominującym czynnikiem związanym z 40-krotnym przyspieszeniem przy zastosowaniu narzędzia numpy nie jest wektoryzowany zestaw instrukcji, lecz lokalizacja w pa mięci i zmniejszona fragmentacja pamięci. Na podstawie wcześniejszego eksperymentu można stwierdzić, że wektoryzacja decyduje o tak dużym przyspieszeniu 4 zaledwie w około 15%. Fakt, że problemy z pamięcią stanowią dominujący czynnik, który spowalnia kod, nie jest zbyt dużym zaskoczeniem. Komputery są bardzo dobrze zaprojektow ane do tego, aby wykonały dokładnie te obliczenia, które są od nich w ym agane na potrzeby rozw iązania danego pro blem u, czyli m nożenie i dodaw anie liczb. W ąskim gardłem je st przekazanie tych liczb do procesora na tyle szybko, aby m ógł przeprow adzić obliczenia tak szybko, jak potrafi.
Przydziały pamięci i operacje wewnętrzne Aby zoptym alizow ać efekty dominowania przez pam ięć, spróbujmy użyć tej samej metody co w przykładzie 6 .6 . Umożliwia ona zmniejszenie liczby przydziałów dokonywanych w kodzie narzędzia numpy. Przydziały są trochę gorsze od w cześniej om ów ionych chybień w pam ięci podręcznej. Zamiast po prostu znaleźć właściwe dane w pamięci RAM, jeśli nie było ich w pa mięci podręcznej, operacja przydziału m usi też skierować do systemu operacyjnego żądanie
3 W t y m c elu n a r z ę d z i e numpy j e s t k o m p i l o w a n e z w y k o r z y s t a n i e m flag i -O0. N a p o t r z e b y t e g o e k s p e r y m e n t u z o s t a ł a s k o m p i l o w a n a w e r s j a 1.8.0 n a r z ę d z i a numpy z a p o m o c ą n a s t ę p u ją c e g o p olec en ia: $ OPT='-O0' FOPT='-O0' BLAS=None LAPACK=None ATLAS=None python set up .p y build. 4 W b a r d z o d u ż y m s t o p n i u je st to z a l e ż n e o d te go , ja k i p ro c e s o r je st u ż y w a n y .
Zastosowanie narzędzia numpy w przypadku problemu dotyczącego dyfuzji
|
121
dotyczące dostępnej porcji danych, a następnie zarezerwować ją. Takie żądanie generuje znacz nie większe obciążenie niż zw ykłe w ypełnienie pamięci podręcznej. W ypełnienie po chybie niu w pamięci podręcznej ma postać sprzętowej funkcji zoptymalizowanej na płycie głównej, przyd zielanie pam ięci natom iast w ym aga do zakończenia działania kom unikacji z innym procesem, czyli jądrem. W celu usunięcia alokacji z przykładu 6.9 na początku kodu dokonam y w stępnej alokacji miejsca początkowego, a następnie będą używane wyłącznie operacje w ewnętrzne. Operacje te (np. +=, *= itp.) jako swojego w yjścia używają ponow nie jednego z wejść. Oznacza to, że do przechowania wyniku obliczenia nie jest w ymagane przydzielenie miejsca. Aby w yraźnie to pokazać, przyjrzym y się temu, jak zmienia się identyfikator id tablicy numpy podczas w ykonyw ania dla niej operacji (przykład 6.12). Korzystanie z tego identyfikatora stanowi dobry sposób śledzenia tego działania dla tablic narzędzia numpy, ponieważ pozwala on określić, do jakiej sekcji pam ięci następuje odw ołanie. Jeśli dw ie tablice narzędzia numpy m ają taki sam identyfikator id, odwołują się do tej samej sekcji pam ięci5. P r z y k ł a d 6 .1 2 . O p e r a c je w e w n ę t r z n e , k t ó r e z m n i e j s z a j ą lic z b ę p r z y d z i a ł ó w p a m i ę c i >>> import numpy as np >>> array1 = np.random.random((10,10)) >>> array2 = np.random.random((10,10)) >>> i d (array 1) 140199765947424 # O >>> array1 += array2 >>> i d (array 1) 140199765947424 # 0 >>> array1 = array1 + array2 >>> i d (array 1) 140199765969792 # ©
O 0 Te dwa identyfikatory id są identyczne, ponieważ wykonywana jest operacja wewnętrzna. Oznacza to, że nie zm ienia się adres pam ięci tablicy array1. Po prostu m odyfikow ane są zaw arte w niej dane.
© W tym miejscu adres pamięci zm ienił się. Podczas wykonywania operacji array 1 + array 2 przydzielany jest nowy adres pamięci, który jest wypełniany wynikiem obliczenia. Zapew nia to jednak korzyści w sytuacji, gdy trzeba zachow ać oryginalne dane (oznacza to, że instrukcja array3 = array1 + array2 pozw ala nadal używ ać tablic array1 i array2, ope racje wew nętrzne natom iast usuwają część oryginalnych danych). Co w ięcej, w idoczne jest oczekiw ane spow olnienie spow odow ane operacją, która nie jest w ewnętrzna. W przypadku niew ielkich tablic numpy to obciążenie może stanowić naw et 50% całkowitego czasu obliczeniowego. Choć przy większych obliczeniach przyspieszenie jest wy rażane raczej w przedziale kilku procent, w dalszym ciągu oznacza to m nóstw o czasu, jeśli obliczenia są w ykonyw ane miliony razy. W przykładzie 6.13 w idoczne jest, że w przypadku niewielkich tablic użycie operacji wewnętrznych daje przyspieszenie wynoszące 20%. Margines ten zwiększy się przy większych tablicach, ponieważ przydziały pamięci staną się intensywniejsze.
5 N i e je st to c a ł k o w i tą p r a w d ą , g d y ż d w i e ta b lice n a r z ę d z i a numpy m o g ą o d w o ł y w a ć się d o tej s a m e j se k cji p a m ię c i, lecz u ż y w a ć r ó ż n y c h i n f o rm a c ji d o r e p r e z e n t o w a n i a t y ch s a m y c h d a n y c h w r ó ż n y sp osó b . T a k i e d w i e t a b lic e b ę d ą m i e ć i n n e i d e n t y fi k a t o r y id. Z e s t r u k tu r ą id e n t y f i k a t o r ó w id ta b lic n a r z ę d z i a numpy z w i ą z a n y c h je st w ie le su b tel n o śc i, k t ó r y c h o m ó w i e n i e w y k r a c z a p o z a z a k r e s t reści rozd ziału .
122
|
Rozdział 6. Obliczenia macierzowe i wektorowe
P r z y k ł a d 6 .1 3 . O p e r a c je w e w n ę t r z n e , k t ó r e z m n i e j s z a j ą lic z b ę p r z y d z i a ł ó w p a m i ę c i >>> %%timeit a rr a y 1 , array2 = np.random.ran dom((10,10)), np.random.random((10,10)) # O . . . array1 = array1 + array2 100000 loops, b est o f 3: 3 .0 3 us per loop >>> %%timeit a rr a y 1 , array2 = np.random.ran dom((10,10)), np.random.random((10,10)) . . . array1 += array2 100000 loops, b est o f 3: 2 .4 2 us per loop
O Zauważ, że zam iast funkcji %timeit używana jest funkcja %%timeit umożliwiająca określe nie kodu do przygotowania eksperymentu, w którym nie jest ustalany przedział czasu. W adą jest to, że choć zm odyfikow anie kodu z przykładu 6.9 w celu użycia operacji w e w nętrznych nie jest zbyt skomplikowane, pow oduje, że w ynikow y kod staje się trochę trud niejszy do zrozumienia. W przykładzie 6.14 widoczne są wyniki takiej refaktoryzacji. Tworzone są instancje wektorów grid i next_grid, a ponadto są one nieustannie wzajem nie zamieniane. W ektor grid zaw iera znane bieżące inform acje o system ie. Po uruchom ieniu funkcji evolve w ektor next_grid przechowuje zaktualizow ane informacje. P r z y k ł a d 6 .1 4 . Z a m ie n ia n ie w i ę k s z o ś c i o p e r a c j i n a r z ę d z ia n u m p y n a o p e r a c j e w e w n ę t r z n e def l a p l a c i a n ( g r i d , o u t ): n p.c opyto(o ut, grid) out *=-4 out += n p . r o l l ( g r i d , +1, 0) out += n p . r o l l ( g r i d , - 1 , 0) out += n p . r o l l ( g r i d , +1, 1) out += n p . r o l l ( g r i d , - 1 , 1) def e v o l v e ( g r i d , d t , out , D=1): l a p l a c i a n ( g r i d , out) out *= D * dt out += grid def run_ exp er im en t(nu m _ite ra tion s) : n ex t_ g rid = np.zero s( g ri d_sh ap e) gr id = np.zero s( g ri d_s h ape) block_low = in t (g r i d _ s h a p e [ 0 ] * .4) block_high = i n t (g r i d _ s h a p e [ 0 ] * .5) gr id [b loc k_low :b lo c k_h i gh, block_low:bl ock_hi gh] = 0.0 05 s t a r t = ti m e . ti m e () f o r i in r a n g e (n u m _ ite r a ti o n s ): e v o l v e ( g r i d , 0 . 1 , n ext_g rid) g r i d , n ex t_ g rid = n e x t_ g ri d , gr id # O re tu rn t i m e . ti m e () - s t a r t
O Poniew aż dane w yjściow e funkcji evolve są przechow yw ane w w ektorze w yjściow ym next_grid, konieczna jest zamiana tych dwóch zmiennych w taki sposób, aby przy następ nej iteracji pętli w ektor grid zawierał najbardziej aktualne informacje. Taka operacja za miany nie jest zbyt kosztowna, ponieważ m odyfikowane są tylko odwołania do danych, nie one same. Trzeba pamiętać, że ze względu na to, że każda operacja ma być wew nętrzna, każdorazowo przy wykonywaniu operacji wektorowej musi ona znajdow ać się w e w łasnym w ierszu kodu. M oże to spraw ić, że coś tak prostego jak A = A *B+C m oże stać się dość zaw iłe. Poniew aż w języku Python duży nacisk kładziony jest na czytelność kodu, należy zapewnić, że dokona ne zm iany zaow ocują wystarczającym przyspieszeniem, aby zostały uznane za uzasadnione.
Zastosowanie narzędzia numpy w przypadku problemu dotyczącego dyfuzji
|
123
Porównanie liczników wydajności z przykładów 6.15 i 6.10 pozwala stwierdzić, że usunięcie nieuzasadnionych alokacji przyspieszyło kod o 29% . Po części w ynika to ze zm niejszenia liczby chybień w pam ięci podręcznej, ale w w iększym stopniu jest spow odow ane zreduko waniem liczby błędów stronicowania. P r z y k ł a d 6 .1 5 . L ic z n ik i w y d a jn o ś c i d la n a r z ę d z ia n u m p y w p r z y p a d k u w e w n ę t r z n y c h o p e r a c ji p a m ię c i o w y c h $ p e r f s t a t -e c y c l e s , s t a l l e d - c y c l e s - f r o n t e n d , s t a l l e d - c y c l e s - b a c k e n d , i n s t r u c t i o n s , \ c a c h e -r e f e r e n c e s ,c a c h e -m is s e s , b r a n c h e s ,b r a n c h -m is s e s , ta s k -c lo c k ,f a u l ts ,\ m in o r - f a u l t s ,c s ,m i g r a t i o n s - r 3 python diffusion_numpy_memory.py Performance co un ter s t a t s f o r 'python diffusion_numpy_memory.py' (3 ru ns ): 7,864,072,570 cycles # 3.3 3 0 GHz 3,055,151,931 s ta lle d -cy cle s-fro n te n d # 38.85% frontend c y c l e s i d l e 1 , 3 6 8 ,2 3 5 , 5 0 6 s t a l l e d - c y c l e s - b a c k e n d # 17.40% backend c y c l e s i d l e 1 3 , 2 5 7 ,4 8 8 , 8 4 8 i n s t r u c t i o n s # 1.69 insns per c y c l e # 0.23 s t a l l e d c y c l e s per insn 2 3 9 ,1 9 5 ,4 0 7 c a c h e - r e f e r e n c e s # 101.291 M/sec 2 , 8 8 6 ,5 2 5 cache-mi ss es # 1.207 % o f a l l cache r e f s 3 , 1 6 6 , 5 0 6 , 8 6 1 branches # 1340.903 M/sec 3 , 2 0 4 ,9 6 0 branch-misses # 0.10% o f a l l branches 2361. 473922 t a s k - c l o c k # 0.9 99 CPUs u t i l i z e d 6,527 p a g e -fa u l ts # 0.0 03 M/sec 6,527 mi n o r - f a u l t s # 0.0 03 M/sec 6 c o n t e x t -sw i t c h e s # 0.0 03 K/sec 2 CPU-migrations # 0. 001 K/sec 2.3637 278 76 seconds time elapsed
Optymalizacje selektywne: znajdowanie tego, co wymaga poprawienia Po przyjrzeniu się kodowi z przykładu 6.14 można odnieść wrażenie, że udało nam się pora dzić sobie z większością problemów: zmniejszyliśmy obciążenie procesora za pomocą narzędzia numpy i zredukowaliśmy liczbę alokacji wymaganych do rozwiązania problemu. Niemniej jed nak zaw sze możliwe jest przeprow adzenie dodatkowych analiz. Jeśli zostanie w ykonane pro filowanie w ierszy tego kodu (przykład 6.16), okaże się, że w iększość działań jest realizowana w obrębie funkcji laplacian. 93% czasu, jaki zajm uje działanie funkcji evolve, jest zw iązane z przetw arzaniem w obrębie funkcji laplacian. P r z y k ł a d 6 .1 6 . P r o f i l o w a n i e w i e r s z y p o k a z u je , ż e w y k o n y w a n i e f u n k c j i la p la c ia n z a j m u je z d e c y d o w a n i e z b y t d u ż o c z a s u Wrote p r o f i l e r e s u l t s to diffusion_numpy_memory.py.lprof Timer u n i t : 1e-06 s F i l e : diffusion_numpy_memory.py Function: l a p l a c i a n at l i n e 8 Total time: 3.6 7347 s Line # Hits Time Per Hit % Time Line Contents 8 9 10 11 12 13 14 15
124
I
500 500 500 500 500 500
162009 111044 464810 432518 1261692 1241398
32 4 .0 222. 1 92 9 .6 865.0 252 3.4 24 82 .8
4.4 3.0 12.7 11.8 34 .3 33 .8
Rozdział 6. Obliczenia macierzowe i wektorowe
@ pr ofil e def l a p l a c i a n ( g r i d , out) n p.copyto(o ut, grid) out *= -4 out += n p . r o l l ( g r i d , out += n p.rol l ( g r i d , out += n p.rol l ( g r i d , out += n p.rol l ( g r i d ,
+ 1, -1, +1, -1,
0) 0) 1) 1)
F i l e : diffusion_numpy_memory.py Function: evolve a t l i n e 17 Total time: 3 .97 768 s Line # Hits Time Per I Hit 17 18 19 20 21
500 500 500
3691674 111687 174320
% Time
73 83.3 223.4 34 8 .6
Line Contents
@ pro fi le def e v o l v e ( g r i d , d t , out, D=1): 92.8 l a p l a c i a n ( g r i d , out) 2.8 out *= D * dt 4.4 out += grid
M oże być wiele powodów, dla których funkcja laplacian jest taka wolna. Istnieją jednak dwie główne, bardziej ogólne kwestie, które należy rozważyć. Po pierwsze, wygląda na to, że wy w ołania funkcji np.roll pow odują przydzielanie nowych wektorów (można to zweryfikować, sprawdzając dokumentację funkcji). Oznacza to, że naw et pomimo tego, że w opisanej wcze śniej refaktoryzacji usunięto siedem alokacji pamięci, nadal występują cztery nierozstrzygnięte alokacje. Co więcej, np.roll to bardzo uogólniona funkcja, która zawiera wiele kodu służącego do obsługi specjalnych przypadków. Ponieważ w iadomo dokładnie, co ma zostać osiągnięte (czyli po prostu przemieszczenie pierwszej kolumny danych, aby była ostatnią w każdym wy miarze), możliwe jest zm odyfikowanie tej funkcji w celu wyeliminowania większości nieuza sadnionego kodu. M ożliwe jest naw et połączenie logiki funkcji np.roll z operacją dodawania, która ma miejsce w przypadku obróconych danych. Ma to na celu utworzenie bardzo wyspe cjalizow anej funkcji roll_add, która realizuje dokładnie to, czego żądam y, przy użyciu naj mniejszej liczby alokacji i minimalnej ilości dodatkowej logiki. Przykład 6.17 pokazuje, jak m ogłaby w yglądać taka refaktoryzacja. W ym agane jest jedynie utworzenie nowej funkcji roll_add i zapewnienie użycia jej przez funkcję laplacian. Ponieważ narzędzie numpy obsługuje wyszukane indeksowanie, implementowanie takiej funkcji wymaga jedynie tego, aby nie pom ieszać indeksów . Jak jednak w cześniej w spom niano, choć kod ten m oże być bardziej w ydajny, będzie znacznie mniej czytelny. P r z y k ł a d 6 .1 7 . T w o r z e n ie w ła s n e j f u n k c j i o b r o t u import numpy as np def r o l l _ a d d ( r o l l e e , s h i f t , a x i s , ou t): D la danej macierzy, m acierzy w yjściow ej oraz param etrów rollee i out fun kcja przeprow adzi następujące obliczenie: > > > out + = np.roll(rollee, shift, axis=axis) J e s t to realizow ane przy następujących założeniach: * param etr rollee je s t dwuwymiarowy * param etr shift będzie przyjm ow ać w yłącznie w artości +1 lub —1 * param etr axis będzie przyjm ow ać wyłącznie w artości 0 lub 1 (jest to również implikowane przez pierw sze założenie) Przy korzystaniu z tych założeń możliwe je s t przyspieszenie tej fu n kcji przez uniknięcie dodatkow ych mechanizmów używanych przez narzędzie numpy do uogólniania fu n kcji obrotu, a pon adto przez sprawienie, że op era cja ta sam a w sobie będzie wewnętrzna i f s h i f t == 1 and a x is == 0: o u t [ 1 : , :] += r o l l e e [ : - 1 , :] out[0 , :] += r o l l e e [ - 1 , :] e l i f s h i f t == -1 and a x is == 0: o u t [ : - 1 , :] += r o l l e e [ 1 : , :] o u t [-1 , :] += r o l l e e [ 0 , :] e l i f s h i f t == 1 and a x is == 1: o u t [ : , 1:] += r o l l e e [ : , : - 1 ] o u t [ : , 0 ] += r o l l e e [ : , -1]
Zastosowanie narzędzia numpy w przypadku problemu dotyczącego dyfuzji
|
125
e l i f s h i f t == -1 and a x i s == 1: o u t [ : , : - 1 ] += r o l l e e [ : , 1:] o u t[:, -1] += r o l l e e [ : , 0] def t e s t _ r o l l _ a d d ( ) : ro llee = n p .a sa r ra y ([[1 ,2 ],[3 ,4 ]]) f o r s h i f t in ( - 1 , +1): f o r a x i s in (0 , 1): out = n p . a s a r r a y ( [ [ 6 , 3 ] , [ 9 , 2 ] ] ) ex p ect ed _ resu lt = n p . r o l l ( r o l l e e , s h i f t , a x i s = a x i s ) + out r o l l _ a d d ( r o l l e e , s h i f t , a x i s , out) a s s e r t n p . a l l ( e x p e c t e d _ r e s u l t == out) def l a p l a c i a n ( g r i d , o u t ): np.co pyto(o ut, grid) out *= -4 r o l l _ a d d ( g r i d , +1, 0 , out) r o l l _ a d d ( g r i d , - 1 , 0 , out) r o l l _ a d d ( g r i d , +1, 1, out) r o l l add(grid, - 1 , 1, out)
Z w r ó ć u w a g ę n a d o d a tk o w e działania, o p ró c z p rz e p ro w a d z e n ia p e łn y c h testów , w y k o n a n e w c e l u z a p e w n i e n i a d l a f u n k c j i i n f o r m a c y j n e g o o p i s u je j d z i a ł a n i a . P r z y t a k i m p o stę p o w a n iu w a ż n e je st u trz y m a n ie czytelności kod u . O p isa n e k r o k i z d e cy d o w a n ie zap ew niają, ż e k od za w s z e będ zie działać z g o d n ie z o czekiw aniam i. P on ad to w p rz y
&
szłości p r o g r a m iś c i b ę d ą m o g li m o d y f i k o w a ć kod , w ie d z ą c , ja k ie o p e r a cje są p rz e z te n k o d w y k o n y w a n e , a ta k ż e k ie d y c o ś n ie działa.
Po przyjrzeniu się licznikom wydajności z przykładu 6.18 dotyczącego zmodyfikowanego kodu stwierdzimy, że choć kod jest znacznie szybszy od kodu z przykładu 6.14 (okazuje się, że 70% szybszy), większość liczników ma mniej więcej takie same wartości. Wartość licznika page-faults zmniejszyła się, ale nie o 70%. Podobnie w artości liczników cache-misses i cache-references są mniejsze, lecz nie na tyle, aby miało to wpływ na całkowite przyspieszenie. W tym przypadku jednym z najważniejszych liczników jest licznik instructions. Określa on liczbę instrukcji pro cesora, które m usiały zostać w ykonane w celu uruchom ienia program u. Innymi słowy, licz nik informuje o liczbie operacji, które musiał wykonać procesor. W ydaje się, że modyfikacja dostosowywanej funkcji roll_add spowodowała zredukow anie łącznej liczby niezbędnych in strukcji około 2,86 razy. W ynika to z tego, że zam iast bazow ać na w szystkich mechanizmach narzędzia numpy w celu obrotu macierzy, można utw orzyć krótszy i prostszy mechanizm, któ ry m oże skorzystać z założeń dotyczących danych (czyli z tego, że dane są dwuwym iarowe, a ponadto że obrót będzie w ykonyw any tylko o w artość 1). O m aw ianie tego zagadnienia dotyczącego eliminowania zbędnych mechanizmów zarówno w przypadku narzędzia numpy, jak i kodu Python będzie kontynuowane w podrozdziale „Cython". P r z y k ł a d 6 .1 8 . L i c z n i k i w y d a j n o ś c i d la n a r z ę d z ia n u m p y w p r z y p a d k u w e w n ę t r z n y c h o p e r a c j i w p a m i ę c i i n i e s t a n d a r d o w e j f u n k c j i la p la c ia n $ p e r f s t a t -e c y c l e s , s t a l l e d - c y c l e s - f r o n t e n d , s t a l l e d - c y c l e s - b a c k e n d , i n s t r u c t i o n s , \ c a c h e -r e f e r e n c e s ,c a c h e -m is s e s , b r a n c h e s ,b r a n c h -m is s e s , ta s k -c lo c k ,f a u l ts ,\ m in o r - f a u l t s ,c s ,m i g r a t i o n s - r 3 python diffusion_numpy_memory2.py Performance co un ter s t a t s f o r 'python diffusion_numpy_memory2.py' (3 ru ns): 4,3 0 3 ,7 9 9 ,2 4 4 cycles # 3 . 1 0 8 GHz 2,814,678,053 s ta lle d -c y c le s-fro n te n d # 65.40% frontend c y c l e s i d l e 1,635,172,736 stalled -cycles-backend # 37.99% backend cycles idle 4 ,6 3 1 ,882,411 instru ctions # 1.08 insns per c y c l e # 0 .6 1 s t a l l e d c y c l e s per insn
126
I
Rozdział 6. Obliczenia macierzowe i wektorowe
2 7 2 ,1 5 1 ,9 5 7 2 , 8 3 5 ,9 4 8 6 2 1 ,5 6 5 ,0 5 4 2 , 9 0 5 ,8 7 9 1384.555494 5,5 5 9 5,5 5 9 6 3 1.386148918
cache-references cache-mi ss es branches branch-misses task-clock p a g e -fa u l ts m i n o r -fa u lt s c o n t e x t -sw i t c h e s CPU-mi g r ati o n s seconds time elapsed
# # # # # # # # #
196.563 1.042 4 48.928 0.47% 0.9 99 0. 004 0.0 04 0.00 4 0.002
M/sec % of a l l cache r e f s M/sec o f a l l branches CPUs u t i l ized M/sec M/sec K/sec K/sec
Moduł numexpr: przyspieszanie i upraszczanie operacji wewnętrznych M ankamentem optymalizacji operacji w ektorowych przez narzędzie numpy jest to, że jedno cześnie w ystępuje ona tylko dla jednej operacji. Oznacza to, że podczas wykonywania opera cji A*B+C z wykorzystaniem wektorów narzędzia numpy najpierw realizowana jest cała operacja A*B, a dane są przechow yw ane w tymczasowym w ektorze. Ten now y w ektor jest następnie sum owany z C. Dość w yraźnie prezentuje to wersja kodu dyfuzji z przykładu 6.14 z używ a nym i operacjami wewnętrznym i. Istnieje jednak wiele m odułów, które m ogą być pom ocne w tym przypadku. numexpr to m o duł, który może pobrać całe w yrażenie w ektorow e i skompilować je do postaci bardzo efek tywnego kodu zoptym alizow anego pod kątem m inim alizacji chybień w pam ięci podręcznej i używanego miejsca tymczasowego. Ponadto w celu m aksym alizacji przyspieszenia w yraże nia m ogą korzystać z wielu rdzeni procesora (więcej inform acji zam ieszczono w rozdziale 9.) oraz wyspecjalizow anych instrukcji przeznaczonych dla układów Intela. Bardzo prosta jest m odyfikacja kodu w celu zastosow ania m odułu numexpr. W ym agane jest jedynie przebudow anie wyrażeń do postaci łańcuchów z odwołaniam i do zmiennych lokal nych. W yrażenia są kompilowane w tle (i buforowane, aby wywołania tego samego wyrażenia nie pow odow ały identycznego obciążenia zw iązanego z kompilacją) i urucham iane za pom o cą zoptymalizowanego kodu. Przykład 6.19 prezentuje, jak łatwe jest modyfikowanie funkcji evolve w celu użycia modułu numexpr. W tym przypadku decydujem y się na zastosowanie pa rametru out funkcji evaluate, aby moduł numexpr nie przydzielał nowego w ektora, do którego zostanie zwrócony w ynik obliczenia. P r z y k ł a d 6 .1 9 . U ż y c ie m o d u łu n u m e x p r d o d o d a t k o w e g o z o p t y m a l i z o w a n i a d u ż y c h o p e r a c j i m a c ie r z o w y c h from numexpr import eval ua te def e v o l v e ( g r i d , d t , n e x t_ g rid , D=1): l a p l a c i a n ( g r i d , ne xt_grid ) e v a l u a te ("n e x t _ g r i d * D * d t+ g r id ", out=next_grid)
Istotną w łaściw ością m odułu numexpr jest uw zględnianie przez niego pam ięci podręcznych procesora. M oduł dokładnie przem ieszcza dane, tak aby różne pam ięci podręczne procesora zaw ierały popraw ne dane w celu zminimalizowania chybień w pam ięci podręcznej. Po uru chom ieniu narzędzia perf dla zaktu alizow anego kodu (przykład 6 .20 ) zauw ażalne będzie przyspieszenie. Jeśli jednak sprawdzimy wydajność dla mniejszej siatki 512x512 (rysunek 6.4 na końcu rozdziału), okaże się, że szybkość zmniejszyła się o około 15%. Z czego to wynika?
Moduł numexpr: przyspieszanie i upraszczanie operacji wewnętrznych
| 127
P r z y k ł a d 6 .2 0 . L i c z n i k i w y d a j n o ś c i d la n a r z ę d z ia n u m p y w p r z y p a d k u w e w n ę t r z n y c h o p e r a c j i w p a m ię c i , n i e s t a n d a r d o w e j f u n k c j i l a p la c ia n i m o d u łu n u m e x p r $ p e r f s t a t -e c y c l e s , s t a l l e d - c y c l e s - f r o n t e n d , s t a l l e d - c y c l e s - b a c k e n d , i n s t r u c t i o n s , \ c a c h e -r e f e r e n c e s ,c a c h e -m is s e s , b r a n c h e s ,b r a n c h -m is s e s , ta s k -c lo c k ,f a u l ts ,\ m in o r - f a u l t s ,c s ,m i g r a t i o n s - r 3 python diffusion_numpy_memory2_numexpr.py Performance co un ter s t a t s f o r 'python diffusion_numpy_memory2_numexpr.py' (3 ru ns): 5 ,940,414,581 cycles # 1.4 47 GHz 3 ,706,635,857 s t a lle d -c y c le s-fro n te n d # 62.40% fr ontend c y c l e s i d l e 2 ,3 2 1 ,606,960 stalled -cycles-b ackend # 39.08% backend c y c l e s i d l e 6 ,909,546,082 instru ctions # 1. 16 insns per c y c l e # 0 . 5 4 s t a l l e d c y c l e s per insn 2 6 1 , 1 3 6 ,7 8 6 c a c h e - r e f e r e n c e s # 6 3 .628 M/sec 1 1 , 6 2 3 ,7 8 3 cach e- misse s # 4 . 4 5 1 % o f a l l cache r e f s 6 2 7 , 3 1 9 ,6 8 6 branches # 152. 851 M/sec 8 , 4 4 3 , 8 7 6 branch-misses # 1.35% o f a l l branches 4104.1 275 07 t a s k - c l o c k # 1.3 64 CPUs u t i l i z e d 9.786 page-faults # 0.0 0 2 M/sec 9 . 7 8 6 m i n o r - f a u lt s # 0.0 0 2 M/sec 8 , 7 0 1 c o n t e x t -s w i t c h e s # 0 .0 0 2 M/sec 60 CPU-migrations # 0.0 1 5 K/sec 3.0 09 8 11418 seconds time elapsed
W iększość dodatkowych mechanizmów wprow adzanych przez m oduł numexpr do programu ma związek z pamięciami podręcznymi. Gdy siatka jest niewielka, a wszystkie dane niezbędne do obliczeń m ieszczą się w pam ięci podręcznej, te dodatkow e m echanizm y po prostu spo w odują dodanie większej liczby instrukcji, które nie będą korzystne pod kątem wydajności. Poza tym kompilowanie operacji wektorowej zakodowanej w postaci łańcucha generuje duże obciążenie. Gdy łączny czas działania program u jest niew ielki, obciążenie to m oże być dość wyraźne. Jednak przy zwiększaniu wielkości siatki należy spodziewać się, że m oduł numexpr lepiej w ykorzysta pam ięć podręczną niż samo narzędzie numpy. Ponadto m oduł numexpr do przeprowadzania swoich obliczeń używa wielu rdzeni i próbuje w ypełnić pamięci podręczne każdego rdzenia. W przypadku niew ielkiej siatki dodatkow e obciążenie zw iązane z zarzą dzaniem wieloma rdzeniami eliminuje wszelki m ożliw y w zrost szybkości. Komputer, na którym uruchomiono kod, był wyposażony w pam ięć podręczną o wielkości 20 480 kB (procesor Intel® Xeon® E5-2680). Ponieważ przetw arzane są dwie tablice (po jed nej dla danych wejściowych i wyjściowych), z łatwością można przeprow adzić obliczenie dla wielkości siatki, która spowoduje wypełnienie pamięci podręcznej. Liczba elementów siatki możliwa do przechow yw ania ma łączną wielkość 20 480 kB/64 bity = 2 560 000. Ze względu na to, że używane są dwie siatki, liczba ta jest dzielona między dwa obiekty (w efekcie każdy obiekt może zaw ierać co najwyżej 2 560 000/2 = 1 280 000 elementów). Zastosowanie pier w iastka kwadratowego dla tej liczby daje w ielkość siatki, która korzysta z takiej liczby ele mentów. Podsumowując, oznacza to, że w przybliżeniu dwie tablice dwuwym iarowe o wiel kości 1131x1131 zapełniłyby pam ięć podręczną (^20480
kB
/64
b it y
/2 = 1131 ). W praktyce
jednak nie uda się całkow icie wypełnić pamięci podręcznej na własne potrzeby (inne pro gramy zapełnią części tej pamięci), dlatego realistyczna jest m ożliw ość pom ieszczenia w niej dwóch tablic 800x800. Po przyjrzeniu się tabelom 6.1 i 6.2 można stwierdzić, że gdy w ielkość siatki zwiększa się z 512x512 do 1024x1024, kod m odułu numexpr zaczyna przew yższać pod względem wydajności czysty kod narzędzia numpy.
128
|
Rozdział 6. Obliczenia macierzowe i wektorowe
T a b e la 6 .1 . Ł ą c z n y c z a s d z ia ła n ia w s z y s tk ic h s c h e m a t ó w d la r ó ż n y c h w ie lk o ś c i s ia t k i i 5 0 0 ite r a c ji f u n k c j i e v o lv e
Metoda
256x256
512x512
1024x1024
2048x2048
4096x4096
Kod Python
2,32 s
9,49 s
39,00 s
155,02 s
617,35 s
Kod Python + pamięć
2,56 s
10,26 s
40,87 s
162,88 s
650,26 s
Narzędzie numpy
0,07 s
0,28 s
1,61 s
11,28 s
45,47 s
Narzędzie numpy + pamięć
0,05 s
0,22 s
1,05 s
6,95 s
28,14 s
Narzędzie numpy + pamięć + laplacian
0,03 s
0,12 s
0,53 s
2,68 s
10,57 s
Narzędzie numpy + pamięć + la pl ac ian + numexpr
0,04 s
0,13 s
0,50 s
2,42 s
9,54 s
Narzędzie numpy + pamięć + scipy
0,05 s
0,19 s
1,22 s
6,06 s
30,31 s
T a b e la 6 .2 . P r z y s p i e s z e n i e p o r ó w n a n e z s z y b k o ś c i ą p r o s t e g o k o d u P y t h o n ( p r z y k ła d 6 .3 ) d la w s z y s t k ic h s c h e m a t ó w i z m i e n n y c h w i e l k o ś c i s i a t k i p r z y l ic z b ie i t e r a c ji f u n k c j i e v o lv e w i ę k s z e j n i ż 5 0 0
Metoda
256x256
512x512
1024x1024
2048x2048
4096x4096
Kod Python
0,00 razy
0,00 razy
0,00 razy
0,00 razy
0,00 razy
Kod Python + pamięć
0,90 razy
0,93 razy
0,95 razy
0,95 razy
0,95 razy
Narzędzie numpy
32,33 razy
33,56 razy
24,25 razy
13,74 razy
13,58 razy
Narzędzie numpy + pamięć
42,63 razy
42,75 razy
37,13 razy
22,30 razy
21,94 razy
Narzędzie numpy + pamięć + laplacian
77,98 razy
78,91 razy
73,90 razy
57,90 razy
58,43 razy
Narzędzie numpy + pamięć + la pl ac ian + numexpr
65,01 razy
74,27 razy
78,27 razy
64,18 razy
64,75 razy
Narzędzie numpy + pamięć + scipy
42,43 razy
51,28 razy
32,09 razy
25,58 razy
20,37 razy
Przestroga: weryfikowanie „optymalizacji" (biblioteka scipy) Nauką, jaką należy w ynieść z lektury tego rozdziału, jest sposób postępowania w przypadku każdej optymalizacji: profilowanie kodu w celu zorientowania się w tym, co ma miejsce, okre ślenie możliwego rozwiązania dla części kodu o małej wydajności, a następnie profilowanie w celu upewnienia się, że wprowadzona poprawka rzeczywiście przyniosła efekty. Choć brzmi to zrozumiale, wszystko szybko może się skomplikować, tak jak to było w przypadku modułu numexpr, którego wydajność w dużym stopniu zależała od wielkości rozpatrywanej siatki. Oczywiście proponow ane rozwiązania nie zaw sze działają zgodnie z oczekiwaniam i. Pod czas tworzenia kodu na potrzeby tego rozdziału jeden z autorów stwierdził, że funkcja laplacian okazała się najwolniejszą funkcją, a ponadto przyjął hipotezę, że biblioteka scipy może być znacznie szybsza. Taki w niosek wynika z faktu, że operatory Laplace'a stanowią typową operację w analizie obrazów i praw dopodobnie dysponują bardzo dobrze zoptym alizowaną biblioteką, która przyspieszy w yw ołania. Poniew aż biblioteka scipy oferuje podm oduł do przetwarzania obrazów, oznacza to, że szczęście nam sprzyja! Im plem entacja była dość prosta (przykład 6.21) i wym agała zastanowienia się przez chwilę nad zawiłościami stosowania okresowych warunków brzegow ych (lub warunków „opako w ania", jak to jest określane w przypadku biblioteki scipy).
Przestroga: weryfikowanie „optymalizacji" (biblioteka scipy)
| 129
P r z y k ł a d 6 .2 1 . U ż y c ie f i l t r u la p la c e b ib l i o t e k i s c i p y from s c i p y . n d i m a g e . f i l t e r s import la p l a c e def l a p l a c i a n ( g r i d , o u t ): l a p l a c e ( g r i d , out, mode='wrap')
Łatwość im plementacji jest dość ważna i przed rozważeniem kwestii w ydajności z pewnością spraw i, że ta m etoda będzie uznaw ana za lepszą. Jednakże po przeprow adzeniu testu po rów naw czego dla kodu biblioteki scipy (przykład 6 .22 ) odkryliśm y następującą rewelację: metoda ta nie oferuje żadnego znacznego przyspieszenia w porównaniu z kodem, na którym bazuje (przykład 6.14). Okazuje się, że w m iarę zwiększania się wielkości siatki metoda ta za czyna cechować się coraz gorszą wydajnością (przyjrzyj się rysunkowi 6.4 na końcu rozdziału). P r z y k ł a d 6 .2 2 . L i c z n i k i w y d a j n o ś c i d la d y f u z j i w p r z y p a d k u f u n k c j i la p la c e b ib l i o t e k i s c i p y $ p e r f s t a t -e c y c l e s , s t a l l e d - c y c l e s - f r o n t e n d , s t a l l e d - c y c l e s - b a c k e n d , i n s t r u c t i o n s , \ c a c h e -r e f e r e n c e s ,c a c h e -m is s e s , b r a n c h e s ,b r a n c h -m is s e s , ta s k -c lo c k ,f a u l ts ,\ m in o r - f a u l t s ,c s ,m i g r a t i o n s - r 3 python diff usio n_ sci py.p y Performance co un ter s t a t s f o r 'python d i f f u s i o n _ s c i p y .p y ' (3 ru ns): 6,5 7 3 ,168,470 cycles # 2 . 9 2 9 GHz 3,574,258,872 s t a lle d -c y c le s-fro n te n d # 54.38% frontend c y c l e s i d l e 2,357,614,687 stalled -cycles-backend # 35.87% backend c y c l e s i d l e 9,850,025,585 instru ctions # 1. 50 insns per c y c l e # 0 .3 6 s t a l l e d c y c l e s per insn 4 1 5 ,9 3 0 ,1 2 3 c a c h e - r e f e r e n c e s # 185. 361 M/sec 3 , 1 8 8 , 3 9 0 cach e- mis se s # 0.7 6 7 % o f a l l cache r e f s 1 , 6 0 8 ,8 8 7 , 8 9 1 branches # 7 17.006 M/sec 4 , 0 1 7 , 2 0 5 branch-misses # 0.25% o f a l l branches 2243.8 978 43 t a s k - c l o c k # 0 . 9 9 4 CPUs u t i l i z e d 7. 3 1 9 p a g e - f a u l t s # 0 .0 0 3 M/sec 7. 3 1 9 m i n o r - f a u lt s # 0.0 0 3 M/sec 12 c o n t e x t -s w i t c h e s # 0.0 0 5 K/sec 1 CPU-migrations # 0 . 0 0 0 K/sec 2.258 396 66 7 seconds time elapsed
Porównanie liczników wydajności wersji kodu biblioteki scipy z licznikami niestandardowej funkcji laplacian (przykład 6.18) pozw ala uzyskać inform acje w skazujące przyczynę braku przyspieszenia spodziewanego po w prow adzonych modyfikacjach. Najbardziej wyróżniające się liczniki to page-faults i instructions. Wartości obydwu są znacznie większe w przypadku wersji kodu biblioteki scipy. W zrost wartości licznika page-faults po kazuje, że choć funkcja laplacian biblioteki scipy obsługuje operacje w ewnętrzne, nadal przy dziela w iele pam ięci. O kazuje się, że w artość licznika page-faults w w ersji kodu biblioteki scipy jest w iększa niż dla pierwszej modyfikacji kodu narzędzia numpy (przykład 6.15). Najważniejszy jest jednak licznik instructions. Informuje on o tym, że kod biblioteki scipy żąda, aby procesor wykonał ponad dwa razy więcej działań niż w przypadku kodu niestan dardowej funkcji laplacian. Naw et pomimo tego, że instrukcje te są bardziej zoptymalizowane (potwierdza to większa wartość licznika insns per cycle, która informuje o liczbie instrukcji, jakie procesor może wykonać w jednym cyklu zegarow ym ), dodatkowa optymalizacja nie zaradzi samej liczbie dodanych instrukcji. Po części może to być spowodowane tym, że kod biblioteki scipy utworzono jako bardzo ogólny. Z tego powodu może on przetwarzać wszelkiego rodzaju dane wejściowe przy użyciu różnych warunków brzegowych (wymagają one dodatkowego kodu, a tym samym w iększej liczby instrukcji). M ożna to stw ierdzić na podstaw ie dużej w artości licznika branches, czyli liczby rozgałęzień wym aganych przez kod biblioteki scipy.
130
|
Rozdział 6. Obliczenia macierzowe i wektorowe
Podsumowanie Analizując ponownie dokonane optymalizacje, można odnieść wrażenie, że podążono dwie ma podstaw ow ym i ścieżkam i: skrócono czas przekazyw ania danych procesorow i i zm niej szono liczbę działań, jakie procesor musiał w ykonać. W tabelach 6.1 i 6.2 dokonano porów nania wyników osiągniętych po zastosowaniu różnych optym alizacji z w ynikam i oryginalnej im plementacji czystego kodu Python dla zbioru danych o zmiennej wielkości. Na rysunku 6.4 w wersji graficznej zaprezentowano rezultat tego porównania. W idoczne są trzy pasma wydajności, które odpowiadają dwóm w ym ienionym m etodom. Pasmo przebie gające w zdłuż dolnej części rysunku prezentuje niew ielki w zrost w ydajności w odniesieniu do im plementacji czystego kodu Python w przypadku początkow ych działań poczynionych w celu zmniejszenia liczby alokacji pamięci. Środkowe pasmo pokazuje, co się stało po zasto sowaniu narzędzia numpy i dalszym zredukowaniu liczby alokacji. Z kolei górne pasm o ilu struje wyniki osiągnięte przez ograniczenie liczby podejm ow anych działań. W ażnym w nioskiem jest to, że zaw sze należy zadbać o wszystkie działania administracyjne, jakie kod musi wykonać podczas inicjowania. M oże to obejmować przydzielanie pam ięci lub wczytywanie konfiguracji z pliku, a naw et w stępne obliczanie wybranych w artości, które bę dą potrzebne w trakcie trwania cyklu życia programu. Jest to istotne z dwóch powodów. Po pierwsze, zmniejszana jest całkowita liczba koniecznych uruchom ień tych zadań przez jedno razow e w ykonanie ich na początku. Ponadto w iadom o, że w przyszłości m ożliw e będzie użycie tych zasobów bez generowania zbyt dużego obciążenia. Po drugie, nie jest zakłócany przepływ programu. Umożliwia to bardziej efektywne potokowanie i zapewnia wypełnienie pamięci podręcznych bardziej trafnymi danymi. Dowiedziałeś się też więcej o znaczeniu lokalizacji danych, a także o tym, jak istotny jest pro sty sposób przekazywania danych procesorowi. Pamięci podręczne procesora mogą być dość złożone. Często najlepszym rozwiązaniem jest zezw olenie różnym mechanizmom zaprojek tow anym pod kątem ich optym alizacji na zajęcie się w szystkim . Jednakże decydujące zna czenie może m ieć zrozum ienie zachodzących procesów, a ponadto realizowanie w szystkich m ożliw ych operacji w celu zoptymalizow ania sposobu obsługi pamięci. Na przykład dzięki opanowaniu zasad działania pamięci podręcznych wiemy, że spadek wydajności prow adzą cy do zaniku przyspieszenia niezależnie od wielkości siatki (rysunek 6.4) praw dopodobnie można przypisać wypełnieniu pamięci podręcznej L3 przez dane siatki. Gdy do tego dojdzie, przestaną być widoczne korzyści wynikające ze stosowania warstwowej architektury pamięci jako rozwiązania problemu wąskiego gardła Von Neumanna. Kolejny ważny w niosek dotyczy użycia bibliotek zewnętrznych. Język Python jest znakomity ze w zględu na jego łatw ość użycia i czytelność kodu, co um ożliw ia szybkie tw orzenie i debugowanie kodu. Jednakże zasadnicze znaczenie ma dostrojenie wydajności do bibliotek ze wnętrznych. M ogą one być w yjątkowo szybkie, ponieważ m ogą być tworzone za pom ocą ję zyków niskopoziomowych. Ponieważ jednak kom unikują się z interpreterem języka Python przy użyciu interfejsu, możliwe jest też szybkie napisanie kodu, który będzie korzystać z tych bibliotek. Pokazaliśmy też, jak w ażną rolę przed rozpoczęciem eksperym entów odgrywa wykonywanie testów porów nawczych i definiowanie hipotez dotyczących wydajności. Sformułowanie hi potezy przed uruchomieniem testu porównawczego umożliwia określenie warunków infor m ujących o tym, czy optym alizacja faktycznie zadziałała. Czy dana zm iana m ogła skrócić
Podsumowanie
|
131
W i e l k o ś ć sia tk i R y s u n e k 6 .4 . Z e s t a w ie n ie p r z y s p i e s z e ń w y n ik a ją c y c h z u ż y c ia m e t o d w y p r ó b o w a n y c h w r o z d z ia le
czas działania? Czy spowodowała ona zredukow anie liczby alokacji? Czy m niejsza jest liczba chybień w pamięci podręcznej? Optymalizacja niekiedy może być sztuką. Wynika to z ogrom nej złożoności systemów komputerowych. Ponadto niezm iernie pomocna m oże być analiza ilościowa zachodzących procesów. O statnia uw aga dotycząca optym alizacji odnosi się do tego, że trzeba szczególnie zadbać o to, by przeprow adzane optym alizacje m ogły zostać uogólnione dla różnych kom puterów (przyjm ow ane założenia i uzyskiw ane w yniki testów porów naw czych m ogą być zależne od architektury używ anego kom putera, sposobu skom pilow ania stosow anych m odułów itp.). Ponadto podczas dokonywania optymalizacji niezm iernie w ażne jest uwzględnienie innych program istów oraz tego, jak zm iany w płyną na czytelność kodu. Na przykład rozw iązanie zastosow ane w przykładzie 6.17 okazało się potencjalnie niejasne. W związku z tym postara liśmy się o pełne udokum entow anie kodu i przetestowanie go, aby optym alizacje okazały się pom ocne nie tylko dla nas, ale również dla innych osób z zespołu. W następnym rozdziale zostanie przedstaw iony sposób tw orzenia w łasnych m odułów ze w nętrznych, które m ogą być dokładnie dostrajane pod kątem rozw iązyw ania konkretnych problem ów przy jednoczesnym zapewnieniu jeszcze większej efektywności. Jest to m ożliwe dzięki zastosowaniu metody pisania programów wykorzystującej szybkie tworzenie prototy pów. Polega ona na rozwiązaniu najpierw problemu z powolnym kodem, następnie zidenty fikowaniu elementów o małej wydajności, a na końcu znalezieniu sposobów na przyspieszenie ich działania. Częste profilow anie i podejm ow anie próby optym alizacji jedynie sekcji kodu, w przypadku których w iadom o, że są pow olne, pozw ala zaoszczędzić czas, a jednocześnie sprawić, że program y będą działać tak szybko, jak to m ożliwe.
132
|
Rozdzia ł 6. Obliczenia macierzowe i wektorowe
_________________________________ ROZDZIAŁ 7.
Kompilowanie do postaci kodu C
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Jak m ożesz sprawić, że kod Python będzie działać jako kod niższego poziomu? • Jaka jest różnica m iędzy kompilatorem JIT i kompilatorem AOT? • Jakie zadania mogą być wykonyw ane przez skompilowany kod Python szybciej niż w przypadku zwykłego kodu Python? • Dlaczego adnotacje typu zw iększają szybkość skompilowanego kodu Python? • Jak m ożesz utw orzyć moduły dla kodu Python za pom ocą języka C lub Fortran? • Jak m ożesz użyć w kodzie Python bibliotek języka C lub Fortran?
N ajprostszym sposobem przyspieszenia kodu jest ograniczenie liczby operacji, jakie będzie wykonywać. Zakładając, że zostały już w ybrane dobre algorytmy i zm niejszono ilość prze twarzanych danych, najprostsza metoda, by w ykonyw ać m niejszą liczbę instrukcji, polega na skompilowaniu kodu do postaci kodu m aszynowego. W tym zakresie język Python oferuje kilka opcji obejm ujących narzędzia do kom pilow ania oparte na czystym kodzie C, takie jak Cython, Shed Skin i Pythran, kom pilow anie bazujące na kompilatorze LLVM za pośrednictwem narzędzia Numba oraz zastępczą m aszynę w irtu alną PyPy, która zawiera wbudowany kompilator JIT Ju s t in Time). Przy podejm owaniu de cyzji dotyczącej ścieżki, jaka zostanie obrana, konieczne jest zrównoważenie w ym agań zw ią zanych z łatwością dostosowania kodu i impetu zespołu. Każde z w ym ienionych narzędzi dodaje now ą zależność do szeregu używ anych narzędzi. Dodatkowo narzędzie Cython w ym aga pisania w języku nowego typu (hybrydzie języków Python i C), co w iąże się z koniecznością zdobycia nowej umiejętności. W ykorzystanie no w ego języka narzędzia Cython może m ieć negatyw ny w pływ na im pet zespołu, ponieważ jego członkowie bez znajomości języka C m ogą m ieć problem z obsługą takiego kodu. Jednak w praktyce jest to raczej niewielki kłopot, gdyż kod narzędzia Cython będzie używ any tylko w dobrze wybranych, niewielkich obszarach kodu.
133
Godne uwagi jest to, że przeprowadzanie profilowania kodu w odniesieniu do pamięci i proce sora prawdopodobnie spowoduje, że zaczniesz się zastanawiać nad optymalizacjami algoryt micznymi wyższego poziomu, które m ogą zostać zastosowane. Takie zm iany algorytm iczne (czyli użycie dodatkowej logiki w celu uniknięcia obliczeń lub buforow anie elim inujące po now ne obliczenia) m ogą pom óc Ci zapobiegać w ykonyw aniu w kodzie zbędnych działań. Ekspresywność kodu Python ułatwia zauważenie takich możliwości algorytmicznych. W pod rozdziale „Technika głębokiego uczenia prezentowana przez firmę RadimRehurek.com" z roz działu 12. Radim Rehurek wyjaśnia, jak implementacja kodu Python m oże w ygrać z czystą im plementacją stworzoną w języku C. W rozdziale dokonamy przeglądu następujących narzędzi: • Cython — jest to najczęściej używane narzędzie do kompilowania do postaci kodu C, które uwzględnia zarówno narzędzie numpy, jak i zwykły kod Python (wymagana jest znajomość języka C). • Shed Skin — zautom atyzowany konwerter Python-C przeznaczony dla kodu, który nie bazuje na narzędziu numpy. • Numba — now y kompilator stworzony z m yślą o kodzie bazującym na narzędziu numpy. • Pythran — nowy kompilator przeznaczony zarówno dla kodu bazującego na narzędziu numpy, jak i innego kodu. • PyPy — stabilny kompilator JIT dla kodu, który nie bazuje na narzędziu numpy. Kod taki zastępuje zwykły plik wykonyw alny Python. W dalszej części rozdziału przyjrzymy się interfejsom funkcji zewnętrznych, które umożliwiają skompilowanie kodu C do postaci m odułów rozszerzeń dla języka Python. W budow any in terfejs API tego języka jest używ any razem z narzędziami ctypes i c f f i (twórców kompilatora PyPy) oraz z konwerterem Fortran-Python f 2py.
Jakie wzrosty szybkości są możliwe? Jeśli problem zw iązany jest z m etodą kom pilow ania, całkiem praw dopodobne są w zrosty szybkości w ynoszące rząd wielkości lub więcej. Przyjrzymy się tutaj różnym metodom osią gania przyspieszeń w ynoszących jeden lub dwa rzędy w ielkości dla pojedynczego rdzenia, a także w przypadku zastosowania wielu rdzeni za pośrednictwem interfejsu OpenMP. Kod Python, który zwykle będzie działać szybciej po skompilowaniu, prawdopodobnie służy do zastosowań matematycznych, a ponadto zawiera raczej wiele pętli wielokrotnie powtarzających te same operacje. W obrębie tych pętli możliwe jest tworzenie wielu obiektów tymczasowych. Mało prawdopodobne jest to, że kod wywołujący biblioteki zewnętrzne (np. wyrażenia regu larne, operacje na łańcuchach, wywołania bibliotek bazy danych) wykaże jakiekolw iek przy spieszenie po skompilowaniu. Programy powiązane z operacjami wejścia-wyjścia również raczej nie pozwolą osiągnąć znacznych przyspieszeń. Jeśli kod Python koncentruje się na w ywoływaniu wektoryzowanych funkcji narzędzia numpy, wcale może nie działać szybciej po skompilowaniu. Inaczej będzie tylko wtedy, gdy kompilo wany kod to głównie kod Python (a ponadto prawdopodobnie w sytuacji, kiedy w kodzie jest wykonywana pętla). W rozdziale 6 . omówiono operacje narzędzia numpy. Okazuje się, że w ich przypadku kompilowanie nie będzie pomocne, ponieważ nie występuje wiele obiektów pośrednich.
134
|
Rozdział 7. Kompilowanie do postaci kodu C
O gólnie rzecz biorąc, bardzo m ało praw dopodobne jest to, że skom pilow any kod będzie w ogóle szybszy od funkcji napisanej w języku C. N iem niej jednak taki kod nie będzie też działać znacznie wolniej. Całkiem możliwe jest to, że kod C wygenerowany z kodu Python będzie działać tak samo szybko jak ręcznie napisana funkcja C, chyba że programista uży wający języka C dysponuje szczególnie dużą wiedzą na tem at m etod dostrajania kodu C do architektury docelowej platformy sprzętowej. W przypadku kodu stw orzonego z m yślą o operacjach m atem atycznych m ożliw e jest, że ręcznie stworzona funkcja języka Fortran przewyższy odpowiadającą jej funkcję C. Jednakże i tym razem będzie to raczej w ym agać w iedzy eksperckiej. G eneralnie rzecz biorąc, w ynik kom pilacji (uzyskany praw dopodobnie z w ykorzystaniem narzędzia Cython, Pythran lub Shed Skin) będzie zbliżony do wyniku dla ręcznie napisanego kodu C w stopniu wymaganym przez w iększość programistów. Podczas profilowania algorytmu i korzystania z niego trzeba pam iętać o diagramie z rysun ku 7.1. Trochę czasu poświęconego na zrozum ienie kodu poprzez jego profilow anie powinno dać m ożliw ość podjęcia lepszych decyzji na poziom ie algorytmicznym . Skoncentrow anie się w dalszej kolejności na kompilatorze powinno zaow ocow ać dodatkowym przyspieszeniem. Prawdopodobnie możliwe będzie dalsze dostrajanie algorytmu, ale nie należy być zaskoczo nym coraz mniejszymi przyspieszeniami wynikającymi z coraz większej ilości włożonej pracy. Trzeba samemu stwierdzić, kiedy dodatkowe starania przestaną być opłacalne.
„Szybkie wygrane" i malejące korzyści
Nakład pracy R y s u n e k 7 .1 . T r o c h ę c z a s u p o ś w i ę c o n e g o n a p r o f i l o w a n i e i k o m p i l o w a n i e z a p e w n i a s p o r e k o r z y ś c i, a le d a ls z e d z ia ł a n i a z w y k le s ą c o r a z m n i e j o p ła c a ln e
Jakie w zrosty szybkości są możliwe?
|
135
Jeśli używasz kodu Python i całej grupy dołączonych bibliotek bez narzędzia numpy, podsta w owym i opcjami w yboru będą narzędzia Cython, Shed Skin i PyPy. Jeśli używasz narzędzia numpy, odpowiednimi propozycjam i są narzędzia Cython, Numba i Pythran. Obsługują one język Python 2.7, a część z nich jest też zgodna z językiem Python w wersji 3.2 lub nowszej. Niektóre z przedstawionych dalej przykładów w ym agają ogólnej znajom ości kompilatorów kodu C oraz samego kodu C. W przypadku braku takiej w iedzy przed zagłębieniem się w tę tematykę powinieneś w podstawowym zakresie poznać język C i skompilować działający pro gram napisany w tym języku.
Porównanie kompilatorów JIT i AOT Om awiane narzędzia można podzielić na dwie grupy: służące do w cześniejszego kompilo w ania (kompilatory AO T: Cython, Shed Skin, Pythran) oraz do kompilowania przy pierwszej próbie użycia kodu (kompilatory JIT: Numba, PyPy). Kompilowanie za pom ocą kompilatora AOT (Ahead o f Time) pow oduje utw orzenie biblioteki statycznej przeznaczonej dla używanej platformy sprzętowej. Po pobraniu narzędzia numpy, scipy lub scik it-learn nastąpi skompilowanie przez jedno z nich części biblioteki z wykorzy staniem kompilatora Cython na danej platform ie sprzętowej (ewentualnie w przypadku uży cia dystrybucji takiej jak Continuum Anaconda, zostanie zastosowana w cześniej zbudowana biblioteka kompilowana). Dzięki kompilacji kodu przed jego użyciem uzyskuje się bibliotekę, która może od razu zostać zastosowana przy rozwiązywaniu problemu. Kompilowanie za pom ocą kompilatora JIT (Just in Time) w dużym stopniu (lub całkowicie) eliminuje początkow e działania. Kom pilator może rozpocząć kom pilację tylko odpowiednich części kodu w m omencie ich użycia. Oznacza to wystąpienie problemu zimnego startu. Polega on na tym, że może w ystąpić sytuacja, gdy w iększość kodu program u została już skompilo wana, a aktualnie używana porcja kodu jeszcze nie, więc w momencie rozpoczynania urucha miania kodu w czasie trwania kompilacji będzie on działać bardzo wolno. Jeśli ma to m iejsce każdorazow o przy urucham ianiu skryptu, który jest uaktyw niany w ielokrotnie, zw iązany z tym spadek w ydajności może stać się znaczny. Ponieważ problem ten dotyczy kompilatora PyPy, korzystanie z niego w przypadku krótkich, lecz często w ykonyw anych skryptów może okazać się niepożądane. W tym miejscu omówienia widać, że w cześniejsze kom pilow anie daje korzyść w postaci naj lepszych przyspieszeń, ale często w ym aga też największego nakładu pracy. Kompilatory JIT oferują duże przyspieszenia przy bardzo małej liczbie ręcznie wprowadzanych zmian, ale mogą pow odow ać opisany problem. Przy w yborze właściwej technologii na potrzeby konkretnego zastosowania konieczne będzie rozważenie tych kwestii.
Dlaczego informacje o typie ułatwiają przyspieszenie działania kodu? W języku Python typy są dynamicznie określane. Zmienna może odwoływać się do obiektu do wolnego typu, a dowolny wiersz kodu może zmienić typ przywoływanego obiektu. Utrudnia to maszynie wirtualnej optymalizację metody wykonywania kodu na poziomie kodu maszynowego,
136
|
Rozdział 7. Kompilowanie do postaci kodu C
ponieważ nie dysponuje ona informacją o tym, jaki podstawowy typ danych będzie używany dla przyszłych operacji. Utrzym yw anie kodu w uogólnionej postaci pow oduje, że będzie on dłużej w ykonywany. W poniższym przykładzie v identyfikuje liczbę zm iennoprzecinkow ą lub parę takich liczb, które reprezentują liczbę zespoloną complex. Oba w arunki m ogą w ystąpić w tej sam ej pętli w różnym czasie lub w pow iązanych kolejnych sekcjach kodu: v = -1.0 p r i n t t y p e ( v ) , abs(v) 1.0 v = 1-1j p r i n t t y p e ( v ) , abs(v) 1.41421356237
Funkcja abs działa różnie w zależności od bazowego typu danych. Funkcja ta użyta dla liczby całkow itej lub zm iennoprzecinkow ej po prostu pow oduje przekształcenie w artości ujemnej w w artość dodatnią. W przypadku liczby zespolonej funkcja abs pobiera pierw iastek kwa dratowy sumy elementów podniesionych do kwadratu: a b s ( c ) = -yjc . r e a l 2 + c . i m a g 2
Kod m aszynow y dla przykładu liczby zespolonej complex uw zględnia w ięcej instrukcji i do wykonania wymaga więcej czasu. Przed w ywołaniem funkcji abs dla zmiennej interpreter ję zyka Python m usi najpierw poszukać typu zm iennej, a następnie zdecydow ać, jaką w ersję funkcji wywołać. Związane z tym obciążenie zwiększa się w przypadku wykonywania wielu pow tarzanych wywołań. W obrębie kodu Python każdy podstaw ow y obiekt, taki jak liczba całkow ita, zostanie opa kow any za pom ocą obiektu języka Python w yższego poziom u (np. za pom ocą obiektu int w przypadku liczby całkow itej). Tego rodzaju obiekt oferuje dodatkow e funkcje, takie jak hash (ułatwia przechowywanie) i str (obsługuje wyśw ietlanie łańcuchów). W ewnątrz sekcji kodu pow iązanego z procesorem częstą sytuacją jest to, że typy zm iennych nie zmieniają się. Daje to m ożliw ość zastosowania kompilacji statycznej i szybszego w yko nywania kodu. Jeśli w ym aganych jest jedynie w iele pośrednich operacji m atem atycznych, nie są potrzebne funkcje w yższego poziom u, a ponadto m ogą być zbędne m echanizm y służące do zliczania odwołań. W tym przypadku można po prostu przejść do poziomu kodu maszynowego i prze prowadzić szybko obliczenia przy użyciu kodu maszynowego i bajtów, a nie poprzez m ody fikowanie obiektów w yższego poziomu języka Python, z czym wiąże się większe obciążenie. W tym celu w cześniej określane są typy obiektów, aby m ożliw e było w ygenerow anie po prawnego kodu C.
Użycie kompilatora kodu C W dalszych przykładach zostaną zastosowane kompilatory gcc i g++ z zestawu narzędziowego GNU C Compiler. Jeśli popraw nie skonfigurujesz środowisko, możesz skorzystać z alterna tywnego kompilatora (np. icc Intela lub cl M icrosoftu). Narzędzie Cython korzysta z kom pilatora gcc, a narzędzie Shed Skin używa kompilatora g++.
Użycie kompilatora kodu C
|
137
Kompilator gcc stanowi znakomity wybór w przypadku w iększości platform, ponieważ jest dobrze obsługiwany i dość zaawansowany. Często możliwe jest uzyskanie większej wydajności za pom ocą dostrojonego kom pilatora (np. w przypadku urządzeń Intela kom pilator icc tej firmy może w ygenerować szybszy kod niż kompilator gcc), ale wiąże się to z koniecznością poszerzenia wiedzy specjalistycznej i uzyskania informacji o sposobie dostosowywania flag dla alternatywnego kompilatora. Języki C i C++ często są używ ane do kom pilacji statycznej w m iejsce innych języków , takich jak Fortran, ze względu na ich w szechobecność i bogatą gamę bibliotek pom ocniczych. Kom pilator i konwerter (w tym przypadku konwerterem jest narzędzie Cython i inne podobne) m ają m ożliw ość analizowania kodu z adnotacją w celu określenia, czy m ogą zostać zastoso w ane kroki optym alizacji statycznej (np. w staw ianie funkcji i rozw ijanie pętli). Agresyw na analiza pośredniego drzewa składni abstrakcyjnej (przeprowadzana przez narzędzia Pythran, Numba i PyPy) zapewnia możliwości łączenia wiedzy o tym, jak w języku Python wyrażane są informacje o najlepszej metodzie wykorzystania napotkanych wzorców w celu przekaza nia ich bazowemu kompilatorowi.
Analiza przykładu zbioru Julii W rozdziale 2. dokonano profilowania generatora zbioru Julii. Użyty kod generuje obraz wyj ściowy z wykorzystaniem liczb całkowitych i liczb zespolonych. Obliczenia obrazu są pow ią zane z procesorem. Główne obciążenie zw iązane z wykonywaniem kodu miało postać powiązanej z procesorem pętli w ewnętrznej, która oblicza listę output. Lista może m ieć postać kwadratowej tablicy pik seli, w której każda w artość reprezentuje koszt w ygenerowania piksela. Kod funkcji wewnętrznej został zaprezentow any w przykładzie 7.1. P r z y k ł a d 7 1 . A n a l i z a p o w i ą z a n e g o z p r o c e s o r e m k o d u f u n k c j i z b io r u J u l i i def c a l c u la t e _ z _ s e r i a l _ p u r e p y t h o n (m a x i te r , z s , c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) for i in ran ge(len (zs)): n = 0 z = zs[i] c = cs[i] while n < m axite r and abs( z) < 2: z = z * z + c n += 1 o u t p u t [i ] = n r e tu r n output
W przypadku laptopa jednego z autorów obliczenie oryginalnego zbioru Julii dla siatki 1000x 1000 przy wartości maxit równej 300 zajęło w przybliżeniu 11 sekund (użyto implementacji czystego kodu Python wykonywanego za pomocą narzędzia CPython 2.7).
138
|
Rozdział 7. Kompilowanie do postaci kodu C
Cython C ython (http://cython.org/) to kom pilator, który przekształca kod Python z adnotacją typu w skompilowany moduł rozszerzenia. Adnotacje typu przypominają te stosowane w języku C. Takie rozszerzenie może być im portow ane za pom ocą narzędzia import jako zw ykły moduł języka Python. Choć rozpoczęcie działań nie przysparza trudności, wiąże się z tym koniecz ność poszerzania wiedzy w coraz większym stopniu wraz z każdym dodatkowym pozio mem złożoności i optymalizacji. Przez jednego z autorów narzędzie to jest wykorzystyw ane do przekształcania funkcji w ym agających wielu obliczeń w szybszy kod. W ybrał to narzędzie z powodu jego powszechnego użycia, dojrzałości i obsługi interfejsu OpenMP. W przypadku standardu OpenM P m ożliw e jest radzenie sobie z problem am i dotyczącym i przetwarzania równoległego poprzez zastosow anie m odułów obsługujących wieloprocesorowość, które są uruchamiane w wielu procesorach jednego komputera. W ątki są ukrywane przed kodem Python. Działają za pośrednictwem w ygenerowanego kodu C. Kompilator Cython (opublikowany w 2007 r.) w ywodzi się z kompilatora Pyrex (wprowa dzonego w 2002 r.). Cython rozszerza możliwości pierwotnych zastosowań kompilatora Pyrex. Biblioteki, które używają kompilatora Cython, to: scipy, scikit-learn, lxml i zmq. Kom pilator Cython może być stosowany za pośrednictwem skryptu setup.py do kompilacji m odułu. M oże też zostać użyty interaktyw nie w pow łoce IPython, co umożliwia „m agiczne" polecenie. Adnotacja typów jest zw ykle przeprow adzana przez program istę, choć m ożliw a jest pewna forma zautom atyzowanego tworzenia adnotacji.
Kompilowanie czystego kodu Python za pomocą narzędzia Cython Prosta metoda rozpoczęcia tworzenia kompilowanego m odułu rozszerzenia uwzględnia trzy pliki. W przypadku użycia zbioru Julii jako przykładu są to następujące pliki: • Plik wywołującego kodu Python (spora część wcześniej przedstawionego kodu zbioru Julii). • Nowy plik .pyx z funkcją do skompilowania. • Plik setup.py, który zawiera instrukcje wywołujące kompilator Cython do utworzenia mo dułu rozszerzenia. Przy użyciu tej metody wyw oływ any jest skrypt setup.py w celu wykorzystania kompilatora Cython do skompilowania pliku .pyx do postaci skompilowanego modułu. W systemach uniksow ych skom pilow any m oduł będzie praw dopodobnie plikiem .so. W system ie W indows powinien to być plik .pyd (biblioteka języka Python przypominająca bibliotekę DLL). W przypadku przykładu zbioru Julii zostaną zastosow ane następujące pliki: • julia1.py. Służy do zbudow ania list w ejściow ych i w ywołania funkcji obliczeniowej. • cythonfn.pyx. Zawiera funkcję powiązaną z procesorem, dla której można utworzyć adnotacje. • setup.py. Zawiera instrukcje procesu budowania. Wynikiem uruchomienia skryptu setup.py jest możliwy do zaimportowania moduł. W skrypcie julia1.py z przykładu 7.2 wymagane jest jedynie wprow adzenie kilku drobnych zmian w celu zaim portow ania nowego modułu za pomocą instrukcji import i w ywołania funkcji.
Cython
| 139
Przykład 7.2. Importowanie nowo skompilowanego modułu do głównego kodu import c a l c u l a t e
# zgodnie z definicją w skrypcie setup.py
def cal c_ pu re_python(des ired_w idt h, m a x _ i t e r a t i o n s ) : # ... s t a r t _ t i m e = t i m e . ti m e () output = c a l c u l a t e . c a l c u l a t e _ z ( m a x _ i t e r a t i o n s , z s , cs) end_time = ti m e . ti m e () se c s = end_time - s t a r t _ t i m e p r i n t "Czas t r w a n i a : " , s e c s , "s"
W przykładzie 7.3 zaczniemy od czystego kodu Python bez adnotacji typu. Przykład 7.3. Niezmieniony czysty kod Python z pliku cythonfn.pyx (ze zmienionym rozszerzeniem na .py) dla skryptu setup.py kompilatora Cython # cythonfn.pyx def c a l c u l a t e _ z ( m a x i t e r , z s , c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while n < m axite r and abs( z) < 2: z = z * z + c n += 1 o u t p u t [i ] = n r e tu r n output
Skrypt setup.py z przykładu 7.4 jest krótki. Zdefiniowano w nim sposób przekształcenia pliku cythonfn.pyx w plik calculate.so. Przykład 7.4. Skrypt setup.py przekształca plik cythonfn.pyx w kod C, który ma zostać skompilowany przez kompilator Cython from d i s t u t i l s . c o r e import setup from d i s t u t i l s . e x t e n s i o n import Extension from C y t h o n . D i s t u t i l s import build_ext setu p( cmdclass = { ' b u i l d _ e x t ' : b u i l d _ e x t } , ext_modules = [ E x t e n s i o n ( " c a l c u l a t e " , [" c y t h o n f n . p y x " ] )] )
Po uruchomieniu skryptu setup.py z przykładu 7.5 z argumentem build_ext kompilator Cython poszuka pliku cythonfn.pyx i utworzy plik calculate.so. Przykład 7.5. Uruchamianie skryptu setup.py w celu zbudowania nowo skompilowanego modułu $ python setup.py bu ild _e xt - - i n p l a c e running build_ext cythoning cythonfn.pyx to cyth on fn .c buildin g ' c a l c u l a t e ' ext en sion gcc -pt hread - f n o - s t r i c t - a l i a s i n g -DNDEBUG -g -fwrapv -O2 -Wall - W s t r i c t-p r o to t y p e s -fPI C -I/usr /include/python2.7 - c cyth on fn .c -o build/ tem p.l in ux-x 86_64-2.7 / c yth on fn .o gcc -pt hread -shared -Wl,-O1 -W l,-B sy m b o lic -f u n c ti o n s -Wl, -B sy m b olic -f un ct ions - W l , -z , r e l r o bu ild / tem p .l in u x-x 86_ 64-2.7 / c yth on fn .o -o c a l c u l a t e . s o
140
|
Rozdział 7. Kompilowanie do postaci kodu C
Pamiętaj o tym, że jest to krok wykonywany ręcznie. Gdy zaktualizujesz plik .pyx lub setup.py i zapomnisz ponownie uruchomić polecenie do budowania, nie będzie dostępny zaktualizowany moduł .so do zaimportowania. Jeśli nie masz pewności, czy kod został skompilowany, sprawdź znacznik czasu pliku .so. W razie wątpliwości usuń wygenerowane pliki kodu C oraz plik .so, a następnie zbuduj je ponownie.
^
Argum ent --inplace nakazuje kompilatorowi Cython zbudow anie skompilowanego modułu w bieżącym katalogu, a nie w osobnym katalogu build. Po zakończeniu procesu budow ania dostępny będzie plik cythonfn.c, który jest raczej mało czytelny, a także plik calculate.so. Po uruchomieniu kodu z pliku julia1.py im portowany jest skompilowany moduł. Na laptopie jednego z autorów zbiór Julii został obliczony w czasie wynoszącym 8,9 sekundy, a nie w bar dziej typow ym czasie równym 11 sekund. Jest to niew ielki w zrost w ydajności kosztem zni kom ego nakładu pracy.
Użycie adnotacji kompilatora Cython do analizowania bloku kodu W poprzednim przykładzie pokazano, że możliwe jest szybkie zbudow anie skompilowanego modułu. W przypadku intensywnych pętli i operacji matematycznych już samo to często pro wadzi do wzrostu szybkości. Oczywiście nie należy po omacku przeprowadzać optymalizacji. Konieczne jest stwierdzenie, jaka część kodu jest wolna, aby możliwe było zdecydowanie o tym, co wymaga większego nakładu pracy. Kom pilator Cython oferuje opcję tworzenia adnotacji, która zapewnia plik wyjściow y HTML m ożliw y do w yśw ietlenia w przeglądarce. Do w ygenerow ania adnotacji używ ane jest po lecenie cython -a cythonfn.pyx, które generuje plik w yjściow y cythonfn.html. Po wyśw ietleniu w przeglądarce zaw artość pliku przypomina w idoczną na rysunku 7.2. Podobny rysunek jest dostępny w dokumentacji kompilatora Cython (http://docs.cython.org/src/quickstart/cythonize.html).
G enerated b y Cython 0.21.1 Raw
o u t p u t ; c y t h o n f n .c
+ 0 1 : der calculate_z(maxiter, zs, cs) OZ: """ U t u c z a n i e listy output za pomocą reguiy aktualizacji ztioru Julii""" +03: output * [OJ * len(zs) +04: for i in ra n g e (len(zs)) : +05: n = 0 +06: z = zs[i] +07: c = cs[i] +08: while n < maxiter and abs(z) < 2: +09: z= z * z + c +10: n += 1 +11: output[i] * n +12: return output
Rysunek 7.2. Kolorem wyróżniono dane wyjściowe funkcji bez adnotacji uzyskane za pomocą kompilatora Cython Dwukrotne kliknięcie każdego wiersza pow oduje jego rozw inięcie i wyśw ietlenie wygene rowanego kodu C. Intensywniejszy żółty kolor oznacza więcej wyw ołań w obrębie maszyny wirtualnej języka Python, bardziej białe wiersze natomiast wskazują na kod C, który w mniej szym stopniu przypom ina kod Python. Celem jest usunięcie jak najw iększej liczby żółtych w ierszy i zakończenie działań z jak najm niejszą liczbą białych wierszy.
Cython
|
141
Choć bardziej żółte w iersze oznaczają więcej w yw ołań w obrębie maszyny wirtualnej, nieko niecznie spow oduje to w olniejsze działanie kodu. Każde w yw ołanie w m aszynie w irtualnej wiąże się z obciążeniem, ale dla wszystkich takich wywołań będzie ono znaczne tylko w przy padku w yw ołań w ystępujących w ew nątrz dużych pętli. W yw ołania poza obrębem dużych pętli (np. wiersz kodu używ any do utw orzenia listy output na początku funkcji) nie są kosz towne w porównaniu z kosztem obliczeń w pętli w ewnętrznej. Nie m arnuj czasu na wiersze, które nie pow odują spowolnienia kodu. W przykładzie w iersze z najw iększą liczbą w yw ołań w obrębie m aszyny w irtualnej języka Python (najbardziej żółte) m ają num ery 4 i 8 . Na podstaw ie wyników dotychczasow ych ope racji profilow ania można stwierdzić, że w iersz 8 . zostanie praw dopodobnie wyw ołany ponad 30 milionów razy, dlatego jest znakomitym kandydatem do tego, by się na nim skoncentrować. W iersze 9., 10. i 11. są praw ie żółte. Ponadto wiadomo, że znajdują się w środku intensywnej pętli w ew nętrznej. O gólnie rzecz biorąc, odpow iadają za sporą część czasu w ykonyw ania funkcji. Z tego powodu w pierwszej kolejności trzeba się nimi zająć. Jeśli musisz przypomnieć sobie, ile czasu trwało wykonywanie tej sekcji kodu, zajrzyj do podrozdziału „Użycie narzędzia line_profiler do pomiarów dotyczących kolejnych wierszy kodu" z rozdziału 2 . W iersze 6 . i 7. są m niej żółte. Poniew aż są w yw oływ ane tylko m ilion razy, m ają znacznie mniejszy wpływ na końcową szybkość. Oznacza to, że później można skupić uwagę na nich. Okazuje się, że ponieważ są one obiektami li s t , w łaściw ie nic nie można zrobić, aby skrócić czas dostępu do nich. Jak w spom niano w podrozdziale „Cython i num py", w yjątkiem jest operacja polegająca na zastąpieniu obiektów l i s t tablicam i narzędzia numpy, które zapew nią niewielki przyrost szybkości. Aby lepiej zrozumieć żółte obszary, możesz rozwinąć każdy wiersz przez dwukrotne kliknię cie. Na rysunku 7.3 widać, że do utworzenia listy output iterowana jest długość elementu zs. Powoduje to utworzenie obiektów języka Python, w przypadku których maszyna w irtualna tego języka zlicza odwołania. Choć te w yw ołania są kosztow ne, tak napraw dę nie m ają w pływ u na czas w ykonyw ania tej funkcji. G e n e r a t e d b y C y th o n 0 . 2 1 . 1 Raw o u t p u t :
c y t h o n f n .c
+0 1 : d e r c a l c u l a r e _ z (m a x l te r , z s , c s ) : 02: ’" " ’ O b lic z a n ie l i s t y u u tp u t z a poiaucą r e g u ły a k t u a l i z a c j i zbiu+u J u l i i ” ” " +03: o u tp u t - [0 ] * l e n (z s ) p y x _ t _ l — r y O b jc ct_ L c n g th (__p y x _ v _ z s ) ; i f (u n l ik e ly (__ p y x _ t _ l — 1 ) ) _{ _p y x _ f ilcn an tc — ___ p y x _ f [0 ]; ___py x_] p y x _ t 2 * P y L ia t_ N e v (l * ( ( _p y x_t_l-< 0) "i 0 : _p y x _ t _ l ) ) ; i f (u n l ik e ly (__! _p y x _ t_ 2 ) ) (_p y x _ f ilanaave pyx_ Pyx_GOTRFF( p y x_r._? ) ; ( P y _ 3 5 iz e _ t pyx_tenr>: f o r ( pvx_tem D=0; pvx_teniD < p v x _ t_ l ; pvx_temD++) f Pyx_INCREF ( p y x _ in t_ 0 ) ; PyList_SET_ITE>i { p y x _ t_ 2 , pyx_tem p, p y x _ in t_ 0 ) ; Pyx_GIVEREF ( p y x _ in t_ 0 ) ;
}
} p y x _ v _ o u tp u t = (( F y O b j e c t* ) p y *_L _2 * 0 ;
+04: 105: +06: +07:
+06: +09:
+10: +11: +12:
p y x _ t_ 2 ) ;
for i in range(len(zs)): n -0 z — za[i] r.
m
rjt [ i ]
while n < maxiter and abs(z) < 2: z= z * z + c
n += 1 output[i] = n return output
R y s u n e k 7 .3 . K o d C u k r y t y w w ie r s z u k o d u P y t h o n
142
|
Rozdział 7. Kompilowanie do postaci kodu C
Aby popraw ić czas w ykonyw ania funkcji, konieczne jest rozpoczęcie deklarow ania typów obiektów, które są uwzględniane w pętlach wewnętrznych generujących duże obciążenie. Dzięki temu pętle te mogą tworzyć mniej dość kosztownych wywołań kierowanych do maszyny wir tualnej języka Python. W ten sposób oszczędza się czas. Ogólnie rzecz biorąc, do w ierszy kodu, które praw dopodobnie zajm ują najwięcej czasu pro cesora, zaliczają się następujące: • wiersze znajdujące się w intensywnych pętlach wewnętrznych, • wiersze usuw ające odwołania do elementów obiektów li s t , array lub np.array, • wiersze w ykonujące operacje m atematyczne.
^
Je śli n ie w ie sz , ja k ie w ie rsz e są n a jcz ęściej w y k o n y w a n e , w y k o rz y s ta j n a rz ę d z ie do p ro filo w a n ia l in e _ p r o f ile r , k tó re o m ó w io n o w p o d ro z d z ia le „ U ży cie n a rz ęd z ia lin e _ p r o f ile r d o p o m i a r ó w d o t y c z ą c y c h k o le jn y c h w ie r s z y k o d u " z r o z d z ia łu 2. D o w i e s z się, j a k i e w i e r s z e s ą n a jc z ę ś c i e j w y k o n y w a n e , a t a k ż e k t ó r e z n i c h p o w o d u j ą n a jw ię k s z e o b cią ż e n ie w e w n ą tr z m a s z y n y w irtu aln ej ję z y k a P y th o n . D z ię k i te m u u z y s k a s z w y r a ź n y d o w ó d n a to , j a k i e w i e r s z e w y m a g a j ą u w a g i w c e l u o s i ą g n i ę c i a n a jlep sz e g o p rz y ro stu szybkości.
Dodawanie adnotacji typu Na rysunku 7.2 pokazano, że praw ie każdy w iersz funkcji jest w yw oływ any w m aszynie w irtualnej języka Python. W szystkie obliczenia num eryczne rów nież są w yw oływ ane w tej m aszynie, poniew aż używ ane są obiekty języka Python w yższego poziom u. Konieczne jest przekształcenie tych obiektów w lokalne obiekty języka C, a następnie, po przeprowadzeniu kodowania num erycznego, przekształcenie wyniku z powrotem w obiekt języka Python. W przykładzie 7.6 widoczny jest sposób dodawania typów podstawowych za pomocą składni słowa kluczowego cdef. P r z y k ł a d 7 .6 . D o d a w a n i e t y p ó w p o d s t a w o w y c h j ę z y k a C w c e lu r o z p o c z ę c ia p r z y s p i e s z a n i a d z ia ła n ia s k o m p i l o w a n e j f u n k c j i . W t y m c e lu u ż y w a n y j e s t w w ię k s z y m s t o p n i u j ę z y k C , a w m n i e js z y m k o d w y k o r z y s t u j ą c y m a s z y n ę w ir t u a ln ą j ę z y k a P y t h o n def c a l c u l a t e _ z ( i n t m a x ite r , z s , c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... cde f unsigned i n t i , n cde f double complex z, c output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while n < m axite r and ab s( z ) < 2: z = z * z + c n += 1 o u t p u t [i ] = n re tu rn output
^
G o d n e u w a g i j e s t to , ż e t a k i e t y p y b ę d ą z r o z u m i a ł e d l a k o m p i l a t o r a C y t h o n , l e c z n ie d la in terp retera ję z y k a P y th o n . K o m p ila to r C y th o n u ż y w a ty ch ty p ó w d o p r z e k sz ta łc a n ia k o d u P y th o n w o b ie k ty ję z y k a C, k tó re n ie m u s z ą b y ć w y w o ły w a n e w s to s ie ję z y k a P y th o n . O z n a c z a to, ż e o p e r a c je są s z y b s z e , a le to w a r z y s z y te m u utrata elasty czn o ści i sz y b k o ści tw o rz en ia kodu.
Cython
| 143
Dodawane są następujące typy: • int dla liczby całkowitej ze znakiem, • unsigned int dla liczby całkowitej, która może być tylko dodatnia, • double complex dla liczb zespolonych podwójnej precyzji. Słowo kluczow e cdef um ożliw ia zadeklarow anie zm iennych w ew nątrz zaw artości funkcji. Musi ono być deklarowane na początku funkcji, ponieważ jest to wymóg specyfikacji języka C.
^
P o d c z a s d o d a w a n ia ad n o tacji k o m p ilato ra C y th o n d o d ajesz k o d in n y n iż k o d P y th o n d o p l i k u .p y x . O z n a c z a to , ż e r e z y g n u j e s z z i n t e r a k t y w n o ś c i t w o r z e n i a k o d u P y t h o n w in t e r p r e t e r z e . Z m y ś l ą o o s o b a c h z a z n a j o m i o n y c h z p i s a n i e m k o d u w j ę z y k u C p o ^
w racam y d o cyklu tw orzen ie k od u - k o m p ilo w an ie - u ru ch am ian ie - d e bu gow an ie.
M ożesz zastanaw iać się, czy m ożliw e jest dodanie adnotacji typu do przekazyw anych list. Choć używ ane jest słow o kluczow e l i s t , w om aw ianym przykładzie nie m a to praktycznie żadnego znaczenia. Obiekty l i s t nadal muszą być sprawdzane na poziomie interpretera języka Python w celu wyodrębnienia ich zawartości. Jest to bardzo wolna operacja. Przypisyw anie typów niektórym podstaw ow ym obiektom odzw ierciedlane jest w danych wyjściowych w idocznych na rysunku 7.4. Co ważne, w iersze 11. i 12., czyli dwa z najczęściej wywoływanych wierszy kodu, zmieniły teraz kolor z żółtego na biały. W skazuje to, że nie są one już w yw oływ ane w m aszynie w irtualnej języka Python. W porów naniu z poprzednim przykładem można spodziewać się znacznego wzrostu szybkości. Wiersz 10. jest wywoływany ponad 30 milionów razy, dlatego nadal warto się na nim koncentrować. G e n e r a te d by C ython 0 . 2 1 . 1 Raw o u t p u t : c y t h o n f n .c +01: def c a lc u la te _ z ( in t m axiter, zs, cs) 02: """O b lic z a n ie l i s t y output za pomocą reg u ły a k t u a liz a c ji zbioru J u l i i " " " 03: cd ef unsigned i n t i , n 04: cd ef double complex z, c +05: output = [0] * le n (z s ) +06: f o r i in ran g e( l e n ( z s ) ) : +07: n = 0 +08: z = z s [i] +09: c * c s [i] +10: w hile n < m axiter and ab s(z) < 2: +11: z= z * z + c +12: n += 1 +13: outpu t[ i ] = n +14: re tu rn output
R y s u n e k 7 .4 . P i e r w s z e a d n o t a c j e t y p u
Po skompilowaniu zakończenie wykonywania tej wersji kodu zajmuje 4,3 sekundy. Po wpro wadzeniu zaledwie kilku zmian w funkcji uzyskujemy szybkość dwukrotnie większą niż w przy padku oryginalnego kodu Python. Godne uwagi jest to, że w zrost szybkości wynika z tego, że więcej często wykonywanych ope racji kierowanych jest do poziomu kodu C (w tym przypadku są to aktualizacje do wartości zmiennych z i n). Oznacza to, że kompilator kodu C może optymalizować sposób przetw a rzania przez funkcje niskiego poziomu bajtów, które reprezentują te zmienne, bez wyw oły wania funkcji w stosunkowo wolnej m aszynie wirtualnej języka Python.
144
|
Rozdział 7. Kompilowanie do postaci kodu C
Na rysunku 7.4 widać, że pętla while nadal w pew nym stopniu generuje koszty (ma kolor żółty). Kosztow ne w yw ołanie w m aszynie w irtualnej języka Python dotyczy funkcji abs na potrzeby liczby zespolonej z. Kom pilator Cython nie zapew nia w budow anej funkcji abs dla liczb zespolonych. Zam iast niej można udostępnić własne lokalne rozszerzenie. Jak wspom niano w cześniej w rozdziale, użycie funkcji abs dla liczby zespolonej uwzględnia obliczenie pierwiastka kwadratowego sumy kwadratów składowych rzeczywistych i urojo nych. W teście pożądane jest sprawdzenie, czy pierwiastek kwadratowy wyniku jest mniejszy niż 2. Zamiast wyznaczania pierwiastka kwadratowego można obliczyć kwadrat drugiej strony porównania. Oznacza to, że < 2 zostanie przekształcone w < 4. Dzięki temu eliminuje się ko nieczność obliczania pierwiastka kwadratowego jako ostatniej części funkcji abs. Rozpoczęto od postaci: ■Jc.real2 + c.im ag 2 < V 4 Operację uproszczono do następującej postaci: c.real2 + c.im ag2 < 4 Jeśli w poniższym kodzie zostałaby zachowana operacja sqrt, w dalszym ciągu byłby zauwa żalny w zrost szybkości wykonywania. Jednym z sekretów optymalizowania kodu jest spra wienie, aby realizował jak najmniej działań. Dzięki usunięciu stosunkowo kosztownej operacji po zastanowieniu się nad ostatecznym celem funkcji kompilator kodu C będzie m ógł w yko nać to, z czym sobie dobrze radzi, zam iast próbow ać „odgadnąć", jakiego efektu końcowego oczekuje programista. Tw orzenie równoważnego, lecz bardziej wyspecjalizow anego kodu do rozwiązania tego sa m ego problem u, jest określane m ianem redukowania m ocy (ang. strength reduction). Kosztem mniejszej elastyczności (i być m oże czytelności) zyskuje się krótszy czas w ykonywania. To m atematyczne rozwinięcie prowadzi do przykładu 7.7, w którym dość kosztowna funkcja abs została zastąpiona uproszczonym wierszem rozszerzonych działań m atematycznych. Przykład 7.7. Rozwijanie funkcji abs za pomocą kompilatora Cython def c a l c u l a t e _ z ( i n t m a x ite r , z s , c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... cde f unsigned i n t i , n cde f double complex z, c output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while n < m axite r and ( z . r e a l * z . r e a l + z.imag * z.imag) < 4: z = z * z + c n += 1 o u t p u t [i ] = n re tu rn output
T w orzenie adnotacji dla kodu pozw ala nieznacznie popraw ić w yd ajność instrukcji while w w ierszu 10. (rysunek 7.5). Obecnie instrukcja obejmuje mniej w ywołań w wirtualnej m a szynie języka Python. Choć skala wzrostu szybkości, jaki zostanie uzyskany, nie jest od razu oczywista, wiadomo, że w iersz ten jest wywoływany ponad 30 m ilionów razy. Oznacza to, że przewidywane jest odpowiednie zwiększenie wydajności.
Cython
| 145
G en erated by Cython 0 .2 1 .1 Raw o u tp u t: c y th o n f n .c +01: def ca lc u la te _ z (in t m axiter, zs, cs) 02: " " "Obliczanie l i s t y output za pomocą reguły a k tu a liz a c ji zbioru J u l i i " '”' 03: cdef unsigned in t i , n 04: cder double ccrp iex z, c +05: output » [0] * len(zs) +ut>: ror i m rangę (le n ( z s )) : +07: n = 0 +08: z = z s [i] +09: c * c s [i] +10: while n < m axiter and (z .r e a l * z .r e a l + z.imag * z . imag) < 4: +11: z* z * z + c +12: n +- 1 +13: o utpu t[i] = n +14: return output
R y s u n e k 7.5 . R o z s z e r z o n e d z ia ła n ia m a t e m a t y c z n e p o z w a la ją c e n a o s t a t e c z n ą w y g r a n ą w p r o c e s ie o p ty m a liz a c ji
Ta zmiana ma diametralne znaczenie. Przez zmniejszenie liczby w yw ołań w najbardziej w e w nętrznej pętli znacząco skracany jest czas obliczeniow y funkcji. Czas w ykonania nowej wersji kodu wynosi zaledwie 0,25 sekundy, co oznacza niesamowite 40-krotne przyspieszenie w porównaniu z oryginalną wersją kodu.
^
K o m p ila to r C y th o n o b słu g u je kilka m e to d k o m p ilo w a n ia d o po sta ci k o d u C. N ie k t ó r e z n i c h s ą p r o s t s z e o d o p i s a n e j t u ta j m e t o d y t w o r z e n i a p e ł n e j a d n o t a c j i t y p u . A b y u ła tw ić so b ie ro z p o cz ęcie k o rzy stan ia z k o m p ila to ra C y th o n , n a le ż y za z n a jo m ić si ę z t r y b e m c z y s t e g o k o d u P y t h o n , a p o n a d t o p r z y j r z e ć s i ę n a r z ę d z i u pyximport, które u ła tw ia za p re z e n to w a n ie te g o k o m p ila to ra w s p ó łp ra c o w n ik o m .
Aby dla omawianej porcji kodu uzyskać dodatkowy możliwy wzrost wydajności, możesz wy łączyć sprawdzanie ograniczeń dla każdego zastąpienia odwołania na liście. Celem spraw dzania ograniczeń jest zapewnienie, że program nie będzie korzystał z danych poza obrębem przydzielonej tablicy. W przypadku kodu C z łatwością m ożna przypadkowo uzyskać dostęp do pamięci poza granicami tablicy, co spow oduje nieoczekiwane wyniki (i prawdopodobnie błąd segmentacji!). Dom yślnie kompilator Cython chroni program istę przed przypadkowym adresowaniem po za granicami listy. Choć taka ochrona w iąże się z niewielkim wykorzystaniem czasu proceso rowego, w ystępuje w zewnętrznej pętli funkcji. Z tego powodu sumarycznie nie pow oduje znacznego wydłużenia czasu wykonywania. Zw ykle bezpieczne jest wyłączenie sprawdzania ograniczeń, o ile nie przeprowadzasz w łasnych obliczeń zw iązanych z adresowaniem tablicy. W tym przypadku konieczne będzie zadbanie o to, aby nie zostały przekroczone granice listy. Kompilator Cython oferuje zestaw flag, które mogą być określane na różne sposoby. Najprostszy polega na dodaniu ich jako jednowierszowych komentarzy na początku pliku .pyx. Do zmiany tych ustawień możliwe jest też użycie dekoratora lub flagi czasu kompilowania. W celu wy łączenia sprawdzania granic dodajemy dyrektywę kompilatora Cython w obrębie komentarza na początku pliku .pyx. #cython: boundscheck=False def c a l c u l a t e _ z ( i n t m a x ite r , z s , c s ) :
146
|
Rozdzia ł 7. Kompilowanie do postaci kodu C
Jak widać, wyłączenie sprawdzania ograniczeń spow oduje tylko nieznaczne skrócenie czasu, ponieważ ma to miejsce w pętli zewnętrznej, a nie wewnętrznej, co jest kosztowniejsze. W przy padku omawianego przykładu nie zapewni to żadnego skrócenia czasu. S p r ó b u j w y ł ą c z y ć s p r a w d z a n i e o g r a n i c z e ń i p r z e p e ł n i e n i a , je ś l i k o d p o w i ą z a n y z p r o c e s o r e m z n a j d u j e si ę w pętli, k t ó r a c z ę s t o z a s t ę p u j e o d w o ł a n i a d l a e l e m e n t ó w .
i
Shed Skin Shed Skin (http://code.google.com/p/shedskin/) to eksperymentalny kompilator Python-C++, który w spółdziała z językiem Python w w ersjach 2.4 - 2.7. Kom pilator używa inferencji typów do automatycznego sprawdzenia programu Python w celu tworzenia adnotacji typów stosowanych dla każdej zmiennej. Taki kod z adnotacjami jest następnie przekształcany w kod C, aby można go było skom pilow ać za pom ocą standardowego kompilatora (np. g++). Autom atyczna introspekcja to bardzo interesująca funkcja kompilatora Shed Skin. Użytkownik musi jedynie za pewnić przykład prezentujący sposób w ywołania funkcji z wykorzystaniem właściwego ro dzaju danych, a kompilator sam określi resztę. Zaletą inferencji typów jest to, że programista nie musi jaw nie określać typów. Aby tak było, analizator musi mieć m ożliw ość zidentyfikowania typów dla każdej zmiennej w programie. W bieżącej wersji kompilatora tysiące w ierszy kodu Python m ogą być automatycznie prze kształcane do postaci kodu C. Kompilator korzysta z narzędzia Boehma oczyszczającego pa m ięć, które um ożliw ia dynam iczne zarządzanie pam ięcią. N arzędzie to jest używ ane także w przypadku kom pilatorów M ono i GNU Com piler for Java. W adą kom pilatora Shed Skin jest to, że dla standardowych bibliotek stosuje zew nętrzne im plementacje. W szystko, co nie zostało zaim plementow ane (dotyczy to również narzędzia numpy), nie będzie obsługiwane. Projekt kom pilatora Shed Skin zaw iera ponad 75 przykładów , w tym w iele m odułów m a tem atycznych utw orzonych w czystym kodzie Python, a naw et w pełni działający em ulator Commodore 64. Każdy z przykładowych kodów działa znacznie szybciej po skompilowaniu za pom ocą kompilatora Shed Skin (nawet w porów naniu z uruchom ieniem w obrębie narzę dzia CPython). Kom pilator Shed Skin m oże tworzyć odrębne programy wykonywalne, które nie zależą od używanej instalacji interpretera języka Python lub modułów rozszerzeń wykorzystywanych wraz z instrukcją import w zwykłym kodzie Python. Skompilowane m oduły zarządzają w łasną pamięcią. Oznacza to, że pam ięć z procesu kodu Python jest kopiow ana, a w yniki są z pow rotem kopiow ane — nie w ystępuje żadne jaw ne w spółużytkow anie pam ięci. W przypadku dużych bloków pam ięci (np. dużej m acierzy) koszt w ykonyw ania operacji kopiow ania m oże być znaczny. Przyjrzym y się temu na końcu tego podrozdziału. Kompilator Shed Skin zapewnia podobny zestaw korzyści co kom pilator PyPy (więcej infor m acji zamieszczono w podrozdziale „PyPy"). Oznacza to, że kom pilator PyPy m oże być ła twiejszy w użyciu, ponieważ nie wymaga żadnych kroków kompilacji. Sposób automatycznego dodawania adnotacji typu przez kompilator Shed Skin może być interesujący dla niektórych użytkow ników . Jeśli ponadto zam ierzasz m odyfikow ać w ynikow y kod C, w ygenerow any
Shed Skin
|
147
kod C może być bardziej czytelny niż kod C utw orzony przez kompilator Cython. Podejrze wam y, że kod z automatyczną inferencją typów będzie szczególnie interesujący dla innych twórców kompilatorów w społeczności.
Tworzenie modułu rozszerzenia W przedstawionym tutaj przykładzie zostanie zbudowany m oduł rozszerzenia. Za pomocą instrukcji import można zaimportować wygenerowany moduł tak, jak to miało miejsce w przy padku przykładów dotyczących kompilatora Cython. M oduł ten m oże też zostać skompilo wany w postaci odrębnego programu wykonywalnego. W przykładzie 7.8 zamieszczono kod w osobnym module. Zaw iera on zwykły kod Python, dla którego w żaden sposób nie są tworzone adnotacje typu. Zauważ również, że dodano test main , który pow oduje, że m oduł ten ma niezależną postać na potrzeby analizy typów. Kompilator Shed Skin może użyć tego bloku main , który zapewnia przykładowe argu menty, do identyfikacji typów przekazyw anych do funkcji calculate_z, a także do określenia typów wykorzystyw anych wew nątrz funkcji powiązanej z procesorem. Przykład 7.8. Przenoszenie funkcji powiązanej z procesorem do osobnego modułu (jak w przy padku kompilatora Cython) w celu umożliw ienia działania systemu automatycznej inferencji typów kompilatora Shed Skin # shedskinfn.py def c a l c u l a t e _ z ( m a x i t e r , z s , c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while n < m axite r and abs( z) < 2: z = z * z + c n += 1 o u t p u t [i ] = n r e tu r n output if name == 11 main ” : # Tworzenie tryw ialnego przykładu za p o m o c ą popraw nych typów w celu umożliwienia # w yw ołania fu n kcji przez inferencję typów, aby kom pilator S h ed Skin m ógł analizow ać typy output = c a l c u l a t e _ z ( 1 , [ 0 j ] , [ 0 j ] )
M oduł ten można zaim portow ać w zwykły sposób (przykład 7.9) zarówno przed skompilo waniem go, jak i po kompilacji. Ponieważ kod nie jest modyfikowany (inaczej niż w przypadku kompilatora Cython), przed kompilacją możliwe jest wywołanie oryginalnego modułu Python. Jeśli kod nie zostanie skompilowany, nie uzyska się przyrostu szybkości, ale możliwe będzie przeprowadzenie debugowania w uproszczony sposób za pomocą zwykłych narzędzi powią zanych z językiem Python. P r z y k ł a d 7 .9 . I m p o r t o w a n i e m o d u łu z e w n ę t r z n e g o w c e lu u m o ż liw ie n ia k o m p i l a t o r o w i S h e d S k in s k o m p ilo w a n ia t y lk o t e g o m o d u łu import shedskinfn def calc_pu re_python(desired_width, m a x _ i t e r a t i o n s ) : #... s t a r t _ t i m e = t i m e . ti m e ()
148
|
Rozdział 7. Kompilowanie do postaci kodu C
output = s h e d s k i n f n . c a l c u l a t e _ z ( m a x _ i t e r a t i o n s , z s , cs) end_time = t i m e . ti m e () se c s = end_time - s t a r t _ t i m e p r i n t "Czas t r w a n i a : " , s e c s , "s"
Jak zaprezentowano w przykładzie 7.10, m ożliw e jest sprawienie, by kompilator Shed Skin udostępnił dane wyjściowe z adnotacją związane z jego analizą. Umożliwia to polecenie shedskin -ann shedskinfn.py, które generuje plik shedskinfn.ss.py. W przypadku kompilowania modułu rozszerzenia konieczne jest jedynie zainicjowanie analizy za pomocą fikcyjnej fun kcji main . Przykład 7.10. Sprawdzanie danych wyjściowych z adnotacją kompilatora Shed Skin w celu stwierdzenia, jakie typy zostały przez niego zidentyfikowane # s h e d s k i n f n . s s .p y c a l c u l a t e z (m a x i te r , z s , c s ) :
II
CO
N
N
# m axiter: [ int], # zs: [list(com plex)], # cs: [list(com plex)] " " " O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru Ju lii output = [0] * l e n ( z s ) # [list(int)] f o r i in r a n g e ( l e n ( z s ) ) : # [__iter(int)] n = 0 # [int] # [com plex] c = cs[i] # [com plex] while n < m ax ite r and a b s ( i) < 2: # [com plex] z = z * z + c # [com plex] n += 1 # [int] o u t p u t [i ] = n # [int] re tu rn output # [list(int)] name == 11 main ” : # O # Tworzenie tryw ialnego przykładu za p o m o c ą popraw nych typów w celu umożliwienia # w yw ołania fu n kcji przez inferencję typów, aby kom pilator S hed Skin m ógł analizow ać typy output = c a l c u l a t e _ z ( 1 , [ 0 j ] , [ 0 j ] ) # [list(int)]
Po przeanalizowaniu ty p ó w main wewnątrz funkcji calculate_z m ogą być identyfikowane zmienne, takie jak z i c, na podstaw ie obiektów, z którymi prow adzą one interakcję. M oduł jest kompilowany przy użyciu polecenia shedskin --extmod shedskinfn.py. Generowane są następujące pliki: • shedskinfn.hpp (plik nagłów kow y C++), • shedskinfn.cpp (plik źródłowy C++), • M akefile. Uruchomienie programu make pow oduje w ygenerowanie pliku shedskinfn.so. Instrukcja import shedskinfn pozwala użyć tego pliku w kodzie Python. Czas w ykonyw ania skryptu ju lia t.p y z wykorzystaniem pliku shedskinfn.so wynosi 0,4 sekundy. Jest to ogromna poprawa wydajności w porównaniu z wersją bez kompilacji, która wymagała bardzo niewielkiego nakładu pracy. Tak jak w przypadku kompilatora Cython w przykładzie 7.7, możliwe jest również rozwinięcie funkcji abs. Po uruchomieniu tej wersji kodu (ze zmodyfikowanym tylko jednym wierszem funk cji abs) i użyciu kilku dodatkow ych flag (--nobounds --nowrap) ostatecznie uzyskujem y czas wykonywania w ynoszący 0,3 sekundy. Choć jest to czas trochę dłuższy (o 0,05 sekundy) niż w przypadku wersji z kompilatorem Cython, nie było konieczne podawanie wszystkich informacji o typach. Oznacza to, że eksperym entow anie z wykorzystaniem kompilatora Shed Skin jest bardzo łatwe. Kompilator PyPy uruchamia tę samą w ersję kodu z podobną szybkością.
Shed Skin
| 149
^
T o , ż e w p r z y p a d k u o m a w ia n e g o p r z y k ła d u k o m p ila to r y C y th o n , P y P y i S h e d S k in k o rzystają z p o d o b n y c h ś ro d o w isk w y k o n a w c z y c h , n ie o zn acza, ż e u z y s k a n y w y n ik m o ż e z o s ta ć u o g ó ln io n y . A b y w r e a liz o w a n y m p ro je k c ie o s ią g n ą ć n a jle p sz e czasy w y k o n y w a n ia , trzeb a sp ra w d z ić ró żn e n a rz ęd z ia i p rz e p ro w a d z ić w łasn e ek sperym enty.
Kom pilator Shed Skin um ożliw ia określenie dodatkow ych flag dotyczących kom pilacji, ta kich jak -ffast-math lub -O3. W dwóch krokach (w pierwszym gromadzone są statystyki doty czące w ykonyw ania, a w drugim w ygenerow any kod je st optym alizow any na podstaw ie uzyskanych statystyk) można dodać optymalizację PGO (Profile-Guided Optimization) w celu podjęcia próby osiągnięcia dodatkowego wzrostu szybkości. Optymalizacja PGO nie spowo dowała jednak przyspieszenia wykonywania kodu dla przykładu zbioru Julii. W praktyce optymalizacja ta często zapewnia niewielki rzeczywisty w zrost wydajności lub żaden. N ależy zauw ażyć, że dom yślnie liczby całkow ite są 32-bitow e. Jeśli w ym agane są w iększe zakresy z 64-bitow ym i liczbam i całkow itym i, podaj flagę --long. N ależy też unikać p rzy dzielania m ałych obiektów (np. now ych krotek) w obrębie pętli w ew nętrznych, poniew aż proces czyszczenia pamięci nie obsługuje ich tak efektywnie, jak można byłoby oczekiwać.
Koszt związany z kopiami pamięci W p rzykładzie kom pilator Shed Skin kopiuje do sw ojego środow iska obiekty l i s t języka Python, upraszczając dane do postaci podstaw ow ych typów języka C. Kom pilator prze kształca następnie w ynik funkcji języka C na końcu jej w ykonyw ania z pow rotem w obiekt l i s t języka Python. Takie przekształcenia i kopiowania zajm ują czas. Czy m oże to oznaczać brakujący czas w ynoszący 0,05 sekundy, o którym w spom niano przy okazji poprzedniego wyniku? Aby określić jedynie koszt zw iązany z kopiowaniem danych do/z funkcji za pośrednictwem kompilatora Shed Skin, można zmodyfikować plik shedskinfn.py w celu usunięcia kodu odpo wiedzialnego za realizowanie rzeczywistych operacji. Następujący w ariant funkcji calculate_z to w łaśnie to, co jest potrzebne: def c a l c u l a t e _ z ( m a x i t e r , z s , c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... output = [0] * l e n ( z s ) re tu rn output
W przypadku w ykonyw ania skryptu ju l i a 1 . py za pom ocą tej funkcji szkieletow ej czas wy nosi w przybliżeniu 0,05 sekundy (oczywiście skrypt nie oblicza popraw nego wyniku!). Czas ten stanowi koszt kopiowania 2 milionów liczb zespolonych do funkcji calculate_z oraz po now nego kopiow ania z niej m iliona liczb całkow itych. Zasadniczo kom pilatory Shed Skin i C ython generują ten sam kod m aszynow y. R óżnica w szybkości w ykonyw ania w ynika z tego, że kom pilator Shed Skin działa w niezależnym obszarze pam ięci, oraz z obciążenia związanego z koniecznością kopiowania danych. Z drugiej strony w przypadku kompilatora Shed Skin nie ma potrzeby tw orzenia na początku adnotacji, co zapew nia dość znaczne oszczędności czasu.
150
|
Rozdział 7. Kompilowanie do postaci kodu C
Cython i numpy Obiekty listy (więcej informacji zam ieszczono w rozdziale 3.) pow odują obciążenie w przy padku każdej operacji zastępowania odwołania, ponieważ przyw oływ ane przez nie obiekty m ogą zn ajd ow ać się w dow olnym m iejscu w pam ięci. D la po ró w n an ia, obiekty tablicy przechow ują typy podstaw ow e w ciągłych blokach pam ięci RAM , co pozw ala na szybsze adresowanie. Język Python oferuje m oduł array, który zapewnia jednow ym iarowe przechow yw anie typów podstawowych (w tym liczb całkowitych, liczb zmiennoprzecinkowych i łańcuchów Unicode). M oduł numpy.array narzędzia numpy umożliwia wielow ym iarow e przechow yw anie oraz ofe ruje szerszą gamę typów podstawowych, w tym liczby zespolone. W przypadku iterowania obiektu array w sposób możliwy do przewidzenia kom pilator może zostać poinstruow any w celu uniknięcia żądania od interpretera języka Python, by obliczył odpow iedni adres. Zam iast tego interpreter m oże zająć się następnym elem entem podsta wowym w sekwencji, co polega na bezpośrednim przejściu do jego adresu pamięci. Ponieważ dane są rozm ieszczone w ciągłym bloku, tryw ialnym zadaniem jest obliczenie za pom ocą przesunięcia adresu następnego elementu kodu C. Dzięki temu nie ma potrzeby instruowa nia narzędzia CPython, aby obliczyło taki sam wynik, co wiązałoby się z użyciem w olnego w ywołania w obrębie maszyny wirtualnej. N ależy zauważyć, że jeśli zostanie uruchom iona wersja kodu narzędzia numpy bez żadnych adnotacji kom pilatora Cython (czyli kod po prostu zostanie w ykonany jako zw ykły skrypt Python), zajm ie to około 71 sekund. Jest to zdecydow anie gorszy w ynik niż dla w ersji kodu ze zwykłym obiektem l i s t języka Python, którego wykonanie zajęło około 11 sekund. Spowol nienie jest spowodowane obciążeniem wynikającym z zastępowania odwołań dla poszcze gólnych elem entów list narzędzia numpy. N ie zostało przew idziane używ anie tych list w ten sposób, naw et mimo tego, że dla początkujących program istów może się to w ydać intuicyjną m etodą obsługi operacji. Kom pilow anie kodu eliminuje to obciążenie. W odniesieniu do tego kom pilator C ython oferuje dw ie specjalne postaci składni. Starsze w ersje kom pilatora udostępniają specjalny typ dostępu dla tablic narzędzia numpy, a później za pośrednictw em interfejsu memoryview w prow adzono ogólny protokół interfejsu bufora. Zapew nia on ten sam niskopoziom ow y dostęp do dow olnego obiektu, który im plem entuje interfejs bufora, uwzględniając tablice narzędzia numpy i języka Python. Dodatkową korzyścią oferowaną przez interfejs bufora jest to, że umożliwia łatw e w spół użytkow anie bloków pam ięci z innym i bibliotekam i języka C bez potrzeby przekształcania ich z obiektów języka Python w inną postać. Blok kodu z przykładu 7.11 przypom ina trochę oryginalną implementację, z tym wyjątkiem, że zostały dodane adnotacje interfejsu memoryview. Drugi argument funkcji to double complex[:] zs. Oznacza to, że używ any jest obiekt liczb zespolonych o podw ójnej precyzji, który korzysta z protokołu bufora (określony za pom ocą znaków [] ) zaw ierającego jednow ym iarow y blok danych (określony przy użyciu dwukropka : ).
Cython i numpy
|
151
Przykład 7.11. Wersja kodu z adnotacjami narzędzia numpy dla funkcji obliczającej zbiór Julii # cython_np.pyx import numpy as np cimport numpy as np def c a l c u l a t e _ z ( i n t m a x ite r , double complex[:] z s , double comp lex[:] c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... cde f unsigned i n t i , n c d e f double complex z , c cdef i n t [ : ] output = n p .e m p t y (l e n ( z s), dtype =np.int32) f o r i in r a n g e ( l e n ( z s ) ) : n = 0 z = zs[i] c = cs[i] while n < m axite r and ( z . r e a l * z . r e a l + z.imag * z.imag) < 4: z = z * z + c n += 1 o u t p u t [i ] = n r e tu r n output
Oprócz podawania argumentów wejściowych przy użyciu składni adnotacji bufora tworzone są też adnotacje dla zmiennej output przez przypisanie jej obiektu tablicy jednowymiarowej array narzędzia numpy za pośrednictw em funkcji empty. W yw ołanie tej funkcji spow oduje przydzielenie bloku pam ięci, ale nie zainicjuje pam ięci przy użyciu rozsądnych w artości, dlatego może ona zaw ierać cokolwiek. Ponieważ zaw artość takiej tablicy zostanie nadpisana w pętli w ew nętrznej, nie będ zie konieczne ponow ne przypisyw anie tablicy przy użyciu w artości dom yślnej. Jest to trochę szybsze niż przydzielanie i ustaw ianie zaw artości tablicy za pom ocą wartości domyślnej. U żyw ając szybszej i bardziej jaw nej w ersji m atem atycznej, rozw inięto rów nież w yw ołanie funkcji abs. Czas działania tej wersji w ynosi 0,23 sekundy, czyli jest to w ynik nieznacznie lep szy niż w przypadku oryginalnej wersji kodu używającego kompilatora Cython, która bazuje na czystym kodzie Python z przykładu 7.7 zbioru Julii. Czysta wersja kodu pow oduje obcią żenie każdorazowo przy zastępowaniu odwołania dla obiektu complex kodu Python, ale ope racje te występują w pętli zewnętrznej, dlatego nie mają dużego udziału w czasie wykonywania. Po pętli zewnętrznej tworzone są macierzyste w ersje zmiennych, które działają z „szybkością kodu C ". Pętla wewnętrzna, zarówno w przypadku przykładowego kodu narzędzia numpy, jak i wcześniejszego przykładu czystego kodu Python, realizuje te same działania dla tych samych danych. Oznacza to, że różnica w czasie wykonywania wynika z operacji zastępowania odwo łań w pętli zewnętrznej oraz tworzenia tablic output.
Przetwarzanie równoległe rozwiązania na jednym komputerze z wykorzystaniem interfejsu OpenMP W ram ach ostatniego kroku rozw ijania om aw ianej w ersji kodu przyjrzyjm y się użyciu roz szerzeń języka C++ interfejsu O penM P do zastosow ania przetw arzania rów noległego dla trudnego, ale umożliwiającego takie rozwiązanie problemu. Jeśli problem, który rozpatrujesz, jest podobny, m ożesz szybko skorzystać z wielu rdzeni obecnych w komputerze. OpenM P (Open M ulti-Processing) to dobrze zdefiniowany interfejs API dla wielu platform , który obsługuje w ykonyw anie rów noległe i w spółużytkow anie pam ięci dla kodu utw orzo nego przy użyciu języków C, C++ i Fortran. Interfejs ten w budow any jest w w iększość no w oczesnych kom pilatorów kodu C. Jeśli kod C zostanie w łaściw e napisany, przetw arzanie rów noległe w ystępuje na poziom ie kom pilatora. Oznacza to stosunkow o niew ielki nakład pracy dla programisty, który korzysta z kompilatora Cython.
152
|
Rozdział 7. Kompilowanie do postaci kodu C
W przypadku tego kompilatora interfejs OpenM P m oże zostać dodany za pomocą operatora prange (parallel range) i przez dodanie do skryptu setup.py dyrektyw y kom pilatora -fopenmp. Działania w obrębie pętli tego operatora m ogą być w ykonyw ane równolegle, ponieważ wy łączana jest blokada GIL (Global Interpreter Lock). Przykład 7.12 prezentuje zm odyfikowaną w ersję kodu z obsługą operatora prange. Instrukcja with nogil: określa blok, w którym w yłączana jest blokada GIL. W ew nątrz tego bloku ope rator prange umożliwia pętli for przetwarzania równoległego interfejsu OpenM P niezależne obliczenie każdej wartości zmiennej i . Przykład 7.12. Dodawanie operatora prange w celu zastosowania przetwarzania równoległego za pomocą interfejsu OpenMP # cython_np.pyx from c y th o n .p a r a l l e l import prange import numpy as np cimport numpy as np def c a l c u l a t e _ z ( i n t m a x ite r , double comp lex[:] z s , double complex[:] c s ) : O bliczanie listy output za p o m o c ą reguły aktualizacji zbioru J u lii..... c def unsigned i n t i , length c def double complex z , c c def i n t [ :] output = n p .e m p t y (l e n ( z s), dtype =np.int32) length = l e n ( z s ) with n o g i l : f o r i in pran ge (l en gth , sche du le= "g uided"): z = zs[i] c = cs[i] o u t p u t [i ] = 0 while o u tp u t[i ] < m axite r and ( z . r e a l * z . r e a l + z.imag * z.imag) < 4: z = z * z + c ou t p u t [i ] += 1 re tu rn output
^
P od czas w y łączan ia b lo kad y G IL
nie
m o ż n a p rz e tw a rz a ć zw y k ły c h o b iektów ję zy k a
P y t h o n (n p . list). K o n i e c z n e j e s t p r z e t w a r z a n i e w y ł ą c z n i e o b i e k t ó w p o d s t a w o w y c h i o b i e k t ó w , k t ó r e o b s ł u g u j ą i n t e r f e j s memoryview. W p r z y p a d k u p r z e t w a r z a n i a r ó w n o ległego z w y k ły c h o b iektó w ję z y k a P y th o n w y m a g a n e b y ło b y ro zw iązan ie p ro b le m ó w to w a rz y s z ą cy ch z a rząd zan iu p am ięcią, któ rych b lo k a d a G IL celo w o unika. K o m p i l a t o r C y t h o n n i e z a p o b i e g a m o d y f i k o w a n i u o b i e k t ó w j ę z y k a P y t h o n . J e ś li s a m to zro b isz , s p o w o d u je to ty lk o p r o b le m y i z am iesz a n ie!
Aby skompilować plik cython_np.pyx, konieczne jest zmodyfikowanie skryptu setup.py w sposób pokazany w przykładzie 7.13. Po modyfikacji skrypt instruuje kompilator kodu C o użyciu flagi -fopenmp jako argumentu podczas kompilacji w celu w łączenia interfejsu OpenM P i po łączenia z jego bibliotekami. Przykład 7.13. Dodawanie do skryptu setup.py flag kompilatora i programu konsolidującego interfejsu OpenMP dla kompilatora Cython #setup.py from d i s t u t i l s . c o r e import setup from d i s t u t i l s . e x t e n s i o n import Extension from C y t h o n . D i s t u t i l s import build_ext setup( cmdclass = { ' b u i l d _ e x t ' : b u i l d _ e x t } , ext_modules = [ E x t e n s i o n ( " c a l c u l a t e " , ["cy thon _n p.p yx "] , e xt ra _ c o m p ile _ a rg s= ['- fo p e n m p '], e x t r a _ l in k _ a rg s = [ '- fo p e n m p '] )] )
Cython i numpy
| 153
O perator prange kom pilatora C ython um ożliw ia w ybranie różnych m etod szeregow ania. W przypadku opcji s ta tic obciążenie jest równom iernie rozkładane m iędzy dostępne proce sory. Część obliczeń wymaga więcej czasu, a część nie. Jeśli kompilator Cython zostanie po instruow any, aby rów nom iernie szeregow ać porcje zadań m iędzy procesoram i przy użyciu opcji s ta tic , w yniki dla części obliczeń zostaną uzyskane szybciej niż dla innych. Szybsze wątki przejdą następnie w stan bezczynności. Dzięki opcjom szeregow ania dynamic i guided m ożna zm niejszyć skalę tego problem u przez dynamiczne przydzielanie zadań w mniejszych porcjach podczas w ykonyw ania kodu. Dzięki temu w przypadku zmiennego czasu obliczeń obciążenie jest równom iernie rozkładane mię dzy procesoram i. W łaściw y w ybór dla utw orzonego kodu będzie zm ieniał się w zależności od natury obciążenia. Zastosowanie interfejsu OpenM P i opcji schedule="guided" pozwala skrócić czas wykonywania w przybliżeniu do 0,07 sekundy. Szeregowanie guided spowoduje dynamiczne przydzielanie zadań, dzięki czemu mniej wątków będzie oczekiwać na now e zadania. Używając instrukcji #cython: boundscheck=False, można też wyłączyć dla omawianego przykładu sprawdzanie ograniczeń, ale nie spowodowałoby to skrócenia czasu wykonywania.
Numba Narzędzie Numba (http://numba.pydata.org/) firmy Continuum Analytics to kompilator JIT spe cjalizujący się w kodzie narzędzia numpy, który dokonuje kompilacji tego kodu w czasie wy konywania za pośrednictwem kompilatora LLVM (a nie, tak jak w e wcześniej prezentowanych przykładach, za pom ocą kompilatora g++ lub gcc). Numba nie wym aga kroku prekompilacji, dlatego po uruchom ieniu dla now ego kodu kom piluje każdą funkcję z adnotacją, która jest wymagana przez używ any sprzęt. Zaletą jest to, że kompilatorowi udostępniany jest deko rator, który informuje go o tym, jakim i funkcjami ma się zająć, po czym Numba zaczyna re alizow ać sw oje zadania. Kom pilator Num ba przeznaczony jest do stosow ania dla każdego standardowego kodu narzędzia numpy. Numba to krócej istniejący projekt (w książce użyto wersji 0.13), a zw iązany z nim interfejs API m oże się nieznacznie zm ieniać z każdą wersją. Z tego pow odu na chwilę obecną należy traktować go jako bardziej przydatny w środowisku badawczym. Jeśli korzystasz z tablic na rzędzia numpy i kodu bez wektoryzacji, który dokonuje iteracji dla wielu elementów, kompi lator Numba powinien umożliwić Ci uzyskanie szybkiego efektu optymalizacji bez większego nakładu pracy. M ankam entem zw iązanym z użyciem kom pilatora N um ba jest łańcuch narzędzi. Korzysta on z kompilatora LLVM , a ponadto ma wiele zależności. Zalecam y zastosow anie dystrybucji Continuum Anaconda, ponieważ zapewnia wszystkie składniki. W przeciwnym razie instalo wanie kompilatora Numba w nowym środowisku może być bardzo czasochłonnym zadaniem. Przykład 7.14 prezentuje dodanie dekoratora @ jit do podstawowej funkcji zbioru Julii. Nie jest wymagane nic w ięcej. To, że kompilator numba został zaim portow any, oznacza, że m echa nizmy kompilatora LLVM zostaną uruchomione w czasie wykonywania w celu skompilowania tej funkcji w tle.
154
|
Rozdział 7. Kompilowanie do postaci kodu C
Przykład 7.14. Zastosowanie dekoratora @jit dla funkcji from numba import j i t @ jit() def c a lcu la t e _ z _ s e r i a l _ p u r e p y t h o n (m a x i te r , z s , c s , output):
Po usunięciu dekoratora @ jit będzie to jedynie wersja kodu narzędzia numpy z demonstracją zbioru Julii obsługiwaną przez interpreter języka Python 2.7. W ykonanie takiego kodu zajmie 71 sekund. Dodanie tego dekoratora powoduje skrócenie czasu wykonywania do 0,3 sekundy. Jest to czas bardzo zbliżony do wyniku osiągniętego w przypadku kompilatora Cython, lecz bez całego nakładu pracy związanego z tworzeniem adnotacji. Jeśli ta sama funkcja zostanie uruchom iona drugi raz w tej samej sesji interpretera języka Python, zadziała jeszcze szybciej. Nie ma potrzeby kom pilowania funkcji docelowej w dru gim przejściu, jeśli jednakow e są typy argumentów. W efekcie ogólny czas wykonywania jest krótszy. W przypadku drugiego uruchomienia w ynik kompilatora Numba odpowiada w cze śniej uzyskanemu w ynikow i zastosowania kompilatora Cython z narzędziem numpy (a zatem przy znikomym nakładzie pracy kompilator Num ba okazał się równie szybki jak Cython!). Kompilator PyPy ma takie same wymagania zw iązane z uruchamianiem. W przypadku debugowania za pom ocą kompilatora Numba warto zauważyć, że m ożna go poinstruow ać w celu pokazania typu zmiennej, którą się zajm uje w obrębie skompilowanej funkcji. W przykładzie 7.15 widać, że zmienna zs jest rozpoznawana przez kompilator JIT jako tablica liczb zespolonych. Przykład 7.15. Debugowanie identyfikowanych typów print("Zmienna zs ma t y p : " , numba.typeof(zs)) array(complex128, 1d, C))
Kompilator Numba obsługuje też inne formy introspekcji, takie jak inspect_types, która umoż liwia przegląd skompilowanego kodu w celu stwierdzenia, gdzie zostały zidentyfikowane infor macje o typach. W przypadku braku typów możliwe jest doprecyzowanie, jak wyrażono funkcję, aby ułatwić kompilatorowi Numba określenie większej liczby możliwości inferencji typów. Płatna wersja kompilatora Numba, czyli NumbaPro (http://docs.continuum.io/numbapro/), oferuje eksperymentalną obsługę operatora przetwarzania równoległego prange z wykorzystaniem in terfejsu OpenMP. Dostępna jest również eksperymentalna obsługa układów GPU. Projekt ten ma na celu uproszczenie przekształcania wolniejszego kodu Python z pętlami bazującego na narzędziu numpy w bardzo szybki kod, który może być wykonywany w procesorze lub układzie GPU. Kompilator NumbaPro jest w art uwagi.
Pythran Pythran (http://pythonhosted.org/pythran/) to kompilator Python-C++ przeznaczony dla pod zbioru instrukcji języka Python, który oferuje częściową obsługę narzędzia numpy. Kompilator ten działa trochę podobnie do kompilatorów Numba i Cython. Po utworzeniu przez programi stę adnotacji argum entów funkcji kompilator Pythran zajm uje się dalej dodatkowymi adnota cjami typu i specjalizacją kodu. W ykorzystuje możliwości związane z wektoryzacją i prze tw arzaniem rów noległym opartym na interfejsie OpenM P. Działa w yłącznie w przypadku języka Python 2.7.
Pythran
|
155
Bardzo interesującą funkcją kom pilatora Pythran jest to, że próbuje autom atycznie w ykryć możliwości przetwarzania równoległego (np. w sytuacji, gdy używ asz instrukcji map) i prze kształcić kod w kod przetwarzania równoległego bez konieczności wykonywania przez pro gramistę dodatkowych działań. Możliwe jest też określenie przy użyciu dyrektyw pragma omp sekcji przetwarzania równoległego. Pod tym względem sposób działania kompilatora Pythran bardzo przypomina obsługę interfejsu OpenMP przez kompilator Cython. W tle kom pilator Pythran pobiera zarów no zw ykły kod Python, jak i kod narzędzia numpy, a następnie próbuje agresywnie kom pilow ać je do postaci bardzo szybkiego kodu C++, który zapew nia w yniki jeszcze lepsze od uzyskanych dla kom pilatora Cython. N ależy zauw ażyć, że projekt ten jest stosunkowo nowy i mogą w nim występować błędy. Godne uwagi jest także to, że zespół programistów jest bardzo przyjaźnie nastawiony i zwykle usuwa zgłoszone błędy w ciągu kilku godzin. Ponownie przyjrzyj się równaniu dyfuzji z przykładu 6.9. Część obliczeniową funkcji w yod rębniono do osobnego modułu, aby mogła zostać skompilowana do postaci biblioteki binarnej. Przydatną funkcją kompilatora Pythran jest to, że przy użyciu tego kompilatora nie jest two rzony kod niezgodny z językiem Python. Przypomnij sobie, że w przypadku kompilatora Cython konieczne było tworzenie plików .pyx z dołączonym kodem Python, który nie mógł być bez pośrednio uruchom iony przez interpreter języka Python. W przypadku kompilatora Pythran dodawane są jednow ierszow e komentarze, które m ogą zostać przez niego wykryte. Oznacza to, że jeśli zostanie usunięty generowany moduł kompilowany .so, można po prostu uruchomić kod przy użyciu interpretera języka Python. Jest to znakom ita m ożliw ość w odniesieniu do debugowania. W przykładzie 7.16 zaprezentow ano równanie przewodnictwa cieplnego. Funkcja evolve za wiera jednow ierszow y komentarz, który dołącza dla niej inform acje o typach (ponieważ jest to kom entarz, u ruchom ienie kodu bez kom pilatora Pythran spraw i, że in terp reter języka Python po prostu go zignoruje). Po załadowaniu kompilator Pythran w ykryje ten komentarz i dokona propagacji informacji o typach (bardzo podobnie jak w przypadku narzędzia Shed Skin) w każdej powiązanej funkcji. Przykład 7.16. Dodawanie jednowierszowego komentarza w celu dołączenia punktu wejścia do funkcji evolve() import numpy as np def l a p l a c i a n ( g r i d ) : r e tu r n n p . r o l l ( g r i d , +1, 0) + n p . r o l l ( g r i d , - 1 , 0) + n p . r o l l ( g r i d , +1, 1) + n p . r o l l ( g r i d , - 1 , 1) - 4 * grid #pythran export ev olve(float64[][], flo a t) def e v o l v e ( g r i d , d t , D=1): r e tu r n gr id + dt * D * l a p l a c i a n ( g r i d )
M oduł ten można skom pilow ać za pomocą polecenia pythran diffusion_numpy.py, które zwróci plik diffusion_num py.so. Z poziom u funkcji testow ej m ożna zaim portow ać ten now y m oduł i w yw ołać funkcję evolve. Na laptopie jednego z autorów bez zainstalow anego kom pilatora Pythran czas wykonywania tej funkcji dla siatki 8192x8192 wyniósł 3,8 sekundy. W przypadku kompilatora Pythran czas zmniejszył się do 1,5 sekundy. Oczywiście jeśli kompilator Pythran obsługuje wymagane funkcje, może zapewnić naprawdę imponujące wzrosty wydajności przy bardzo niewielkim nakładzie pracy.
156
|
Rozdział 7. Kompilowanie do postaci kodu C
Przyczyną przyspieszenia jest to, że kompilator Pythran używa własnej wersji funkcji roll , która cechuje się mniejszą funkcjonalnością. Oznacza to, że przeprowadza kom pilację do po staci mniej złożonego kodu, który może działać szybciej. Ponadto kod ten jest mniej elastycz ny niż kod narzędzia numpy (twórcy kompilatora Pythran zw racają uwagę na to, że im ple m entuje on tylko niektóre elementy narzędzia numpy). Pod względem wyników Pythran może jednak prześcignąć inne w cześniej omówione narzędzia. Zastosujm y teraz tę sam ą m etodę dla przykładu rozszerzonych operacji m atem atycznych w przypadku zbioru Julii. Sam o dodanie jednow ierszow ej adnotacji do funkcji calculate_z pow oduje skrócenie czasu wykonywania do 0,29 sekundy, czyli wyniku trochę gorszego niż w przypadku w yniku kom pilatora Cython. D odanie jednow ierszow ej deklaracji interfejsu OpenM P na początku pętli zewnętrznej pow oduje skrócenie czasu wykonywania do 0,1 se kundy. Czas ten nie odbiega zbytnio od najlepszego wyniku uzyskanego dla interfejsu OpenMP kompilatora Cython. Kod z adnotacją został zaprezentowany w przykładzie 7.17. Przykład 7.17. Dodawanie adnotacji do funkcji calculate_z dla kompilatora Pythran z obsługą interfejsu OpenMP #pythran export calculate_z(int, com plex[], com plex[], int[]) def c a l c u l a t e _ z ( m a x i t e r , z s , c s , o utp ut ): #omp p a ra lle l f o r schedule(guided) f o r i in r a n g e ( l e n ( z s ) ) :
Przedstawione dotychczas technologie uwzględniają użycie kompilatora, który towarzyszy zw ykłem u interpreterow i CPython. Przyjrzym y się teraz narzędziu PyPy, które oferuje cał kowicie now y interpreter.
PyPy PyPy (http://pypy.org/) to alternatywna im plementacja języka Python, która obejm uje śledzący kompilator JIT. Implementacja jest zgodna z językiem Python 2.7. Dostępna jest też ekspery m entalna wersja dla języka Python 3.2. Docelow o narzędzie PyPy zastępuje narzędzie CPython, oferując w szystkie w budow ane m oduły. Projekt składa się z łańcucha narzędziowego RPython Translation Toolchain, który służy do budow ania interpretera PyPy (i m oże zostać w ykorzystany do tw orzenia innych interp reterów ). K om pilator JIT w interp reterze PyPy jest bardzo w ydajny. O dpow iednie przyspieszenia można zauważyć przy niewielkim lub żadnym nakładzie pracy programisty. W podrozdziale „Interpreter PyPy zapew niający pow odzenie system ów przetw arzania da nych i systemów internetow ych" z rozdziału 12 . opisano historię dużego wdrożenia zakoń czonego sukcesem, które bazowało na interpreterze PyPy. Interpreter PyPy urucham ia bez żadnych m odyfikacji prezentację zbioru Julii bazującą na czystym kodzie Python. W przypadku narzędzi CPython i PyPy czas wykonywania tego ko du w ynosi odpow iednio 11 sekund i 0,3 sekundy. Oznacza to, że interpreter PyPy osiąga w ynik bardzo zbliżony do wyniku uzyskanego dla przykładowego kodu, dla którego zasto sowano kompilator Cython (przykład 7.7). Nie jest z tym związany zupełnie żaden nakład pracy, co napraw dę robi wrażenie! Jak zaobserwowano przy okazji omawiania kompilatora Numba, jeśli obliczenia są przeprowadzane ponow nie w tej samej sesji, drugie i kolejne uruchomienia są szybsze od pierwszego, ponieważ w ich przypadku kompilacja została już wykonana.
PyPy
|
157
Interesujące jest to, że interpreter PyPy obsługuje wszystkie w budowane moduły. Oznacza to, że m oduł multiprocessing działa tak jak w przypadku narzędzia CPython. Jeśli zajmujesz się problemem wykorzystującym m oduły z dołączonymi bibliotekami, który może być przetw a rzany rów nolegle za pom ocą m odułu multiprocessing, oczekuj, że dostępne będą w szystkie przyrosty szybkości, jakich m ożesz się spodziewać. Szybkość interpretera PyPy zw iększała się z czasem. W ykres na rysunku 7.6 uzyskany z ser w isu speed.pypy.org pozwala zorientow ać się w dojrzałości interpretera. Pokazane testy szyb kości reprezentują szeroki zestaw przypadków użycia, a nie tylko operacje m atem atyczne. Oczywiste jest to, że interpreter PyPy oferuje w iększą w ydajność niż narzędzie CPython.
6.94
5.78
4.63
3.47
2.31
1.16
0.00 CPython 2 .7yP y 1.3PyPy 1.4PyPy 1.5PyPy 1.6PyPy 1.7PyPy 1.8PyPy U V > y 2 .0 SftyPy 2.1 PyPy 2.PyPy tnink
Rysunek 7.6. Każda nowa wersja interpretera PyPy oferuje zwiększenie szybkości
Różnice związane z czyszczeniem pamięci Interpreter PyPy korzysta z procesu czyszczenia pamięci innego typu niż narzędzie CPython. Może to spowodować w kodzie pewne nieoczywiste zmiany w działaniu. Narzędzie CPython używa zliczenia odwołań, interpreter PyPy natom iast korzysta ze zmodyfikowanej m etody zaznaczania i usuwania, która może znacznie później oczyścić nieużywany obiekt. Oba wa rianty są popraw nym i im plementacjami specyfikacji języka Python. Trzeba tylko być świa domym tego, że niektóre modyfikacje kodu mogą być niezbędne podczas czyszczenia. Niektóre m etody tworzenia kodu omawiane w odniesieniu do narzędzia CPython zależą do działania licznika odwołań. Dotyczy to zwłaszcza opróżniania plików bez ich jawnego zamknię cia, gdy wykonywane są dla nich operacje otwierania i zapisywania. W przypadku interpretera PyPy ten sam kod zostanie uruchomiony, ale aktualizacje pliku m ogą zostać w późniejszym czasie um ieszczone na dysku przy następnym uruchom ieniu procesu czyszczenia pam ięci. Alternatywną formą, która sprawdza się zarówno dla interpretera PyPy, jak i interpretera ję zyka Python, jest użycie menedżera kontekstu, korzystającego z instrukcji with do otwierania i automatycznego zamykania plików. Na stronie Differences between PyPy and CPython (różnice między interpreterem PyPy i narzędziem CPython) witryny internetowej interpretera PyPy podano odpowiednie szczegóły (http://pypy.readthedocs.org/en/latest/cpython_differences.html).
158
|
Rozdział 7. Kompilowanie do postaci kodu C
Uruchamianie interpretera PyPy i instalowanie modułów Jeśli nigdy nie urucham iałeś alternatywnego interpretera języka Python, powinno Ci pomóc zaznajom ienie się z krótkim przykładem. Zakładając, że pobrałeś i rozpakowałeś interpreter PyPy, zobaczysz strukturę folderów zawierającą katalog bin. W celu uruchomienia interpretera PyPy użyj polecenia z przykładu 7.18. Przykład 7.18. Uruchamianie interpretera PyPy w celu stwierdzenia, że implementuje język Python 2.7.3 $ ./bin/pypy Python 2 . 7 . 3 (8 4 e fb 3b a05f1 , Feb 18 201 4, 2 3 : 0 0 : 2 1 ) [PyPy 2 . 3 . 0 - a l p h a 0 with GCC 4 . 6 . 3 ] on linux2 Type " h e l p " , "c o p y r i g h t " , " c r e d i t s " or " l i c e n s e " f o r more information. And now f o r something completely d i f f e r e n t : ' ' < a r i g a t o > (not t h r e a d - s a f e , but w e l l , nothing i s ) ' '
Zauważ, że interpreter PyPy 2.3 działa jako interpreter języka Python 2.7.3. Konieczne jest te raz skonfigurowanie narzędzia pip. Pożądane będzie zainstalow anie narzędzia ipython (zwróć uwagę, że narzędzie IPython jest urucham iane z tą samą instalacją interpretera języka Python 2.7.3, o której w cześniej w spomniano). Kroki przedstaw ione w przykładzie 7.19 są takie same jak te, które zostałyby w ykonane w przypadku narzędzia CPython, gdyby zainstalowano na rzędzie pip bez korzystania z istniejącej dystrybucji lub m enedżera pakietów. Przykład 7.19. Instalowanie narzędzia pip dla interpretera PyPy w celu zainstalowania modułów zewnętrznych, takich jak IPython $ mkdir sources # tworzenie lokaln ego katalogu p o b iera n ia $ cd sources # p o b iera n ie dystrybucji i narzędzia p ip $ curl -O h t t p : / / p y t h o n - d i s t r i b u t e .o r g / d i s t r i b u t e _ s e t u p .p y $ curl -O h ttp s :// ra w .g i th u b .c o m / p y p a /p ip / m a s te r /c o n tr ib / g e t- p ip .p y # urucham ianie za p o m o c ą p o lec e n ia pypy plików instalacyjnych dla pobran ych plików $ . ./ b in /p y p y . / d i s t r i b u t e _ s e t u p . p y $
. ./ b in /p y p y ge t -p i p .p y
$
. . / b i n / p i p i n s t a l l ipython
$ ../b i n /i p y t h o n Python 2 . 7 . 3 (8 4e fb 3b a05f1 , Feb 18 201 4, 2 3 : 0 0 : 2 1 ) Type "c o p y r i g h t " , " c r e d i t s " or " l i c e n s e " f o r more inf ormation . IPython 2 .0 .0 -A n enhanced I n t e r a c t i v e Python. ? -> In tro d uction and overview o f IPython's features. %quickref -> Quick r e f e r e n c e . help -> Python's own help system. o b ject? -> D e t a i l s about ' o b j e c t ' , use 'o b je c t ? ? ' fo r extra d e ta ils .
Zauważ, że interpreter PyPy nie obsługuje projektów takich jak narzędzie numpy w przydatny sposób (istnieje w arstw a pom ostow a udostępniana przez narzędzie cpyext (https://bitbucket. org/pypy/com patibility/w iki/c-api), ale jest ono zbyt w olne, aby m ogło okazać się w artościow e w przypadku narzędzia numpy). Z tego powodu nie należy oczekiwać dużego w sparcia na rzędzia numpy ze strony interpretera PyPy. O feruje on eksperym entalny port narzędzia numpy o nazw ie num pypy (instrukcje instalacji są dostępne na blogu jednego z autorów książki http:// ianozsvald.com/2014/01/14/installing-the-numpy-module-in-pypy/), który jednak nie zapewnia na razie żadnych godnych uw agi w zrostów szybkości1.[BS1] 1 M o ż e się to z m i e n i ć d o k o ń c a r o k u 2 0 1 4 (w ięcej i n f o rm a c ji p o d a d r e s e m h ttp ://bit.ly /n u m p y p y ).
PyPy
I
159
Jeśli w ym agasz innych pakietów , w szystko, co ma postać czystego kodu Python, praw dopo dobnie zostanie zainstalow ane, a to, co bazuje na bibliotekach rozszerzeń języka C, raczej nie będzie działać w przydatny sposób. Interpreter PyPy nie zawiera procesu czyszczącego pa m ięć ze zliczaniem odw ołań. W szystko, co zostanie skom pilow ane dla narzędzia CPython, będzie korzystać z w yw ołań biblioteki, które obsługują proces czyszczący pam ięć narzędzia CPython. Interpreter PyPy zapewnia obejście tego problemu, ale w iąże się ono ze znacznym dodatkowym obciążeniem. W praktyce podejmowanie próby wymuszania współpracy starszych bibliotek rozszerzeń bezpośrednio z interpreterem PyPy nie ma żadnej wartości. W przypadku tego interpretera należy w miarę możliwości spróbować usunąć wszelki kod rozszerzenia C (taki kod m oże istnieć tylko w celu przyspieszenia kodu Python, za co obecnie ma być od powiedzialny interpreter PyPy). Na stronie wiki dla interpretera PyPy utrzym ywana jest lista zgodnych modułów (https://bitbucket.org/pypy/compatibility/wiki/Home). Inną w adą interpretera PyPy jest to, że m oże zużyw ać w iele pam ięci RAM . W tym zakresie każda kolejna wersja interpretera jest lepsza, ale w praktyce może on używ ać więcej pamięci RA M niż narzędzie CPython. Poniew aż jednak pam ięć RA M jest dosyć tania, sensow ne jest podjęcie próby pośw ięcenia jej dla zw iększonej w ydajności. N iektórzy użytkow nicy zgłosili również mniejsze zużycie pamięci RA M podczas korzystania z interpretera PyPy. Jak zawsze, jeśli jest to ważna kwestia, przeprowadź eksperyment przy użyciu reprezentatywnych danych. Choć interpreter PyPy jest powiązany z blokadą GIL (Global Interpreter Lock), zespół programi stów realizuje projekt o nazwie STM (Software Transactional M emory), który ma na celu podjęcie próby usunięcia wymogu stosowania blokady GIL. Projekt STM przypomina trochę transakcje bazy danych. Jest to mechanizm kontroli współbieżności stosowany dla operacji dostępu do pamięci. Mechanizm ten może wycofać zmiany, jeśli w tym samym obszarze pamięci wystąpią operacje pow odujące konflikt. Celem integracji projektu STM jest um ożliw ienie system om 0 w ysokim stopniu w spółbieżności dysponow ania pew ną form ą kontroli w spółbieżności. Będzie się to wiązać z utratą części efektywności w przypadku operacji, ale w zamian poprawi się wydajność programistów, którzy nie będą zmuszeni do zajmowania się wszystkimi aspek tami kontroli jednoczesnego dostępu. Na potrzeby profilow ania polecane narzędzia to jitview er (https://bitbucket.org/pypy/jitview er) 1 logparser (http://m orepypy.blogspot.co.uk/2009/11/hi-all-this-w eek-i-w orked-on-im proving.htm l).
Kiedy stosować poszczególne technologie? Jeśli realizujesz projekt o charakterze numerycznym, użycie każdej z opisanych technologii może okazać się przydatne. W tabeli 7.1 podsumowano główne opcje. Tabela 7.1. Podsumowanie opcji kompilatorów Cython
Shed Skin
Dojrzałość
T
T
Rozpowszechnienie
T
Obsługa narzędzia numpy
T
Zmiany w kodzie niepowodujące rozdzielania
T
Wymóg znajomości języka C
T
Obsługa interfejsu OpenMP
T
160
|
Rozdział 7. Kompilowanie do postaci kodu C
Numba
Pythran
PyPy T
T
T
T
T
T
T
T
T
Jeśli problem mieści się w ograniczonym zakresie obsługiwanych funkcji, kompilator Pythran oferuje praw dopodobnie największe przyrosty szybkości w przypadku problem ów rozwią zyw anych za pom ocą narzędzia numpy przy najm niejszym nakładzie pracy. Kom pilator ten zapewnia też kilka prostych w użyciu opcji przetwarzania równoległego interfejsu OpenMP. Ponadto jest stosunkowo nowym projektem. Kom pilator Numba m oże oferować szybkie przyrosty szybkości przy niewielkim nakładzie pracy, ale ma zbyt w iele ograniczeń, które m ogą spraw ić, że nie będzie dobrze działać dla napisanego kodu. Kom pilator ten także jest stosunkowo nowym projektem . Kom pilator Cython oferuje praw dopodobnie najlepsze wyniki w przypadku najszerszej gru py problemów, ale wymaga większego nakładu pracy, a ponadto ponieważ korzysta z kom binacji kodu Python i adnotacji kodu C, jego obsługa jest utrudniona. Kompilator PyPy stanowi m ocną propozycję, jeśli ma zostać przeprowadzona kompilacja do kodu C, a ponadto nie jest używane narzędzie numpy ani żadna inna biblioteka zewnętrzna. Shed Skin może okazać się przydatny, gdy kod ma zostać skom pilowany do postaci kodu C, a ponadto nie używasz narzędzia numpy lub innych bibliotek zewnętrznych. Jeśli wdrażasz narzędzie produkcyjne, praw dopodobnie pożądane będzie pozostanie przy dobrze znanych narzędziach. Kompilator Cython powinien być podstaw ow ą opcją wyboru. M ożesz przeczytać treść podrozdziału „Technika głębokiego uczenia prezentow ana przez firm ę R adim R ehurek.com " z rozdziału 12. W konfiguracjach produkcyjnych używ a się też kom pilatora PyPy (więcej inform acji zaw iera podrozdział „Interpreter PyPy zapew niający pow odzenie systemów przetwarzania danych i systemów internetow ych" z rozdziału 12 .). Jeśli masz do czynienia z niewielkimi wymaganiami num erycznym i, zauważ, że interfejs bu foru kompilatora Cython akceptuje macierze array.array. Jest to prosta metoda przekazyw a nia bloku danych kompilatorowi Cython w celu przeprow adzenia szybkiego przetwarzania num erycznego bez konieczności dodawania narzędzia numpy jako zależności projektu. Generalnie rzecz biorąc, kompilatory Pythran i Numba to dość nowe projekty, ale bardzo obiecujące. Z kolei kompilator Cython jest bardzo dojrzały. Kom pilator PyPy jest uważany za dość dojrzały i zdecydow anie pow inien być w ykorzystyw any w przypadku długotrw ałych procesów. Na zajęciach prowadzonych w 2014 r. przez jednego z autorów zdolny student zaim plemen tował w ersję kodu C algorytmu zbioru Julii. Był rozczarowany, gdy stwierdził, że kod wyko nyw any był wolniej niż wersja skompilowana za pom ocą kom pilatora Cython. Okazało się, że student użył 32-bitowych liczb zmiennoprzecinkowych dla komputera 64-bitowego (takie liczby 32-bitow e są w olniej przetw arzane na kom puterze 64-bitow ym niż 64-bitow e liczby o podwójnej precyzji). Pomimo tego, że student był dobrym programistą używającym języka C, nie wiedział, że coś takiego mogło spow odow ać zm niejszenie szybkości. Po zmodyfikowaniu kodu okazało się, że wersja kodu bazująca na kompilatorze C, choć znacznie krótsza od wersji automatycznie w ygenerowanej za pomocą kompilatora Cython, działała w przybliżeniu z tą samą szybkością. Pisanie czystego kodu C, porównywanie jego szybkości i określanie sposobu jego modyfikacji zajęło więcej czasu niż użycie od razu kompilatora Cython. Jest to jedynie anegdota. N ie sugerujem y, że kom pilator Cython w ygeneruje najlepszy kod. Kom petentni program iści tw orzący kod w języku C m ogą praw dopodobnie stw ierdzić, jak sprawić, że ich kod będzie działać szybciej od wersji wygenerowanej przez kompilator Cython.
Kiedy stosować poszczególne technologie?
| 161
G odne uw agi jest jednak to, że nie będzie bezpieczne przyjęcie, że ręcznie napisany kod C będzie szybszy od przekształconego kodu Python. Zaw sze konieczne jest w ykonyw anie te stów porów naw czych i podejm ow anie decyzji na podstaw ie uzyskanego dowodu. Kom pi latory języka C napraw dę dobrze radzą sobie z przekształcaniem kodu w dość w ydajny kod maszynowy. Z kolei język Python sprawdza się napraw dę nieźle w roli języka pozw alającego na opisanie problemu w zrozum iały sposób. Z głową połącz ze sobą te dwie m ocne strony.
Inne przyszłe projekty Na stronie kom pilatorów PyData (http://compilers.pydata.org/) znajduje się lista kom pilatorów i narzędzi o dużej wydajności. Theano (http://deeplearning.net/software/theano/) to język wysokiego poziomu, który umożliwia w yrażanie operatorów m atematycznych w tablicach wielow ym ia rowych. Język ten jest silnie zintegrowany z narzędziem numpy, a ponadto może eksportować kod skom pilow any dla procesorów i układów G PU . Co interesujące, okazał się przydatny członkom społeczności zajmującej się sztuczną inteligencją z wykorzystaniem techniki głębo kiego uczenia. Kompilator Parakeet (http://www.parakeetpython.com/) koncentruje się na kompi lowaniu operacji obejmujących tablice narzędzia numpy o dużej gęstości, które korzystają z pod zbioru instrukcji języka Python. Obsługuje też układy GPU. PyViennaCL (http://viennacl.sourceforge.net/pyviennacl.html) to pow iązanie języka Python z bi blioteką algebry liniowej i obliczeń numerycznych ViennaCL. Obsługuje procesory i układy GPU z wykorzystaniem narzędzia numpy. Biblioteka ViennaCL napisana w języku C++ generuje kod dla interfejsów CUDA, O penC L i O penM P. O bsługuje ona operacje gęstej i rzadkiej algebry liniowej, bibliotekę BLAS i moduły rozwiązujące. Nuitka (http://nuitka.net/pages/overview.html) to kompilator kodu Python, który ma być alter natywą dla zwykłego interpretera CPython, oferując opcję tworzenia skompilowanych plików w ykonyw alnych. W pełni obsługuje język Python 2.7, choć w naszych testach nie zapew nił żadnych zauważalnych przyrostów szybkości w przypadku prostych testów num erycznych kodu Python. Pyston (https://github.com/dropbox/pyston) to najnowsza branżowa technologia. Korzysta z kom pilatora LLVM i jest obsługiwana przez interfejs Dropbox. Z powodu braku obsługi modułów rozszerzenia kompilatora Pyston może dotyczyć ten sam problem co kompilatora PyPy, ale w ramach projektu planow ane jest podjęcie próby rozwiązania go. W przeciwnym razie mało praw dopodobne jest, że obsługa narzędzia numpy będzie praktycznym rozwiązaniem. Społeczność programistów nie może raczej narzekać na niedobór opcji kompilacji. Choć wszyst kie w ym agają kompromisów, oferują też mnóstwo m ożliw ości, dzięki czemu w złożonych projektach może być wykorzystana pełna moc procesorów i architektur w ielordzeniowych.
Uwaga dotycząca układów GPU Układy graficzne G PU (Graphics Processing U nit) to obecnie modna technologia. Zdecydowa liśmy się jednak nie omawiać jej co najmniej do następnego wydania książki. Wynika to stąd, że w branży zachodzą szybkie zmiany, a ponadto całkiem praw dopodobne jest to, że wszystko, co zawarliśmy w tej książce, ulegnie zmianie, gdy będziesz w trakcie jej czytania. A na poważ nie, nie chodzi o to, że zm iany mogą w ym agać w iersze utw orzonego kodu, ale o to, że wraz z rozwojem architektur konieczna może okazać się znacząca zmiana sposobu, w jaki będziesz rozwiązywać problemy.
162
|
Rozdział 7. Kompilowanie do postaci kodu C
Jeden z autorów zajm ow ał się problem em z fizyki, korzystając przez rok z układu GPU N VIDIA GTX 480 oraz języka Python i środowiska PyCUDA. Po upływie roku została w yko rzystana pełna m oc układu GPU i system działał 25 razy szybciej niż ta sam a funkcja na kom puterze z procesorem 4-rdzeniow ym . W ariant kodu dla tego procesora został napisany w języku C przy użyciu biblioteki przetwarzania równoległego. Z kolei wariant kodu dla ukła du GPU był tworzony głównie w języku C architektury CUDA opakowanym w środowisku PyCUDA w celu obsługi danych. Niedługo później pojawiły się w sprzedaży układy GPU z serii GTX 5xx. W efekcie zm ianie uległo wiele optymalizacji dotyczących serii 4xx. Efekty prawie rocznej pracy ostatecznie zostały zaprzepaszczone na rzecz łatwiejszego do utrzymania roz w iązania w postaci kodu C, który był w ykonyw any z wykorzystaniem procesorów. Choć jest to pojedynczy przykład, zwraca uwagę na zagrożenie wynikające z tworzenia niskopoziomowego kodu dla architektury CUDA (lub OpenCL). Biblioteki bazujące na układach GPU i oferujące funkcje w yższego poziomu ze znacznie większym prawdopodobieństwem będą mogły nadawać się do ogólnego zastosowania (np. biblioteki, które zapewniają interfejsy do analizy obrazu lub transkodowania wideo). Zachęcamy do rozważenia tych kwestii przed sprawdzeniem opcji tworzenia kodu bezpośrednio dla układów GPU. Projekty, których celem jest autom atyczne zarządzanie układam i GPU, obejm ują narzędzia Numba, Parakeet i Theano.
Oczekiwania dotyczące przyszłego projektu kompilatora W śród obecnych opcji kompilatorów dostępnych jest kilka komponentów technologii o dużych możliwościach. Osobiście życzyłbym sobie uogólnienia m echanizm u tworzenia adnotacji kompilatora Shed Skin, aby mógł w spółpracow ać z innymi narzędziami (na przykład gene rując dane w yjściow e zgodne z kom pilatorem Cython w celu w ygładzenia krzyw ej uczenia podczas rozpoczynania korzystania z tego kompilatora, a zw łaszcza w przypadku używania narzędzia numpy). Kompilator Cython jest dojrzały i integruje się silnie z językiem Python i na rzędziem numpy. Jeśli krzywa uczenia oraz wymagania dotyczące obsługi nie byłyby tak bardzo zniechęcające, więcej osób zastosowałoby ten kompilator. W dłuższej perspektyw ie czasowej życzeniem byłoby pojawienie się rozwiązania przypom i nającego kompilatory Numba i PyPy, które oferuje działanie w stylu kompilatora JIT zarów no w przypadku zw ykłego kodu Python, jak i kodu narzędzia numpy. O becnie nie zapew nia tego żadne narzędzie. Narzędzie rozwiązujące ten problem byłoby mocnym kandydatem do zastąpienia zwykłego interpretera CPython, który aktualnie jest używany przez wszystkich, bez konieczności modyfikowania kodu przez projektantów. Przyjazna rywalizacja i duży rynek otwarty na nowe pom ysły sprawiają, że nasz ekosystem staje się napraw dę w artościowym miejscem.
Interfejsy funkcji zewnętrznych Czasem zautom atyzowane rozwiązania po prostu nie są odpowiednie, dlatego sam musisz napisać niestandardow y kod w języku C lub Fortran. M oże to w ynikać z tego, że m etody kompilacji nie znajdują pewnych potencjalnych optymalizacji lub w ym agane jest wykorzy stanie funkcji bibliotek albo języka, które są niedostępne w języku Python. W e w szystkich ta kich przypadkach niezbędne będzie zastosow anie interfejsów funkcji zew nętrznych, które zapewniają dostęp do kodu pisanego i kompilowanego przy użyciu innego języka.
Interfejsy funkcji zewnętrznych
|
163
W pozostałej części rozdziału podejmiemy próbę użycia zewnętrznej biblioteki do rozwiąza nia równania dyfuzji dw uwym iarowej w taki sam sposób jak w rozdziale 62. Przykład 7.20 prezentuje kod tej biblioteki, który może reprezentować zainstalow aną bibliotekę lub być ko dem utworzonym własnoręcznie. M etody, którym się przyjrzym y, znakomicie nadają się do pobrania niewielkich części kodu i przemieszczenia ich do innego języka w celu przeprow a dzenia specjalistycznych optymalizacji bazujących na języku. Przykład 7.20. Przykładowy kod C służący do rozwiązywania problemu dyfuzji dwuwymiarowej void evolve(double i n [ ] [ 5 1 2 ] , double o u t [ ] [ 5 1 2 ] , double D, double dt) { int i , j ; double l a p l a c i a n ; f o r (i = 1; i < 5 11; i++) { f o r ( j = 1; j< 5 1 1 ; j++) { l a p l a c i a n = i n [ i +1 ] [ j ] + i n [ i - 1 ] [ j ] + i n [ i ] [ j + 1 ] + i n [ i ] [ j - 1 ] \ - 4 * in [i][j]; o u t [ i ] [ j ] = i n [ i ] [ j ] + D * dt * l a p l a c i a n ; } } }
Aby użyć tego kodu, konieczne jest skompilowanie go do postaci współużytkow anego mo dułu, który tw orzy plik .so. W tym celu zostanie zastosow any kom pilator gcc (lub dowolny inny kompilator języka C) przez wykonanie następujących poleceń: $ gcc -O3 -std=gnu99 - c d i f f u s i o n .c $ gcc -shared -o d i f f u s io n .s o d i ff u s io n .o
Ostatni plik biblioteki współużytkow anej można umieścić w dowolnym miejscu dostępnym dla kodu Python, ale w przypadku standardowej organizacji plików w systemach uniksowych takie biblioteki są przechowywane w katalogach /usr/lib i /usr/local/lib.
ctypes W przypadku narzędzia CPython 3 najbardziej podstaw ow y interfejs funkcji zewnętrznej jest dostępny za pośrednictw em m odułu ctypes. Generalnie rzecz biorąc, m oduł ten m oże być czasami dość ograniczający. Odpowiadasz za zrealizow anie każdego działania. Trochę czasu może zająć Ci upew nienie się, że w szystko jest w porządku. Taki dodatkowy poziom złożo ności jest w idoczny w kodzie dyfuzji z modułem ctypes (przykład 7.21). Przykład 7.21. Kod dyfuzji dwuwymiarowej z modułem ctypes import ctypes grid_shape = (51 2, 512) _ d i f f u s i o n = c t y p e s .C D L L ( " .. / d i f f u s i o n . s o " ) # O # Tworzenie odw ołań do typów jęz y k a C, które b ęd ą w ym agane do uproszczenia przyszłego kodu TYP E_INT = c t y p e s . c _ i n t TYPE_DOUBLE = c ty pes .c_d ou ble TYPE_DOUBLE_SS = ctypes.POINTER(ctypes.POINTER(ctypes.c_double)) # In icjow anie sygnatury fu n kcji evolve d o p o sta c i: # v o id evolve(int, int, d o u ble**, d o u ble**, double, double) _diffusion.evolve.arg typ es = [
2 D la u p r o s z c z e n ia n ie b ę d ą i m p l e m e n t o w a n e w a r u n k i b r z e g o w e . 3 W b a r d z o d u ż y m s t o p n i u je s t to z a l e ż n e o d n a r z ę d z i a C P y t h o n . I n n a w e r s j a j ę z y k a P y t h o n m o ż e z a w i e r a ć w ł a s n e w e r s j e m o d u ł u cty p es, k t ó r e m o g ą d z i a ł a ć b a r d z o ró ż n ie .
164
|
Rozdział 7. Kompilowanie do postaci kodu C
TYPE_INT, TYPE_INT, TYPE_DOUBLE_SS, TYPE_DOUBLE_SS, TYPE_DOUBLE, TYPE_DOUBLE, ] _ d i f f u s i o n . e v o l v e . r e s t y p e = None def e v o l v e ( g r i d , out, d t , D=1.0): # N ajpierw typy języ k a Python s ą p rzekształcane w odpow iednie typy języ k a C cX = TYPE_INT(grid_shape[0]) cY = TYPE_INT(grid_shape[1]) cdt = TYPE_DOUBLE(dt) cD = TYPE_DOUBLE(D) p o i n te r _ g ri d = grid.ctypes.data_as(TYPE_DOUBLE_SS) # 0 po in te r_out = out.ctypes.data_as(TYPE_DOUBLE_SS) # W tym m iejscu m ożliw e je s t w yw ołanie fu n kcji _ d i f f u s i o n . e v o l v e ( c X , cY, p o i n te r _ g r i d , p o i n te r _ o u t, cD, cdt ) # ©
O Przypomina to im portowanie biblioteki diffusion.so. 0
grid i out to tablice narzędzia numpy.
© Po całkowitym zakończeniu niezbędnej konfiguracji możliwe jest bezpośrednie wywołanie funkcji języka C. Pierwszą realizowaną operacją jest „im portowanie" biblioteki współużytkowanej. W tym celu używane jest wywołanie ctypes.CDLL. W tym wierszu może zostać określona dowolna biblioteka w spółużytkowana dostępna dla interpretera języka Python (na przykład m oduł ctypes-opencv ładuje bibliotekę lib cv .so ). W w yniku tego m ożna u zyskać obiekt _diffusion zaw ierający w szystkie elementy składowe znajdujące się w bibliotece w spółużytkowanej. W przykładzie biblioteka diffusion.so zawiera tylko jedną funkcję evolve, która nie jest właściwością obiektu. Jeśli biblioteka ta zawierałaby wiele funkcji i właściwości, do wszystkich dostęp byłby możliwy za pośrednictwem obiektu _diffusion. Jednakże naw et pom im o tego, że obiekt _diffusion zaw iera funkcję evolve dostępną w jego obrębie, nie ma informacji o tym, jak z niej skorzystać. W języku C typy są określane statycz nie, a funkcja ma bardzo specyficzną sygnaturę. Aby m ieć m ożliw ość popraw nego użycia funkcji evolve, konieczne jest jaw ne ustaw ienie typów argumentów w ejściow ych i typu zwra canej w artości. M oże to okazać się dość żm udne podczas projektow ania bib liotek łącznie z interfejsem języka Python lub w przypadku używ ania szybko zm ieniającej się biblioteki. Co więcej, ponieważ m oduł ctypes nie może sprawdzić, czy zostały m u podane poprawne typy, w przypadku popełnienia błędu bez w yśw ietlenia żad nych inform acji kod m oże przestać działać lub spow odow ać błąd segmentacji! Oprócz ustawienia argumentów i typu zwracanej w artości obiektu funkcji konieczne jest też p rzekształcenie w szelkich danych, które m ają zostać u żyte z obiektem (jest to nazyw ane „rzutowaniem"). Każdy argument wysyłany do funkcji musi zostać poddany uważnemu rzu towaniu do wbudowanego typu języka C. Czasem może to okazać się dość trudne, ponieważ in terp reter języka Python bardzo sw obodnie traktuje sw oje typy zm iennych. Na przykład w przypadku num1 = 1e5 konieczne byłoby stwierdzenie, czy jest to typ flo a t języka Python, co oznaczałoby, że należy użyć typu ctype.c_float. Z kolei dla num2 = 1e30 niezbędne byłoby zastosowanie typu ctype.c_double, ponieważ w przeciwnym razie wystąpiłby błąd przepeł nienia standardowego typu flo at języka C.
Interfejsy funkcji zewnętrznych
|
165
Narzędzie numpy oferuje swoim tablicom właściwość .ctypes, która ułatwia zapewnienie zgod ności z modułem ctypes. Jeśli narzędzie numpy nie udostępniałoby takiej funkcjonalności, ko nieczne byłoby zainicjow anie tablicy poprawnego typu m odułu ctypes, a następnie znalezie nie położenia oryginalnych danych i wskazanie na nie przez now y obiekt m odułu ctypes. J e ś l i o b i e k t , k t ó r y p r z e k s z t a ł c a s z w o b i e k t m o d u ł u c ty p e s , n i e i m p l e m e n t u j e b u f o r a ( p o d o b n i e d o m o d u ł u a rr a y , t a b l i c n a r z ę d z i a numpy, m o d u ł u c S t r i n g l O itp .), d a n e b ę d ą k o p io w a n e do n o w e g o obiektu. W p rz y p a d k u rzu to w a n ia ty p u in t d o ty p u f lo a t n ie m a to d u ż e g o w p ły w u n a w y d a jn o ś ć k o d u . Je śli je d n a k r z u to w a n a je st b a rd z o d ł u g a l i s ta w k o d z i e P y t h o n , m o ż e s i ę to w i ą z a ć z e z n a c z n y m o b c i ą ż e n i e m ! W t a k i c h p r z y p a d k a c h p o m o c n e m o ż e b y ć u ż y c i e m o d u ł u a r r a y l u b t a b l i c y n a r z ę d z i a numpy, a n a w e t z b u d o w a n ie w ła sn eg o ob iektu b u fo ro w a n e g o z a p o m o c ą m o d u łu s tru c t. S p o w o d u je to je d n a k z m n i e js z e n i e c z y te l n o ś c i k o d u , p o n i e w a ż ta k ie o b i e k t y są z w y k le m n iej elasty czn e od ich o d p o w ie d n ik ó w w b u d o w a n y c h w ję z y k P y th o n .
Może się to skomplikować jeszcze bardziej w przypadku konieczności wysłania bibliotece zło żonej struktury danych. Jeśli na przykład biblioteka oczekuje typu złożonego struct języka C, który reprezentuje punkt w przestrzeni z właściw ościam i x i y, niezbędne będzie zdefiniowa nie następującego kodu: from ctypes import S t r u c tu r e class cPoint(Stru cture): _ fie ld s _ = ("x ", c _ in t ) , ("y ", c_int)
Dla tego punktu można rozpocząć tworzenie obiektów zgodnych z językiem C, inicjując obiekt cPoint (czyli point = cPoint(10, 5)). Nie wiąże się z tym ogromna ilość pracy, ale praca ta może stać się żmudna i powodować uzyskanie niezbyt trwałego kodu. Co się stanie, gdy pojawi się nowa wersja biblioteki, która nieznacznie zmieni strukturę? Spowoduje to, że kod będzie bardzo trudny do utrzymania, a ponadto przestanie być rozwijany. W przypadku takiego kodu programiści po prostu zdecydują się zrezygnować z aktualizowania bazowych bibliotek używanych przez kod. Z tych powodów użycie modułu ctypes jest znakomitą opcją, gdy dysponujesz już dobrą zna jomością języka C, a ponadto w ym agasz możliwości dostrojenia każdego aspektu interfejsu. M oduł zapewnia znakom ite możliwości przenoszenia, ponieważ stanowi część standardowej biblioteki. Jeśli realizowane zadanie jest proste, oferuje proste rozwiązania. Trzeba tylko za chować ostrożność, gdyż złożoność rozwiązań opartych na m odule ctypes (i podobnych niskopoziomowych) może szybko sprawić, że staną się niemożliwe do zarządzania.
cffi Uświadomienie sobie przez twórców narzędzia c f f i , że czasami użycie m odułu ctypes może być dość nieporęczne, skutkuje tym, że narzędzie to podejm uje próbę uproszczenia wielu standardow ych operacji w ykonyw anych przez program istów . W tym celu korzysta z w e w nętrznego analizatora składni języka C, który rozpoznaje definicje funkcji i struktur. W rezultacie m ożliw e jest po prostu utw orzenie kodu C definiującego strukturę biblioteki, która ma zostać użyta. W dalszej kolejności narzędzie c f f i zajm ie się całością żm udnych za dań, czyli im portowaniem modułu i upewnieniem się, że dla funkcji wynikow ych określono poprawne typy. Okazuje się, że zadania te m ogą być niemal trywialne, jeśli dostępny jest kod źródłowy biblioteki. Wynika to stąd, że pliki nagłówkowe (zakończone rozszerzeniem .h) będą zaw ierać wszystkie wymagane, odpowiednie definicje. Przykład 7.22 prezentuje w ersję kodu dyfuzji dwuwym iarowej bazującego na narzędziu c f f i .
166
|
Rozdział 7. Kompilowanie do postaci kodu C
P r z y k ł a d 7 .2 2 . K o d d y f u z j i d w u w y m i a r o w e j b a z u j ą c y n a n a r z ę d z iu c f f from c f f i import FFI f f i = FFI() ff i.c d e f (r ''' void ev ol ve ( i n t Nx, i n t Ny, double * * i n , double * * o u t , double D, double dt ); # O ''') l i b = ffi.d lo p e n ("../ d iffu sio n .so ") def e v o l v e ( g r i d , d t , out , D=1.0): X, Y = grid_shape p o i n te r _ g ri d = f f i . c a s t ( ' d o u b l e * * ' , g r i d . c t y p e s . d a t a ) # 0 pointer_out = f f i . c a s t ( ' d o u b l e * * ' , o u t .c t y p e s . d a t a ) l i b . e v o l v e ( X , Y, p o i n t e r _ g r i d , p o i n te r _ o u t, D, dt)
O Zawartość tej definicji może być standardowo uzyskana z dokumentacji używanej biblioteki lub po sprawdzeniu jej plików nagłówkowych.
0 Choć wymagane jest jeszcze rzutowanie obiektów, które nie są wbudowane w język Python, w celu użycia ich z modułem kodu C, składnia będzie wyglądać znajomo dla osób mających doświadczenie z zakresu języka C. Inicjalizacja narzędzia c f f i w poprzednim kodzie może być traktowana jako proces dwukrokowy. Najpierw tworzony jest obiekt FFI, dla którego są podaw ane wszystkie niezbędne glo balne deklaracje języka C. M oże to obejmować typy danych, a także sygnatury funkcji. Dalej m ożliw e jest zaim portow anie za pom ocą funkcji dlopen biblioteki w spółużytkow anej do jej własnej przestrzeni nazw, która jest podrzędną przestrzenią obiektu FFI. Oznacza to, że moż liwe byłoby załadow anie dwóch bibliotek za pom ocą tej samej funkcji evolve do zmiennych lib 1 i lib 2, a następnie użycie ich niezależnie (jest to znakomita opcja w przypadku debugowania i profilowania!). Oprócz zw ykłego importowania biblioteki w spółużytkowanej języka C narzędzie c f f i um oż liwia po prostu napisanie kodu C, który zostanie skom pilow any za pom ocą kom pilatora JIT 1 funkcji verify. Coś takiego zapewnia wiele natychmiastowych korzyści. Z łatwością możesz ponow nie utw orzyć niew ielkie porcje kodu C bez wywoływ ania pokaźnych mechanizmów osobnej biblioteki języka C. Jeśli istnieje biblioteka, której chcesz użyć, ale do idealnego dzia łania interfejsu niezbędny jest kod łączący napisany w języku C, alternatyw nie m ożesz po prostu w staw ić go do kodu narzędzia c f f i (przykład 7.23). Dzięki temu w szystko będzie znajdow ać się w centralnym m iejscu. Poniew aż kod jest kom pilow any za pom ocą kom pi latora JIT, m ożesz określić instrukcje kom pilow ania dla każdej porcji kodu, która w ym aga skompilowania. Zauważ jednak, że z taką kompilacją związane jest jednorazow e obciążenie, które w ystępuje za każdym razem , gdy urucham iana jest funkcja verify w celu faktycznego przeprowadzenia kompilacji. P r z y k ł a d 7 .2 3 . K o d n a r z ę d z ia c ffi z w s t a w io n y m k o d e m d y f u z j i d w u w y m i a r o w e j f f i = FFI() ff i.c d e f (r ''' void ev olve( i n t Nx, i n t Ny, double * * i n , double * * o u t , double D, double dt
Interfejsy funkcji zewnętrznych
|
167
lib = f f i . v e r i f y ( r ' ' ' void e v o l v e ( i n t Nx, i n t Ny, double i n [ ] [ N y ] , double o u t [][ N y ], double D, double dt) { int i , j ; double l a p l a c i a n ; f o r (i = 1; i
ex t r a _ c o m p i le _ a r g s = [" -O 3 " , ]) # O
O Poniew aż kod jest kom pilow any za pom ocą kom pilatora JIT, m ożliw e jest też podanie odpow iednich flag kom pilacji. Inną korzyścią z funkcjonalności funkcji verify jest to, że świetnie radzi sobie ona ze złożo nym i instrukcjami cdef. Jeśli na przykład zostałaby użyta biblioteka z bardzo skomplikowaną strukturą, ale pożądane byłoby zastosow anie tylko jej części, można byłoby skorzystać z czę ściowej definicji typu struct. W tym celu w definicji typu struct dodajemy łańcuch . . . w bloku f fi.c d e f oraz instrukcję #include dla odpowiedniego pliku nagłów kow ego w zamieszczonej dalej w kodzie funkcji verify. Dla przykładu załóżmy, że korzystamy z biblioteki z plikiem nagłów kow ym complicated.h zaw ierającym strukturę o następującej postaci: s t r u c t Point { double x; double y; bool i s A c t i v e ; char * i d ; i n t num_times_visited; }
Jeśli interesowałyby nas tylko w łaściw ości x i y, moglibyśmy utworzyć prosty kod narzędzia c f f i , który zajmie się w yłącznie w artościam i tych właściwości: from c f f i import FFI f f i = FFI() f f i . c d e f ( r ..... s t r u c t Point { double x; double y; }; s t r u c t Point d o _ c a l c u l a t i o n ( ) ; l i b = f f i . v e r i f y ( r ..... #include
W dalszej kolejności m ożna uruchom ić funkcję do_calculation z biblioteki pliku nagłów ko w ego complicated.h, która zw róci obiekt Point z jego dostępnym i w łaściw ościam i x i y. Pod kątem możliwości przenoszenia jest to znakom ite rozwiązanie, gdyż taki kod będzie działać bez zarzutu w systemach z różną im plementacją obiektu Point lub po pojawieniu się nowych wersji pliku complicated.h, pod warunkiem że wszystkie będą m ieć właściwości x i y.
168
|
Rozdział 7. Kompilowanie do postaci kodu C
W szystko to sprawia, że c f f i to napraw dę znakom ite narzędzie, gdy używasz kodu C w ko dzie Python. Jest znacznie prostsze niż m oduł ctypes, a jednocześnie oferuje taki sam poziom szczegółow ej kontroli, która m oże być pożądana podczas pracy bezpośrednio z interfejsem funkcji zewnętrznej.
f2py W przypadku wielu zastosowań naukowych język Fortran w dalszym ciągu ma status złotego standardu. Choć minęły już czasy, gdy pełnił rolę języka do ogólnych zastosowań, nadal ofe ruje wiele pożytecznych funkcji, które ułatwiają dość szybkie tworzenie operacji wektorowych. Ponadto w języku Fortran napisano w iele w ydajnych bibliotek m atem atycznych (LAPA CK (http://www.netlib.org/lapack/), BLA S (http://www.netlib.org/blas/) itp.). Kluczowe znaczenie może m ieć możliwość użycia ich w tworzonym wydajnym kodzie Python. W takich sytuacjach m oduł f2py zapew nia w yjątkow o prostą m etodę im portow ania kodu Fortran do kodu Python. M oduł ten może być tak łatwy w obsłudze z powodu przejrzystości typów w języku Fortran. Poniew aż typy m ogą być bez problem u analizow ane i rozpozna wane, moduł f2py bez trudu może sprawić, że moduł CPython, który korzysta z w budowanej w język C obsługi funkcji zewnętrznych, użyje kodu Fortran. Oznacza to, że podczas stoso wania modułu f2py w rzeczywistości ma miejsce automatyczne generowanie modułu C, który potrafi użyć kodu Fortran! W efekcie wiele niejasności związanych z rozwiązaniami bazującymi na narzędziach ctypes i c f f i po prostu nie istnieje. Przykład 7.24 prezentuje prosty kod zgodny z modułem f 2py, który służy do rozwiązywania równania dyfuzji. Okazuje się, że cały w budow any kod Fortran jest zgodny z tym modułem. Jednakże adnotacje argum entów funkcji (instrukcje poprzedzone przez ! f 2py) upraszczają w ynikow y m oduł Python i p rzyczyniają się do u zyskania interfejsu prostszego w użyciu. Adnotacje niejaw nie inform ują m oduł f2py o tym, czy argum entem m ają być jedynie dane wyjściowe, czy tylko dane wejściowe, czy też ma nim być coś, co ma zostać zm odyfikowane lokalnie lub całkowicie ukryte. Ukryty typ jest szczególnie przydatny w przypadku wielkości wektorów: w kodzie Fortran może być w ym agane jaw ne określenie tych wartości, w kodzie Python natom iast takie inform acje są od razu dostępne. Po ustaw ieniu typu jako ukrytego m oduł f 2py m oże autom atycznie określić te w artości, co zasadniczo oznacza ukryw anie ich w ostatecznym interfejsie Python. Przykład 7.24. Kod Fortran dyfuzji dwuwymiarowej z adnotacjami modułu f2py SUBROUTINE e v o l v e ( g r i d , n e x t_ g ri d , D, d t , N, M) !f2py th re a d sa fe !f2py i n t e n t ( i n ) grid !f2py i n t e n t ( i n p l a c e ) ne xt_grid !f2py i n t e n t ( i n ) D !f2py i n t e n t ( i n ) dt !f2py i n t e n t ( h i d e ) N !f2py i n t e n t ( h i d e ) M INTEGER : : N, M DOUBLE PRECISION, DIMENSION(N,M) : : g r i d , nex t_grid DOUBLE PRECISION, DIMENSIOn ( n- 2, M-2) : : l a p l a c ia n DOUBLE PRECISION : : D, dt l a p l a c i a n = g r id (3 :N , 2 : M-1) + g r i d ( 1 : N - 2 , 2:M-1) + & g r i d ( 2 : N - 1 , 3 : m) + g r id ( 2 : N - 1 , 1:M-2) - 4 * g r i d ( 2 : N - 1 , 2:M-1) n e x t_ g r i d ( 2 :N -1 , 2:M-1) = g r i d ( 2 : N - 1 , 2:M-1) + D * dt * l a p l a c ia n END SUBROUTINE evolve
Interfejsy funkcji zewnętrznych
|
169
W celu przekształcenia kodu w moduł Python zostanie uruchom ione następujące polecenie: $ f2py - c -m di ff usio n -- fc om pil er= gfortr an -- o p t = '-O 3 ' d i f f u s i o n .f 9 0
Spowoduje to utworzenie pliku diffusion.so, który m oże zostać zaim portowany bezpośrednio do kodu Python. Poeksperymentowanie z wynikowym modułem w trybie interaktywnym pozwala zauważyć korzyści, jakie zapewnił m oduł f 2py dzięki użyciu adnotacji, a także możliw ość analizowania kodu Fortran: In [ 1 ] : import d i f f u s i o n In [ 2 ] : d i f f u s i o n ? Type: module S t r i n g form: File: . . ./e xa mples/compil ation/f2py/diffusion.so D oc st ring: This module ' d i f f u s i o n ' i s au to -gene rate d with f2py ( v e r s i o n : 2 ) . Function s: evolve(grid,next_grid,d,dt) In [ 3 ] : d i f f u s i o n .e v o l v e ? Type: fortran S t r i n g form: f o r t r a n ob je ct> D oc st ring: evolve(grid,next_grid,d,dt) Wrapper f o r " e v o l v e " . Parameters gr id : input rank-2 a r r a y ( ' d ' ) with bounds (n,m) n ex t_ g rid : rank-2 a r r a y ( ' d ' ) with bounds (n,m) d : input f l o a t dt : input f l o a t
Pow yższe dane pokazują, że w ynik generow ania przeprow adzonego przez m oduł f 2py jest automatycznie dokumentowany, a interfejs jest dość uproszczony. Na przykład zamiast zmu szać nas do w yodrębniania w ielkości w ektorów , m oduł f 2py sprawdza, jak autom atycznie znaleźć te informacje, i po prostu ukrywa je w wynikowym interfejsie. Okazuje się, że wyni kowa funkcja evolve w swojej sygnaturze wygląda tak samo jak wersja czystego kodu Python utworzonego w przykładzie 6.14. Jedyną rzeczą, o którą trzeba zadbać, jest uporządkowanie tablic narzędzia numpy w pamięci. Ponieważ w iększość zadań realizow anych z wykorzystaniem narzędzia numpy i języka Python skupia się na kodzie uzyskanym z kodu C, zaw sze stosuj konwencję języka C dotyczącą upo rządkow ania danych w pamięci (określanego m ianem głównego uporządkowania wierszowego). W języku Fortran w ykorzystyw ana jest inna konw encja (główne uporządkowanie kolumnowe), która musi być przestrzegana przez używ ane w ektory. Uporządkowania te po prostu okre ślają, czy dla tablicy dw uw ym iarow ej kolum ny lub w iersze są ciągłe w pam ięci . Na szczę ście sprowadza się to jedynie do określenia parametru order='F' dla narzędzia numpy podczas deklarowania wektorów.
4 W i ę c e j i n f o r m a c ji z a w i e r a s t r o n a s e r w i s u W i k i p e d i a p o d n a s t ę p u ją c y m a d r e se m : h ttp ://e n .w ik ip e d ia .o rg /w ik i/ R ow -m ajor_ o rd er.
170
|
Rozdział 7. Kompilowanie do postaci kodu C
^
R óżnica m ięd z y g łó w n y m u p o rząd k o w an ie m w ie rsz o w y m i k o lu m n o w y m oznacza, ż e m a c i e r z [[1, 2], [3, 4]] z o s t a n i e z a p i s a n a w p a m i ę c i w p o s t a c i [1, 2 , 3 , 4] ( g ł ó w n e u p o r z ą d k o w a n i e w i e r s z o w e ) o r a z j a k o [1, 3 , 2 , 4] w p r z y p a d k u g ł ó w n e g o u p o r z ą d k o w a n i a k o l u m n o w e g o . R ó ż n i się to je d y n i e k o n w e n c ją . Je śli je st w ła ś c iw ie stos o w a n a , w rzecz y w isto ści n ie p o w o d u je ż a d n y c h n e g a ty w n y c h k o n sek w en c ji d o ty c z ą c y c h w y d ajno ści.
W rezultacie uzyskujemy poniższy kod, który umożliwia użycie podprocedury kodu Fortran. Kod jest taki sam jak użyty w przykładzie 6.14. Różni się jedynie im portowaniem z biblioteki bazującej na module f2py i jawnym uporządkowaniem danych zgodnie z wymaganiami języka Fortran: from d i f f u s i o n import evolve def ru n_e xperim en t( nu m _it era tions) : n ex t_ g rid = n p .z e ro s(g rid _sh ap e, dtype=np.double, o r d e r = 'F ' ) # O gr id = n p .z e ro s(g rid _sh ap e, dtype=np.double, o r d e r = 'F ' ) # ...standardow a inicjalizacja... f o r i in r a n g e (n u m _ ite r a ti o n s ): e v o l v e ( g r i d , n e x t_ g ri d , 1 . 0 , 0 . 1 ) g r i d , n ex t_ g rid = n e x t_ g ri d , grid
0
W przypadku języka Fortran liczby są porządkow ane w pam ięci w odm ienny sposób, dlatego trzeba pam iętać o ustawieniu tablic narzędzia numpy pod kątem korzystania z tego standardu.
Moduł narzędzia CPython Zaw sze m ożliw e jest przejście bezpośrednio do poziom u interfejsu API narzędzia CPython 1 utw orzenie jego m odułu. W ym aga to napisania kodu w ten sam sposób, w jaki zaprojek towano narzędzie CPython, a także zadbania o wszystkie interakcje między kodem i im ple m entacją narzędzia CPython. Zaletą tego rozwiązania są jego niebyw ałe m ożliw ości dotyczące przenoszenia (zależnie od wersji języka Python). Nie są konieczne żadne moduły lub biblioteki zewnętrzne, ale jedynie kom pilator kodu C i język Python! Niemniej jednak rozwiązanie to niekoniecznie zapewnia odpow iednie skalow anie w przypadku now ych w ersji języka Python. Na przykład m oduły narzędzia CPython utw orzone dla języka Python 2.7 współpracują z językiem Python 3. Z możliwościami przenoszenia w iąże się jednak duży koszt. Odpowiadasz za każdy aspekt interfejsu między kodem Python i modułem. M oże to oznaczać, że naw et najprostsze zadania będą liczyć dziesiątki w ierszy kodu. Aby na przykład uzyskać interfejs z biblioteką procesu dyfuzji z przykładu 7.20, konieczne jest napisanie 28 w ierszy kodu tylko w celu w czytania argumentów do funkcji i poddania ich analizie (przykład 7.25). Oczywiście oznacza to, że masz możliwość niezwykle dokładnego kontrolowania tego, co ma miejsce. Sprowadza się to nawet do możliwości ręcznej zmiany liczników odwołań dla procesu czyszczenia pamięci kodu Python (może to być przyczyną wielu utrudnień podczas tworzenia m odułów narzędzia CPython, które korzystają z wbudowanych typów języka Python). Z tego powodu wynikowy kod będzie zwykle nieznacznie szybszy niż w przypadku innych metod interfejsu.
Interfejsy funkcji zewnętrznych
|
171
P r z y k ł a d 7 .2 5 . M o d u ł n a r z ę d z ia C P y t h o n z a p e w n i a j ą c y i n t e r fe js d la b ib l i o t e k i d y f u z j i d w u w y m i a r o w e j // python_interface.c // —interfejs modułu narzędzia CPython dla pliku diffusion.c #def ine NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include #include " d i f f u s i o n . h " /* Literały docstring */ s t a t i c char module_docstring[] = "Zapewnia zoptymalizowaną metodę rozwiązywania równania d y f u z j i " ; s t a t i c char c d i ff u s i o n _ e v o l v e _ d o c s tr in g [ ] = "Rozwijanie s i a t k i dwuwymiarowej za pomocą równania d y f u z j i " ; PyArrayObject* py_evolve(PyObje ct* s e l f , PyObject* ar gs) { PyArrayObject* data; PyArrayObject* n ext_ g rid ; double d t , D=1.0; /* Funkcja „ rozw ijania ” będzie m ieć sygnaturę: * evolve(data, next_grid, dt, D=1) */ i f (!PyAr g_P arseTuple(args, "OOd|d", &data, &next_grid, &dt, &D)) { PyErr_SetString(PyExc_RuntimeError, "Niepoprawne argumenty"); re tu rn NULL; } /* Sprawdzenie, czy tablice narzędzia numpy s ą ciąg łe w p a m ięci */ i f (!PyArray_Check(data) || !PyArray_ISCONTIGUOUS(data)) { Py Err_ Se tSt rin g (P y Exc _R un tim eE rr or,"T ab lica data n ie j e s t c i ą g ł ą t a b l i c ą . " ) ; re tu rn NULL; } i f (!PyArray_Check(next_grid) || !PyArray_ISCONTIGUOUS(next_grid)) { Py Err_ Se tSt rin g (P y Exc _R un tim eE rr or,"T ab lica n ex t_ grid n ie j e s t c i ą g ł ą t a b l i c ą . " ) ; re tu rn NULL; } /* Sprawdzenie, czy tablice siatki data i next_grid s ą takiego sam ego typu i m ają identyczne wymiary */ i f (PyArray_TYPE(data) != PyArray_TYPE(next_grid)) { PyErr_SetString (PyExc_RuntimeError, " T a b l ic e n ex t_ g rid i data powinny być tego samego t y p u . " ) ; re tu rn NULL; } i f (PyArray_NDIM(data) != 2) { Py Err_ Se tSt rin g (P y Exc _R un tim eE rr or,"T ab lica data powinna być dwuwymiarowa."); re tu rn NULL; } i f (PyArray_NDIM(next_grid) != 2) { Py Err_ Se tSt rin g (P y Exc _R un tim eE rr or,"T ab lica n ex t_ grid powinna być dwuwymiarowa."); re tu rn NULL; } i f ((PyArray_DIM(data,0) != PyArrayDim(next_grid,0)) || (PyArray_DIM(data,1) != PyArrayDim(next_grid,1)) ) { PyErr_SetString (PyExc_RuntimeError, " T a b l ic e data i n ex t_ g rid muszą mieć t a k i e same wymiary."); re tu rn NULL; } /* P o bra n ie w ielkości przetw arzanej siatki */ con st i n t N = ( i n t ) PyArray_DIM(data, 0 ) ; con st i n t M = ( i n t ) PyArray_DIM(data, 1 ); ev olve( N, M, PyArray_DATA(data), PyArray_DATA(next_gri d ) , D,
172
|
Rozdział 7. Kompilowanie do postaci kodu C
dt ); Py_XINCREF(next_grid); re tu rn n e x t_ g rid ; } /* Specyfikacja modułu */ s t a t i c PyMethodDef module_methods[] = { /* { nazwa metody, fu n kcja języ k a C, typy argumentów, docstring } */ { "evolve" , py_evolve , METH_VARARGS , c d iff u s i o n _ e v o l v e _ d o c s tr in g } { NULL , NULL , 0 , NULL } }; /* In icjow anie modułu */ PyMODINIT_FUNC ini tc d i f f u s i o n ( v o i d ) { PyObject *m = P y_ I n i t M o d u l e 3 ( "c d i ffu si o n " , module_methods, modu le_docstr ing); i f (m == NULL) r e tu r n ; /* Ładow an ie fu n kcji narzędzia numpy */ i m p o r t_ a r r a y ( ); }
P o d s u m o w u j ą c , z tej m e t o d y n a l e ż y k o r z y s t a ć t y l k o w o s t a t e c z n o ś c i . C h o ć z a p e w n i a sp o r o in fo rm a cji p o d c z a s tw o rz e n ia m o d u łu n a rz ę d z ia C P y th o n , w y n ik o w y k o d nie oferu je tak ich m o ż liw o śc i p o n o w n e g o w y k o rz y sta n ia lub u trz y m y w a n ia ja k inne p o ten c jaln e m e to d y . W p ro w a d z a n ie m in im a ln y c h z m ia n w m o d u le w y m a g a często j e g o c a ł k o w i t e j p r z e b u d o w y . T a k n a p r a w d ę k o d m o d u ł u i p l i k s e t u p .p y w y m a g a n y d o je g o k o m p ila c ji (p rz y k ła d 7.26) d o łą c z a m y k u p rz e str o d z e .
Aby utworzyć powyższy kod, konieczne jest napisanie skryptu setup.py, który używa modułu d istu tils do określenia sposobu budowania kodu zgodnego z językiem Python (przykład 7.26). Oprócz standardowego modułu d istu tils narzędzie numpy zapewnia własny moduł, aby ułatwić dodawanie integracji narzędzia numpy do modułów narzędzia CPython. Przykład 7.26. Plik konfiguracyjny dla interfejsu dyfuzji modułu narzędzia CPython P lik setup.py dla modułu dyfuzji narzędzia CPython. Rozszerzenie m oże zostać utworzone za p o m o c ą p o lec e n ia : $ python setup.py build_ext --inplace Utworzy on o p l i k cdiffusion.so , który m oże być importowany bezpośredn io d o kodu Python from d i s t u t i l s . c o r e import setu p , Extension import n u m p y . d i s t u ti l s . m i s c _ u t i l ve rsion = "0.1" c d i f f u s i o n = Ex tension( 'cd iffu sio n ', sources = [ 'c d i f f u s i o n / c d i f f u s i o n . c ' , 'cdi ffusion/python_i n t e r f a c e . c ' ] , ext ra_c om pile_args = [" -O 3 ", " - s t d = c 9 9 " , " - W a ll " , " - p " , " - p g " , ] , extra_link_args = [ " - l c " ] , ) setup ( name = ' d i f f u s i o n ' , ve rsio n = __ve rsion __ , ext_modules = [ c d i f f u s i o n , ] , packages = [ " d i f f u s i o n " , ] , in c lu d e_d ir s = numpy.di s t u ti l s . m i s c _ u t i l. g e t _ n u m p y _ i n c l u d e _ d i r s ( ) , )
Interfejsy funkcji zewnętrznych
|
173
W ynikiem wykonania tego kodu jest plik cdiffusion.so, który może być im portowany bezpo średnio z kodu Python i całkiem łatwo używany. Ponieważ uzyskano pełną kontrolę sygnatury funkcji wynikowej, a także tego, jak dokładnie kod C prow adził interakcję z biblioteką, m oż liwe było (po wykonaniu sporej ilości pracy) stworzenie prostego do zastosowania modułu: from c d i f f u s i o n import evolve def ru n_e xperim en t( nu m _it era tions) : n ext_ g ri d = n p .z e ro s(g rid _sh ap e, dtype=np.double) g r id = n p .z e ro s(g rid _sh ap e, dtype=np.double) # ...standardow a inicjalizacja... f o r i in r a n g e (n u m _ ite r a ti o n s ): e v o l v e ( g r i d , n e x t_ g ri d , 1 . 0 , 0 . 1 ) g r id , n ex t_ grid = n e x t_ g ri d , grid
Podsumowanie Różne strategie zaprezentow ane w tym rozdziale umożliwiają dostosowanie kodu w różnym stopniu w celu zmniejszenia liczby instrukcji, jakie procesor musi wykonać, a także zwiększe nia efektywności programów. Choć czasem można to uzyskać w sposób algorytmiczny, czę sto trzeba zrobić to ręcznie (zajrzyj do podrozdziału „Porównanie kompilatorów JIT i A O T"). Ponadto metody te należy sporadycznie zastosow ać po prostu w celu użycia bibliotek, które zostały już utw orzone w innych językach. Niezależnie od motywacji język Python pozwala wykorzystać w zrost w ydajności możliwy do uzyskania przy użyciu innych języków w przy padku niektórych problemów przy jednoczesnym zachowaniu szczegółowości i elastyczności, jeśli jest to wymagane. Godne uwagi jest jednak to, że opisane optymalizacje są przeprowadzane w celu poprawienia wydajności wyłącznie instrukcji procesora. W przypadku operacji wejścia-wyjścia pow iąza nych z problemem bazującym na użyciu procesora skompilowanie kodu może nie zapewnić rozsądnych przyspieszeń. W odniesieniu do takich problem ów konieczne będzie ponow ne przeanalizow anie dostępnych rozw iązań i ew entualne użycie przetw arzania rów noległego, które pozwala na jednoczesne urucham ianie różnych zadań.
174
|
Rozdział 7. Kompilowanie do postaci kodu C
_________ ROZDZIAŁ 8.
Współbieżność
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Czym jest współbieżność i jak może okazać się pomocna? • Jaka jest różnica m iędzy współbieżnością i przetwarzaniem równoległym? • Jakie zadania m ogą być w ykonywane współbieżnie, a jakie nie? • Jakie są różne paradygm aty współbieżności? • Kiedy warto skorzystać ze współbieżności? • Jak współbieżność może spowodować przyspieszenie Twoich programów?
O peracje w ejścia-w yjścia m ogą być uciążliw e dla przepływ u program u. Każdorazow o, gdy kod dokonuje odczytu z pliku lub zapisu do gniazda sieciowego, musi w strzym ać działanie w celu skontaktowania się z jądrem, żądania w ykonania operacji, a następnie poczekania na jej zakończenie. M oże to nie w yglądać jeszcze na koniec świata, zwłaszcza gdy uświadom imy sobie, że podobna operacja ma miejsce za każdym razem przy przydzielaniu pamięci. Jeśli jed nak przyjrzymy się ponownie rysunkowi 1.3, dostrzeżemy, że większość operacji wejścia-wyjścia wykonywana jest dla urządzeń, które są o rzędy wielkości wolniejsze od procesora. Na przykład w czasie, jaki zajm uje dokonanie zapisu w gnieździe sieciowym (operacja taka trwa zw ykle około 1 m s), w przypadku kom putera z procesorem o częstotliw ości 2,4 GHz m ożliwe jest wykonanie 2 400 000 instrukcji. Najgorsze w tym wszystkim jest to, że program jest w strzym any przez większość tego czasu. Podczas oczekiwania na sygnał potwierdzający zakończenie operacji zapisu wykonywanie jest wstrzymane. Czas, jaki upływa w stanie wstrzy mania, jest nazywany „oczekiwaniem na operację wejścia-wyjścia". W spółbieżność ułatw ia w ykorzystanie traconego w ten sposób czasu przez um ożliw ienie wykonywania innych operacji podczas oczekiwania na zakończenie operacji w ejścia-wyjścia. Na przykład na rysunku 8.1 przedstawiono opis program u, który musi uruchom ić trzy za dania, podczas których w ystępują okresy oczekiwania na operację wejścia-wyjścia. Jeśli za dania zostaną uruchomione w sposób szeregowy, trzykrotnie zostanie stracony czas z powodu oczekiwania na operację wejścia-wyjścia.
175
Porównanie czasu działania programów szeregowych i współbieżnych Szeregowy
LU 1 I 1 LU 1 1 I 1 I 1 M I I I I 1 I 1 I
Rysunek 8.1. Porównanie programów szeregowych i współbieżnych Jeśli jednak te zadania zostaną zrealizowane współbieżnie, zasadniczo m ożliw e będzie wyko rzystanie czasu oczekiwania i uruchomienie w tym czasie kolejnego zadania. Godne uwagi jest to, że wszystko to ma miejsce w jednym wątku, a ponadto w każdym momencie używany jest tylko jeden procesor! Choć w spółbieżność nie jest ograniczona tylko do operacji w ejścia-w yjścia, w łaśnie w tym przypadku osiągane są największe korzyści. Kodu nie trzeba w ykonyw ać w sposób szerego wy (czyli w iersz po wierszu), zam iast tego w program ie współbieżnym kod jest tworzony do obsługi zdarzeń. Oznacza to, że różne części kodu są uruchamiane w momencie występowania różnych zdarzeń. Dzięki m odelow aniu program u w ten sposób m ożliw a jest obsługa konkretnego zdarzenia, które nas interesuje, czyli oczekiwania na operację w ejścia-wyjścia.
Wprowadzenie do programowania asynchronicznego W momencie wystąpienia podczas działania programu oczekiwania na operację wejścia-wyjścia wstrzymywane jest wykonyw anie programu, aby jądro m ogło zrealizow ać niskopoziomowe operacje powiązane z żądaniem wejścia-wyjścia (jest to określane mianem przełączenia kontek stu). Praca programu nie zostanie wznowiona do chwili zakończenia operacji wejścia-wyjścia. Przełączanie kontekstu to dość obciążająca operacja. W ym aga ona zapisania stanu programu (powoduje to utratę wszelkiego rodzaju buforowania, jakie występowało na poziomie procesora) i zwolnienia zasobów procesora. Gdy później możliwe będzie wznowienie działania programu, konieczne będzie pośw ięcenie czasu na jego ponow ną inicjalizację w obrębie płyty głównej komputera i przygotow anie do w znowienia (oczywiście wszystko to dzieje się w tle). Z kolei w przypadku współbieżności zw ykle ma miejsce coś, co jest określane m ianem „pętli zd arzeń". Taka pętla zarządza tym, co zostanie uruchom ione w program ie, decyduje rów nież, w jakim m om encie będzie to miało miejsce. Zasadniczo pętla zdarzeń to po prostu lista funkcji niezbędnych do uruchomienia. Najpierw w ykonywana jest funkcja na początku listy, po czym następna w kolejności itd. Przykład 8.1 prezentuje prostą pętlę zdarzeń. Przykład 8.1. Prosta pętla zdarzeń from Queue import Queue from fu n c to o l s import p a r t i a l eventloop = None c l a s s EventLoop(Queue): def s t a r t ( s e l f ) :
176
|
Rozdział 8. Współbieżność
while True: fu n c ti on = s e l f . g e t ( ) f u n c ti o n () def do_hel l o ( ) : global eventloop print "W itaj, " eventloop.put(do_world) def do_world(): global eventloop p r i n t "św iec ie " e v en tl oop.p ut( do_h ello) if name == " main ": eventloop = EventLoop() e v en tl oop.p ut( do_h ello) even tlo o p.start()
Być może nie wygląda to na dużą zmianę. W przypadku w ykonyw ania zadań bazujących na urządzeniach wejścia-wyjścia możliwe jest jednak sprzężenie pętli zdarzeń z asynchronicz nym i operacjami wejścia-wyjścia w celu uzyskania ogromnego wzrostu wydajności. Operacje takie są nieblokujące. Oznacza to, że jeśli realizowany jest zapis sieciowy z w ykorzystaniem funkcji asynchronicznej, zwróci ona w ynik od razu, naw et pomimo tego, że zapis jeszcze nie w ystąpił. Po zakończeniu zapisu zostanie wygenerowane zdarzenie w celu poinformowania o tym programu. Połączenie tych dw óch zagadnień pozw ala uzyskać program , który po zażądaniu operacji w ejścia-w yjścia w ykonuje inne funkcje podczas oczekiw ania na zakończenie początkow ej operacji wejścia-wyjścia. Zasadniczo umożliwia to wykonyw anie w dalszym ciągu istotnych obliczeń. W innym razie w ystąpiłoby oczekiwanie na operację wejścia-wyjścia.
^
P rzełącza n ie m ię d z y fu n k cjam i o z n a cz a koszt. Jąd ro p o trzeb u je c zasu n a sk o n fig u ro w a n ie fu nk cji, k tó ra m a z o s ta ć w y w o ła n a w p a m ię ci. P o n a d to sta n p a m ię c i p o d rę c z n y c h n ie b ę d z ie ta k ła tw y d o p rz e w id z e n ia . W y n ik a to z tego, ż e w s p ó łb ie ż n o ś ć z a p e w n ia n a jle p s z e re zu ltaty , g d y w cz a sie d z ia ła n ia p r o g r a m ó w w y s tę p u je w ie le o c z e k iw a ń n a o p era cję w e jścia -w y jścia . C h o ć p rz e łą cz a n ie p o w o d u je w ty m p rz y p a d k u o b c i ą ż e n i e , a l e j e s t to n i e w i e l k i k o s z t w p o r ó w n a n i u z t y m , c o z y s k u j e si ę d z ię k i w y k o r z y s ta n iu c z a su o c z e k iw a n ia n a o p e r a cję w e jścia -w y jścia .
Konstrukcje program istyczne wykorzystujące pętle zdarzeń m ogą m ieć dwie postaci: w yw o łań zw rotnych lub konstrukcji future. W modelu w yw ołań zw rotnych funkcje są wyw oływ a ne z argumentem, który zwykle nazywany jest wywołaniem zwrotnym. Zam iast zw racać swoją w artość, funkcja w yw ołuje funkcję w yw ołania zw rotnego z w artością. W w yniku tego po w stają długie łańcuchy wyw oływ anych funkcji, z których każda uzyskuje w ynik poprzedniej funkcji w łańcuchu. Przykład 8.2 prezentuje prosty m odel w yw ołań zwrotnych. Przykład 8.2. Przykład z wywołaniami zwrotnymi from fu n c to o l s import p a r t i a l def sav e_value (v alu e, c a l l b a c k ) : p r i n t "Zapisywanie {} w bazie danyc h".form at(va lue) sav e_resu lt_to_d b(resu lt, callback) # O def p ri n t_ resp on se (db _re s ponse ): p r i n t "Odpowiedź z bazy danych: {} ".f o rm a t( d b _ resp o n se ) if name == " main ": e v e n tl o o p .p u t( p a r t i a l ( s a v e _ v a l u e , "W i ta j , ś w i e c i e " , pri n t_ res pon se ) )
W prowadzenie do programowania asynchronicznego
|
177
o
save_result_to_db to funkcja asynchroniczna. Zwróci ona od razu wartość i zakończy działa nie, umożliwiając uruchomienie innego kodu. Gdy jednak dane będą gotowe, zostanie wy wołana funkcja print_response.
Z kolei w przypadku konstrukcji fu tu re funkcja asynchroniczna zam iast rzeczywistego wyni ku zwraca obietnicę przyszłego wyniku. Z tego powodu konieczne jest poczekanie na zakoń czenie i w ypełnienie żądaną w artością (albo przez wykonanie instrukcji yield dla funkcji albo uruchomienie funkcji, która jaw nie oczekuje na przygotow anie wartości) konstrukcji future, zwracanej przez tego rodzaju funkcję asynchroniczną. W trakcie oczekiwania na wypełnienie przyszłego obiektu żądanym i danymi możliwe jest realizowanie innych obliczeń. Jeśli zosta nie to powiązane z pojęciem generatorów (funkcji, których działanie może być wstrzymywane, a później w znawiane), można utw orzyć kod asynchroniczny bardzo podobny do kodu sze regowego. Kod asynchroniczny ma następującą postać: @coroutine def sav e_v alu e (v alu e, c a l l b a c k ) : p r i n t "Zapisywanie {} w bazie danyc h".form at(va lue) db_response = y i e l d s a v e _ r e s u l t _ t o _ d b ( r e s u l t , c a l l b a c k ) # O p r i n t "Odpowiedź z bazy danych: {} ". f o rm a t(d b _ resp o n se ) if name == " main ": e v e n t l o o p .p u t ( p a r t i a l ( s a v e _ v a l u e , " W i t a j , ś w ie c i e " ) )
0
W tym przypadku funkcja save_result_to_db zwraca typ Future. Stosując instrukcję yield dla funkcji, zapewniamy, że funkcja save_value pozostanie wstrzym ana do momentu przygo towania wartości. Gdy to nastąpi, funkcja w znowi działanie i zakończy swoje operacje.
W języku Python w spółprogram y (ang. coroutine) są im plementowane jako generatory. Jest to wygodne, ponieważ generatory zaw ierają już mechanizm wstrzym ywania ich w ykonywania 1 późniejszego w znawiania. A zatem współprogram zapewni konstrukcję future, a pętla zda rzeń będzie czekać do momentu przygotowania w artości dla tej konstrukcji. Gdy to nastąpi, pętla zdarzeń wznowi wykonyw anie tej funkcji, odsyłając jej w artość konstrukcji future. W przypadku im plementacji kodu w spółbieżności Python 2.7 opartej na konstrukcjach fu tu re w szystko m oże okazać się trochę dziwne przy próbie użycia współprogram ów jako rzeczy wistych funkcji. Pamiętaj o tym, że generatory nie m ogą zw racać wartości. W zw iązku z tym istnieją różne metody, przy użyciu których biblioteki radzą sobie z tym problemem. W języku Python 3.4 wprow adzono now y mechanizm, który ma za zadanie ułatw ienie two rzenia współprogram ów przy jednoczesnym zapewnieniu zw racania przez nie wartości. W rozdziale dokonam y analizy serwera sieci W W W , który pobiera dane z serwera HTTP z domyślnym opóźnieniem. Przypadek ten reprezentuje ogólne opóźnienie czasu odpowiedzi, które wystąpi każdorazowo przy stosowaniu operacji wejścia-wyjścia. Najpierw zostanie utwo rzony przeszukiw acz szeregowy, który stanowi bardzo proste rozwiązanie tego problemu bazujące na kodzie Python. Później zajmiemy się dwoma rozwiązaniami opartymi na języku Python 2.7: gevent i tornado. Na końcu przyjrzym y się bibliotece asyncio napisanej w języku Python 3.4 i temu, jak wygląda przyszłość programowania asynchronicznego z w ykorzysta niem języka Python.
178
|
Rozdział 8. Współbieżność
^
Z a s to s o w a n y se r w e r W W W m o ż e o b słu g iw a ć je d n o cz e śn ie w iele połączeń . B ę d zie tak d la w ię k s z o ś c i u słu g , z a p o m o c ą k tó ry c h re a liz o w a n e b ę d ą o p era cje w ejścia -w y jścia . D u ż a c z ę ś ć b a z d a n y c h m o ż e j e d n o c z e ś n i e o b s ł u g i w a ć w i e l e ż ą d a ń . W i ę k s z o ś ć ser w e r ó w W W W obsług u je w ty m s a m y m czasie p o n a d 10 0 0 0 p ołączeń. Je d n a k ż e p o d c z a s in terakc ji z u s ł u g ą , k t ó r a n i e m o ż e o b s ł u g i w a ć je d n o c z e ś n i e w i e l u p o ł ą c z e ń 1, z a w s z e zostan ie u z y sk a n a tak a sa m a w y d a jn o ść ja k w p rz y p a d k u ro zw iązania szeregow ego.
Przeszukiwacz szeregowy Do kontrolow ania przeprow adzanego eksperym entu dotyczącego w spółbieżności zostanie utw orzone szeregowe oprogram owanie w yodrębniające dane sieciowe. Pobiera ono listę ad resów URL, ładuje je i sumuje całkowitą długość treści stron. Zostanie użyty niestandardowy serwer HTTP, który pobiera dwa parametry: name i delay. Pole delay informuje serwer o tym, przez jaki czas (w milisekundach) ma być w strzym any przed udzieleniem odpowiedzi. Pole name jest wykorzystyw ane do celów związanych z rejestrowaniem. Kontrolując param etr delay, m ożna sym ulow ać czas, jaki zajm uje serw erow i udzielenie od pow iedzi na zapytanie. W praktyce m oże to odpow iadać w olnem u serw erow i W W W , ob ciążającemu wywołaniu dotyczącemu bazy danych lub dowolnemu wywołaniu operacji wej ścia-wyjścia, którego wykonanie zajm uje wiele czasu. W przypadku programu szeregowego będzie to po prostu oznaczać w ięcej czasu, przez jaki program będzie w strzym any podczas oczekiw ania na operację w ejścia-w yjścia. Jednakże w zam ieszczonych dalej przykładach w spółbieżności będzie to oznaczać więcej czasu, jaki program m oże przeznaczyć na realizo w anie innych działań. Dodatkowo w ykorzystany został m oduł requests, aby zrealizow ać wywołanie HTTP. Zade cydow ała o tym prostota tego m odułu. W podrozdziale używ any jest protokół HTTP, gdyż stanowi prosty przykład operacji wejścia-wyjścia. Żądania HTTP m ogą być dość łatwo wy konywane. Ogólnie rzecz biorąc, dowolne wywołanie biblioteki HTTP można zastąpić dowolną operacją wejścia-wyjścia. Przykład 8.3 prezentuje w ersję szeregową tworzonego oprogramo wania wyodrębniającego dane sieciowe HTTP. Przykład 8.3. Szeregowe oprogramowanie wyodrębniające dane sieciowe HTTP import re qu es ts import s t r i n g import random def g e n e r a t e _ u r l s ( b a s e _ u r l , num_urls): N a końcu adresu URL dodaw ane s ą znaki losow e w celu wyelim inow ania wszelkich mechanizm ów buforow ania w bibliotece żądań lub na serw erze f o r i in xrange(num_urls): y i e l d base_url + " " . jo i n ( r a n d o m . s a m p l e ( s t r i n g . a s c i i _ l o w e r c a s e , 10)) def ru n_ exp er im en t(base _ur l, num_iter=500): re sp o n s e_size = 0 f o r url in g e n e r a t e _ u r l s ( b a s e _ u r l , num _iter ): response = r e q u e s t s . g e t ( u r l ) re sp o n s e_size += l e n ( r e s p o n s e . t e x t )
1 D la n ie k tó r y c h b a z d a n y c h , t a k ich ja k R e d is , je st to o p c ja p r o j e k t o w a s t o s o w a n a sp e cjaln ie w celu u t r z y m a n i a s p ó jn o śc i d an y c h .
Przeszukiwacz szeregowy
|
179
if
r e tu r n re sp o nse_ si ze name == " main ": import time delay = 100 num_iter = 500 base_url = " h tt p :/ / 127.0 .0.1 :8080/ ad d ? n am e = s erial& d e la y = {} & ". fo rmat(delay) s t a r t = t i m e . ti m e () r e s u l t = ru n_exp er im en t( bas e_ ur l, num_iter) end = t i m e . ti m e () print("Wynik: { } , Czas: { } " . f o r m a t ( r e s u l t , end - s t a r t ) )
Po uruchomieniu tego kodu w arto zw rócić uwagę na pom iar, który określa czas rozpoczęcia i zakończenia każdego żądania po stronie serwera HTTP. Dane te pozw alają stwierdzić, jak efektywny był kod podczas oczekiwania na operację wejścia-wyjścia. Ponieważ w ykonywane zadanie sprowadza się jedynie do utworzenia żądań HTTP, a następnie zsum ow ania liczby zw róconych znaków , pow inno być m ożliw e w ygenerow anie w iększej liczby takich żądań i przetw arzanie w szystkich odpowiedzi podczas oczekiwania na zakończenie innych żądań. Na rysunku 8.2 widać, że zgodnie z oczekiwaniami nie w ystępuje przeplatanie żądań. W da nym m omencie realizowane jest jedno żądanie, po czym następuje oczekiwanie na zakończe nie poprzedniego żądania, zanim rozpocznie się obsługa kolejnego. O kazuje się, że całkowity czas działania procesu szeregowego jest w pełni przewidywalny, gdyż każde żądanie zajmuje 0,1 sekundy (z pow odu użycia parametru delay). Ze względu na utworzenie 500 żądań ocze kujemy, że całkowity czas działania w yniesie około 50 sekund.
Rysunek 8.2. Chronologia żądań HTTP z przykładu 8.3
1BC
I
Rozdział B. Współbieżność
gevent g e v e n t to jedna z najprostszych bibliotek asynchronicznych. Jest ona zgodna z modelem funkcji asynchronicznych zw racających konstrukcje future. Oznacza to, że w iększość logiki w kodzie może pozostać bez zmian. Ponadto biblioteka g e v e n t modyfikuje w czasie działania standardowe funkcje wejścia-wyjścia, aby były asynchroniczne. Dzięki temu przew ażnie możesz po prostu używ ać standardowych pakietów wejścia-wyjścia i korzystać z działania asynchronicznego.
Biblioteka g e v e n t zapewnia dwa m echanizm y um ożliw iające program owanie asynchroniczne. Jak wcześniej wspom niano, modyfikuje ona standardową bibliotekę, rozszerzając ją o asyn chroniczne funkcje wejścia-wyjścia. Biblioteka g e v e n t zawiera również obiekt G r e e n l e t , który m oże posłużyć do w ykonyw ania w spółbieżnego. Term inem greenlet określa się typ współprogramu, który można potraktow ać jako w ątek (w rozdziale 9. om ówiono wątki). Jednakże w szystkie takie w spółprogram y działają w tym sam ym fizycznym w ątku. O znacza to, że zam iast stosow ać wiele procesorów do uruchamiania w szystkich współprogramów greenlet, algorytm szeregujący biblioteki g e v e n t dokonuje przełączenia między nimi z w ykorzystaniem pętli zdarzeń podczas oczekiwania na zakończenie operacji wejścia-wyjścia. Używ ając funkcji w a i t , biblioteka przew ażnie próbuje spraw ić, aby obsługa pętli zdarzeń była jak najbardziej transparentna. Funkcja w a i t uruchom i pętlę zdarzeń i będzie ją w ykonyw ać tak długo, jak to będzie w ym agane, czyli do m om entu zakończenia w szystkich w spółprogram ów g reenlet. Z tego powodu w iększość kodu biblioteki g e v e n t będzie działać w sposób szeregowy. Później w pew nym m om encie skonfigurujesz w iele w spółprogram ów greenlet w celu realizow ania zadania współbieżnego, a ponadto uruchomisz pętlę zdarzeń za pom ocą funkcji w a i t . W cza sie działania tej funkcji w szystkie kolejkowane zadania współbieżne będą w ykonywane aż do m om entu zakończenia (lub w ystąpienia w arunku zatrzym ania). N astępnie kod ponow nie będzie działać w sposób szeregowy. Konstrukcje fu tu re są tw orzone za pom ocą m etody g e v e n t . s p a w n , która pobiera funkcję i jej argum enty, po czym w yw ołuje w spółprogram greenlet odpow iedzialny za uruchom ienie tej funkcji. W spółprogram greenlet można potraktow ać jako konstrukcję fu tu re, ponieważ po za kończeniu działania określonej funkcji jej w artość będzie znajd ow ać się w polu v a l u e tego w spółprogramu. Rozszerzanie w ten sposób standardowych m odułów Python m oże utrudnić kontrolowanie niuansów określających, jakie są uruchamiane funkcje asynchroniczne i kiedy są uruchamiane. Na przykład w przypadku wykonywania asynchronicznej operacji wejścia-wyjścia pożądane jest zapewnienie, że jednocześnie n ie zostanie otwartych zbyt wiele plików lub połączeń. Jeśli do tego dojdzie, m ożliw e jest przeciążenie serw era zdalnego lub spow olnienie procesu na skutek konieczności przełączania kontekstu m iędzy zbyt dużą liczbą operacji. W yw ołanie liczby w spółprogram ów greenlet równej liczbie adresów URL do pobrania nie będzie już tak w ydajnym rozwiązaniem. W tym przypadku niezbędny jest mechanizm ograniczający liczbę jednocześnie tworzonych żądań HTTP. Liczba współbieżnych żądań może być kontrolowana ręcznie za pomocą semafora, aby na przy kład w danym m om encie ze stu współprogram ów greenlet realizowane były tylko żądania GET HTTP. Działanie semafora polega na zapewnianiu, że tylko określona liczba współprogramów będzie m ogła jednocześnie uzyskać dostęp do bloku kontekstu. Choć w rezultacie m ożliw e jest uruchomienie wszystkich współprogramów greenlet niezbędnych do natychmiastowego po brania adresów URL, w danej chwili tylko 100 z nich będzie w stanie utworzyć wywołania HTTP.
gevent
|
181
Semafory to jeden z typów mechanizmów blokowania często używanych w różnych przepły wach kodu przetwarzania równoległego. Ograniczając za pomocą różnych reguł postęp w wy konywaniu kodu, blokady mogą ułatw ić zagwarantowanie, że różne składniki programu nie będą ze sobą kolidowały. Gdy już skonfigurowano wszystkie konstrukcje fu tu re i umieszczono je w mechanizm ie blo kow ania w celu kontrolow ania przepływ u w spółprogram ów greenlet, m ożna poczekać do momentu rozpoczęcia uzyskiw ania wyników z funkcji gevent.iwait, która pobierze sekwencję konstrukcji fu tu re i dokona iteracji dla gotowych elementów. W odwrotnej sytuacji można za stosować funkcję gevent.wait, która zablokuje wykonyw anie program u do chwili zrealizow a nia w szystkich żądań. W ystępuje tu problem z dzieleniem żądań na porcje, a nie z jednoczesnym w ysyłaniem ich wszystkich, ponieważ przeciążenie pętli zdarzeń może spowodować zmniejszenie wydajności (dotyczy to w szystkich przypadków program ow ania asynchronicznego). N a podstaw ie do świadczeń ogólnie wiadomo, że optymalna liczba to około 100 jednocześnie otwartych połą czeń (rysunek 8.3). Jeśli zostałaby użyta ich mniejsza liczba, w dalszym ciągu m iałaby miejsce utrata czasu podczas oczekiw ania na zakończenie operacji w ejścia-w yjścia. W przypadku większej liczby połączeń zbyt często byłyby przełączane konteksty w pętli zdarzeń, a ponadto niepotrzebnie zw iększane byłoby obciążenie program u. Podana liczba 100 zależy od wielu czynników , takich jak kom puter używ any do uruchom ienia kodu, im plem entacja pętli zda rzeń, właściwości hosta zdalnego, oczekiwany czas odpowiedzi serwera zdalnego itp. Przed w yborem konkretnej opcji zalecam y poeksperym entow anie. Przykład 8.4 prezentuje kod utworzonego oprogramowania wyodrębniającego dane sieciowe HTTP w wersji bazującej na bibliotece gevent.
Znajdowanie odpowiedniej liczby żądań współbieżnych Czas trwania żądania 800 s Czas trwania żądania 50 s •- •Czas trwania żądania 300 s ► • ► Czas trwania żądania 550 s ■ ■ * '*
ii li li ‘ 'i -n
\
- **\ '» V
*
%
v
•
\ ' X '- * * ' ■ ! * *
... yfcft.......
***• •.
m
.
i
'''*"■ ,
0
100
200
300
400
Liczba jednoczesnych operacji pobierania Rysunek 8.3. Znajdowanie odpowiedniej liczby żądań współbieżnych
182
|
Rozdział 8. Współbieżność
500
Przykład 8.4. Oprogramowanie wyodrębniające dane sieciowe HTTP bazujące na bibliotece gevent from gevent import monkey monk ey.patch_socket() import gevent from gev en t. c o ro s import Semaphore import u r l l i b2 import s t r i n g import random def g e n e r a t e _ u r l s ( b a s e _ u r l , num_urls): f o r i in xrange(num_url s ) : y i e l d base_url + " " . jo i n ( r a n d o m . s a m p l e ( s t r i n g . a s c i i _ l o w e r c a s e , 10)) def c hu nk ed_ req uests (u rl s, chunk_size=100): semaphore = Semaphore(chunk_size) # O re qu est s = [gevent.spawn(download, u, semaphore) f o r u in u r l s ] # © f o r response in g e v e n t . i w a i t ( r e q u e s t s ) : y i e l d response def download(url, semaphore): with semaphore: # © data = u r l l i b 2 . u r l o p e n ( u r l ) re tu rn d a t a .r e a d ( ) def ru n_ exp er im en t(base _ur l, num_iter=500): u r l s = g e n e r a t e _ u r l s ( b a s e _ u r l , num_iter) resp onse_ fu tu res = c hu nk ed_req uests (u rl s, 100) # © re sp onse_si ze = su m ( le n ( r.v a lu e) f o r r in re sp onse_ fu tu res ) re tu rn re sp onse_si ze i f __name__ == "__main__": import time delay = 100 num_iter = 500 base_url = " h t t p : //127.0.0.1:8080/add?name=gevent&delay={}&".format(del ay) s t a r t = ti m e . ti m e () r e s u l t = ru n_exp er im en t( base _u rl, num_iter) end = t i m e . ti m e () print("Wynik: { } , Czas: { } " . f o r m a t ( r e s u l t , end - s t a r t ) )
O W tym miejscu generowany jest semafor, który um ożliwia wystąpienie operacji pobierania współprogramów greenlet c h u n k _ s i z e . © Używając semafora jako menedżera kontekstu, zapewniamy, że w danym m omencie tylko współprogramy greenlet będą mogły uruchomić zawartość kontekstu. © M ożliw e jest kolejkow anie dowolnej w ym aganej liczby w spółprogram ów greenlet przy założeniu, że żaden z nich nie zostanie uruchom iony do momentu rozpoczęcia pętli zda rzeń za pom ocą funkcji w a i t lub i w a i t . © Zm ienna r e s p o n s e _ f u t u r e s przechow uje teraz iterator zakończonych konstrukcji fu tu re, z których w szystkie zaw ierają żądane dane w e w łaściw ości . v a l u e . Alternatywnie możliwe jest zastosow anie biblioteki g r e q u e s t s w celu znacznego uproszczenia kodu biblioteki g e v e n t . Biblioteka g e v e n t zapewnia wszelkiego rodzaju niskopoziomowe, współ bieżne operacje gniazda, biblioteka g r e q u e s t s natom iast stanowi połączenie biblioteki HTTP r e q u e s t s i biblioteki g e v e n t . W rezultacie uzyskuje się bardzo prosty interfejs API służący do tworzenia żądań w spółbieżnych HTTP (interfejs obsługuje naw et automatycznie logikę sema fora). Dzięki bibliotece g r e q u e s t s kod staje się o wiele prostszy, bardziej zrozumiały i łatwiejszy do utrzymania, a jednocześnie w dalszym ciągu uzyskuje się szybkości porów nyw alne do szybkości niskopoziomowego kodu biblioteki g e v e n t (przykład 8.5).
gevent
| 183
Przykład 8.5. Oprogramowanie wyodrębniające dane sieciowe HTTP bazujące na bibliotece grequests import gr equ est s def ru n_e xp er im en t( base _ur l, num_iter=500): u r l s = g e n e r a t e _ u r l s ( b a s e _ u r l , num_iter) resp onse_ fu tu res = ( g r e q u e s t s . g e t ( u ) f o r u in u r l s ) # O responses = g r eq u est s.i m a p ( resp o n se_ f u tu res , s i z e = 100) # 0 re sp o n s e_size = s u m ( l e n ( r . t e x t ) f o r r in re sponses) r e tu r n re sp on s e_size
O Najpierw tworzone są żądania i uzyskiw ane konstrukcje future. Jest to realizowane przy użyciu generatora, aby później konieczne było sprawdzenie tylko takiej liczby żądań, jaką jesteśm y gotowi utworzyć. 0
M ożliwe jest teraz pobranie obiektów konstrukcji fu tu re i odwzorowanie ich na rzeczywiste obiekty odpowiedzi. Funkcja .imap zapewnia generator przekazujący obiekty odpowiedzi, dla których pobrano dane.
Godne uwagi jest to, że do zapewnienia asynchroniczności żądań wejścia-wyjścia użyto bi bliotek g e v e n t i g r e q u e s t s , ale podczas oczekiw ania na zakończenie operacji w ejścia-w yjścia nie są w ykonyw ane żadne obliczenia inne niż zw iązane z tymi operacjami. Rysunek 8.4 pre zentuje ogromne przyspieszenie, jakie jest uzyskiwane. Dzięki uruchamianiu większej liczby żądań w czasie oczekiwania na zakończenie poprzednich żądań możliwe jest osiągnięcie 69krotnego wzrostu szybkości! W yraźnie widać, jak now e żądania są wysyłane przed zakoń czeniem poprzednich żądań. Obrazują to poziom e linie reprezentujące żądania um ieszczone jedno na drugim. W yraźnie różni się to od w yniku zastosow ania przeszukiwacza działające go w sposób szeregowy (rysunek 8.2), w przypadku którego nowa linia na wykresie rozpo czyna się tylko po zakończeniu poprzedniej linii. Co więcej, można zauważyć więcej intere sujących efektów dotyczących kształtu osi czasu żądań dla biblioteki g e v e n t . Na przykład mniej w ięcej przy setnym żądaniu pojawia się przerwa w w yw oływ aniu now ych żądań. W ynika to z tego, że po raz pierwszy napotykany jest semafor. Możliwe jest zablokowanie go przed zakoń czeniem jakiegokolw iek poprzedniego żądania. Gdy to nastąpi, sem afor osiąga równowagę, w której jest blokowany od razu po zakończeniu kolejnego żądania, po czym zostaje odblokowany. 04 czasu wywołań dla biblioteki qrequests
0,0
0,5
1,0
Czas
Rysunek 8.4. Chronologia żądań HTTP dla przykładu 8.5
184
I
Rozdział 8. Współbieżność
1,5
3,0
tornado tornado to kolejny pakiet bardzo często używany w kodzie Python w przypadku asynchro nicznych operacji w ejścia-w yjścia. Został stw orzony przez firm ę Facebook głów nie z m yślą 0 klientach i serwerach HTTP. W przeciwieństw ie do biblioteki gevent, do zapewnienia asyn chronicznego działania pakiet tornado stosuje metodę w yw ołań zwrotnych. W wersji 3.x zo stał jednak poszerzony o funkcje, które zapewniają działanie podobne do w spółprogramów. Dzięki temu jest zgodny ze starszym kodem. W przykładzie 8.6 zaim plem entow ano ten sam przeszukiwacz sieci W W W co w przypadku biblioteki gevent, ale wykorzystano pętlę operacji wejścia-wyjścia pakietu tornado (jego wersję pętli zdarzeń) i klienta HTTP. Elim inuje to kłopotliw ą konieczność obsługi w sadow ej żądań 1 radzenia sobie z innym i, bardziej niskopoziom ow ym i aspektami kodu. Przykład 8.6. Oprogramowanie wyodrębniające dane HTTP, bazujące na pakiecie tornado from tornado import ioloop from t o r n a d o . h t t p c l i e n t import AsyncHTTPClient from tornado import gen from fu n c to o l s import p a r t i a l import s t r i n g import random Async H T TP C lien t. co n fi gu re ("t orna d o.curl_ h tt pclie nt.C u rl A sy n cH T TP C lien t" , m ax _clients=100) # O def g e n e r a t e _ u r l s ( b a s e _ u r l , num_urls): f o r i in xrange(num_url s ) : y i e l d base_url + " " . jo i n ( r a n d o m . s a m p l e ( s t r i n g . a s c i i _ l o w e r c a s e , 10)) @gen.coroutine def ru n_ exp er im en t(base _ur l, num_iter=500): h t t p _ c l i e n t = AsyncHTTPClient() u r l s = g e n e r a t e _ u r l s ( b a s e _ u r l , num_iter) responses = y i e l d [ h t t p _ c l i e n t . f e t c h ( u r l ) f o r url in u r l s ] # © response_sum = sum(len(r.bod y) f o r r in responses) r a i s e gen.Return(value=response_sum) # © if name == " main ": #...inicjalizacja... _io loop = i o lo o p .I O L o o p .i n sta n c e() run_func = p a rt i a l (ru n _ e x p e ri m e n t, b a s e _ u r l , num_iter) r e s u l t = _i oloop.r un _s yn c( ru n _fu nc) # ©
O M ożliw e jest skonfigurow anie klienta HTTP, w ybranie żądanej biblioteki bazow ej oraz liczby żądań, jakie m ają być przetw arzane w sadow o. © Generowanych jest wiele konstrukcji future, a następnie za pomocą funkcji yield są one prze kazywane pętli operacji wejścia-wyjścia. Funkcja ta ponowi działanie, a zm ienna responses zostanie za pomocą wszystkich konstrukcji future wypełniona wynikami, gdy będą gotowe. © Współprogramy w pakiecie tornado są wspierane przez generatory kodu Python. Aby zwrócić z nich wartość, konieczne jest zgłoszenie specjalnego wyjątku, który dekorator gen.coroutine przekształca w wartość zwracaną. © Funkcja ioloop.run_sync uruchom i pętlę IOLoop tylko na czas działania określonej funkcji. Z kolei funkcja ioloop.start() uruchomi pętlę IOLoop, która musi zostać ręcznie zakończona. Istotną różnicą między kodem pakietu tornado z przykładu 8.6 i kodem biblioteki gevent z przy kładu 8.4 jest m om ent uruchomienia pętli zdarzeń. W przypadku biblioteki pętla zdarzeń jest uruchom iona tylko w czasie działania funkcji iwait. Z kolei w kodzie pakietu tornado pętla
tornado
| 185
zdarzeń działa cały czas i kontroluje pełny przepływ w ykonyw ania program u, a nie tylko asynchroniczne części zw iązane z operacją wejścia-wyjścia. Pow oduje to, że p akiet t o r n a d o id ealnie nadaje się do zastosow ań pow iązanych głów nie z operacjami wejścia-wyjścia, w przypadku których większość, jeśli nie całość kodu aplikacji powinna być asynchroniczna. W tym przypadku pakiet t o r n a d o ma największe szanse na zy skanie sław y jako w ydajny serw er W W W . O kazuje się, że jed en z autorów w ielokrotnie utw orzył oparte na pakiecie t o r n a d o bazy danych i struktury danych, które w ym agają wielu operacji w ejścia-w yjścia2. Poniew aż biblioteka g e v e n t nie określa żadnych w ym agań w zglę dem programu jako całości, stanowi idealne rozwiązanie w przypadku problem ów pow iąza nych głównie z procesorem , które czasami obejmują intensyw ne operacje wejścia-wyjścia (np. program, który w ykonuje w iele obliczeń dla zbioru danych, a następnie musi odesłać wyniki do bazy danych w celu ich przechow yw ania). Staje się to jeszcze prostsze, gdy pod uwagę w eźmie się fakt, że większość baz danych oferuje proste interfejsy API HTTP, co oznacza, że możliwe jest naw et zastosow anie biblioteki g r e q u e s t s . O tym, jak duży poziom kontroli ma pętla zdarzeń pakietu t o r n a d o , możesz się przekonać po przyjrzeniu się tradycyjnem u kodow i pakietu, w którym w ykorzystyw ane są w yw ołania zw rotne z przykładu 8.7. W idać, że w celu rozpoczęcia w ykonyw ania kodu konieczne jest dodanie do pętli operacji wejścia-wyjścia punktu wejścia dla program u, a następnie urucho mienie programu. Aby zakończyć program, trzeba uważnie zastosow ać funkcję s t o p dla pętli operacji w ejścia-w yjścia i w yw ołać ją w odpow iednim m om encie. W rezultacie program y, które m uszą jaw nie używ ać w yw ołań zw rotnych, stają się niezw ykle uciążliw e i w krótkim czasie niemożliwe do utrzymania. Jednym z pow odów takiego stanu rzeczy jest to, że ślady nie mogą już przechowywać wartościowych informacji o tym, co zostało wywołane przez po szczególne funkcje, a także w jaki sposób uzyskano dostęp do w yjątku, od którego rozpoczną się działania. Trudne może się stać naw et zw ykłe stwierdzenie, jakie funkcje są wywoływane, poniew aż w celu w ypełnienia param etrów nieustannie są tw orzone funkcje częściow e. Nie jest zaskoczeniem, że jest to często określane mianem „piekła wyw ołań zw rotnych". Przykład 8.7. Przeszukiwacz oparty na pakiecie tornado z wywołaniami zwrotnymi from tornado import ioloop from t o r n a d o . h t t p c l i e n t import AsyncHTTPClient from fu n c to o l s import p a r t i a l A sy n c H T T P C lien t. co n fi g u re("to r n ad o .c u r l_ h tt p c li ent.CurlAsyncHTTPCli e n t " , max_clients=100) def f e t c h _ u r l s ( u r l s , c a l l b a c k ) : h t t p _ c l i e n t = AsyncHTTPClient() url s = l i s t ( u r l s ) responses = [ ] def _ f i n i s h _ f e t c h _ u r l s ( r e s u l t ) : # O r e sp o n s es.a p p en d (r esu lt ) i f l e n (r e sp o n ses) == l e n ( u r l s ) : c a l lb a c k ( r e s p o n s e s ) f o r url in u r l s : htt p c l i e n t . f e t c h ( u r l , c a l l b a c k = _ f i n i s h _ f e t c h _ u r l s) def ru n_ exp er im en t(base _ur l, num_iter=500, callback=None): u r l s = g e n e r a t e _ u r l s ( b a s e _ u r l , num_iter) callb ac k_ p as sth ro u = p a r t i a l ( _ f i n is h _ r u n _ e x p e r i m e n t ,
2 N a p r z y k ł a d fu g g e ta b o u tit (h tt p s ://g ith u b .c o m /m y n a m e is fib e r/fu g g e ta b o u tit) to s p e c j a l n y ty p p r o b a b i l i s t y c z n e j s t r u k t u r y d a n y c h (w ięc ej i n f o r m a c ji z a m i e s z c z o n o w p o d r o z d z i a l e „ P r o b a b i l i s t y c z n e s t r u k t u r y d a n y c h " ) , k t ó r a k o r z y s t a z p ę t l i IOLoop p a k i e t u tornado w c e l u p l a n o w a n i a z a d a ń b a z u j ą c y c h n a cz asie.
186
|
Rozdział 8. Współbieżność
c a l lb a c k = c a l lb a c k ) # © f e t c h _ u r l s ( u r l s , ca llb ac k _p as sth rou ) def _ fi n ish _r u n _exp e rim en t(r e sp o n ses, c a l l b a c k ) : response_sum = sum(len(r.bod y) f o r r in responses) p r i n t response_sum c allb ack() if name == 11 main ” : # ...inicjalizacja... _io loop = i o lo o p .I O L o o p .i n sta n c e() _ioloop .a d d _ c allb a c k(ru n _exp eri m en t, b a s e _ u r l , num_iter, _ i o l o o p .s t o p ) # © _i ol o o p . s t a r t ()
© Funkcja _ioloop.stop jest wysyłana jako wywołanie zw rotne do run_experiment, aby po za kończeniu eksperymentu automatycznie zakończyła pętlę operacji wejścia-wyjścia. © Kod asynchroniczny korzystający z wywołań zwrotnych obejmuje wiele operacji tworzenia funkcji częściowych. W ynika to z tego, że często konieczne jest zachow anie oryginalnego wywołania zwrotnego, które zostało wysłane, naw et pomimo tego, że w danym momencie niezbędne jest przeniesienie w ykonywania do innej funkcji. O Czasami „zabaw y" z zasięgiem są złem koniecznym w celu zachowania stanu, nie pow o dują jednak nieładu w globalnej przestrzeni nazw. Inną interesującą różnicą m iędzy biblioteką gevent i pakietem tornado jest sposób, w jaki we wnętrzne mechanizmy zmieniają wykresy wywołań żądań. Porównaj rysunki 8.5 i 8.4. Na wy kresie wywołań biblioteki gevent widoczne są obszary, w których linia ukośna wydaje się coraz cieńsza, a także obszary, gdzie linia ta zdaje się coraz grubsza. Cieńsze obszary prezentują czasy oczekiwania na zakończenie starych żądań przed wywołaniem now ych. Grubsze ob szary reprezentują miejsca, gdzie występuje zbyt duże obciążenie, aby odczytać odpowiedzi z żądań, które pow inny już być zakończone. Oba typy obszarów reprezentują czasy, w przy padku których pętla zdarzeń nie realizuje swoich zadań optymalnie. Są to czasy, gdy zasoby są niedostatecznie lub nadm iernie w ykorzystywane.
Rysunek 8.5. Chronologia żądań HTTP dla przykładu 8.6
tornado
|
187
Z kolei wykres wyw ołań pakietu t o r n a d o jest znacznie bardziej jednolity. Prezentuje, że pakiet t o r n a d o może lepiej zoptymalizować użycie zasobów. Może to wynikać z wielu rzeczy. W tym przypadku ważnym czynnikiem jest to, że logika semafora, która ogranicza liczbę jednocze snych żądań do 100, jest wewnętrzna względem pakietu t o r n a d o , dlatego może lepiej alokować zasoby. Oznacza to w stępne alokow anie i ponow ne w ykorzystyw anie połączeń w bardziej inteligentny sposób. Ponadto istnieje w iele pom niejszych efektów w yborów dokonywanych przez m oduły, zw iązanych z ich kom unikacją z jądrem , w celu skoordynow ania odbierania w yników z operacji asynchronicznych.
AsyncIO W odpowiedzi na popularność stosowania funkcji asynchronicznych do radzenia sobie z ob ciążającymi systemami operacji wejścia-wyjścia w języku Python w wersji 3.4 lub nowszych w prow adzono na now o stary, standardow y m oduł biblioteki a s y n c i o . W dużej m ierze na moduł ten ma w pływ metoda współbieżności stosowana w bibliotece g e v e n t i pakiecie t o r n a d o , w przypadku której w spółprogram y są definiowane i uzyskiwane z niej w celu wstrzymania wykonyw ania bieżącej funkcji i zezw olenia na działanie innych w spółprogram ów . Podobnie do kodu pakietu to r n a d o , pętla zdarzeń jest jawnie uruchamiana, aby rozpocząć wykonywanie współprogram ów . Ponadto w języku Python 3 w prow adzono nowe słowo kluczowe y i e l d fro m , które znacznie upraszcza radzenie sobie z tymi w spółprogram am i (nie jest już koniecz ne zgłaszanie w yjątku w celu zw rócenia w artości ze w spółprogram u, jak to m iało m iejsce w przykładzie 8.6). Godne uwagi jest to, że biblioteka a s y n c i o jest bardzo niskopoziomowa, a ponadto nie zapew nia użytkownikowi funkcji wysokopoziomowych. Na przykład choć istnieje bardzo kompletny interfejs API gniazd, nie ma prostej metody realizowania żądań HTTP. W efekcie decydujemy się na zastosowanie w przykładzie 8.8 biblioteki a i o h t t p . Jednakże zaadaptowanie tej biblioteki stanowi jedynie początek trudności, a „krajobraz" modułów pomocniczych będzie prawdopo dobnie zmieniał się bardzo szybko. Przykład 8.8. Oprogramowanie wyodrębniające dane HTTP bazujące na bibliotece asyncio import asyncio import ai o h t tp import random import s t r i n g def g e n e r a t e _ u r l s ( b a s e _ u r l , num_urls): f o r i in range(num_urls): y i e l d base_url + " " . jo i n ( r a n d o m . s a m p l e ( s t r i n g . a s c i i _ l o w e r c a s e , 10)) def chunked_http_client(num_chunks): semaphore = asyncio.Semaphore(num_chunks) # O @asy ncio .coro u tine def h t t p _ g e t ( u r l ) : # 0 nonlocal semaphore with (y ie l d from semaphore): response = y i e l d from a i o h t t p . r e q u e s t ( ' G E T ' , ur l) body = y i e l d from re s p o n s e .c o n t e n t . r e a d ( ) y i e l d from r e s p o n s e .w a i t _ f o r _ c l o s e ( ) re tu r n body re tu r n http _get def ru n_ exp er im en t(base _ur l, num_iter=500): u r l s = g e n e r a t e _ u r l s ( b a s e _ u r l , num_iter) h t t p _ c l i e n t = ch u nk ed_h tt p_clie nt(1 00)
188
|
Rozdział 8. Współbieżność
ta s k s = [ h t t p _ c l i e n t ( u r l ) f o r url in u r l s ] # © responses_sum = 0 f o r fu tu r e in a s y n c i o .a s _ c o m p l e te d (t a s k s ): # © data = y i e l d from fu tu r e responses_sum += le n (d a t a ) re tu rn responses_sum if name == " main ": import time delay = 100 num_iter = 500 base_url = " http://127.0.0 .1:8 08 0/ add?nam e=as yn cio& dela y= {}&". f o rmat(delay) loop = a sy n c io .g e t_ e v e n t_ lo o p () s t a r t = ti m e . ti m e () r e s u l t = l o o p .r un _un ti l_c om p le te (ru n _expe ri m en t( b ase _url , num_iter)) end = t i m e . ti m e () p r in t( " {} {}".fo rm a t(r e s u lt, en d-start))
O Jak w przykładzie kodu biblioteki gevent, w celu ograniczenia liczby żądań konieczne jest użycie semafora. © Zwracany jest nowy współprogram, który asynchronicznie pobierze pliki i uwzględni blo kowania semafora. © Funkcja http_client zwraca konstrukcje future. W celu śledzenia postępu konstrukcje te są zapisyw ane na liście. © Jak w przypadku biblioteki gevent, można poczekać na gotowość konstrukcji fu tu re i do konać dla nich iteracji. Jedną ze znakomitych korzyści uzyskanych dzięki użyciu modułu asyncio jest jego interfejs API, który jest powszechniej znany niż standardowa biblioteka. Upraszcza to tworzenie modułów pomocniczych. M ożliwe jest uzyskanie tego samego rodzaju wyników co w przypadku pakietu tornado lub biblioteki gevent. W razie potrzeby można „zagłębić się" bardziej w stosie i spowo dować, że protokoły asynchroniczne skorzystają z bogatego zestawu obsługiwanych struktur. Ponadto z uw agi na fakt, że jest to standardowy m oduł biblioteki, m am y pewność, że zawsze będzie on zgodny z dokumentami PEP (Python Enhancement Proposals) i możliwy do utrzymania w rozsądnym stopniu3. Co w ięcej, biblioteka asyncio um ożliw ia ujednolicenie m odułów takich jak tornado i gevent przez uruchom ienie ich w tej samej pętli zdarzeń. Okazuje się, że wersja pakietu tornado dla języka Python 3.4 jest w spierana przez bibliotekę asyncio. W rezultacie naw et pom im o tego, że pakietu tornado i biblioteki gevent dotyczą różne przypadki użycia, bazowa pętla zdarzeń będzie ujednolicona. Dzięki temu zmiana jednego modelu na inny model kodu pośredniego staje się trywialna. W dość prosty sposób możesz nawet tworzyć własne moduły opakowujące na bazie modułu asyncio, aby prowadzić interakcję z operacjami asynchronicznymi w możliwie najbardziej efektywny sposób w przypadku rozwiązywanego problemu. Choć jest to obsługiwane tylko w języku Python 3.4 i jego nowszych wersjach4, m oduł asyncio stanowi co najmniej znakom itą zapowiedź tego, że w przyszłości nadal będą prowadzone działania zw iązane z asynchronicznymi operacjami wejścia-wyjścia. Biorąc pod uwagę to, że 3 D o k u m e n t y P E P o kreś la ją, w ja k i s p o s ó b s p o ł e c z n o ś ć z w i ą z a n a z j ę z y k i e m P y t h o n d e c y d u j e o j e g o z m i a n a c h i rozw oju . Z e w z g l ę d u n a to, ż e d o k u m e n ty są częścią st and ar dow ej biblioteki, b ib lio te ka asyncio z a w s z e b ęd zie zg o d n a z n ajn ow szy m i s tand ar da m i P E P do ty czą cy m i języka, a p o n ad to sko rzysta z e w sz ystk ich n o w y c h funkcji. 4 W i ę k s z oś ć aplikacji i m o d u ł ó w do tyczący ch w y d ajn o śc i w d a l sz y m cią gu d ostępn a je st w „ e k o sy ste m ie " ję zyka P y th o n 2.7.
AsyncIO
| 189
język Python w coraz w iększym stopniu dom inuje w przypadku przetw arzania potoków (począw szy od przetw arzania danych, a skończyw szy na przetw arzaniu żądań interneto w ych), taka zm iana nabiera dużego sensu. Na rysunku 8.6 zaprezentowano oś czasu żądań w przypadku wersji oprogram owania wy odrębniającego dane HTTP bazującego na m odule asyncio.
Rysunek 8.6. Chronologia żądań HTTP dla przykładu 8.8
Przykład z bazą danych Aby poprzednie przykłady stały się bardziej konkretne, zostanie określony kolejny prosty problem, który jest powiązany głównie z procesorem , ale zawiera potencjalnie ograniczający składnik w postaci operacji wejścia-wyjścia. Obliczane będą liczby pierwsze. Znalezione licz by będą zapisyw ane w bazie danych, która może być dowolna. Ten przypadek reprezentuje każdy problem, w przypadku którego program musi przeprow adzić intensywne obliczenia, a ich wyniki trzeba zapisać w bazie danych, co może spow odow ać duży spadek wydajności na skutek operacji wejścia-wyjścia. Oto jedyne ograniczenia, jakie obowiązują dla bazy danych: • Baza zawiera interfejs API HTTP, dlatego m ożliw e jest użycie kodu podobnego do zasto sowanego w prezentow anych przykładach5. • Czasy odpowiedzi w ynoszą około 50 ms. • Baza danych może jednocześnie obsługiw ać w iele żądań6.
5 N i e je st to k o n ie c z n e . S ł u ż y je d y n i e u p r o s z c z e n iu k o d u . 6 D o t y c z y to w s z y s t k i c h r o z p r o s z o n y c h i i n n y c h p o p u l a r n y c h b a z d a n y c h , t a k i c h j a k P o s tg r e s , M o n g o D B , R i a k itp.
190
|
Rozdział 8. Współbieżność
Zaczniem y od prostego kodu, który oblicza liczby pierw sze i kieruje żądanie do interfejsu API HTTP bazy danych każdorazowo po znalezieniu liczby pierwszej: from t o r n a d o . h t t p c l i e n t import HTTPClient import math h t t p c l i e n t = HTTPClient() def sa v e _ p rim e _ se ria l(p rim e ): url = " h t t p : / / 1 2 7 .0 . 0 .1 : 8 0 8 0 / a d d ? p r i m e = { } " .f o rmat(prime) response = h t t p c l i e n t . f e t c h ( u r l ) fi n ish _sav e_p r im e (r es p o n se , prime) de f fi n ish _sav e_p r im e (r es p o n se , prime): i f re sponse.code != 200: p r i n t "Błąd podczas zapisywania l i c z b y p ier w szej: { } ".f o r m a t(p r i m e ) def check_prime(number): i f number % 2 == 0: re tu rn F a l s e f o r i in xran g e (3 , in t(m ath .s qrt( n u m b er )) + 1, 2 ) : i f number % i == 0: re tu rn F a l s e re tu rn True def cal cu late_ prim es _se rial (m ax _n um ber ): f o r number in xrange(max_number): i f check_prime(number): s a v e _ p r i m e _ s e r i a l (number) re turn
Podobnie jak w przykładzie przetwarzania szeregowego (przykład 8.3), czasy żądań dla każ dej operacji zapisu w bazie danych (50 ms) nie nakładają się, dlatego takie obciążenie czasowe będzie dotyczyć każdej znalezionej liczby pierwszej. W efekcie w yszukiw anie dla parametru max_number o w artości 8192 (powoduje znalezienie 1028 liczb pierwszych) zajmie 55,2 sekundy. W iem y jednak, że z powodu sposobu działania żądań szeregowych na realizow anie operacji wejścia-wyjścia poświęcane jest co najmniej 51,4 sekundy! Oznacza to, że tylko dlatego, że pro gram jest wstrzymywany podczas wykonywania operacji wejścia-wyjścia, tracone jest 93% czasu. Zależy nam natom iast na określeniu sposobu zm iany schematu żądań tak, aby jednocześnie m ożliwe było utworzenie asynchronicznie wielu żądań. Dzięki temu w yelim inuje się uciąż liwy okres oczekiwania zw iązany z realizowaniem operacji wejścia-wyjścia. W tym celu two rzymy klasę AsyncBatcher, która zajm uje się przetwarzaniem w sadowym żądań i tworzeniem ich w razie potrzeby: import gr equ est s from i t e r t o o l s import iz i p c l a s s A s y n c B a t c h e r (o b je c t ): slo ts = ["b atch ", "b atch _size", "save", "flu sh "] def in it ( s e l f , batch_size): s e l f . b a t c h _ s i z e = b a tc h _ siz e s e l f . b a t c h = [] def s a v e ( s e l f , prime): url = " h t t p :/ / 1 2 7 .0 . 0 .1 : 8 0 8 0 / a d d ? p r i m e = { } " .f o rmat(prime) self.batch .ap p e nd ((u rl,p rim e)) i f l e n ( s e l f . b a t c h ) == s e l f . b a t c h _ s i z e : sel f . f l ush() def f l u s h ( s e l f ) : re sp o ns es_ fu tu res = ( g r e q u e s t s . g e t ( u r l ) f o r u r l , _ in s e l f . b a t c h ) responses = gr eq ues ts.m ap(re sp onse s_ fu ture s) f o r re spon se , ( u r l , prime) in i z i p ( r e s p o n s e s , s e l f . b a t c h ) : fi n ish _sav e_p r im e (r es p o n se , prime) s e l f . b a t c h = []
Przykład z bazą danych
|
191
Na tym etapie m ożliw e jest postępow anie w niem al taki sam sposób jak w cześniej. Jedyną zasadniczą różnicą jest to, że do klasy AsyncBatcher dodaw ane są now e liczby pierw sze, aby decydowała, kiedy będą wysyłane żądania. Ponieważ stosowane jest przetwarzanie wsadowe, konieczne jest zapewnienie wysłania ostatniego zadania wsadowego, naw et jeśli nie jest pełne (oznacza to utworzenie wywołania metody AsyncBatcher.flush()). def calculate_primes_async(max_number): b atc h er = AsyncBatcher(100) # O f o r number in xrange(max_number): i f check_prime(number): batcher.save(number) batch er.flush () re tu rn
O Zdecydowano się na przetw arzanie w sadowe przy 100 żądaniach z pow odów podobnych do przedstawionych na rysunku 8.3. Po takiej zmianie możliwe jest skrócenie czasu działania dla parametru max_number o wartości 8192 do 4,09 sekundy. Oznacza to 13,5-krotne przyspieszenie bez dużego nakładu pracy. W ograniczo nym środowisku, takim jak potok danych czasu rzeczywistego, takie dodatkowe przyspieszenie może decydować o tym, czy system będzie w stanie podołać wymaganiom, czy nie (w tym przy padku niezbędna będzie kolejka, więcej informacji na ten temat zamieszczono w rozdziale 10.). Na rysunku 8.7 zaprezentowano podsum ow anie tego, jak opisane zm iany wpływ ają na czas działania w przypadku różnych obciążeń. W porównaniu z kodem szeregowym przyspieszenie kodu asynchronicznego jest znaczące, choć w dalszym ciągu są to w yniki odległe od szybkości uzyskiwanych w przypadku problemu związanego wyłącznie z procesorem. Aby temu całko wicie zaradzić, niezbędne byłoby użycie takich modułów jak multiprocessing, które zapewnią niezależny proces radzący sobie z obciążeniem operacji wejścia-wyjścia programu bez spo walniania rozwiązywania części problemu zależnej od procesora.
Rysunek 8.7. Czasy przetwarzania dla różnej liczby liczb pierwszych
192
|
Rozdział 8. Współbieżność
Podsumowanie Podczas rozwiązywania problem ów w ystępujących w rzeczywistych systemach produkcyj nych często konieczne jest naw iązanie komunikacji z zew nętrznym źródłem. Takim źródłem może być baza danych działająca na innym serwerze, komputer innego pracownika lub usługa danych, która zapewnia nieprzetworzone dane konieczne do przetworzenia. Gdy ma to miejsce, problem szybko może stać się pow iązany z operacjami wejścia-wyjścia. Oznacza to, że w ięk szość czasu działania może zostać poświęcona na zajm owanie się operacjami wejścia-wyjścia. W spółbieżność jest pomocna w przypadku problem ów dotyczących operacji w ejścia-wyjścia, ponieważ umożliwia przeplatanie obliczeń z potencjalnie wieloma takimi operacjami. Pozwala to w ykorzystać fundam entalną różnicę m iędzy operacjami wejścia-wyjścia i operacjami pro cesora do skrócenia ogólnego czasu wykonywania kodu. Jak w cześniej pokazano, biblioteka gevent zapew nia interfejs najw yższego poziom u na po trzeby asynchronicznych operacji w ejścia-w yjścia. Z kolei pakiet tornado um ożliw ia ręczne kontrolowanie sposobu działania pętli zdarzeń. Dzięki temu m ożesz jej użyć do szeregowa nia dowolnego rodzaju zadania. M oduł asyncio w przypadku języka Python w wersji 3.4 lub nowszej umożliwia pełną kontrolę stosu asynchronicznych operacji wejścia-wyjścia. Oprócz różnych poziom ów abstrakcji każda biblioteka używ a innego m odelu dla sw ojej składni (różnice w ynikają głównie z braku w budowanej obsługi współbieżności przed pojawieniem się języka Python 3 i wprowadzeniem instrukcji yield from). Zalecamy zdobycie doświadczenia z zakresu tych m etod i wybieranie jednej z nich na podstaw ie tego, jak bardzo niskopoziomowa kontrola jest w ymagana. W przypadku trzech om ów ionych bibliotek w ystępują niew ielkie różnice szybkości. W iele z tych różnic w ynika ze sposobu szeregow ania w spółprogram ów . Na przykład biblioteka tornado radzi sobie świetnie z uruchamianiem operacji asynchronicznych i szybkim w zna w ianiem w ykonyw ania w spółprogram u. Z kolei biblioteka asyncio, choć w ydaje się działać trochę gorzej, umożliwia dostęp do interfejsu API na znacznie niższym poziomie, a ponadto m oże być w bardzo dużym stopniu dostrajana. W następnym rozdziale zajmiemy się zagadnieniem obliczania współbieżnego obecnym przy problem ach pow iązanych z operacjami wejścia-wyjścia oraz zastosowaniem go w przypadku problem ów dotyczących procesora. Dzięki temu m ożliw e będzie nie tylko jednoczesne wy konywanie wielu operacji wejścia-wyjścia, ale też wielu operacji obliczeniowych. W ten sposób zaczniemy tworzenie w pełni skalowalnych programów. W ich przypadku m ożesz osiągnąć większą szybkość po prostu przez dodanie kolejnych zasobów komputerowych, które poradzą sobie z problemami cząstkowymi.
Podsumowanie
| 193
194
I
Rozdział 8. Współbieżność
____________________ ROZDZIAŁ 9.
Moduł multiprocessing
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Co oferuje m oduł multiprocessing? • Jaka jest różnica m iędzy procesami i wątkami? • W jaki sposób w ybrać w łaściw ą w ielkość puli procesów? • Jak używ ać kolejek nietrw ałych do przetwarzania zadań? • Jakie są obciążenia i zalety komunikacji międzyprocesowej? • Jak można przetw arzać dane narzędzia numpy za pom ocą wielu procesorów? • Dlaczego konieczne jest blokowanie w celu uniknięcia utraty danych?
Dom yślnie narzędzie CPython nie używa wielu procesorów . Po części wynika to z tego, że język Python został stworzony w czasach dominacji jednordzeniow ych układów , a po części z faktu, że w łaściw ie w ykorzystyw anie przetw arzania rów noległego w w ydajny sposób m oże być dość trudne. Język Python zapewnia um ożliw iające to narzędzia, ale pozw ala nam dokonać w yboru. Ponieważ przykro jest stwierdzić, że kom puter z procesorem w ielordze niowym korzysta tylko z jednego rdzenia do realizowania długotrwałego procesu, w roz dziale dokonamy przeglądu metod jednoczesnego użycia w szystkich rdzeni procesora znaj dujących się w komputerze.
^
W a r t o p r z y p o m n i e ć , ż e w s p o m i n a l i ś m y j u ż o n a r z ę d z i u C P y th o n ( p o w s z e c h n i e u ż y w an ej im p le m e n ta c ji ję z y k a ). W ję z y k u P y th o n n ie m a n icz e g o , co n ie p o z w a la ło b y n a z a sto s o w a n ie sy s te m ó w w ie lo rd z en io w y ch . Im p le m e n ta c ja n a rz ę d z ia C P y th o n nie m o ż e e f e k t y w n i e k o r z y s t a ć z w i e l u r d z e n i , a l e i n n e i m p l e m e n t a c j e (n p . P y P y z m a j ą c ą w k ró tc e się p o ja w i ć p a m ię c ią tr an sak c y jn ą) m o g ą n ie b y ć t y m o g ran ic zo n e .
Ż yjem y w św iecie układów w ielordzeniow ych. W laptopach pow szechnie są stosow ane 4 rdzenie, a wkrótce staną się popularne 8-rdzeniowe konfiguracje komputerów stacjonarnych. Dostępne są procesory serwerowe wyposażone w 10, 12 i 15 rdzeni. Jeśli realizowane zadanie m oże być jednocześnie wykonyw ane na wielu procesorach bez zbyt dużego nakładu pracy inżynieryjnej, w arto rozważyć ten wariant.
195
W przypadku użycia zestaw u procesorów do przetw arzania w sposób rów noległy m ożesz oczekiw ać m aksym alnie n-krotnego przyspieszenia przy korzystaniu z n rdzeni. Jeśli dyspo nujesz kom puterem 4-rdzeniow ym i na potrzeby zadania m ożesz w ykorzystać w szystkie rdzenie, zadanie m oże zostać w ykonane w czasie stanow iącym jedną czw artą pierw otnego czasu działania. M ało praw dopodobne jest przyspieszenie większe niż 4-krotne. W praktyce uzyskasz raczej 3- lub 4-krotne przyrosty szybkości. Każdy dodatkowy proces zwiększy obciążenie zw iązane z kom unikacją i zm niejszy ilość do stępnej pamięci RAM, dlatego rzadko możliwe będzie uzyskanie pełnego n-krotnego przy spieszenia. Zależnie od rozwiązywanego problemu obciążenie zw iązane z komunikacją może stać się na tyle duże, że zauważysz bardzo znaczne spowolnienie. Tego rodzaju problemy wy stępują często tam, gdzie złożoność jest związana z dowolnego typu programowaniem prze twarzania rów noległego i norm alnie w ym aga zm iany w algorytm ie. Z tego w łaśnie pow odu programowanie przetwarzania równoległego często uważane jest za coś w rodzaju sztuki. Jeśli nie jesteś zaznajomiony z prawem Amdahla (http://pl.wikipedia.org/wiki/Prawo_Amdahla), w arto poświęcić trochę czasu na przeczytanie podstaw ow ych informacji na ten temat. Prawo głosi, że jeśli tylko niewielka część kodu może być przetwarzana równolegle, nie ma znaczenia liczba procesorów wykorzystana w tym celu, ponieważ przeważnie taki kod nie będzie działać znacznie szybciej. Jeśli nawet przetwarzanie równoległe może dotyczyć dużej części czasu działania, ist nieje skończona liczba procesorów, które mogą być efektywnie używane w celu przyspieszenia ogólnego procesu przed wystąpieniem momentu, gdy da się zauważyć zmniejszanie się szybkości. M oduł multiprocessing umożliwia przetw arzanie równoległe oparte na procesach i wątkach, współużytkow anie zadań za pośrednictwem kolejek oraz danych między procesami. Moduł obsługuje głównie przetwarzanie równoległe z wykorzystaniem jednego komputera z wieloma rdzeniami (istnieją lepsze opcje w przypadku przetwarzania równoległego bazującego na wielu kom puterach). Gdy rozw iązyw any jest problem pow iązany z procesorem , bardzo częstym wariantem jest przetwarzanie równoległe zadania za pomocą zestawu procesów. Choć moż liwe jest wykorzystanie tego wariantu do przetwarzania równoległego problemu powiązanego z operacjami wejścia-wyjścia, jak zaprezentowano w rozdziale 8., dostępne są narzędzia, które lepiej się do tego nadają (czyli nowy moduł asyncio dla języka Python w wersji 3.4 lub nowszej oraz biblioteka gevent lub tornado dla języka Python w wersji 2 lub nowszej).
^
O p e n M P to n i s k o p o z i o m o w y i n t e r f e js d l a w i e l u r d z e n i . M o ż e s z z a s t a n a w i a ć się, c z y s k o n c e n tr o w a ć się raczej n a n im n iż n a m o d u le m u ltip ro ce ssin g . Z o s ta ł z a p r e z e n to w a n y w r o z d z i a l e 7. p r z y o m a w i a n i u k o m p i l a t o r ó w C y t h o n i P y t h r a n , a l e n i e b ę d z i e o p i s y w a n y w ty m ro zd ziale . M o d u ł m u ltip r o c e ssin g d z ia ła n a w y ż s z y m p o z io m ie , w s p ó łu ż y tk u ją c stru k tu r y d a n y c h P y th o n . Z k o le i in terfejs O p e n M P w s p ó łp ra c u je z p o d s t a w o w y m i o b i e k t a m i j ę z y k a C (n p . z l i c z b a m i c a ł k o w i t y m i i z m i e n n o p r z e c i n k o w y m i ) p o s k o m p i l o w a n i u k o d u d o p o s t a c i k o d u C . U ż y c i e i n t e r fe js u m a s e n s ty l k o w t e d y , g d y k o m p i l u j e s z k o d . W p r z e c i w n y m r a z i e (n p. k i e d y k o r z y s t a s z z w y d a j n e g o k o d u n a r z ę d z i a numpy i c h c e s z g o w y k o n a ć p r z y u ż y c i u w i e l u r d z e n i ) p o z o s t a n i e p r z y m o d u l e m u l t i p r o c e s s i n g b ę d z i e p r a w d o p o d o b n i e w ł a ś c i w ą d e c y z ją .
Gdy zamierzasz przetw arzać równolegle zadanie, m usisz m yśleć trochę inaczej niż w przy padku standardowej m etody tworzenia procesu szeregowego. M usisz też zaakceptow ać to, że debugowanie zadania przetwarzanego równolegle jest trudniejsze, a często może być bardzo frustrujące. Zalecamy zachowanie przetwarzania równoległego w jak najprostszej postaci (jeśli naw et nie zostanie w pełni w ykorzystana m oc kom putera), aby „pojazd program istyczny" nadal unosił się wysoko.
196
|
Rozdział 9. Moduł multiprocessing
Szczególnie złożonym zagadnieniem jest współużytkow anie stanu w systemie przetwarzania równoległego. Choć to zadanie m oże spraw iać wrażenie prostego, pow oduje wiele proble m ów i może być trudne do zrozumienia. Istnieje wiele przypadków użycia, z którymi związane są różne kompromisy, i zdecydow anie nie ma żadnego uniwersalnego rozwiązania. W pod rozdziale „Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej" zaj miemy się współużytkowaniem stanu z uwzględnieniem obciążenia związanego z synchro nizacją. Unikanie stanu współużytkow anego sprawi, że praca będzie znacznie prostsza. O kazuje się, że m ożliw e jest przeanalizow anie algorytm u w celu spraw dzenia, jak dobrze będzie działać w środowisku przetwarzania równoległego, praw ie wyłącznie przez określe nie, w jakim stopniu w ym agane jest w spółużytkow anie stanu. Jeśli na przykład możliwe jest, aby wiele procesów kodu Python rozwiązywało ten sam problem bez kom unikowania się ze sobą (taka sytuacja jest określana mianem niebywale równoległej), dodawanie kolejnych takich procesów nie spow oduje dużego spadku wydajności. Z kolei jeśli każdy proces kodu Python musi komunikować się z pozostałymi, związane z tym obciążenie będzie pow odow ać pow olne zm niejszanie się wydajności przetwarzania. Oznacza to, że w m iarę dodawania kolejnych procesów kodu Python w rzeczywistości może dojść do spadku ogólnej wydajności. W efekcie czasam i w ym agane będzie w prow adzenie nieintuicyjnych zm ian w algorytm ie, aby możliwe było efektywne rozwiązanie problemu w sposób równoległy. Na przykład pod czas rozwiązywania równania dyfuzji (rozdział 6.) z wykorzystaniem przetwarzania równole głego każdy proces wykonuje w rzeczywistości nadmiarowe zadania, które są też realizowane przez inny proces. Taka nadm iarow ość zmniejsza ilość niezbędnej kom unikacji i przyspiesza całość obliczeń! Oto kilka typowych zadań odpowiednich dla modułu multiprocessing: • Przetwarzanie równoległe zadania powiązanego z procesorem z wykorzystaniem obiektów Process lub Pool. • Przetwarzanie równoległe zadania powiązanego z operacją wejścia-wyjścia w obiekcie Pool z wykorzystaniem wątków i modułu dummy (dziwna nazwa). • W spółużytkow anie zadań poddanych serializacji przez m oduł pickle za pośrednictwem obiektu Queue. • Współużytkowanie stanu między przetwarzanymi równolegle procesami roboczymi, w tym bajtów, podstawowych typów danych, słowników i list. Jeśli znasz język, w którym wątki są używane na potrzeby zadań pow iązanych z procesorem (np. C++ lub Java), nie powinno być dla Ciebie tajemnicą to, że choć wątki w języku Python są rzeczywistymi w ątkami systemu operacyjnego, a nie wątkam i symulowanymi, są ograni czane przez blokadę GIL (Global Interpreter Lock). Oznacza to, że w danym m om encie tylko jeden w ątek m oże prow adzić interakcję z obiektami języka Python. Procesy um ożliw iają równoległe uruchamianie kilku interpreterów języka Python, z których każdy dysponuje prywatnym obszarem pamięci z własną blokadą GIL. Ponadto poszczegól ne interpretery są uruchamiane po kolei (a zatem nie ma miejsca rywalizowanie o każdą blo kadę GIL). Jest to najprostszy sposób przyspieszenia wykonania zadania powiązanego z pro cesorem w przypadku kodu Python. Jeśli w ym agane jest w spółużytkow anie stanu, będzie się to wiązać z koniecznością zastosowania komunikacji. Zostanie to omówione w podrozdziale „W eryfikowanie liczb pierw szych za pom ocą komunikacji m iędzyprocesow ej".
Moduł multiprocessing
|
197
Jeśli korzystasz z tablic narzędzia numpy, m ożesz zastanaw iać się, czy możliwe jest utworzenie większej tablicy (np. dużej macierzy dwuwymiarowej) i zażądanie od procesów równoległego przetwarzania segmentów tablicy. Choć jest to możliwe, trudno stwierdzić metodą prób i błę dów w jaki sposób, dlatego w podrozdziale „Współużytkowanie danych narzędzia numpy za pomocą modułu multiprocessing" zajmiemy się przykładem współużytkowania tablicy narzę dzia numpy o wielkości 6,4 GB przez cztery procesory. Zamiast wysyłać częściowe kopie danych (co spow odow ałoby co najm niej podw ojenie w ym aganej w ielkości roboczej pam ięci RAM i w ygenerowanie ogromnego obciążenia związanego z komunikacją), bazow e bajty tablicy są współużytkow ane między procesami. Jest to idealna m etoda współużytkowania dużej tablicy między lokalnymi procesam i roboczymi na jednym komputerze.
^
W ro z d z ia le o m a w ia n e je st z a s t o s o w a n ie m o d u łu m u ltip ro ce s sin g n a k o m p u te r a c h z sy ste m a m i u n ik so w y m i (rozdział n a p isa n o , k o rzystając z sy stem u U b u n tu ; kod w n ie zm ie n ion ej p o sta ci p o w in ie n d z ia ła ć n a k o m p u te r a c h M ac ). W p rz y p a d k u k o m p u t e r ó w z s y s t e m e m W i n d o w s n a l e ż y z a j r z e ć d o o fic ja ln e j d o k u m e n ta c ji (h tt p s ://d o c s . p y th o n .o r g / 2 flib m r y /m u t t ip m c e s s m g M m r).
W dalszej części rozdziału trw ale określim y liczbę procesów (NUM_PROCESSES=4) jako zgodną z czterema fizycznymi rdzeniami obecnymi w laptopie jednego z autorów. Dom yślnie moduł multiprocessing użyje takiej liczby rdzeni, jaką w ykryje (system laptopa ma osiem rdzeni — cztery procesory i cztery hiperwątki). Jeśli nie ma specyficznego zarządzania zasobam i, zw y kle będziesz unikać trwałego określania w kodzie liczby procesów do utworzenia.
Moduł multiprocessing Moduł multiprocessing został zaprezentowany w języku Python 2.6. W tym celu istniejący moduł pyProcessing został opakow any za pom ocą w budow anego zestaw u bibliotek języka Python. Oto główne komponenty modułu: Process Rozwidlona kopia bieżącego procesu. Kom ponent tworzy identyfikator nowego procesu, a zadanie działa jako niezależny proces podrzędny w system ie operacyjnym . M ożliw e jest załadow anie komponentu Process i odpytywanie jego stanu, a także udostępnienie go z m etodą target w celu uruchomienia. Pool K om ponent opakow uje interfejs API Process lub threading.Thread do postaci w ygodnej w użyciu puli procesów roboczych, które współużytkują zadania i zwracają zagregowany wynik. Queue Kolejka FIFO umożliwiająca użycie wielu producentów i konsumentów. Pipe Jedno- lub dwukierunkowy kanał komunikacji między dwom a procesami. Manager Interfejs zarządzany w ysokiego poziom u, który służy do w spółużytkow ania obiektów kodu Python m iędzy procesam i. ctypes Pozw ala na w spółu żytkow anie podstaw ow ych typów danych (np. liczb całkow itych, liczb zm iennoprzecinkow ych i bajtów ) m iędzy procesam i po ich rozw idleniu. Elementy podstawowe synchronizacji Blokady i sem afory synchronizujące przepływ kontroli między procesami.
198
|Rozdział 9. Moduł multiprocessing
^
W ję z y k u P y th o n 3.2 w p r o w a d z o n o m o d u ł c o n c u r r e n t.fu tu r e s (za p o ś r e d n ic tw e m d o k u m e n t u P E P 3 1 4 8 (h ttp ://le g a c y .p y th o n .o r g /d e v /p e p s /p e p -3 1 4 8 /) ). Z a p e w n i a o n p o d s ta w o w ą fu n k cjo n a ln o ś ć m o d u łu m u ltip ro ce ssing z p ro s ts z y m interfejsem b a z u ją c y m n a p a k ie cie j a v a .u t i l .c o n c u r r e n t ję z y k a Jav a. M o d u ł je st d o stę p n y ja k o p op raw ka do w c z e ś n ie js z y c h w e r s ji ję z y k a P y t h o n (h t t p s :// p y p i.p y t h o n .o r g / p y p i/fu t u r e s ) . N ie z o s t a n i e t u ta j o m ó w i o n y , p o n i e w a ż n i e j e s t t a k e l a s t y c z n y j a k m o d u ł m u l t i p r o c e s s i n g , a l e p o d e jrz e w a m y , ż e z p o w o d u co raz w ię k szej p o p u la r n o ś ci ję z y k a P y th o n w w e rsji 3 lub n o w szej w m iarę u p ły w u czasu m o d u ł co n cu rren t.fu tu res zastąp i go.
W pozostałej części rozdziału zostanie zaprezentow any zestaw przykładów demonstrujących typowe m etody użycia tego modułu. Zostanie określone przybliżenie liczby pi przy użyciu m etody M onte Carlo. W tym celu dla zw ykłego kodu Python oraz kodu narzędzia numpy zostan ie zastosow any kom ponent Pool z procesam i lub w ątkam i. Jest to prosty problem o dobrze rozpoznanej złożoności, dlatego w łatw y sposób m ożna przetw orzyć go rów nolegle. Ponadto użycie w ątków w przypadku kodu narzędzia numpy spowoduje uzyskanie nieoczekiwanego wyniku. Korzystając z tej samej m etody bazującej na kom ponencie Pool , poszukam y następnie liczb pierw szych. Przeanali zujem y nieprzewidyw alną złożoność takiej operacji, a także przyjrzym y się temu, jak można efektyw nie (i nieefektyw nie!) dzielić obciążenie robocze w celu najlepszego w ykorzystania zasobów obliczeniowych. W yszukiw anie liczb pierw szych zostanie zakończone przez przełą czenie na kolejki, w przypadku których zostaną przed staw ione obiekty Process zam iast obiektu Pool , a ponadto zostanie użyta lista „trujących pigułek" do kontrolowania cyklu życia procesów roboczych. N astępnie zajm iem y się kom unikacją m iędzyprocesow ą IPC (Interprocess Com m unication) w celu sprawdzenia poprawności niewielkiego zbioru możliwych liczb pierwszych. Przez roz dzielenie obciążenia związanego z każdą liczbą m iędzy w iele procesorów w przypadku zna lezienia dzielnika komunikacja IPC umożliwia wcześniejsze zakończenie wyszukiwania. Dzięki temu m ożliw e jest znaczące zw iększenie szybkości w porównaniu z procesem wyszukiwania realizowanym z wykorzystaniem jednego procesora. W celu przeanalizowania kompromisów dotyczących złożoności i możliwości każdego rozwiązania zostaną omówione też współużyt kowane obiekty języka Python, obiekty podstawowe systemu operacyjnego oraz serwer Redis. Aby rozdzielić duże obciążenie bez kopiowania danych, możliwe jest w spółużytkow anie ta blicy narzędzia numpy o wielkości 6,4 GB z wykorzystaniem czterech procesorów. Jeśli istnieją duże tablice z operacjam i um ożliw iającym i przetw arzanie rów noległe, m etoda ta pow inna zapew nić duże przyspieszenie, gdyż w ym aga przydziału m niejszego obszaru w pam ięci RAM i skopiowania mniejszej ilości danych. Na końcu przyjrzym y się synchronizacji dostępu do pliku i zmiennej (np. Value) między procesam i bez uszkadzania danych. Ma to na celu za prezentow anie sposobu popraw nego blokowania współużytkowanego stanu.
^
K o m p i l a t o r P y P y ( o m ó w i o n o g o w r o z d z i a le 7.) w p e ł n i o b s ł u g u je b i b l io t e k ę m u ltip ro ce ssin g . W z a m i e s z c z o n y c h dalej p r z y k ła d a c h z k o d e m C P y th o n d z ia ła ł o n z n a c z n i e s z y b c i e j w p r z y p a d k u u ż y c i a k o m p i l a t o r a P y P y ( w c z a s i e p i s a n i a tej k s i ą ż k i n i e d o t y c z y ł o to p r z y k ł a d ó w z k o d e m n a r z ę d z i a numpy). J e ś l i n a p o t r z e b y p r z e tw a rz a n ia ró w n o le g łe g o u ż y w a s z w y łą c z n ie k o d u C P y th o n (bez ro z sz e rz e ń ję z y k a C lu b b a rd z iej z ło ż o n y c h b ib lio tek), k o m p ila to r P y P y m o ż e s z y b k o z a p e w n ić w z ro st w y d ajności.
Moduł multiprocessing
|
199
W rozdziale (oraz w reszcie książki) skoncentrowano się na systemie Linux, w którym znaj duje się proces rozwidlający. Jego zadaniem jest tworzenie nowych procesów przez klonowanie procesu nadrzędnego. Ponieważ system W indow s pozbaw iony jest procesu rozwidlającego, m oduł multiprocessing nakłada pew ne ograniczenia specyficzne dla tego system u (https://docs. python.org/2/library/m ultiprocessing.htm l). Jeśli korzystasz z tej platform y, zalecam y ich prze analizow anie.
Przybliżenie liczby pi przy użyciu metody Monte Carlo Przybliżenie liczby pi jest możliwe przez rzucenie tysiącami wyimaginowanych rzutek w stro nę tarczy reprezentowanej przez pełne koło. Relacja między liczbą rzutek trafiających w obrębie koła i poza nim umożliwi określenie przybliżenia liczby pi. Jest to pierwszy idealny problem, ponieważ możliwe jest równom ierne rozdzielenie łącznego obciążenia m iędzy daną liczbę procesów , z których każdy działa na osobnym procesorze. Każdy proces zostanie zakończony w tym samym czasie, gdyż m ają jednakow e obciążenie. Oznacza to, że m ożliw e jest analizowanie dostępnych przyspieszeń w m om encie użycia dla problemu nowych procesorów i hiperwątków. Na rysunku 9.1 przedstawiono sytuację, w której na kwadratowej powierzchni umieszczono 10 000 rzutek. Część procentowa rzutek trafiła w ćwiartkę narysowanego koła. Takie przybli żenie jest raczej kiepskie. 10 000 rzutek nie zapewni wiarygodnego wyniku z trzema miejscami dziesiętnymi po przecinku. Po uruchomieniu w łasnego kodu zauważysz, że to przybliżenie będzie w ahać się dla każdego wykonania między wartościam i 3,0 i 3,2.
Przybliżenie liczby pi jako wartości 3,1472 przy użyciu metody Monte Carlo z 10 000 rzutek
Rysunek 9.1. Przybliżenie liczby pi przy użyciu metody Monte Carlo
200
|
Rozdział 9. Moduł multiprocessing
Aby być pewnym trzech pierw szych miejsc dziesiętnych po przecinku, konieczne jest w yge nerow anie 10 000 000 losow ych rzutów rzutkam i . C hoć je st to n ieefektyw ne rozw iązanie (istnieją lepsze m etody przybliżania liczby pi), pozwala w raczej w ygodny sposób zademon strować korzyści przetwarzania równoległego za pom ocą m odułu multiprocessing. W przypadku m etody M onte Carlo używ ane jest twierdzenie Pitagorasa (http://pl.wikipedia.org/ wiki/Twierdzenie_Pitagorasa) do sprawdzenia, czy rzutka wylądowała w obrębie koła:
V(x 2 + y 2) < 1
2
Poniew aż stosow any jest obszar koła, m ożna go zoptym alizow ać przez usunięcie operacji wyznaczania pierwiastka kwadratowego (1 = 1). W efekcie do zaim plem entow ania pozosta nie następujące uproszczone wyrażenie: x 2 + y2 < 1 W przykładzie 9.1 przyjrzymy się wersji tego przybliżenia wykorzystującej pętlę. Zaim ple m entow ana zostanie zarów no norm alna w ersja kodu Python, ja k i w ersja kodu narzędzia numpy. W obu przy p ad kach będ ą u żyw ane w ątki i pro cesy do p rzetw o rzen ia problem u w sposób równoległy.
Przybliżanie liczby pi za pomocą procesów i wątków Ponieważ łatwiej zrozum ieć zw ykłą im plementację kodu Python, zaczniem y od niej w tym podrozdziale, korzystając w pętli z obiektów typu zmiennopozycyjnego. Przetwarzanie rów noległe zostanie przeprowadzone za pom ocą procesów, które użyją w szystkich dostępnych procesorów. Zostanie też w ykonana wizualizacja stanu komputera w przypadku zastosow a nia dodatkowych procesorów.
Zastosowanie obiektów języka Python Implem entacja kodu Python jest łatwa do prześledzenia, ale w iąże się z nią obciążenie, po nieważ każdy obiekt typu zmiennopozycyjnego języka Python w ym aga zarządzania, przy woływania i synchronizowania. Takie obciążenie wydłuża czas działania, ale zapewnia czas na przemyślenia, gdyż im plementacja ta może zostać szybko przygotowana. Przetwarzanie rów noległe tej w ersji kodu um ożliw ia uzyskanie dodatkow ych przyspieszeń przy bardzo niewielkim dodatkowym nakładzie pracy. Na rysunku 9.2 pokazano następujące trzy im plem entacje przykładowego kodu Python: • bez zastosowanego modułu multiprocessing (na wykresie z etykietą Wykonywanie szeregowe), • z zastosowanym i wątkami, • z użytymi procesami.
1 W i ę c e j i n f o rm a c ji d o s t ę p n y c h je st p o d adrese m : h ttp ://m a th .m isso u ristate.ed u /a ssets/M ath /brett.p p tx .
Przybliżanie liczby pi za pomocą procesów i w ątków
|
201
Rysunek 9.2. Zastosowanie wykonywania szeregowego, wątków i procesów Gdy używ any jest więcej niż jeden w ątek lub proces, od interpretera języka Python żąda się obliczenia takiej samej całkowitej liczby rzutek i równom iernego rozdzielenia zadań między procesy robocze. Jeśli w przypadku im plementacji kodu Python w sum ie ma być 100 000 000 rzutek, a ponadto używ ane są dwa procesy robocze, od obu w ątków lub procesów żądane będzie w ygenerowanie po 50 000 000 rzutek dla każdego procesu roboczego. Użycie jednego wątku zajm uje w przybliżeniu 120 sekund. Zastosowanie dwóch lub większej liczby w ątków wymaga dłuższego czasu. Korzystając z dwóch lub większej liczby procesów, skracamy czas działania. Koszt zrezygnowania z użycia procesów lub wątków (dotyczy imple mentacji z wykonywaniem szeregowym) jest taki sam jak dla implementacji z jednym procesem. W przypadku użycia procesów uzyskano liniowe przyspieszenie na laptopie jednego z auto rów, gdy zastosowano dwa lub cztery rdzenie. W wariancie z ośmioma procesami roboczymi zastosowano technologię Hyper-Threading Technology firmy Intel. Laptop zawiera tylko cztery fizyczne rdzenie, dlatego uruchom ienie ośmiu procesów pow oduje jedynie znikom e dodat kowe przyspieszenie. Przykład 9.1 prezentuje w ersję kodu Python przybliżającego liczbę pi. Jeśli używane są wątki, każda instrukcja jest ograniczana przez blokadę GIL. Oznacza to, że choć każdy w ątek może działać na osobnym procesorze, będzie wykonywany tylko wtedy, gdy nie są aktywne żadne inne wątki. W ersji kodu bazującej na procesach nie dotyczy to ograniczenie, ponieważ każdy proces rozw idlony m a pryw atny interp reter języka Python, który działa jako pojedynczy wątek. Jako że żadne obiekty nie są współużytkowane, nie występuje rywalizowanie o blo kadę GIL. Choć w przykładzie zastosowano wbudowany generator liczb losowych języka Python, w podrozdziale „Liczby losowe w systemach przetwarzania równoległego" zamieszczono kilka uw ag dotyczących zagrożeń w ynikających z przetw arzania rów noległego sekw encji liczb losow ych. Zapoznaj się z nim i.
202
|
Rozdział 9. Moduł multiprocessing
Przykład 9.1. Przybliżanie liczby pi za pomocą pętli w kodzie Python def e st im ate_n b r_ poin ts _i n _ q u a r t e r _ c i r c l e ( n b r _ e s t i m a t e s ) : n b r_trials_in _q u arter_u n it_circle = 0 f o r st ep in x r a n g e ( i n t ( n b r _ e s t i m a t e s ) ) : x = random.uniform(0, 1) y = random.uniform(0, 1) i s _ i n _ u n i t _ c i r c l e = x * x + y * y <= 1 .0 n b r _ t r i a l s _ i n _ q u a r t e r _ u n i t _ c i r c l e += i s _ i n _ u n i t _ c i r c l e re tu rn n b r _ t r i a l s _ i n _ q u a r t e r _ u n i t _ c i r c l e
Przykład 9.2 prezentuje blok __main__. Zauważ, że przed uruchomieniem licznika czasu two rzony jest obiekt Pool. Tworzenie wątków odbywa się stosunkowo szybko. Tworzenie procesów obejmuje rozwidlanie, dlatego zajmuje to dużą część sekundy. Na rysunku 9.2 zignorowano to obciążenie, ponieważ będzie ono stanowić znikomą część ogólnego czasu wykonywania. Przykład 9.2. Funkcja main przybliżania liczby pi za pomocą pętli from m u lti p r oce ssin g import Pool if
name == 11 main 11: nbr_sam pl es_i n_total = 1e8 n br_parallel_blocks = 4 pool = P oo l ( p ro c e s s e s = n b r_ p a r a l le l _ bl o c k s ) nbr_samples_per_worker = nbr_sam pl es_i n_total / n b r _ p a r a l l e l _ b l o c k s p r i n t "Tworzenie próbek {} dla procesu roboczego".format(nbr_samples_per_worker) n b r_ tr i a ls_p er _p ro c ess = [nbr_samples_per_worker] * nbr_paral le l _ b l o c k s t1 = t i m e . ti m e () n b r _ i n _ u n i t _ c i r c l e s = p o o l .m a p ( c a l c u l a te _ p i , n b r _ t r i a ls _ p e r _ p r o c e s s ) p i_ e st im a t e = su m ( n b r_ i n _ u n i t_ c i rc l e s ) * 4 / nbr_sam ples_in_total p r i n t "Przyb liżon a l i c z b a p i " , p i_ estim ate p r i n t " D e l t a : " , t i m e . ti m e () - t1
Tworzona jest lista zawierająca argument nbr_estimates dzielony przez liczbę procesów robo czych. Ten now y argum ent będzie w ysyłany do każdego procesu roboczego. Po w ykonaniu zostanie uzyskana taka sama liczba w yników . Zostaną one zsum ow ane w celu otrzymania przybliżenia liczby rzutek znajdujących się w obrębie koła. Z modułu multiprocessing importowany jest obiekt Pool bazujący na procesach. M ożliwe było też użycie instrukcji from multiprocessing.dumny import Pool w celu uzyskania wersji kodu z wąt kami. Nazwa dummy (fikcyjny) pow oduje raczej niewłaściwe zrozum ienie (przyznajemy, że nie wiemy, dlaczego posłużono się taką w łaśnie nazwą). dummy to po prostu uproszczony obiekt opakowujący m oduł threading w celu prezentowania tego samego interfejsu co w przypadku obiektu Pool bazującego na procesach. G o d n e u w a g i j e s t to , ż e k a ż d y t w o r z o n y p r o c e s z u ż y w a t r o c h ę s y s t e m o w e j p a m i ę c i R A M . M o ż e s z s p o d z i e w a ć się, ż e p r o c e s r o z w i d l a j ą c y u ż y w a j ą c y s t a n d a r d o w y c h b i b lio te k w y k o r z y s ta p a m i ę ć R A M w ie lk o ś c i rz ę d u 10 - 2 0 M B . Je śli u ż y w a s z w ie lu b ib lio tek i d a n y ch , m o ż e sz o cz ek iw a ć, ż e k a ż d a ro z w id lo n a k o p ia z a jm ie setki m e g a b a jtó w . W s y s t e m ie z o g r a n ic z o n ą ilo ścią p a m i ę c i R A M m o ż e to b y ć p o w a ż n y p roblem . W p rz y p ad k u b rak u pam ięci R A M i p o w rócen ia przez system d o d y sko w e g o o b sz a ru w y m ia n y w s z elk ie k o rz y śc i w y n ik a ją ce z p rz e tw a rz a n ia ró w n o leg łeg o zo stan ą w z n a cz n y m stopniu z m arn o w a n e z p o w o d u w oln ego stronicow an ia z na p rz e m ie n n y m w y k o rz y sta n ie m p a m ię ci R A M i dysku!
Przybliżanie liczby pi za pomocą procesów i w ątków
| 203
Na zamieszczonych dalej rysunkach pokazano wykresy średniego wykorzystania procesorów z czterem a fizycznym i rdzeniam i, które znajdują się w laptopie jednego z autorów , a także z towarzyszącymi im czterema hiperwątkami (każdy z nich działa na bazie niewykorzysty wanych układów fizycznego rdzenia). Dane zebrane do wygenerowania tych rysunków obej mują czas uruchamiania pierwszego procesu kodu Python oraz obciążenie związane z urucha mianiem podprocesów. M oduł próbkujący procesorów rejestruje cały stan dla laptopa, a nie tylko czas procesora zajmowany przez dane zadanie. Zauważ, że zamieszczone dalej diagramy są tworzone za pomocą innej metody pomiaru czasu bazującej na mniejszej szybkości próbkowania niż w przypadku rysunku 9.2. Oznacza to, że ogólny czas działania jest trochę dłuższy. Sposób wykonywania zobrazowany na rysunku 9.3, gdzie użyto jednego procesu w puli (wraz z procesem nadrzędnym), uwidacznia obciążenie w pierw szych sekundach podczas tworze nia puli, a następnie przez resztę czasu działania ciągłe w ykorzystanie procesora bliskie 100%. W przypadku jednego procesu efektywnie używany jest jeden rdzeń. Wykorzystanie procesora z upływem czasu
0
15
30
45
60
75
90
105
Czas (sekundy)
Rysunek 9.3. Przybliżanie liczby pi za pomocą obiektów Python i jednego procesu Następnie dodamy drugi proces, co oznacza: Pool(processes=2). Jak w idać na rysunku 9.4, do danie drugiego procesu pow oduje skrócenie czasu wykonywania do 56 sekund, czyli mniej w ięcej o połowę. W tym przypadku dwa procesory są w pełni w ykorzystywane. Jest to naj lepszy wynik, jakiego można oczekiwać. Skutecznie użyto w szystkich now ych zasobów obli czeniowych, a ponadto w żadnym stopniu nie jest tracona szybkość z powodu innych obciążeń, takich jak komunikacja, stronicowanie na dysk lub rywalizujące procesy, które chcą skorzystać z tych samych procesorów. Na rysunku 9.5 pokazano wyniki w przypadku zastosowania czterech fizycznych procesorów. Oznacza to w ykorzystanie w pełni mocy obliczeniowej laptopa. Czas wykonyw ania stanowi w przybliżeniu jedną czwartą (27 sekund) czasu wykonania wersji kodu bazującej na jednym procesie.
204
|Rozdział 9. Moduł multiprocessing
0
7
14
21
28
35
42
49
Czas (sekundy)
Rysunek 9.4. Przybliżanie liczby pi za pomocą obiektów Python i dwóch procesów Wykorzystanie procesora z upływem czasu
Czas (sekundy)
Rysunek 9.5. Przybliżanie liczby pi za pomocą obiektów Python i czterech procesów W porównaniu z w ersją z czterema procesami przełączenie na osiem procesów (rysunek 9.6) pozwala osiągnąć znikom e przyspieszenie. W ynika to z tego, że cztery hiperw ątki m ogą za pewnić jedynie niew ielką ilość dodatkowej mocy obliczeniowej na bazie w olnych układów scalonych czterech procesorów , które są już maksym alnie w ykorzystywane. Rysunki pokazują, że z każdym krokiem efektywnie w ykorzystywana jest w iększa ilość do stępnych zasobów procesorowych, a ponadto że zasoby technologii hiperwątkowości stanowią niewiele wnoszący dodatek. W przypadku stosowania hiperwątków największym problemem jest to, że kompilator CPython używa wiele pamięci RAM. Ponieważ hiperw ątkow ość nie jest przyjazna dla pamięci podręcznej, wolne zasoby w każdym procesorze są kiepsko wykorzysty wane. Jak się okaże w dalszej części rozdziału, narzędzie numpy lepiej korzysta z tych zasobów.
Przybliżanie liczby pi za pomocą procesów i w ątków
| 205
Wykorzystanie procesora z upływem czasu
9
1
6
9
12
15
18
21
24
Czas (sekundy) Rysunek 9.6. Przybliżanie liczby pi za pomocą obiektów Python i ośmiu procesów przy znikomym dodatkowym przyspieszeniu d o św iad czen ia w iem y , ż e h ip e rw ą tk o w o ść m o ż e z a p e w n ić w zro st w y d ajn ości do 3 0 % , je ś li d o s t ę p n a j e s t w y s t a r c z a j ą c a i l o ś ć w o l n y c h z a s o b ó w o b l i c z e n i o w y c h . B ę d z i e ta k n a p rz y k ła d w ted y , g d y u ż y w a n a je st raczej k o m b in a c ja o p eracji a ry tm e ty c z n y c h n a licz b a ch ca łk o w ity ch i z m ie n n o p r z e c in k o w y c h n iż o p e ra cje z m ie n n o p r z e c in k o w e , ja k w o m a w ia n y m przy kładzie. D zięki p o łą cz en iu w y m a g a ń d o ty cz ą cy ch zaso b ó w h ip e rw ą tk i m o g ą „ z a p la n o w a ć " u ż y c ie w iększej liczby u k ła d ó w p ro c es ora p o d k ą te m d ziałan ia w s p ó łb ież n eg o . O g ó ln ie rzecz biorąc, h ip e rw ą tk i są trak to w a n e ja k o d o d a t k o w y b o n u s, a n ie zasó b , w odn iesien iu do którego m a b y ć p rz e p ro w a d z a n a o p ty m a liz a c ja . W y n i k a to stą d , ż e d o d a n ie w ię k s z e j lic z b y p r o c e s o r ó w b ę d z ie p r a w d o p o d o b n i e b a r d z i e j e k o n o m i c z n y m r o z w i ą z a n i e m n i ż d o s t r a j a n i e k o d u (c o p o w o d u j e o b cią ż e n ie z w ią z a n e z e w s p a r c ie m p ro g ra m isty cz n y m ).
Użyjemy teraz w ątków w jednym procesie, a nie w wielu. Jak się okaże, obciążenie spowo dowane rywalizacją o blokadę GIL w rzeczywistości spowolni kod. Na rysunku 9.7 pokazano dwa wątki rywalizujące w systemie z dwom a rdzeniami w przy padku kodu Python 2.6 (ten sam efekt dotyczy w ersji 2.7 języka). Jest to obraz prezentujący rywalizację o blokadę GIL, który za zgodą pobrano z w pisu na blogu Davida Beazleya zaty tułowanego The Python GIL Visualized (Wizualizacja rywalizacji o blokadę GIL) http://dabeaz. blogspot.co.uk/2010/01/python-gil-visualized.html. Ciemniejszy odcień czerwieni reprezentuje wątki kodu Python, które wielokrotnie próbują uzyskać blokadę GIL, lecz bez powodzenia. Jaśniejszy odcień zieleni reprezentuje działający wątek. Biały kolor identyfikuje krótkie okresy bezczynno ści wątku. Widać, że w przypadku kompilatora CPython występuje obciążenie podczas doda wania w ątków do zadania powiązanego z procesorem . Obciążenie wynikające z przełączania kontekstu właściwie ma wpływ na ogólny czas działania. David Beazley wyjaśnia to w artykule Understanding the Python GIL (Opis blokady GIL dla języka Python) http://www.dabeaz.com/GIL/. W ątki w przypadku języka Python n adają się znakom icie na potrzeby zadań zw iązanych z operacjami wejścia-wyjścia, ale stanowią kiepską opcję przy problemach, w przypadku któ rych bazuje się głównie na procesorach.
206
|
Rozdział 9. Moduł multiprocessing
Rysunek 9.7. Wątki kodu Python rywalizujące w przypadku komputera z dwoma rdzeniami Każdorazowo, gdy w ątek uaktywnia się i próbuje uzyskać blokadę GIL (niezależnie od tego, czy jest ona dostępna, czy nie), używ a zasobów system ow ych. Jeśli jeden w ątek jest zajęty, inny będzie w ielokrotnie uaktyw niany, a następn ie spróbuje uzyskać blokadę GIL. Takie powtarzane próby stają się kosztowne. David Beazley udostępnił interaktywny zestaw wykresów (http://www.dabeaz.com/GIL/gilvis/index.html), które demonstrują problem. M ożesz pow iększyć je, aby ujrzeć każdą nieudaną próbę uzyskania blokady GIL dla wielu wątków w przypadku wielu procesorów . Zauważ, że jest to jedyny problem dotyczący wielu wątków działających w system ie w ielordzeniow ym . W system ie jed nord zeniow ym z w ielom a w ątkam i nie ma miejsca rywalizacja o blokadę GIL. Z łatwością można to stwierdzić dzięki możliwej do powięk szenia wizualizacji czterowątkowej (http://www.dabeaz.com/GIL/gilvis/fourthread.html), która znajduje się w witrynie internetowej Davida. Jeśli w ątki nie rywalizow ałyby o blokadę GIL, lecz w efektyw ny sposób przekazyw ałyby ją na przemian między sobą, nie należałoby oczekiwać w ystąpienia żadnego ciemniejszego od cienia czerwieni. Zam iast tego można spodziew ać się oczekującego wątku, który kontynuuje oczekiwanie bez zużywania zasobów. Uniknięcie rywalizacji o blokadę GIL skróciłoby ogólny czas działania, ale z powodu tej blokady w dalszym ciągu nie uzyskano by wydajności w ięk szej niż w przypadku zastosowania pojedynczego wątku. Jeśli nie istniałaby blokada GIL, każ dy w ątek mógłby działać równolegle bez żadnego oczekiwania, a tym samym wątki w yko rzystywałyby wszystkie zasoby systemowe. G odne uw agi jest to, że niekorzystny w pływ w ątków w odniesieniu do problem ów po wiązanych z procesorem w łaściw ie w yelim inow ano w języku Python w wersji 3.2 lub nowszej (https://docs.python.org/dev/whatsnew/3.2.html): P r z e b u d o w a n o m e c h a n i z m s z e r e g o w e g o w y k o n y w a n ia w s p ó łb ie ż n ie d z ia ła ją c y c h w ą t k ó w k o d u P y th o n (o g ó ln ie z n a n y ja k o b lo k a d a G IL lu b G lo b a l In terp reter Lock). M o d y fik a c ja m ia ła n a celu m ię d z y in n y m i u z y sk a n ie b ard ziej p rz e w id y w a ln y c h in terw ałó w p rz e łą cz a n ia i m n ie jsz eg o o b ciążenia s p o w o d o w a n e g o ryw alizacją o blokadę, a tak że rozw iązanie kilku zaistn iałych p ro b le m ó w sy stem o w y ch . W y c o fa n o pojęcie „interw ału sp r a w d z a n ia " u m o ż liw ia ją ceg o p rzełączenia w ą tk ó w i z astąp io n o je b e z w z g lę d n y m c z a se m trw an ia w y r a ż o n y m w sek u n d ach . — R a y m o n d H ettin ger
Na rysunku 9.8 pokazano wyniki uruchomienia tego samego kodu, który został zastosowany w przypadku rysunku 9.5, lecz z w ątkami zam iast procesów. Choć używ anych jest kilka pro cesorów , każdy z nich w nieznacznym stopniu dzieli się obciążeniem . Jeśli każdy w ątek działałby bez blokady GIL, w ystąpiłoby 100-procentow e w ykorzystanie każdego z czterech procesorów. Zamiast tego każdy procesor jest częściowo wykorzystywany (z powodu blokady GIL). Ponadto procesory działają wolniej, niż oczekiwano, na skutek rywalizacji o blokadę GIL. Dla porównania przyjrzyj się rysunkowi 9.3, na którym jeden proces w ykonuje to samo za danie w czasie w ynoszącym w przybliżeniu 120, a nie 160 sekund.
Przybliżanie liczby pi za pomocą procesów i w ątków
| 207
W yko rzysta n ie p ro ce so ra z u p ły w e m c za su
0
20
40
00
60
100
120
140
Czas (sekundy) Rysunek 9.8. Przybliżanie liczby pi za pomocą obiektów Python i czterech wątków
Liczby losowe w systemach przetwarzania równoległego G enerowanie dobrych sekw encji liczb losow ych stanowi pow ażny problem . Jeśli podejm ie się próbę ich samodzielnego tworzenia, z łatwością można uzyskać niew łaściw e sekwencje. Szybkie otrzymanie dobrej sekwencji w przypadku przetwarzania równoległego jest jeszcze trudniejsze. Od razu trzeba będzie m artw ić się o to, czy w procesach rów noległych zostaną uzyskane pow tarzające się lub skorelowane sekwencje. W przykładzie 9.1 użyto wbudowanego generatora liczb losowych języka Python. W przykła dzie 9.3 w następnym podrozdziale zostanie zastosowany generator liczb losowych narzędzia numpy. W obu przypadkach dla generatorów liczb losowych w artości początkow e są określane w ich procesie rozw idlonym . W przykładzie z generatorem random języka Python ustalanie w artości początkow ych jest obsługiw ane w ew nętrznie przez m oduł multiprocessing. Jeśli podczas rozwidlania m oduł „stwierdzi", że generator random znajduje się w przestrzeni nazw, wymusi wywołanie w celu określenia w artości początkowej dla generatorów w każdym no wym procesie. W zamieszczonym dalej przykładzie z narzędziem numpy będzie trzeba jaw nie określić w arto ści początkow e. Jeśli w przypadku tego narzędzia zapom nisz określić w artości początkow e dla sekw encji liczb losow ych, każdy z procesów rozw idlonych w ygeneruje identyczną se kwencję liczb losowych. Jeśli istotna jest jakość liczb losowych używ anych w procesach równoległych, zalecam y zaję cie się tym zagadnieniem w szerszym zakresie, ponieważ nie jest ono tutaj omawiane. Choć prawdopodobnie generatory liczb losowych narzędzia numpy i języka Python są w ystarczająco dobre, jeśli znaczące w yniki zależą od jakości sekwencji liczb losowych (np. w przypadku systemów medycznych lub finansowych), będziesz musiał zapoznać się z tym zagadnieniem.
208
|Rozdział 9. Moduł multiprocessing
Zastosowanie narzędzia numpy W tym punkcie zostanie użyte narzędzie numpy. Przykładowy problem z rzucaniem rzutkami spraw dza się idealnie w przypadku w ektoryzow anych operacji narzędzia numpy. Te sam e przybliżenia są generow ane ponad 50 razy szybciej niż we w cześniej zam ieszczonych przy kładach z kodem Python. Główna przyczyna tego, że narzędzie to jest szybsze niż czysty kod Python przy rozwiązy w aniu identycznego problem u, tkwi w tw orzeniu i m odyfikow aniu przez narzędzie numpy tych samych typów obiektów na bardzo niskim poziom ie w ciągłych blokach pam ięci RAM. Zastępuje to rozwiązanie polegające na tworzeniu wielu obiektów kodu Python na wysokim poziomie, z których każdy wymaga indywidualnego zarządzania i adresowania. Ponieważ narzędzie numpy jest znacznie bardziej przyjazne, jeśli chodzi o pam ięć podręczną, niewielkie przyspieszenie zostanie też uzyskane podczas korzystania z czterech hiperwątków. Nie było to możliwe w przypadku wersji z czystym kodem Python, gdyż pam ięci podręczne nie są używane efektywnie przez większe obiekty kodu Python. Na rysunku 9.9 przedstawiono następujące trzy scenariusze: • bez zastosowanego modułu multiprocessing (na wykresie z etykietą Wykonywanie szeregowe), • z zastosowanym i wątkami, • z użytymi procesami.
Rysunek 9.9. Zastosowanie wykonywania szeregowego, wątków i procesów w przypadku narzędzia numpy W ersje kodu z w ykonyw aniem szeregow ym i jednym procesem roboczym są w ykonyw ane z taką samą szybkością — nie w ystępuje obciążenie zw iązane z zastosowaniem wątków wraz z narzędziem numpy (w przypadku tylko jednego procesu roboczego nie ma też miejsca wzrost wydajności).
Przybliżanie liczby pi za pomocą procesów i w ątków
| 209
Gdy używ anych jest wiele procesów, zauważalne jest klasyczne stuprocentowe w ykorzysta nie każdego dodatkowego procesora. W ynik stanowi kopię lustrzaną wykresów z rysunków 9.3, 9.4, 9.5 i 9.6, ale oczywiście kod działa znacznie szybciej, gdy bazuje na narzędziu numpy. Co ciekaw e, w ersja kodu z w ątkam i działa szybciej, gdy u żyje się w iększej liczby w ątków . O dw rotnie było w przypadku czystego kodu Python, w przypadku którego w ątki spow ol niły wykonyw anie przykładowego kodu. Jak zostało to opisane na stronie wiki serwisu SciPy (http://wiki.scipy.org/ParallelProgramming), dzięki działaniu bez korzystania z blokady GIL na rzędzie numpy m oże osiągnąć na bazie wątków pew ien poziom dodatkowego przyspieszenia. Użycie procesów zapewnia przew idyw alne przyspieszenie, tak jak miało to m iejsce w przy kładzie czystego kodu Python. Zastosowanie drugiego procesora podwaja szybkość, a użycie czterech procesorów spowoduje czterokrotny w zrost szybkości. Przykład 9.3 prezentuje w ektoryzowaną postać kodu. Zauważ, że wartości początkow e dla generatora liczb losowych są określane w momencie wywoływania funkcji z kodem. W przy padku wersji kodu z wątkami nie jest to konieczne, ponieważ każdy wątek współużytkuje ten sam generator liczb losowych, a ponadto wątki uzyskują do niego dostęp w sposób szeregowy. Ponieważ w wersji kodu z procesami każdy nowy proces jest rozwidleniem, wszystkie wersje z rozwidleniem będą w spółużytkow ać ten sam stan. Oznacza to, że w każdym procesie wy w ołania liczb losow ych zw rócą identyczną sekw encję! W yw ołanie funkcji seed() pow inno zapewnić, że każdy z procesów z rozw idleniem w ygeneruje unikalną sekw encję liczb loso wych. W podrozdziale „Liczby losowe w systemach przetwarzania rów noległego" zam iesz czono kilka uwag dotyczących zagrożeń zw iązanych z sekwencjami liczb losow ych w przy padku przetwarzania równoległego. Przykład 9.3. Przybliżanie liczby pi za pomocą narzędzia numpy def estim ate_n b r_p oin ts _i n_qu ar ter _c irc le( n b r_ sa m pl e s ) : # ustawianie losow ej w artości p oczątkow ej d la narzędzia numpy w każdym nowym p r o c e sie # je ś li nie zostanie to wykonane, p r o c e s z rozwidleniem przyjm ie, że wszystkie p ro c esy współużytkują ten sam stan np.random.seed() xs = np.random.uniform(0, 1, nbr_samples) ys = np.random.uniform(0, 1, nbr_samples) e s t i m a t e _ i n s i d e _ q u a r t e r _ u n i t _ c i r c l e = (xs * xs + ys * ys) <= 1 n b r _ t r i a l s _ i n _ q u a r t e r _ u n i t _ c i r c l e = n p .s u m (e s t im a t e _ i n s i d e _ q u a r te r _ u n it _ c ir c l e ) r e tu r n n b r _ t r i a l s _ i n _ q u a r t e r _ u n i t _ c i r c l e
Krótka analiza kodu pokazuje, że wywołania generatora random działają trochę wolniej na da nym komputerze, gdy są wykonyw ane za pom ocą wielu wątków, a wywołanie dla (xs * xs + ys * ys) <= 1 jest dobrze przetw arzane równolegle. W ywołania generatora liczb losowych są pow iązane z blokadą GIL, ponieważ wew nętrzna zmienna stanu to obiekt kodu Python. Proces pozw alający to zrozum ieć okazał się prosty, lecz wiarygodny. Składa się on z nastę pujących kroków: 1. U żyw ając szeregow ej w ersji kodu, w staw znak kom entarza w e w szystkich w ierszach kodu narzędzia numpy i wykonaj go bez korzystania z żadnych wątków. Uruchom kod kil kakrotnie i zarejestruj czasy w ykonyw ania za pom ocą funkcji time.tim e() um ieszczonej w fu n k cji main . 2. Z pow rotem dodaj w iersz (jako pierw szy dodaliśm y w iersz xs = np.random.uniform(.. . ) ) i uruchom go kilkakrotnie, ponow nie rejestrując czasy w ykonyw ania.
210
|
Rozdział 9. Moduł multiprocessing
3. Z powrotem dodaj następny wiersz (tym razem dodawany jest w iersz ys = . . . ) , uru chom go i zarejestruj czas wykonywania. 4. Powtórz proces, uwzględniając w iersz nbr_trials_in_quarter_unit_circle = np.sum (...). 5 . Ponow nie pow tórz ten proces, lecz tym razem z czterem a w ątkam i. Pow tórz proces w iersz po w ierszu. 6 . Porów naj różnicę w czasie działania dla każdego kroku w przypadku braku w ątków i czterech w ątków . Ponieważ kod jest w ykonyw any w sposób równoległy, trudniejsze staje się użycie takich na rzędzi jak line_p rofiler lub cProfile. Rejestrowanie samych czasów wykonywania i obser w ow anie różnic w sposobie działania w przypadku różnych konfiguracji w ym aga trochę cierpliwości, ale zapewnia solidny dowód, na podstawie którego można w yciągnąć wnioski.
^
A b y z r o z u m i e ć d z i a ł a n i e w s p o s ó b s z e r e g o w y w y w o ł a n i a f u n k c j i uniform, p r z y j r z y j s i ę k o d o w i g e n e r a t o r a m t r a n d w k o d z ie ź r ó d ło w y m n a r z ę d z ia n u m p y (h t t p s :/ / g it h u b .c o m / n u m p y /n u m p y /t r e e / m a s t e r /n u m p y /r a n d o m /m t r a n d ) i p r z e ś l e d ź w y w o ł a n i e tej f u n k c j i w p l i k u s k r y p t u m tr a n d .p y x . B ę d z i e to p r z y d a t n e d o ś w i a d c z e n i e , j e ś l i w c z e ś n i e j n i e a n a l i z o w a ł e ś k o d u ź r ó d ł o w e g o n a r z ę d z i a numpy.
Biblioteki używ ane podczas tworzenia kodu narzędzia numpy są istotne pod kątem niektórych możliwości przetwarzania równoległego. Zależnie od bazowych bibliotek stosowanych w trak cie pisania kodu narzędzia numpy (na przykład czy dołączono bibliotekę Intel M ath Kernel Li brary lub OpenBLAS, czy nie), widoczne będzie inne przyspieszenie. Konfigurację narzędzia numpy m ożesz sprawdzić za pomocą funkcji numpy.show_config(). Jeśli jesteś ciekaw, jakie są możliwości, w serwisie StackOverflow zam ieszczono przykładowe czasy (http://stackoverflow.com/questions/7596612/benchmarking-python-vs-c-using-blas-and-numpy). Tylko niektóre wywołania kodu narzędzia numpy użyją przetwarzania równoległego z w ykorzysta niem bibliotek zewnętrznych.
Znajdowanie liczb pierwszych W dalszej kolejności zajm iem y się testow aniem liczb pierw szych dla dużego zakresu liczb. Jest to inny problem niż przybliżanie liczby pi, ponieważ obciążenie zm ienia się zależnie od położenia w zakresie liczb. Ponadto ze spraw dzeniem każdej pojedynczej liczby zw iązana jest nieprzewidywalna złożoność. M ożliwe jest utworzenie procedury szeregowej, która do konuje sprawdzenia pod kątem liczby pierwszej, a następnie przekazuje zestawy możliwych dzielników do każdego procesu w celu ich w eryfikacji. Problem ten cechuje się w yjątkow ą równoległością. Oznacza to, że nie w ystępuje stan, który w ym aga współużytkowania. M oduł multiprocessing ułatw ia kontrolow anie obciążenia, d latego należy spraw dzić, jak można dostrajać kolejkę zadań pod kątem właściwego (oraz niewłaściwego!) używania zaso bów obliczeniow ych. Ponadto należy znaleźć prosty sposób pozw alający na trochę bardziej efektyw ne w ykorzystanie zasobów . Oznacza to, że zajm iem y się równoważeniem obciążenia, aby spróbować efektywnie rozmieszczać zadania o zmiennej złożoności w obrębie ustalonego zestawu zasobów.
Znajdowanie liczb pierwszych
|
211
W porów naniu z algorytm em zam ieszczonym w cześniej w książce zostanie użyty trochę ulepszony algorytm (zajrzyj do podrozdziału „Porównanie wyidealizow anego przetwarzania z m aszyną w irtualną języka P ython" z rozdziału 1.), którego działanie w przypadku liczby parzystej zostanie w cześniej zakończone (przykład 9.4). Przykład 9.4. Znajdowanie liczb pierwszych za pomocą kodu Python def check_prime(n): i f n % 2 == 0: re tu rn False from_i = 3 t o _ i = m a th .s q r t(n ) + 1 f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : i f n % i == 0: r e tu r n False r e tu r n True
Jak bardzo zm ienne obciążenie będzie w idoczne podczas w ykonyw ania spraw dzenia pod kątem liczby pierwszej w przypadku tej metody? Na rysunku 9.10 pokazano w ydłużający się czas zw iązany ze sprawdzaniem pod kątem liczby pierwszej, gdy w artość n określająca liczbę m ożliwych liczb pierw szych zwiększa się z 10 000 do 1 000 000. Czas związany ze sprawdzeniem pod kątem liczb pierwszych
Liczby całkowite do sprawdzenia Rysunek 9.10. Czas wymagany do sprawdzenia pod kątem liczb pierwszych przy zwiększającej się n liczbie liczb całkowitych W iększość liczb nie jest liczbami pierwszymi. Na wykresie reprezentują je kropki. W przy padku części liczb sprawdzenie może nie być czasochłonne. Z kolei część liczb może wymagać sprawdzenia wielu dzielników. Liczby pierwsze są zaznaczone na rysunku jako krzyżyki i two rzą szeroki, ciemniejszy pas. W ich przypadku koszt sprawdzenia jest największy. Czas niezbędny do sprawdzenia liczby zwiększa się wraz ze wzrostem wartości n, ponieważ zakres dzielników możliwych do sprawdzenia zwiększa się wraz z pierwiastkiem kwadratowym liczby n. Sekwen cja liczby pierwszych jest nieprzewidywalna, dlatego nie można określić spodziewanego kosztu dla zakresu liczb (choć można go oszacować, nie można być pewnym jego złożoności).
212
|
Rozdział 9. Moduł multiprocessing
W przypadku zaprezentowanego rysunku 20 razy sprawdzana jest każda liczba n, a następnie w celu wyeliminowania zaburzeń z wyników pobierany jest w ynik z najkrótszym czasem. Po rozłożeniu zadań między procesy obiektu Pool można określić, ile zadań przekazywanych jest do każdego procesu roboczego. Całość zadań może zostać równomiernie podzielona z za miarem jednokrotnego przekazania. M ożliwe jest też utworzenie wielu porcji zadań i przeka zywania ich każdorazowo, gdy procesor będzie wolny. Jest to kontrolowane za pomocą para metru chunksize. Większe porcje zadań oznaczają mniejsze obciążenie związane z komunikacją, mniejsze porcje natomiast zapewniają większą kontrolę nad sposobem przydzielania zasobów. W przypadku przykładow ego kodu znajdującego liczby p ierw sze jed n ą porcją zadań jest liczba n, która jest spraw dzana przez funkcję check_prime. W artość 10 param etru chunksize będzie oznaczać, że każdy proces obsługuje jed n ocześn ie jed ną listę zaw ierającą 10 liczb całkow itych. Na rysunku 9.11 w idoczny jest efekt zmieniania wartości parametru chunksize w przedziale od 1 (każde zadanie stanow i jedną porcję) do 64 (każde zadanie to lista złożona z 64 liczb). Choć użycie wielu niew ielkich zadań zapewnia największą elastyczność, pow oduje też naj w iększe obciążenie zw iązane z kom unikacją. W szystkie cztery procesory będą efektyw nie w ykorzystywane, ale potok komunikacji stanie się w ąskim gardłem, ponieważ każde zadanie i w yniki są przekazyw ane za pom ocą takiego pojedynczego kanału. Jeśli w artość parametru chunksize zostanie podwojona do w artości 2, zadanie zostanie zrealizow ane dwa razy szyb ciej, gdyż w potoku kom unikacji będzie m niejsza ryw alizacja. N aiw nie m ożna przyjąć, że przez zw iększanie w artości tego param etru w dalszym ciągu będzie skracany czas w ykony w ania. Jak jed n ak m ożna zauw ażyć na rysunku, ponow nie w ystąpi punkt, po osiągnięciu którego wydajność zacznie się zmniejszać. Obciążenie czasowe związane ze zmienną wielkością porcji w przypadku czterech procesów dla sprawdzania liczb pierwszych w zakresie [100 000 000-100 099 999] —
Eksperymenty
1 2,46 s
0,5 0
10
20
30
40
50
60
70
P a ra m e tr c h u n k s iz e
Rysunek 9.11. Wybieranie rozsądnej wartości parametru chunksize
Znajdowanie liczb pierwszych
|
213
Zw iększanie w artości parametru chunksize m oże być kontynuowane do m omentu, gdy za cznie być w idoczne pogarszanie się działania. Na rysunku 9.12 rozszerzamy zakres wielkości porcji, przez co mogą one być zarówno niewielkie, jak i ogromne. Na końcu skali z większy mi wartościami najgorszy widoczny w ynik to 1,32 sekundy, w przypadku którego dla para metru chunksize ustawiono w artość 50000. Oznacza to, że 100 000 elem entów podzielono na dwie porcje zadań roboczych, pow odując bezczynność dwóch procesorów na czas trwania całego kroku. W przypadku w artości 10 000 parametru chunksize tworzymy 10 porcji zadań roboczych. Oznacza to, że 4 porcje zadań zostaną dwa razy uruchom ione w sposób równole gły, a następnie zostaną w ykonane dwie pozostałe porcje. Pow oduje to bezczynność dwóch procesorów w trzeciej fazie procesu, co jest nieefektywnym wykorzystaniem zasobów. Obciążenie czasowe związane ze zmienną wielkością porcji w przypadku czterech procesów dla sprawdzania liczb pierwszych w zakresie [100 000 000-100 099 999] Eksperymenty •
Domyślne
Z13s
10°
101
W 10* Parametr chunksize
104
10*
Rysunek 9.12. Wybieranie rozsądnej wartości parametru chunksize (ciąg dalszy) W tym przypadku optymalnym rozwiązaniem jest podzielenie całkowitej liczby zadań przez liczbę procesorów. Jest to domyślne działanie w m odule multiprocessing reprezentowane na rysunku przez czarną kropkę z etykietą domyślne. Ogólnie rzecz biorąc, domyślne działanie jest sensowne. Modyfikuj je tylko wtedy, gdy ocze kujesz ujrzenia rzeczywistego przyspieszenia, a ponadto koniecznie potwierdź własną hipo tezę w odniesieniu do domyślnego działania. Inaczej niż w przypadku problemu przybliżania liczby pi m etodą M onte Carlo, przy testo w aniu liczb pierw szych obliczenia m ają zm ienną złożoność. Czasem zadanie jest kończone szybko (liczba parzysta jest wykrywana najszybciej), a czasem obliczenia dotyczą dużej licz by pierwszej (sprawdzenie takiej liczby zajm uje znacznie więcej czasu). Co się stanie po zastosowaniu losowości dla sekwencji zadań? Jak w idać na rysunku 9.13, dla omawianego problemu udało się uzyskać 2-procentowy wzrost wydajności. Dzięki użyciu losowości zmniejsza się prawdopodobieństwo tego, że ostatnie zadanie w sekwencji zajmie więcej cza su niż pozostałe, co spowoduje, że aktywne pozostaną wszystkie procesory z wyjątkiem jednego.
214
|
Rozdział 9. Moduł multiprocessing
Obciążenie czasowe związane ze zmienną wielkością porcji w przypadku czterech procesów dla sprawdzania liczb pierwszych w zakresie [100 000 000-100 099 999] •
Eksperymenty Domyślne
2.16 s
10»
10'
101
10’
10*
10»
P a ra m e tr c h u n k s iz e
Rysunek 9.13. Zastosowanie losowości dla sekwencji zadań Jak zadem onstrow ano w e w cześniejszym przykładzie z param etrem chunksize o wartości 10000, niew łaściw e dopasowanie obciążenia do liczby dostępnych zasobów pow oduje nie efektywność. W tym przypadku utworzono trzy fazy procesu roboczego: w pierwszych dwóch fazach wykorzystano 100% zasobów, a w ostatniej tylko 50%. Na rysunku 9.14 pokazano dziwny efekt występujący w przypadku niewłaściwego ustaw ie nia liczby porcji zadań roboczych w odniesieniu do liczby procesorów. Coś takiego powoduje zbyt małe wykorzystanie dostępnych zasobów. Najdłuższy ogólny czas w ykonywania wy stępuje, gdy zostanie utw orzona tylko jedna porcja zadań roboczych. W efekcie niew ykorzy stane będą trzy procesory. Dwie porcje zadań sprawiają, że niew ykorzystane są dwa proceso ry, itd. Tylko w przypadku czterech porcji zadań używane są wszystkie zasoby. Jeśli jednak zostanie dodana piąta porcja, ponownie będzie mieć miejsce zbyt małe wykorzystanie zasobów — cztery procesory będą zajęte sw oim i porcjam i, a następnie jeden procesor przeprow adzi obliczenie dla piątej porcji. W miarę zwiększania się liczby porcji zadań roboczych zauważalne jest zwiększenie efek tywności — różnica w czasie działania dla 29 i 32 porcji zadań roboczych wynosi w przybli żeniu 0,01 sekundy. Ogólna zasada jest taka, że jeśli zadania cechują się zmiennym czasem działania, należy tworzyć wiele małych zadań w celu efektywnego wykorzystania zasobów. Oto niektóre strategie skutecznego użycia modułu multiprocessing w efektywny sposób w przy padku problem ów niezwykle nadających się do zastosow ania przetwarzania równoległego: • Dziel zadania na niezależne jednostki robocze. • Jeśli procesy robocze zajm ują różną ilość czasu, rozważ zastosow anie losowości dla se kwencji zadań (inny przykład dotyczyłby przetwarzania plików o zmiennej wielkości).
Znajdowanie liczb pierwszych
|
215
O bciążenie czasow e zw iązane ze zm ienną wielkością porcji w przypadku czterech procesów dla sprawdzania liczb pierw szych w zakresie [100 000 0 0 0 -1 0 0 099 999] ——
Eksperym enty
58 s
\
1,30 s \
1.03 s
oĄ \ / 0 ,6ty
0
oA s 0,86 s 0,75 5 0 ^ 0
5
10
15
20
25
30
35
Liczba porcji Rysunek 9.14. Zagrożenie związane z wybraniem niewłaściwej liczby porcji • Równie przydatną strategią może być sortow anie kolejki zadań roboczych w taki sposób, aby najwolniejsze zadania zostały w ykonane jako pierwsze. • Używaj domyślnej wartości parametru chunksize, chyba że m asz sprawdzone powody, by ją zmienić. • Dopasuj liczbę zadań do liczby fizycznych procesorów (również w tym przypadku od powiada za to parametr chunksize o wartości domyślnej, choć spowoduje on użycie wszyst kich hiperwątków, co m oże nie zapewnić żadnego dodatkowego wzrostu wydajności). Zauważ, że domyślnie moduł multiprocessing rozpozna hiperwątki jako dodatkowe procesory. Oznacza to, że w przypadku laptopa jednego z autorów przez moduł zostanie przydzielonych osiem procesów, gdy tylko cztery z nich będą faktycznie działać z pełną szybkością. Dodat kow e cztery procesy m ogą zająć cenną pam ięć RAM , oferując ledw ie znikom y dodatkow y przyrost szybkości. W przypadku obiektu Pool możliwe jest rozdzielenie porcji predefiniow anych zadań robo czych między dostępne procesory. Jest to jednak mniej pom ocne przy dynamicznych obcią żeniach, a szczególnie w tedy, gdy są to obciążenia pojaw iające się z czasem. Dla tego rodzaju obciążenia można użyć obiektu Queue zaprezentowanego w następnym punkcie.
^ 216
|
Je śli z a jm u je s z się p r o b le m a m i n a u k o w y m i, w p r z y p a d k u k tó r y c h k a ż d e z a d a n ie z a j m u j e w i e l e s e k u n d ( l u b w i ę c e j c z a s u ) , m o ż e s z s p r a w d z i ć n a r z ę d z i e jo b lib (h t t p :/ / p y th o n h o s te d .o r g /jo b lib /) G a e l a V a r o q u a u x a . O b s ł u g u j e o n o u p r o s z c z o n e p o t o k o w a n i e . N a r z ę d z i e b a z u je n a m o d u le m u ltip r o c e ssin g i o fe ru je p r o s ts z y interfejs p r z e tw a r z a n ia ró w n o leg łeg o , b u fo ro w a n ie w y n ik ó w i fu nk cje d e b u g o w a n ia .
Rozdział 9. Moduł multiprocessing
Kolejki zadań roboczych Obiekty multiprocessing.Queue oferują nietrwałe kolejki, które um ożliw iają w ysyłanie między procesam i w szelkich obiektów kodu Python m ożliw ych do serializacji przez m oduł pickle. Z tymi kolejkam i zw iązane jest obciążenie, poniew aż każdy obiekt m usi być serializow any przez ten moduł, aby m ógł zostać wysłany, a następnie jest poddawany deserializacji za po m ocą modułu pickle po stronie konsumenta (wraz z operacjami blokowania). W poniższym przykładzie okaże się, że zw iązane z tym obciążenie nie jest pom ijalne. Jeśli jednak procesy robocze przetw arzają w iększe zadania, obciążenie kom unikacyjne będzie praw dopodobnie m ożliwe do zaakceptowania. Korzystanie z kolejek jest dość proste. W omawianym przykładzie zostanie dokonane spraw dzenie pod kątem liczb pierw szych przez zastosowanie listy liczb kandydujących i przekaza nie potwierdzonych liczb pierwszych z powrotem do funkcji definite_primes_queue. Operacja zostanie przeprow adzona przy użyciu jednego, dwóch, czterech i ośmiu procesów. Zostanie potw ierdzone, że w szystkie z ośm iu procesów zajm ują w ięcej czasu niż po prostu urucho m ienie jednego procesu, który sprawdza identyczny zakres. Obiekt Queue daje m ożliw ość wykonania wielu operacji kom unikacji międzyprocesowej przy użyciu wbudow anych obiektów języka Python. M oże to być przydatne przy przekazywaniu obiektów z w ielom a inform acjam i o stanie. Poniew aż jed nak obiekt Queue pozbaw iony jest trw ałości, praw dopodobnie nie użyjesz go dla zadań, które m ogą w ym agać niezaw odności na w ypadek wystąpienia awarii (np. po zaniku zasilania lub uszkodzeniu dysku twardego). Przykład 9.5 prezentuje funkcję check_prime. Poznałeś już podstawowy test liczb pierwszych. Działanie kodu odbywa się w pętli nieskończonej. W celu użycia elementu z kolejki ma miejsce blokowanie w funkcji possible_primes_queue.get() (podczas oczekiwania na dostępność zadania). W danym momencie tylko jeden proces może pobrać element, ponieważ obiekt Queue zajmuje się synchronizowaniem dostępów. Jeśli w kolejce nie ma żadnych zadań, funkcja .get() stosuje blokadę do momentu dostępności zadania. Znalezione liczby pierwsze są umieszczane w ko lejce definite_primes_queue do wykorzystania przez proces nadrzędny. Przykład 9.5. Użycie dwóch kolejek dla komunikacji międzyprocesowej IPC FLAG_ALL_DONE = b"WORK_FINISHED" FLAG_WORKER_FINISHED_PROCESSING = b"WORKER_FINISHED_PROCESSING" def check_prime(possible_primes_q ueue, de fin it e _ p rim e s_ q u e u e ): while True: n = po ssib le_p r im e s_q u eu e .g et() i f n == FLAG_ALL_DONE: # oznaczenie, że wszystkie wyniki zostały um ieszczone w k o lejc e wyników de fi nite_primes_queue.put(FLAG_WORKER_FINISHED_PROCESSING) break else: i f n % 2 == 0: continue f o r i in x ra n g e (3 , i n t ( m a t h . s q r t ( n ) ) + 1, 2 ) : i f n % i == 0: break else: defi nite_ pri m es _q u eu e.p u t( n )
Znajdowanie liczb pierwszych
|
217
Definiowane są dwie flagi: jedna jest pobierana przez proces nadrzędny jako „trująca pigułka" w celu wskazania, że nie ma już dostępnych zadań, druga flaga natom iast jest uzyskiwana przez proces roboczy, aby potwierdzić, że proces ten zidentyfikował „trującą pigułkę" i sam się zamknął. Pierwsza „trująca pigułka" jest też określana mianem stra ż nika (http://en.wikipedia.org/ wiki/Sentinel_value), ponieważ gwarantuje zakończenie pętli przetwarzania. Podczas korzystania z kolejek zadań roboczych i zdalnych procesów roboczych pom ocne może być użycie takich flag do rejestrowania wysłania „trujących pigułek" i sprawdzania te go, czy odpowiedzi zostały w ysłane z procesów podrzędnych w rozsądnym przedziale czasu w skazującym zam knięcie procesów . Choć nie realizujem y tutaj tego procesu, dodanie m e chanizmu utrzymyw ania czasu stanowi dość prosty dodatek do kodu. Informacja o otrzyma niu flag może być rejestrowana lub w yświetlana podczas debugowania. Obiekty Queue są tworzone na bazie obiektu Manager (przykład 9.6). Używany będzie znany Ci już proces budow ania listy obiektów Process, z których każdy zaw iera proces rozw idlony. Dwie kolejki są w ysyłane jako argumenty, a moduł multiprocessing obsługuje ich synchroni zację. Po uruchomieniu nowych procesów kolejce possible_primes_queue przekazywana jest li sta zadań. Ostatecznie na proces przypada jedna „trująca pigułka". Zadania będą używane zgodnie z kolejnością m etody FIFO (First-In First-Out), a „trujące pigułki" będą pozostaw ione na koniec. W funkcji check_prime używana jest blokująca funkcja .g e t(), ponieważ nowe pro cesy będą musiały poczekać na pojaw ienie się zadania w kolejce. Ze względu na stosowanie flag możliwe jest dodanie zadań roboczych i przetw orzenie wyników, a następnie przepro w adzenie iteracji przez dodanie większej liczby zadań oraz sygnalizowanie końca istnienia procesów roboczych przez późniejsze dołączenie „trujących pigułek". Przykład 9.6. Tworzenie dwóch kolejek dla komunikacji międzyprocesowej IPC if
name == 11 main__ primes = [] manager = multip ro cessing .M an ager() possible_primes_queue = manager.Queue() de fin ite_prim es_ queue = manager.Queue() NBR_PROCESSES = 2 pool = Pool(processes=NBR_PROCESSES) pro ces se s = [] f o r _ in range(NBR_PROCESSES): p = m u lti p r o c e ssi n g .P ro c e ss(t a rg e t= c h e c k _ p ri m e , arg s=(possible_ prim es_ queue, d e fin it e _ p rim e s_ q u e u e )) processes.append(p ) p .start() t1 = t i m e . ti m e () number_range = xran ge (100 00 00 00, 101000000) # dodan ie zadań do k o lejk i zadań przychodzących f o r po ssible_pr ime in number_range: possib le_p rim es_ qu eu e.put(p oss ib le_pr im e) # dodan ie „trujących p ig u łek ” w celu zatrzymania zdalnych p rocesó w roboczych f o r n in xrange(NBR_PROCESSES): possible_primes_queue.put(FLAG_ALL_DONE)
Aby wykorzystać wyniki, w przykładzie 9.7 przy użyciu blokującej funkcji .g et() w kolejce definite_primes_queue rozpoczynana jest kolejna pętla nieskończona. Jeśli zostanie znaleziona flaga finished-processing, pobierana jest liczba procesów, które sygnalizowały swoje zakoń czenie. W przeciwnym razie pojawi się nowa liczba pierwsza, która jest dodawana do listy primes. Wyjście z pętli nieskończonej ma miejsce, gdy wszystkie procesy zasygnalizują swoje zakończenie.
218
|
Rozdział 9. Moduł multiprocessing
Przykład 9.7. Użycie dwóch kolejek do komunikacji międzyprocesowej IPC p r o c e s so rs_ in d ica t in g _ th e y _ h a v e _ fin ish e d = 0 while True: new _result = d e f i n i t e _ p rim es _q u eu e. get( ) # blokow anie p od czas oczekiw an ia na wyniki i f new_result == FLAG_WORKER_FINISHED_PROCESSING: p r o c e s so rs_ in d ica t in g _ th e y _ h a v e _ fin ish e d += 1 i f p r o c e s so rs_ in d ica t in g _ th e y _ h a v e _ fin ish e d == NBR_PROCESSES: break else: primes.append(new_result) a s s e r t p ro c e s so rs_ in d ica t in g _ th e y _ h a v e _ fin ish e d == NBR_PROCESSES p r i n t "Czas t r w a n i a : " , t i m e . ti m e () - t1 p r i n t l e n (p r im e s ), p r i m e s [ : 1 0 ] , p r i m e s [ -1 0 : ]
Z powodu użycia modułu serializacji pickle i synchronizacji ze stosowaniem obiektu Queue zw iązane jest dość spore obciążenie. Jak w idać na rysunku 9.15, użycie rozwiązania z jednym procesem bez obiektu Queue jest znacznie szybsze niż zastosowanie dwóch lub większej liczby procesów . W tym przypadku w ynika to z tego, że obciążenie jest bardzo m ałe. Obciążenie zw iązane z kom unikacją stanow i dom inującą część ogólnego czasu realizow ania zadania. Gdy stosuje się obiekt Queue, w om aw ianym przykład zie dw a procesy są kończone trochę szybciej niż jeden proces, w przypadku czterech i ośmiu procesów natom iast czas działania jest dłuższy.
Obciążenie kolejek w przypadku prostych zadań
1 proces podrzędny za pośrednictwem kolejek
„ **
'■
^ * •* **
Q_
I
<1>
i
O
'c
£
• - ■ Użycie kolejek
J ■o ■D
•
Brak kolejki
Brak kolejki
3
4
5
6
Liczba procesów Rysunek 9.15. Obciążenie wynikające z użycia obiektów Queue Jeśli zadanie ma długi czas wykonywania (jest to co najmniej pokaźna część sekundy) przy niewielkiej ilości komunikacji, metoda oparta na obiekcie Queue może być w łaściwym rozwią zaniem. Konieczne będzie sprawdzenie, czy obciążenie związane z komunikacją nie spowo duje, że to rozwiązanie okaże się niezbyt przydatne.
Znajdowanie liczb pierwszych
|
219
M ożesz zastanaw iać się, co się stanie po usunięciu nadm iarow ej połow y kolejki zadań (wszystkie liczby parzyste, które są bardzo szybko odrzucane w funkcji check_prime). Podzie lenie na pół w ielkości kolejki w ejściow ej pow odu je w tym przypadku pod zielenie na pół czasu wykonywania. W dalszym ciągu jednak rezultat będzie gorszy niż w przypadku przy kładu z kodem , który korzysta z jednego procesu bez obiektu Queue! Ułatw ia to pokazanie, że ob ciążen ie zw iązane z kom u n ik acją stanow i pod staw ow ą tru d n o ść p rezen tow an ego p roblem u .
Asynchroniczne dodawanie zadań do kolejki Queue Przez dodanie obiektu Thread do głównego procesu m ożliwe jest asynchroniczne dostarczanie zadań do funkcji possible_primes_queue. W przykładzie 9.8 zdefiniowano funkcję feed_new_jobs: w ykonuje ona to samo zadanie co procedura konfiguracji zadania, która została umieszczona w funkcji __main__, ale odbywa się to w osobnym wątku. Przykład 9.8. Funkcja asynchronicznego dostarczania zadań def feed_new_jobs(number_range, possible_primes_queue, n b r _ p o i s o n _ p i ll s ) : f o r po ssible_pr ime in number_range: possib le_p rim es_ qu eu e.put(p oss ib le_pr im e) # dodaw anie „trujących p ig u łek ” w celu zatrzymania zdalnych p rocesó w roboczych f o r n in x r a n g e ( n b r _ p o i s o n _ p i ll s ) : possible_primes_queue.put(FLAG_ALL_DONE)
W przykładzie 9.9 fu n k c ja main skonfiguruje obiekt Thread za pom ocą funkcji possible_ primes_queue, a następnie przed udostępnieniem jakiegokolwiek zadania przejdzie do fazy gro madzenia wyników. Funkcja asynchronicznego dostarczania zadań może pobierać zadania ze źródeł zew nętrznych (np. z bazy danych lub w ramach kom unikacji powiązanej z operacjami wejścia-wyjścia), wątek funkcji __main__ natomiast obsługuje każdy przetworzony wynik. Ozna cza to, że sekwencje wejściowe i wyjściowe nie m uszą być w cześniej tworzone. M ogą być ob sługiwane dynamicznie. Przykład 9.9. Użycie wątku do skonfigurowania funkcji asynchronicznego dostarczania zadań if
name == 11 main ” : primes = [] manager = multip ro cessing .M an ager() possible_primes_queue = manager.Queue() import threadin g thrd = th re adin g.T h re ad (t arget = fe ed _new _jo bs, args=(number_range, possible_primes_queue, NBR_PROCESSES)) th rd .sta rt() # przetw arzanie wyników
Aby uzyskać niezaw odne system y asynchroniczne, praw ie na pew no należy przyjrzeć się zewnętrznej bibliotece, która ma status dojrzałej. M ocnym i kandydatami są biblioteki gevent, tornado i Twisted. Z kolei biblioteka tulip języka Python 3.4 jest dla nich nowym konkurentem. Choć przedstaw ione przykłady pozw oliły Ci zaznajom ić się z zagadnieniem , z praktycznego punktu widzenia są one bardziej przydatne w przypadku bardzo prostych systemów i pod czas nauki niż przy rozpatrywaniu systemów produkcyjnych.
220
|
Rozdział 9. Moduł multiprocessing
^
P y R e s (h t t p s :// g it h u b .c o m /b i n a r y d u d / p y r e s ) t o i n n a k o l e j k a b a z u j ą c a n a j e d n y m k o m p u te r z e , k tó rej m o ż e s z b liżej się p rz y jr z e ć . M o d u ł te n k o r z y s ta z s y s t e m u R e d is ( o m ó w io n o g o w p u n k c ie „ U ż y cie sy ste m u R ed is ja k o fla g i") w celu p r z e c h o w y w a n i a s t a n u k o l e j k i . R e d i s to s y s t e m p r z e c h o w y w a n i a d a n y c h , k t ó r y n i e b a z u j e n a j ę z y k u P y t h o n . O z n a c z a to , ż e k o l e j k a d a n y c h u t r z y m y w a n a w s y s t e m i e R e d i s m o ż e b y ć o d c z y t y w a n a p o z a o b r ę b e m k o d u P y t h o n (a z a t e m m o ż l i w e j e s t s p r a w d z a n i e s t a n u k o l e j k i ) , a p o n a d t o m o ż e b y ć w s p ó ł u ż y t k o w a n a z s y s t e m a m i i n n y m i n iż o p a rte na ję z y k u P y th on .
Zw róć szczególną uwagę na to, że systemy asynchroniczne w ym agają wyjątkowo dużo cier pliw ości. Ich debugow aniu będzie tow arzyszyć spora daw ka frustracji. Proponujem y prze strzeganie następujących wytycznych: • Stosowanie zasady „utrzymywania w szystkiego w sposób prosty i zrozum iały". • Unikanie w razie możliwości autonomicznych systemów asynchronicznych (podobnych do przedstawionego w przykładzie), ponieważ będzie zw iększać się ich złożoność, a po nadto szybko staną się trudne do utrzymania. • Używ anie dojrzałych bibliotek, takich jak gevent (opisano ją w poprzednim rozdziale), które zapewniają sprawdzone metody radzenia sobie z określonymi zestawami problemów. Ponadto bardzo polecamy korzystanie z zewnętrznego systemu kolejek (np. Gearman, 0MQ, Celery, PyRes lub H otQueue), który zapew nia dostępność stanu kolejek na zew nątrz. Choć wymaga to większego nakładu pracy, prawdopodobnie pozwoli zaoszczędzić czas w wyniku zwiększonej efektywności debugowania i lepszej dostępności systemu kolejek dla systemów produkcyjnych.
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej Liczby pierw sze to liczby, które nie mają dzielnika oprócz samych siebie i liczby 1. Oznacza to oczyw iście, że najw iększym w spólnym dzielnikiem jest dla nich liczba 2 (żadna liczba parzysta nie m oże być liczbą pierw szą). W konsekw encji m niejsze liczby pierw sze (np. 3, 5, 7) stają się wspólnymi dzielnikami w iększych liczb pierw szych (np. są to odpowiednio liczby 9, 15, 21). Załóżmy, że dla dużej liczby ma zostać przeprowadzone sprawdzenie tego, czy jest ona licz bą pierwszą. Prawdopodobnie konieczne będzie przeszukanie dużej przestrzeni dzielników. Rysunek 9.16 prezentuje częstość występowania dla każdego dzielnika dla liczb innych niż pierwsze aż do liczby 10 000 000. Choć prawdopodobieństwo wystąpienia mniejszych dziel ników jest znacznie w iększe niż dużych dzielników, nie istnieje przewidywalny wzorzec. Zdefiniujm y now y problem. Załóżmy, że istnieje niewielki zbiór liczb, a naszym zadaniem jest efektywne użycie zasobów procesora do określenia, czy każda liczba jest liczbą pierw szą (po jednej naraz). Być m oże konieczne będzie jedynie spraw dzenie jednej dużej liczby. N ie ma już sensu stosow anie do spraw dzenia jednego procesora. Pożądane jest skoordynow anie działań między wieloma procesorami.
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
|
221
Liczba dzielników liczb innych niż pierwsze aż do liczby 10 000 000 3000
f 2500 O CL fsl
1«*
10'
10! 101 10* 105 Częstość występowania dzielnika
10*
Rysunek 9.16. Częstość występowania dzielników dla liczb złożonych W podrozdziale przyjrzym y się kilku w iększym liczbom . To jedna liczba 15-cyfrow a oraz cztery liczby 18-cyfrowe: • mała liczba złożona: 112 272 535 095 295, • pierwsza duża liczba złożona: 100 109 100 129 100 369, • druga duża liczba złożona: 100 109 100 129 101 027, • pierwsza liczba pierwsza: 100 109 100 129 100 151, • druga liczba pierwsza: 100 109 100 129 162 907. U żyw ając m niejszej i w iększych liczb złożonych, m am y zw eryfikow ać nie tylko to, czy wy brany proces jest szybszy w przypadku sprawdzania pod kątem liczb pierw szych, ale też czy nie okaże się w olniejszy przy spraw dzaniu dotyczącym liczb. Przyjm iemy, że nie są znane wielkość ani typ danych liczb. Z tego powodu pożądany będzie najszybszy m ożliw y w ynik dla w szystkich przypadków użycia. W spółpraca oznacza obciążenie zw iązane z synchronizow aniem danych i spraw dzaniem współużytkowanych danych. Może ono być dość duże. W podrozdziale zostanie użytych kilka metod, które mogą być w ykorzystywane na różne sposoby na potrzeby koordynacji zadań. Zauważ, że w tym miejscu nie uwzględniono wyspecjalizow anego interfejsu przekazywania kom unikatów M PI (M essage Passing Interface). Przyjrzym y się m odułom z dołączonym i bi bliotekami oraz systemowi Redis (bardzo często stosowanemu). Jeśli zam ierzasz użyć interfejsu M PI, zakładam y, że w iesz już, jak postępow ać. Na początek dobrą propozycją może być projekt M PI4PY (https://pypi.python.org/pypi/mpi4py). Jeśli wyma gane jest kontrolowanie opóźnienia, gdy współpracuje ze sobą wiele procesów, projekt ten sta nowi idealną technologię, niezależnie od tego, czy używ any jest jeden komputer, czy wiele.
222
|
Rozdział 9. Moduł multiprocessing
W ramach kolejnych uruchom ień każdy test jest przeprowadzany 20 razy. M inim alny czas jest pobierany w celu pokazania największej szybkości możliwej dla danej m etody. W oma wianych przykładach do współużytkow ania flagi (często w postaci 1 bajta) używane są różne techniki. Choć można zastosować podstawowy obiekt, taki jak Lock, możliwe będzie też współ użytkowanie tylko jednego bitu stanu. Decydujemy się na zaprezentowanie sposobu w spół użytkowania typu podstawowego, aby um ożliw ić bardziej ekspresywne współużytkowanie stanu (naw et pom im o tego, że na potrzeby przedstaw ionego przykładu nie jest w ym agany bardziej ekspresywny stan). K onieczne jest pod kreślenie, że przy w spółużytkow aniu stanu w szystko się kom plikuje. Z łatwością m ożesz ostatecznie osiągnąć inny stan — stan sfrustrowania. Zachowaj ostroż ność i próbuj utrzym ać w szystko tak proste, jak to m ożliw e. M oże się okazać, że od m niej efektywnego wykorzystania zasobów większe znaczenie ma pośw ięcenie przez program istę czasu na inne problemy. Najpierw zostaną omówione wyniki, a następnie zostanie przeanalizow any kod. Na rysunku 9.17 pokazano pierw sze m etody w ypróbow yw ane w celu użycia kom unikacji międzyprocesowej do szybszego testowania pod kątem liczb pierwszych. Test porównawczy to w ersja z etykietą Przetw arzanie szeregowe, która nie korzysta z żadnej kom unikacji m iędzyprocesow ej. Każda próba przyspieszenia kodu m usi okazać się szybsza co najm niej od tej wersji.
Wolniejsze metody stosowania komunikacji międzyprocesowej
Rysunek 9.17. Wolniejsze metody stosowania komunikacji międzyprocesowej do sprawdzania pod kątem występowania liczb pierwszych
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
| 223
Wersja z etykietą Bardzo prosty obiekt Pool dla mniejszych liczb zapewnia przewidywalną (i niezłą) szybkość. W ersja ta jest na tyle dobra, że raczej trudno będzie osiągnąć większą szybkość niż w jej przypadku. W trakcie szukania rozwiązań zapewniających dużą szybkość nie przeocz oczywistego faktu — czasami banalne rozwiązanie, lecz wystarczająco dobre, jest wszystkim, co jest potrzebne. W przypadku rozwiązania z etykietą Bardzo prosty obiekt Pool dla mniejszych liczb m etoda po lega na pobraniu testowanej liczby, równom iernym rozdzieleniu między dostępne procesory jej zakresu możliwych dzielników, a następnie skierowaniu zadań do każdego procesora. Jeśli dowolny z nich znajdzie dzielnik, zakończy wcześniej działanie, lecz nie poinform uje o tym fakcie. Pozostałe procesory nadal będą kontynuować przetwarzanie związane ze swoją czę ścią zakresu. W przypadku 18-cyfrowej liczby (cztery wcześniej podane przykładowe więk sze liczby) oznacza to, że czas wyszukiw ania będzie taki sam niezależnie od tego, czy liczba jest liczbą pierwszą, czy nie. Rozw iązania z etykietam i Flaga w postaci system u Redis i Flaga w postaci obiektu M anager są wolniejsze przy testowaniu większej liczby dzielników pod kątem liczb pierwszych, co wy nika z obciążenia zw iązanego z komunikacją. Rozw iązania te używ ają w spółużytkowanej flagi do wskazania faktu znalezienia dzielnika, a także w celu poinform ow ania o tym, że wy szukiwanie powinno zostać wstrzymane. System Redis umożliwia współużytkowanie stanu nie tylko z innymi procesami kodu Python, ale też z innym i narzędziam i i kom puteram i, a naw et ujaw nianie stanu za pośrednictw em interfejsu przeglądarki internetowej (może to być przydatne przy monitorowaniu zdalnym). O biekt Manager, który stanow i część m odułu multiprocessing, zapew nia zsynchronizow any zestaw obiektów Python w ysokiego poziom u (w tym obiekty podstaw ow e oraz obiekty l i s t i dict). Choć w przypadku większych liczb innych niż liczby pierw sze w ystępuje obciążenie zwią zane ze spraw dzaniem w spółużytkow anej flagi, jest to niw elow ane przez skrócenie czasu wyszukiwania wynikające z w czesnego sygnalizowania faktu znalezienia dzielnika. Jednakże w przypadku liczb pierw szych nie ma możliwości wczesnego zakończenia prze twarzania, gdy nie znaleziono żadnego dzielnika. Z tego powodu sprawdzanie w spółużyt kowanej flagi stanie się dominującym obciążeniem. Na rysunku 9.18 pokazano, że przy niewielkim nakładzie możliwe jest osiągnięcie wyniku zapew niającego znacznie w iększą szybkość. W ynik dla rozw iązania z etykietą Bardzo prosty obiekt Pool dla m niejszych liczb w dalszym ciągu stanow i punkt odniesienia, ale w yniki uzy skane dla obiektu RawValue i modułu mmap są znacznie lepsze od wyników osiągniętych wcze śniej dla systemu Redis i obiektu Manager. Prawdziwa m agia pojawia się w m om encie wybra nia najszybszego rozwiązania i wprowadzenia w kodzie kilku mniej oczywistych modyfikacji w celu utw orzenia praw ie optymalnego rozwiązania modułu mmap. Ta ostateczna wersja jest szybsza od rozw iązania z etykietą Bardzo prosty obiekt Pool dla mniejszych liczb dla liczb zło żonych, a także prawie tak szybka jak w przypadku liczb pierwszych. W dalszej części rozdziału zostaną omówione różne m etody użycia komunikacji m iędzyprocesowej w kodzie Python w celu rozwiązania problemu z wyszukiwaniem w ram ach w spół pracy. M am y nadzieję, że stwierdzisz, że komunikacja międzyprocesowa jest dość prosta, ale generalnie pow oduje obciążenie.
224
|Rozdział 9. Moduł multiprocessing
Szybsze metody stosowania komunikacji międzyprocesowej
Rysunek 9.18. Szybsze metody stosowania komunikacji międzyprocesowej do sprawdzania pod kątem występowania liczb pierwszych
Rozwiązanie z przetwarzaniem szeregowym Rozpoczniem y od tego samego kodu służącego do szeregow ego spraw dzania dzielników , który został wcześniej użyty (przykład 9.10). Jak w cześniej wspom niano, w przypadku do wolnej liczby innej niż liczba pierwsza, która ma duży dzielnik, możliwe jest bardziej wydaj ne przeszukiw anie przestrzeni dzielników w sposób równoległy. W dalszym ciągu przetw a rzanie szeregowe zapewni rozsądny punkt odniesienia do dalszych działań. Przykład 9.10. Weryfikowanie w sposób szeregowy def check_prime(n): i f n % 2 == 0: re tu rn False from_i = 3 to _ i = m a th .s q r t(n ) + 1 f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : i f n % i == 0: re tu rn Fa ls e re tu rn True
Rozwiązanie z prostym obiektem Pool Rozw iązanie z prostym obiektem Pool korzysta z obiektu multiprocessing.Pool w podobny sposób jak w przypadku działań dotyczących czterech procesów rozwidlonych, które omó wiono w podrozdziałach „Znajdowanie liczb pierw szych" i „Przybliżanie liczby pi za pomocą procesów i w ątków ". Istnieje liczba, dla której w ykonyw any jest test pod kątem sprawdzenia,
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
| 225
czy jest liczbą pierwszą. Zakres możliw ych dzielników jest dzielony na cztery krotki podzakresów, które są kierowane do obiektu Pool. W przykładzie 9.11 użyto nowej metody create_range.create (nie będzie prezentowana, gdyż jest mało ciekawa), która dzieli obszar roboczy na regiony o jednakowej w ielkości. W przy padku tych regionów każdy elem ent w obiekcie ranges_to_check jest parą złożoną z dolnego i górnego lim itu, m iędzy którym i m a być przeprow adzane w yszukiw anie. Dla pierw szej 18-cyfrow ej liczby innej niż pierw sza (100 109 100 129 100 369) i czterech procesów zostaną uzyskane zakresy dzielników obiektu ranges_to_check == [(3, 79100057), (79100057, 158200111), (158200111, 237300165), (237300165, 316400222)] (gdzie wartość 316 400 222 jest pierwiastkiem kw adratow ym w artości 100 109 100 129 100 369 pow iększonej o jeden). W fu n k c ji main__ definiowany jest najpierw obiekt Pool. Funkcja check_prime dzieli następnie za pomocą metody map zakresy obiektu ranges_to_check dla każdej liczby n, która może być liczbą pierwszą. Jeśli wynikiem jest wartość False, oznacza to znalezienie dzielnika, czyli liczba nie jest liczbą pierwszą. Przykład 9.11. Rozwiązanie z prostym obiektem Pool def check_prime(n, pool, n b r_ p ro c e ss e s): from_i = 3 to_i = in t(m a th .sq rt(n )) + 1 ra nges_to_che ck = c r e a t e _ r a n g e . c r e a t e ( f r o m _ i , t o _ i , n br_p rocess es) ra nges_to_che ck = zip (l e n (r a n g e s_ to _ c h e c k ) * [ n ], ran ges_to_check) a s s e r t le n (r an ge s_ to _che ck ) == nbr_process es r e s u l t s = pool.map(check_prime_in_range, ran ges_to_check) i f Fals e in r e s u l t s : re tu rn False re tu rn True if name == " main ": NBR_PROCESSES = 4 pool = Pool(processes=NBR_PROCESSES)
W przykładzie 9.12 zmodyfikowano wcześniejszą funkcję check_prime, aby do sprawdzenia wykorzystać dolny i górny limit zakresu. Ponieważ przekazyw anie kompletnej listy dzielni ków m ożliw ych do spraw dzenia nie ma żadnej wartości, m ożliw e jest zaoszczędzenie czasu i pam ięci przez przekazanie tylko dwóch liczb, które definiują zakres. Przykład 9.12. Funkcja check_prime_in_range def che ck_prim e_in_ra nge((n, (f ro m _i, t o _ i ) ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : i f n % i == 0: r e tu r n False r e tu r n True
W przypadku wariantu dotyczącego małej liczby innej niż liczba pierwsza czas sprawdzania za pom ocą obiektu Pool w ynosi 0,1 sekundy, czyli jest znacznie dłuższy niż dla oryginalnego rozwiązania z przetwarzaniem szeregowym (0,000002 sekundy). Pom imo tego jednego gor szego wyniku ogólnym rezultatem jest przyspieszenie. N ie jest problem em zaakceptow anie tego jednego słabszego wyniku, co będzie jednak, gdy do spraw dzenia zostanie przekazanych w iele m niejszych liczb złożonych? O kazuje się, że m ożna uniknąć takiego spow olnienia. Dow iesz się w jaki sposób przy om aw ianiu rozw iązania z bardzo prostym obiektem Pool dla m niejszych liczb.
226
|
Rozdział 9. Moduł multiprocessing
Rozwiązanie z bardzo prostym obiektem Pool dla mniejszych liczb Poprzednie rozw iązanie nie było w ydajne przy spraw dzaniu m niejszych liczb złożonych. W przypadku dowolnej takiej liczby (liczącej mniej niż 18 cyfr) rozwiązanie to m oże praw dopodobnie okazać się wolniejsze od metody z przetwarzaniem szeregowym. Wynika to z ob ciążenia zw iązanego z wysyłaniem dzielonych zadań, a także z braku inform acji o tym, czy zostanie znaleziony bardzo mały dzielnik (tego rodzaju dzielniki są bardziej praw dopodob ne). Jeśli znaleziono m ały dzielnik, proces nadal będzie m usiał czekać na zakończenie wy szukiwania innych w iększych dzielników. M ożliwe jest rozpoczęcie sygnalizowania między procesami dotyczącego znalezienia niewiel kiego dzielnika, ale ponieważ dzieje się to bardzo często, spow oduje duże obciążenie zw ią zane z komunikacją. Rozw iązanie zaprezentow ane w przykładzie 9.13 jest bardziej pragm a tyczne. Sprawdzenie w sposób szeregowy jest szybko przeprow adzane dla praw dopodobnie m ałych dzielników . Jeśli żaden nie zostanie znaleziony, zostanie rozpoczęte w yszukiw anie w sposób równoległy. Uw zględnienie wstępnego sprawdzania szeregowego przed rozpoczę ciem dość kosztownej operacji przetwarzania równoległego stanowi typową metodę pozw a lającą uniknąć części obciążenia wynikającego z obliczeń równoległych. Przykład 9.13. Ulepszanie rozwiązania z bardzo prostym obiektem Pool w przypadku małych liczb złożonych def check_prime(n, pool, n b r_ pro cess es) : # niepow odujące dużego obciążenia spraw dzanie zbioru możliwych dzielników o dużym praw dopodobieństw ie w ystąpienia from_i = 3 to _i = 21 i f not check_prime_in_range((n, (from_i, t o _ i ) ) ) : re turn False # kontynuowanie spraw dzania większych dzielników w sp osób równoległy from_i = to_i to _i = i n t ( m a t h . s q r t ( n ) ) + 1 ranges_to_check = c r e a t e _ r a n g e .c r e a t e ( f r o m _ i , t o _ i , nbr_processes) ranges_to_check = zip(l en (r ang es_to _ c h ec k) * [ n ], ranges_to_check) a s s e r t len (ra nge s_to_che ck ) == nbr_processes r e s u l t s = pool.map(check_prime_in_range, ranges_to_check) i f False in r e s u l t s : return False re turn True
Szybkość tego rozw iązania jest taka sama lub w iększa niż w przypadku oryginalnego w y szukiw ania szeregow ego dla każdej liczby testow ej. Rozw iązanie to jest now ym punktem odniesienia testu porównawczego. Co istotne, to rozwiązanie bazujące na obiekcie Pool zapewnia optymalny w ariant w przy padku sprawdzania pod kątem występow ania liczb pierwszych. Jeśli w ystępuje liczba pierw sza, nie ma możliwości wcześniejszego zakończenia przetwarzania. Konieczne jest uprzednie ręczne sprawdzenie w szystkich m ożliwych dzielników. Nie istnieje szybsza metoda sprawdzania tych dzielników: dowolne rozwiązanie, które zwięk sza złożoność, będzie zaw ierać w ięcej instrukcji, dlatego w ariant spraw dzania w szystkich dzielników spowoduje, że zostanie wykonana w iększość instrukcji. Aby przygotow ać się do dyskusji poświęconej sposobowi osiągnięcia dla liczb pierw szych jak najbardziej zbliżonego do przedstawionego tutaj rezultatu, zaznajom się z omówionymi dalej różnymi rozwiąza niami bazującymi na m odule mmap.
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
\ 227
Użycie obiektu Manager.Value jako flagi Obiekt multiprocessing.Manager() umożliwia współużytkow anie obiektów Python wysokiego poziomu m iędzy procesami jako w spółużytkow anych obiektów zarządzanych. Obiekty niskopoziomowe są opakowywane za pomocą obiektów proxy. Opakowywanie i bezpieczeństwo powodują spadek szybkości, ale oferują również dużą elastyczność. M ożliwe jest w spółużyt kowanie zarówno obiektów niskopoziom ow ych (np. liczb całkowitych i zm iennoprzecinko wych), jak i list oraz słowników. W przykładzie 9.14 tworzony jest obiekt Manager, a następnie 1-bajtowa (znak) flaga manager. '^Value(b"c", FLAG_CLEAR). Jeśli m ają być współużytkow ane łańcuchy lub liczby, m ożliwe jest utw orzenie dow olnych typów podstaw ow ych kom ponentu ctypes (są takie sam e jak typy podstawowe array.array). Przykład 9.14. Przekazywanie obiektu Manager.Value jako flagi SERIAL_CHECK_CUTOFF = 21 CHECK_EVERY = 1000 FLAG_CLEAR = b ' 0 ' FLAG_SET = b ' 1 ' p r i n t "CHECK_EVERY", CHECK_EVERY if name == " main ": NBR_PROCESSES = 4 manager = m ultip roce ss ing.M anag er () value = m a n a g e r . V a l u e ( b 'c ', FLAG_CLEAR)
# znak 1-bajtowy
Zauważ, że flagom FLAG_CLEAR i FLAG_SET jest przypisywany bajt (odpowiednio b '0 ' i b '1 '). Zdecydowano się na użycie początkowej litery b, aby było to bardzo jaw ne (zależnie od uży wanego środowiska i wersji języka Python pozostawienie bajta w postaci niejawnego łańcucha może domyślnie spowodować potraktowanie go jako obiektu Unicode lub obiektu łańcuchowego). M ożliwe jest teraz użycie flagi dla w szystkich procesów w celu poinform ow ania o znalezie niu dzielnika. Dzięki temu w yszukiw anie może zostać wcześniej zakończone. Trudność po lega na zrównoważeniu kosztu odczytywania flagi z możliwym wzrostem szybkości. Ponieważ flaga jest synchronizow ana, nie jest pożądane zbyt częste jej spraw dzanie, bo to pow oduje dodatkowe obciążenie. Funkcja check_prime_in_range będzie teraz rozpoznawać w spółużytkow aną flagę, a procedura będzie przeprow adzać sprawdzenie w celu stwierdzenia, czy liczba pierwsza została zidenty fikowana przez inny proces. N aw et pomimo tego, że konieczne jest jeszcze rozpoczęcie wy szukiw ania rów noległego, przed zainicjow aniem spraw dzania szeregow ego niezbędne jest wyczyszczenie flagi w sposób pokazany w przykładzie 9.15. Jeśli po zakończeniu sprawdza nia szeregowego nie znaleziono dzielnika, wiadomo, że flaga nadal musi m ieć w artość False. Przykład 9.15. Czyszczenie flagi za pomocą obiektu Manager.Value def check_prime(n, pool, nbr_proces ses , va lu e): # niepow odujące dużego obciążenia spraw dzanie zbioru możliwych dzielników o dużym praw dopodobieństw ie wystąpienia from_i = 3 to _i = SERIAL_CHECK_CUTOFF va lu e.v alue = FLAG_CLEAR i f not che ck_prime_in_range((n, (from _i, t o _ i ) , v a l u e ) ) : re turn False from i = t o i
228
|Rozdział 9. Moduł multiprocessing
Jak często należy sprawdzać w spółużytkow aną flagę? Każde sprawdzenie pow oduje obcią żenie, poniew aż do pętli w ew nętrznej dodaw anych jest więcej instrukcji, a ponadto spraw dzanie w ym aga utw orzenia blokady dla w spółużytkow anej zm iennej, z czym zw iązane jest dodatkowe obciążenie. W ybrane rozwiązanie polega na sprawdzaniu flagi dla każdego 1000 iteracji. Każdorazowo podczas sprawdzania stwierdzane jest, czy dla value.value została usta wiona w artość FLAG_SET. Jeśli tak, wyszukiw anie dobiega końca. Jeśli w trakcie wyszukiwania proces znajdzie dzielnik, ustawia value.value = FLAG_SET i kończy działanie (przykład 9.16). Przykład 9.16. Przekazywanie obiektu Manager.Value jako flagi def check _prim e_in_range((n, (f rom _i, t o _ i ) , v a l u e ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 check_every = CHECK_EVERY f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : check_every -= 1 i f not check_every: i f v alu e.v alu e == FLAG_SET: r e tu r n False check_every = CHECK_EVERY i f n % i == 0: v a lu e.v a lu e = FLAG_SET re tu rn Fa ls e re tu rn True
Sprawdzanie w tym kodzie każdego tysiąca iteracji jest przeprow adzane za pom ocą lokalne go licznika check_every. Okazuje się, że choć to rozwiązanie jest zrozum iałe, pod względem szybkości nie jest w pełni optymalne. Na końcu niniejszego podrozdziału zostanie ono zastą pione mniej czytelnym, lecz znacznie szybszym rozwiązaniem. M ożesz być ciekaw całkowitej liczby sprawdzeń dokonywanych dla współużytkowanej flagi. W przypadku dw óch dużych liczb pierw szych i czterech procesów flaga je st spraw dzana 316 405 razy (taka liczba spraw dzeń dotyczy w szystkich kolejnych przykładów ). Poniew aż z każdym spraw dzeniem zw iązane jest obciążenie w ynikające z blokow ania, sum arycznie obciążenie to jest napraw dę duże.
Użycie systemu Redis jako flagi Redis to mechanizm magazynowania w pamięci w postaci pary złożonej z klucza i wartości. Zapewnia on własną funkcję blokowania. Każda operacja jest niepodzielna, dlatego nie trze ba martwić się o korzystanie z blokad w obrębie kodu Python (lub dowolnego innego języka z interfejsem). Redis umożliwia uniezależnienie magazynu danych od języka. Dowolny język lub narzędzie z interfejsem dla systemu Redis m oże współużytkow ać dane w kompatybilny sposób. Z jed nakow ą łatw ością dane m ogą być w spółużytkow ane m iędzy językam i Python, Ruby, C++ i PHP. M ożliwe jest w spółużytkow anie danych na komputerze lokalnym lub za pośrednic twem sieci. Aby współużytkować dane z innymi komputerami, wymagana jest jedynie zmiana domyślnego trybu współużytkowania systemu Redis tylko w obrębie lokalnego komputera.
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
\ 229
System Redis pozwala na magazynow anie następujących danych: • list łańcuchów, • zbiorów łańcuchów, • sortowanych zbiorów łańcuchów, • wartości mieszających łańcuchów. System Redis przechowuje wszystko w pam ięci RAM i w obrazach stanu na dysku (używając opcjonalnie kronikow ania). System obsługuje replikację z serw erem głów nym i serweram i podrzędnymi z wykorzystaniem klastra instancji. W przypadku systemu Redis jedną z moż liwości jest użycie go do w spółużytkow ania obciążenia w obrębie klastra, w którym różne komputery odczytują i zapisują stan, a Redis pełni rolę szybkiego i scentralizowanego repo zytorium danych. Flaga może być odczytywana i zapisywana jako łańcuch tekstowy (wszystkie wartości w sys temie Redis są łańcuchami) dokładnie w taki sam sposób, jak to miało wcześniej miejsce podczas korzystania z flag w kodzie Python. Interfejs StrictRedis jest tworzony jako obiekt globalny, który komunikuje się z zewnętrznym serwerem Redis. W obrębie funkcji check_prime_in_range można utw orzyć now e połączenie, ale jest ono w olniejsze, a ponadto m oże w yczerpać ogra niczoną liczbę dostępnych uchw ytów systemu Redis. Komunikacja z serwerem Redis odbywa się w ramach dostępu podobnego jak w przypadku słownika. Wartość może zostać ustawiona za pomocą instrukcji rds[KLUCZ] = WARTOŚĆ. Łańcuch jest ponownie wczytywany przy użyciu instrukcji rds[KLUCZ]. Przykład 9.17 bardzo przypom ina poprzedni przykład dotyczący obiektu Manager. System Redis zastępuje lokalny obiekt Manager. W tym przypadku w ystępuje pod obne obciążenie zw iązane z dostępem . N ależy zauw ażyć, że system Redis obsługuje inne (bardziej złożone) struktury danych. Jest to bogaty w m ożliw ości m echanizm przechow yw ania, który w tym przykładzie używany jest jedynie do współużytkow ania flagi. Zachęcamy do zaznajomienia się w e w łasnym zakresie z funkcjami systemu. Przykład 9.17. Użycie zewnętrznego serwera Redis dla flagi FLAG_NAME = b 'r e d i s _ p r i m e s _ f l a g ' FLAG_CLEAR = b ' 0 ' FLAG_SET = b ' 1 ' rds = r e d i s . S t r i c t R e d i s ( ) def che ck_prim e_in_ra nge((n, (f ro m _i, t o _ i ) ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 check_every = CHECK_EVERY f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : check_every -= 1 i f not check_every: f l a g = rds[FLAG_NAME] i f f l a g == FLAG_SET: re tu rn Fa ls e check_every = CHECK_EVERY i f n % i == 0: rds[FLAG_NAME] = FLAG_SET re tu rn False r e tu r n True def check_prime(n, poo l, n b r_ p ro c e ss e s):
230
|
Rozdział 9. Moduł multiprocessing
# niepow odujące dużego obciążenia spraw dzanie zbioru możliwych dzielników o dużym praw dopodobieństw ie w ystąpienia from_i = 3 to _i = SERIAL_CHECK_CUTOFF rds[FLAG_NAME] = FLAG_CLEAR i f not check_prime_in_range((n, (from_i, t o _ i ) ) ) : re turn False i f Fals e in r e s u l t s : re turn False re turn True
Aby potw ierd zić, że dane są przechow yw ane poza jed ną z takich instancji kodu Python, z poziomu wiersza poleceń można w yw ołać polecenie re d is-cli (przykład 9.18) i uzyskać w artość znajdującą się w kluczu redis_primes_flag. Zauważysz, że zw racany element jest łań cuchem (a nie liczbą całkowitą). Wszystkie wartości zwracane z systemu Redis są łańcuchami, dlatego jeśli w ymagane jest m odyfikowanie ich w kodzie Python, konieczne będzie najpierw przekształcenie ich w odpowiedni typ danych. Przykład 9.18. Przykład użycia polecenia redis-cli $ r e d is -c li r e d i s 1 2 7 . 0 . 0 . 1:6379> GET "r ed is_ p r im e s_ fla g " "0"
M ocnym argumentem przemawiającym za użyciem systemu Redis do współużytkowania danych jest to, że funkcjonuje on poza „światem języka Python". Dzięki temu wszystko bę dzie zrozum iałe dla program istów z zespołu, którzy nie korzystają z języka Python. Ponadto dla systemu Redis istnieje wiele narzędzi. Programiści będą w stanie spraw dzić stan systemu Redis podczas analizowania (lecz niekoniecznie w trakcie w ykonywania i debugowania) ko du i śledzić jego działanie. Pod względem szybkości pracy zespołu m oże to być duża korzyść pom im o obciążenia kom unikacyjnego w ynikającego z zastosow ania system u Redis. Choć system ten stanow i dodatkow ą zależność w projekcie, należy zauw ażyć, że jest to bardzo pow szechnie w drażane narzędzie, które zapew nia dobre m ożliw ości debugow ania i łatwo można opanow ać jego obsługę. System Redis potraktuj jako bogate w możliwości narzędzie, które warto m ieć na podorędziu. System Redis oferuje wiele opcji konfiguracji. Dom yślnie korzysta z interfejsu protokołu TCP (w łaśnie takiego używ am y), choć w dokum entacji testów porów naw czych zaznaczono, że gniazda m ogą być znacznie szybsze. W spomniano również o tym, że choć zestaw protoko łów TC P/IP um ożliw ia w spółużytkow anie danych za pośrednictw em sieci m iędzy różnego typu system am i operacyjnym i, inne opcje konfiguracji okażą się praw dopodobnie szybsze (jednakże spowodują ograniczenie liczby opcji komunikacyjnych). G d y p ro g ra m y d o testó w p o r ó w n a w c z y c h klien ta i s erw era dzia ła ją n a ty m s a m y m ko m p u terz e, m o ż n a k o r z y sta ć z a r ó w n o z pętli z w r o tn e j T C P / IP , ja k i z u n ik s o w y c h g n iazd d o m e n o w y c h . C h o ć j e s t to z a l e ż n e o d p l a t f o r m y , u n i k s o w e g n i a z d a d o m e n o w e m o g ą o s i ą g n ą ć o k o ł o 5 0 % w ię k s z ą p r z e p u s t o w o ś ć n iż p ętla z w r o tn a T C P / I P (na p rz y k ła d w s y stem ie Lin u x ). D o m y ś ln ie w te sta c h p o r ó w n a w c z y c h s y s te m u R e d is u ż y w a n a je st p ę tla z w r o t n a T C P / IP . K o r z y ś c i w p o s ta ci lep sze j w y d a jn o ś c i u n i k s o w y c h g n ia z d d o m e n o w y c h w p o r ó w n a n i u z p ę tlą z w r o t n ą T C P / I P z w y k l e z m n i e j s z a j ą się, g d y i n t e n s y w n i e w y k o r z y s t y w a n e j e s t p o t o k o w a n i e (n p . d ł u g i e p o t o k i ) . — D o k u m e n t a c j a s y s t e m u R e d i s (http://redis.io/topics/benchmarks)
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
\
231
Użycie obiektu RawValue jako flagi Obiekt multiprocessing.RawValue to „cienkie" opakowanie bloku bajtów komponentu ctypes. Ponieważ pozbaw ione jest ono podstawowych typów synchronizacji, poszukiwanie najszyb szej metody ustawienia flagi między procesami nie będzie w ymagać wielu działań. Metoda ta będzie niemal tak szybka jak w przypadku zamieszczonego dalej przykładu z modułem mmap (mniejsza szybkość wynika jedynie z wykorzystania kilku dodatkowych instrukcji). I tym razem można zastosow ać dowolny typ podstawowy komponentu ctypes. Dostępna jest też opcja RawArray służąca do współużytkow ania tablicy obiektów podstaw ow ych (działanie będzie podobne jak w przypadku array.array). Obiekt RawValue unika blokowania. Zapewnia szybsze użycie, ale nie oferuje operacji niepodzielnych. O gólnie rzecz biorąc, jeśli unikniesz synchronizacji zapew nianej przez interpreter języka Python podczas komunikacji międzyprocesowej IPC, poniesiesz porażkę (i tym razem może temu towarzyszyć spora dawka frustracji). Jednakże w przypadku tego problemu nie ma zna czenia, czy flaga jest jed nocześnie ustaw iana przez jeden, czy w ięcej procesów . Flaga jest przełączana tylko w jednym kierunku. Za każdym innym razem, gdy flaga jest odczytywana, ma to na celu jedynie stwierdzenie, czy w yszukiw anie m oże zostać zakończone. Ponieważ stan flagi nie jest resetowany podczas wyszukiwania równoległego, synchronizacja nie jest potrzebna. Bądź świadom tego, że może to nie dotyczyć problemu, którym się zaj mujesz. Jeśli unikniesz synchronizacji, upewnij się, że wynika to z słusznych powodów. Aby przeprowadzić takie działania jak aktualizowanie współużytkowanego licznika, w doku mentacji poszukaj opisu obiektu Value i użyj m enedżera kontekstów z m etodą value.get_lock() (https://docs.python.org/2/library/multiprocessing.html), gdyż niejawne blokowanie w obiekcie Value nie zezwala na operacje niepodzielne. Omawiany przykład bardzo przypomina wcześniejszy przykład z obiektem Manager. Jedyną różnicą jest to, że w przykładzie 9.19 obiekt RawValue jest tworzony jako 1-znakowa (bajt) flaga. Przykład 9.19. Tworzenie i przekazywanie obiektu RawValue if
name == " main ": NBR_PROCESSES = 4 value = m u l ti p r o c e s s i n g .R a w V a l u e (b 'c ', FLAG_CLEAR) pool = Pool(processes=NBR_PROCESSES)
# 1-bajtow y znak
Elastyczność stosow ania w artości zarządzanych i nieprzetw orzonych stanow i zaletę przej rzystego projektu w spółużytkow ania danych w m odule multiprocessing.
Użycie modułu mmap jako flagi Pora zająć się najszybszą m etodą w spółużytkow ania bajtów . Przykład 9.20 prezentuje roz wiązanie bazujące na module mmap, w przypadku którego wykorzystywana jest pamięć współ użytkowana. Bajty w bloku pamięci współużytkowanej nie są synchronizow ane i powodują bardzo niew ielkie obciążenie. Zachow ują się podobnie do pliku. W tym przypadku są blo kiem pamięci z interfejsem przypominającym stosowany dla plików. Konieczne jest odszu kanie położenia za pomocą funkcji seek oraz sekwencyjny odczyt lub zapis. Zw ykle moduł mmap służy do zapew nienia ogólnego w idoku w iększego pliku (odw zorow yw anej pam ięci),
232
|
Rozdział 9. Moduł multiprocessing
ale w omawianym przykładzie, zamiast podawać num er pliku jako pierwszy argument, prze kazujemy wartość -1 w celu wskazania, że żądany jest anonimowy blok pamięci. Możliwe jest też określenie, czy w ym agany jest dostęp tylko z odczytem, czy z zapisem (domyślnie uży w any jest dostęp w obu wariantach, które są nam potrzebne). Przykład 9.20. Użycie flagi pamięci współużytkowanej za pośrednictwem modułu mmap sh_mem = mmap.mmap(-1, 1) # jed n o ba jto w e odw zorow anie p a m ięci ja k o fla g i def check _prim e_in_range((n, (f ro m _i, t o _ i ) ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 check_every = CHECK_EVERY f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : check_every -= 1 i f not check_every: sh_mem.seek( 0 ) f l a g = sh_mem.read_byte() i f f l a g == FLAG_SET: re tu rn False check_every = CHECK_EVERY i f n % i == 0: sh_mem.seek( 0 ) s h_mem.writ e_ byte( F LAG_SET) re tu rn Fa ls e re tu rn True def check_prime(n, poo l, n b r_ p ro c e ss e s): # n iepow odu jące dużego obcią żen ia spraw dzanie zbioru m ożliwych dzielników o dużym praw dopodobieństw ie w ystąpienia from_i = 3 to _ i = SERIAL_CHECK_CUTOFF sh_mem.seek( 0 ) sh_mem.write_byt e(FLAG_C LEAR) i f not che ck_prim e_in_range((n , (f rom _i, t o _ i ) ) ) : re tu rn False i f False in r e s u l t s : re tu rn False re tu rn True
Moduł mmap obsługuje kilka metod, które m ogą być używ ane do nawigacji w obrębie pliku reprezentow anego przez m oduł (obejm uje to m etody find, readl ine i write). Jest stosow any w najbardziej podstawowy sposób. Przed każdym odczytem lub zapisem za pom ocą funkcji seek szukany jest początek bloku pamięci. Ponieważ współużytkow any jest tylko jeden bajt, metody read_byte i write_byte są używ ane jako jawne. Nie występuje obciążenie kodu Python związane z blokowaniem i interpretowaniem danych. Ze względu na to, że bajty są przetw arzane bezpośrednio z wykorzystaniem systemu opera cyjnego, jest to najszybsza metoda komunikacji.
Użycie modułu mmap do odtworzenia flagi Choć wynik uzyskany w poprzednim przykładzie z modułem mmap okazał się ogólnie najlepszy, nie m ożemy zrezygnow ać z myśli, że powinno być możliwe ponow ne osiągnięcie wyniku metody z bardzo prostym obiektem Pool w przypadku wariantu znajdowania liczb pierwszych o najw iększym obciążeniu. Celem jest zaakceptow anie tego, że nie m a m ożliw ości w cze śniejszego w yjścia z pętli w ew nętrznej, a ponadto zm inim alizow anie obciążenia w szelkich zew nętrznych elementów.
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
\ 233
W tym punkcie zaprezentowano trochę bardziej złożone rozwiązanie. Te same zm iany mogą być dokonywane w innych wcześniej przedstaw ionych metodach opartych na fladze, choć w ynik uzyskany dla tego wariantu wykorzystania modułu mmap będzie nadal najlepszy. W e w cześniejszych przykładach użyto flagi CHECK_EVERY. Oznacza to, że istnieje zmienna lo kalna check_next służąca do śledzenia, dekrementowania i stosowania w testach boolowskich. Ponadto każda operacja dodaje do każdej iteracji krótki dodatkow y czas. W przypadku spraw dzania dużej liczby pierw szej takie dodatkow e obciążenie zw iązane z zarządzaniem występuje ponad 300 000 razy. Pierwsza optymalizacja przedstawiona w przykładzie 9.21 ma na celu uświadom ienie Ci, że możliwe jest zastąpienie dekrementowanego licznika wartością wyprzedzającą. Dzięki temu konieczne będzie jedynie przeprow adzenie porów nania boolow skiego w pętli wewnętrznej. Taka operacja elim inuje dekrem entow anie, które z pow odu sposobu działania interpretera języka Python jest dość w olne. Przedstaw iona optym alizacja w om aw ianym teście działa w im plementacji CPython 2.7, ale raczej nie zapewni żadnych korzyści w przypadku bardziej inteligentnego kompilatora (np. PyPy lub Cython). Podczas sprawdzania jednej z przykła dowych dużych liczb pierw szych operacja zastąpienia dekrementowanego licznika pozwoliła zyskać 0,7 sekundy. Przykład 9.21. Rozpoczęcie optymalizowania obciążającej logiki def che ck_prim e_in_ra nge((n, (f ro m _i, t o _ i ) ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 check_next = from_i + CHECK_EVERY f o r i in xr an ge(f rom _i, i n t ( t o _ i ) , 2 ) : i f check_next == i : sh_mem.seek(0) f l a g = sh_mem.read_byte() i f f l a g == FLAG_SET: re tu rn Fa ls e check_next += CHECK_EVERY i f n % i == 0: sh_mem.seek(0) sh_mem.write_byte(FLAG_SET) r e tu r n False r e tu r n True
M ożliwe jest też całkowite zastąpienie logiki reprezentowanej przez licznik (przykład 9.22) przez rozw inięcie pętli do postaci dw uetapow ego procesu. W pierw szym etapie pętla ze wnętrzna obejmuje spodziewany zakres, lecz krokowo w fladze CHECK_EVERY. W drugim etapie nowa pętla w ewnętrzna zastępuje logikę funkcji check_every. Pętla sprawdza lokalny zakres dzielników, a następnie kończy działanie. Odpowiada to testowi i f not check_every:. W celu sprawdzenia flagi wczesnego zakończenia w dalszej kolejności stosowana jest wcześniej przed stawiona logika obiektu sh_mem. Przykład 9.22. Optymalizowanie obciążającej logiki def che ck_prim e_in_ra nge((n, (f ro m _i, t o _ i ) ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 f o r o uter _c oun ter in xran ge (f rom _i, i n t ( t o _ i ) , CHECK_EVERY): upper_bound = m i n ( i n t ( t o _ i ) , outer _c oun ter + CHECK_EVERY) f o r i in xr an ge (o u ter_c ou n te r, upper_bound, 2 ) :
234
|Rozdział 9. Moduł multiprocessing
i f n % i == 0: sh_mem.seek(0) sh_mem.write_byte(FLAG_SET) re tu rn False sh_mem.seek(0) f l a g = sh_mem.read_byte() i f f l a g == FLAG_SET: re tu rn Fa ls e re tu rn True
W pływ na szybkość jest bardzo znaczny. W przypadku dotyczącym liczb złożonych w zrost jest jeszcze większy, ale co ważniejsze, w w ariancie sprawdzania pod kątem liczb pierwszych uzyskana szybkość jest prawie taka sama jak dla wariantu z bardzo prostym obiektem Pool dla mniejszych liczb (obecnie osiągnięto rezultat z czasem dłuższym zaledw ie o 0,05 sekundy). Biorąc pod uw agę to, że realizow anych jest w iele dodatkow ych działań związanych z ko m unikacją międzyprocesową, jest to bardzo ciekawy w ynik. Zauważ jednak, że jest to specy ficzne dla narzędzia CPython, i raczej nie zostaną zapewnione żadne w zrosty szybkości, gdy zostanie zastosow any kompilator. M ożemy pójść jeszcze o krok dalej (ale szczerze mówiąc, jest to trochę niemądre). W yszuki w anie zm iennych, które nie są zadeklarow ane w zasięgu lokalnym , jest dosyć kosztow ne. M ożliw e jest utw orzenie lokalnych odw ołań do globalnej flagi FLAG_SET oraz często używ a nych m etod .seek() i .read_byte(), aby uniknąć pow iązanych z nim i bardziej obciążających wyszukiwań. W ynikow y kod (przykład 9.23) jest jednak jeszcze mniej czytelny niż dotych czas. Naprawdę zalecamy, aby z tego zrezygnować. Przy sprawdzaniu w iększych liczb pierw szych ostateczny czas jest 1,5% dłuższy niż w przypadku wariantu z bardzo prostym obiek tem Pool dla mniejszych liczb. Biorąc pod uwagę to, że dla liczb złożonych uzyskano 4,8 razy lepszy czas, praw dopodobnie prezentowany przykład zoptym alizow ano w m aksymalnym m ożliwym (tak należało postąpić!) stopniu. Przykład 9.23. Złamanie reguły „nie zmniejszaj szybkości pracy zespołu" w celu zapewnienia dodatkowego przyspieszenia def check _prim e_in_range((n, (f rom _i, t o _ i ) ) ) : i f n % 2 == 0: re tu rn False a s s e r t from_i % 2 != 0 FLAG_SET_LOCAL = FLAG_SET sh_seek = sh_mem.seek sh_read_byte = sh_mem.read_byte f o r outer _coun ter in xr an ge(f ro m _i, i n t ( t o _ i ) , CHECK_EVERY): upper_bound = m i n ( i n t ( t o _ i ) , o uter _c oun ter + CHECK_EVERY) f o r i in x ra n ge (o u ter_ c ou n te r, upper_bound, 2 ) : i f n % i == 0: sh _see k(0) sh_mem.write_byte(FLAG_SET) re tu rn False sh _see k(0) i f sh _rea d_byte() == FLAG_SET_LOCAL: re tu rn Fa ls e re tu rn True
Takie działania, które uwzględniają ręczne rozwijanie pętli i tworzenie lokalnych odwołań do obiektów globalnych, są niem ądre. Zw ykle pow odują zm niejszenie szybkości pracy zespołu z pow odu generow ania kodu trudniejszego do zrozum ienia, a w rzeczyw istości zadania te należą do kom pilatora (np. kom pilatora JIT, takiego jak PyPy, lub kom pilatora statycznego, takiego jak Cython).
Weryfikowanie liczb pierwszych za pomocą komunikacji międzyprocesowej
| 235
Programiści nie powinni w prow adzać tego rodzaju m odyfikacji, ponieważ będzie to bardzo zawodne. Takiej m etody optymalizacji nie testowaliśmy dla języka Python w wersji 3 i now szych, a ponadto nie zamierzamy tego robić. Tak napraw dę nie spodziewamy się, że te stop niowo wprowadzane ulepszenia będą działać w innej wersji języka Python (a z pewnością nie w innej implementacji, takiej jak PyPy lub IronPython). M etoda optymalizacji jest prezentowana, aby było wiadomo, że jest możliwa do zastosowania. Ostrzegamy, że dla uniknięcia frustracji naprawdę warto umożliwić kompilatorom wyręczenie nas w przypadku tego rodzaju działań.
Współużytkowanie danych narzędzia numpy za pomocą modułu multiprocessing Podczas korzystania z dużych tablic narzędzia numpy m ożesz się zastanawiać, czy możliwe jest w spółużytkow anie danych m iędzy procesam i na potrzeby operacji dostępu z odczytem i zapisem bez kopiowania. Choć jest to możliwe, okazuje się trochę nieporęczne. Chcemy po dziękować użytkow nikow i pv z serwisu StackOverflow za inspirację w przypadku przedsta w ionego dalej przykładu2. Nie używaj tej metody do odtwarzania sposobu działania bibliotek BLAS, MKL, Accelerate i ATLAS. Wszystkie te biblioteki obsługują wielowątkowość w swoich obiektach podstawowych, a ponadto prawdopodobne jest, że są lepiej debugowane niż jakakolwiek nowa procedura, która zostanie utworzona. Choć do włączenia ob sługi wielowątkowości może być konieczna ich konfiguracja, przed poświęceniem czasu na pisanie (i debugowanie!) własnego kodu sprawdź, czy te biblioteki mogą zapewnić przyspieszenie bez dodatkowego nakładu pracy. Ze współużytkowaniem dużej macierzy m iędzy procesami związanych jest kilka następują cych korzyści: • tylko jedna kopia oznacza brak zmarnowanej pamięci RAM, • brak straconego czasu na kopiow anie dużych bloków pam ięci RAM, • zapewnienie możliwości współużytkowania częściowych w yników między procesami. W przedstaw ionej w cześniej prezentacji przybliżania liczby pi za pom ocą narzędzia numpy (podrozdział „Zastosowanie narzędzia num py") wystąpił problem polegający na tym, że ge nerowanie liczb losowych było procesem szeregowym. W omawianym tutaj przykładzie mo żem y sobie w yobrazić procesy rozw idlające, które w spółużytkują jedną dużą tablicę. Każdy z procesów korzysta z inicjowanego w różny sposób za pom ocą w artości początkowej gene ratora liczb losowych, aby zapełnić sekcję tablicy liczbami losowymi. Oznacza to, że genero w anie dużego bloku liczb losow ych zostanie zakończone szybciej, niż byłoby to m ożliw e w przypadku pojedynczego procesu.
2 Więcej informacji zamieszczono w tem a cie
w s e r w isie S ta c k O v erflo w (h ttp ://s ta c k o v e rflo w .c o m /q u e stio n s/5 5 4 9 1 9 0 /
is -sh a red -rea d o n ly -d a ta -c o p ied -to -d ifferen t-p ro c esses-fo r-p y th o n -m u ltip ro cessin g /5 5 5 0 1 5 6 ).
236
|
Rozdział 9. Moduł multiprocessing
Aby to sprawdzić, zmodyfikowaliśm y zam ieszczony dalej przykład w celu utw orzenia dużej macierzy liczb losowych (o w ym iarach 10 000 elementów na 80 000 elementów) jako procesu szeregow ego, a ponadto dokonaliśm y podziału m acierzy na cztery segm enty, dla których generator random jest w yw oływ any w sposób rów noległy (w obu przypadkach po jednym wierszu naraz). Proces szeregowy zajął 15 sekund, a równoległy — 4 sekundy. W róć do pod rozdziału „Liczby losow e w system ach przetw arzania rów n oleg łego", aby zaznajom ić się z zagrożeniami w ynikającymi z generowania liczb losowych w sposób równoległy. W pozostałej części podrozdziału zostanie przedstaw iony uproszczony przykład, ilustrujący kwestię, która jednak będzie prosta do w eryfikacji. Na rysunku 9.19 pokazano dane wyjściow e narzędzia htop uruchom ionego na laptopie jed nego z autorów. Dane prezentują cztery procesy podrzędne procesu nadrzędnego (z identy fikatorem procesu PID o num erze 11268). W szystkie pięć procesów współużytkuje jedną ta blicę narzędzia numpy z elem entam i typu double o w ielkości 10 000x80 000 elem entów . Jedna kopia takiej tablicy zużywa 6,4 GB pamięci. Laptop jest wyposażony tylko w 8 GB pamięci RAM. W danych wyjściowych narzędzia htop wyśw ietlonych według liczników procesów widoczne jest dla ustaw ienia Memzużycie pamięci RAM równe 7941 MB.
I I I I I I I I I I I I I I I I I I I I I I I 11 5 8 9 /7 9 4 1 M B
TTTTT1m S 6.6 s 0.0
FM ■ X 26 20 26 20 26 20 26 26 20 26 26 26 26
6 0 6 6 0 6 0 6 0 6 e 0 e
156
512 020 626 G26 772 772
s s s s s 166 s 976 s 976 s 976 s 564 s 564 5
6.6 0.0 0.0 0.0 0.0 6.6 0.0 6.6 0.0 0.6 0.0
77.6 77.0 77.6 19.4 19.4 19.4 19.4 6.6 0.2 6.2 0.2 0.1 0.1
6:6 6 .6 6 0:0 0 .0 0 6:41.84 0:0 0 .7 3 0:00.71 0:0 0 .7 3 0:0 0 .7 5 6:66.61 0:0 1 .2 3 6:6 6 .6 5 0:0 0 .0 0 0:0 0 .1 0 0:0 0 .0 0
Ip ython I I nIp _ sh anre d .p y p ython n p s h a r e d .p y p ython n p s h a r e d .p y p ython np s h a re d .p y p ython np s h a re d .p y p ython np s h a re d .p y p ython n p s h a r e d .p y / u s r / b in / p y t h o n / u s r / b in / m in tu p d a te I p ython / u s r / lib / lin u x m in t / m in t U p ython / u s r / lib / lin u x m in t / m i p ython / u s r / lib / lin u x m in t / m i / u s r / b ln / p y t h o n / u s r / b ln / c ln n a m o n -la u / u s r / b in / p y t h o n / u s r/b in /c in n a m o n -
Rysunek 9.19. Dane wyjściowe narzędzia htop prezentujące wykorzystanie pamięci RAM i obszaru wymiany Aby zrozum ieć ten przykład, najpierw przeanalizujem y dane wyjściowe konsoli, a następnie przyjrzym y się kodowi. W przykładzie 9.24 rozpoczynam y proces nadrzędny: alokuje on ta blicę z elementami typu double o wielkości 10 000x80 000 elementów (6,4 GB), która jest wy pełniona wartościam i zerowymi. Do funkcji procesu roboczego 10 000 w ierszy zostanie prze kazanych jako indeksy. Z kolei proces ten przetw orzy każdą kolum nę 80 000 elem entów . Po przydzieleniu tablicy wypełniana jest ona liczbą 42, która stanowi odpowiedź na „W ielkie pytanie o życie, w szechświat i całą resztę"3. W funkcji procesu roboczego można sprawdzić, że odbierana jest ta zmodyfikowana tablica, a nie tablica w ypełniona wartościami zerowymi, aby potwierdzić, że kod działa zgodnie z oczekiwaniami. 3 Je st t o je d e n z m o t y w ó w k s i ą ż e k D o u g l a s a A d a m s a — przyp. tłum .
W spółużytkowanie danych narzędzia numpy za pomocą modułu multiprocessing
|
237
Przykład 9.24. Konfigurowanie tablicy współużytkowanej $ python np_shared.py Utworzono współużytkowaną t a b l i c ę , która zawiera 6 400 000 000 nbajtów I d e n t y f i k a t o r współużytkowanej t a b l i c y to 20255664 w i d e n t y f i k a t o r z e PID 11268 Rozpoczęcie od t a b l i c y z wartościami zerowymi: [ [ 0. 0. 0 0. 0. 0.] [ 0. 0. 0 0. 0. 0.]] Oryginalna t a b l i c a wypełniona w ar to śc ią 42: [ [ 42. 42. 4 2 .............. 42. 42. 4 2 . ] [ 42. 42. 4 2 .............. 42. 42. 4 2 . ] ] N a c i śn i j klaw isz, aby uruchomić procesy robocze za pomocą modułu m u l t i p r o c e s s i n g . . .
W przykładzie 9.25 uruchomiono cztery procesy, które przetw arzają tę współużytkow aną ta blicę. Nie została utworzona żadna kopia tablicy. Każdy proces sprawdza ten sam duży blok pamięci, a ponadto dysponuje innym zestawem indeksów, na których bazuje. Co kilka tysięcy w ierszy proces roboczy zwraca bieżący indeks i swój identyfikator PID, aby można było ob serw ow ać działanie procesu. Zadanie procesu roboczego jest tryw ialne — spraw dzi on, czy bieżący element nadal jest ustaw iony na w artość dom yślną (dzięki temu wiadomo, że żaden inny proces jeszcze go nie zmodyfikował), a następnie nadpisze tę w artość bieżącym identy fikatorem procesu PID. Po zakończeniu pracy przez procesy robocze następuje pow rót do procesu nadrzędnego i ponow ne w yśw ietlenie tablicy. Tym razem widać, że jest ona w ypeł niona identyfikatorami PID, a nie w artością 42. Przykład 9.25. Uruchamianie funkcji worker_fn dla współużytkowanej tablicy worker_fn: z indeksem 0 i d e n t y f i k a t o r obiekt u sh ared_array to 20255664 w i d e n t y f i k a t o r z e PID 11288 worker_fn: z indeksem 2000 i d e n t y f i k a t o r obiekt u sh ared_array to 20255664 w i d e n t y f i k a t o r z e PID 11291 worker_fn: z indeksem 1000 i d e n t y f i k a t o r obiekt u sh ared_array to 20255664 w i d e n t y f i k a t o r z e PID 11289 worker_fn: z indeksem 8000 i d e n t y f i k a t o r obiekt u sh ared_array to 20255664 w i d e n t y f i k a t o r z e PID 11290 Wartość domyślna z o s t a ł a nadpisana wynikiem f u n k c ji worker_fn: [ [ 11288. 11288. 11288 11288. 11288. 11288.] [ 11291.
11291.
11291............
11291.
11291.
1 1 2 9 1 .]]
W przykładzie 9.26 używ any jest licznik Counter do potw ierdzenia w ystępow ania każdego identyfikatora PID w tablicy. Poniew aż zadania są rów nom iernie rozdzielane, oczekujem y, że każdy z czterech identyfikatorów PID będzie reprezentowany przez jednakow ą liczbę wy stąpień. W przypadku przykładowej tablicy liczącej 800 000 000 elementów widoczne są cztery zestawy złożone z 200 000 000 identyfikatorów PID. Dane w yjściow e w postaci tabeli zapre zentowano za pomocą biblioteki PrettyTable (https://pypi.python.org/pypi/PrettyTable). Przykład 9.26. Weryfikowanie wyniku dla współużytkowanej tablicy Wer yfikac ja - wyodrębnianie unikalnych wartości z elementów {: ,} \ n w t a b l i c y narzędzia numpy (może to być d ł u g o t r w a ł e ) . . . Unikalne wartości w o b i e k c i e shared _a rra y: +--------------+-------------------- + | PID | Liczba | +--------------+-------------------- + | 1128 8. 0 |
238
200000000 |
|Rozdział 9. Moduł multiprocessing
| 1128 9.0 |200000000 | | 1129 0.0 |200000000 | | 1129 1.0 |200000000 | +---------------+------------------- + N a c i śn i j klaw isz, aby z a k o ń c z y ć .. .
Po zakończeniu przetwarzania program kończy działanie, a tablica jest usuwana. Używając poleceń ps i pmap, można przyjrzeć się bliżej każdemu procesowi w systemie Linux. Przykład 9.27 prezentuje wynik w ywołania polecenia ps. Znaczenie poszczególnych składni ków wiersza tego polecenia jest następujące: • Polecenie ps informuje o procesie. • Opcja -A wyszczególnia wszystkie procesy. • Opcja -o pid,size,vsize,cmd zwraca dane wyjściowe, które zawierają identyfikator procesu PID, informacje o wielkości oraz nazwę polecenia. • Polecenie grep służy do filtrowania wszystkich pozostałych wyników i pozostawiania wy łącznie wierszy wymaganych w przykładzie. Przykład 9.27. Użycie poleceń pmap i ps do przeanalizowania widoku procesów zapewnianego przez system operacyjny $ ps -A -o p id ,s iz e ,v s iz e ,c m d | grep np_shared 11268 232464 6564988 python np_shared.py 11288 11232 6343756 python np_shared.py 11289 11228 6343752 python np_shared.py 11290 11228 6343752 python np_shared.py 11291 11228 6343752 python np_shared.py ian @ ian-L atitu de-E 6420 $ pmap -x 11268 | grep s Address Kbytes RSS D ir ty Mode Mapping 00007f1953663000 6250000 6250000 6250000 rw -s- zero (d elet ed) ian @ ian-L atitu de-E 6420 $ pmap -x 11288 | grep s Address Kbytes RSS D ir ty Mode Mapping 00007f1953663000 6250000 1562512 1562512 rw -s- zero (d elet ed)
Proces nadrzędny (z identyfikatorem PID 11268) i jego cztery podrzędne procesy rozwidlone są w yśw ietlane w danych w yjściow ych. W ynik jest podobny do uzyskanego dla narzędzia htop. Po zażądaniu rozszerzonych danych w yjściow ych za pom ocą opcji -x polecenie pmap umożliwia sprawdzenie m apy pamięci każdego procesu. Dla polecenia grep użyto wzorca s-, aby wyśw ietlić listę bloków pam ięci oznaczonych jako współużytkowane. W procesie nad rzędnym i w procesach podrzędnych w idoczny jest blok o wielkości 6 250 000 kB (6,2 GB), który jest przez nie współużytkowany. Przykład 9.28 prezentuje w ażne kroki wykonane w celu współużytkowania tej tablicy. Obiekt multprocessing.Array służy do alokacji współużytkowanego bloku pamięci jako tablicy jedno wymiarowej. Na podstawie tego obiektu tworzona jest następnie instancja tablicy narzędzia numpy i ponownie przekształcana do postaci tablicy dwuwymiarowej. Dysponujemy teraz blo kiem pamięci opakowanym przez narzędzie numpy, który m oże być współużytkow any między procesami i adresowany tak jak w przypadku zwykłej tablicy narzędzia numpy. Narzędzie to nie zarządza pam ięcią RAM. Odpowiada za to obiekt multiprocessing.Array.
W spółużytkowanie danych narzędzia numpy za pomocą modułu multiprocessing
| 239
Przykład 9.28. Współużytkowanie tablicy narzędzia numpy za pomocą modułu multiprocessing import os import m ulti proce ss ing from c o l l e c t i o n s import Counter import ctypes import numpy as np from p r e t t y t a b l e import Pre tty T ab le SIZE_A, SIZE_B = 10000, 80000 # 6,2 GB —rozpoczyna się korzystanie z obszaru wymiany # (nastąpiło m aksym alne w ykorzystanie p a m ię ci RAM)
W przykładzie 9.29 w idać, że każdy proces rozw idlony ma dostęp do globalnego obiektu main_nparray. Proces rozw idlony m a kopię obiektu narzędzia numpy, bazow e bajty używ ane przez ten obiekt są natomiast przechowywane jako pamięć współużytkowana. Funkcja worker_fn nadpisze wybrany wiersz (za pośrednictwem idx) identyfikatorem bieżącego procesu. Przykład 9.29. Funkcja worker_fn służąca do współużytkowania tablic za pomocą modułu multiprocessing def w o rker_fn (i dx): Przetwarzanie współużytkow anej tablicy np dla indeksu w iersza..... # potw ierdzenie, że żaden inny p r o c e s nie zm odyfikow ał ju ż tej w artości a s s e r t main_nparray[idx, 0] == DEFAULT_VALUE # wyświetlenie w ew n ątrzpodprocesu identyfikatora PID oraz identyfikatora tablicy # w celu upew nienia się, że nie istnieje kopia i f idx % 1000 == 0: p r i n t 11 { } : z indeksem {}\n i d e n t y f i k a t o r ob iektu local_n p arr ay_in _p ro c ess to ^ { } w i d e n t y f i k a t o r z e PID {} "\ .f orm at( w ork er_ fn. name , idx , id(main_np array), o s . g e t p i d ( ) ) # tablica m oże zostać przetw orzona w dow olny sp osó b; w tym m iejscu dla każdego elementu w danym wierszu # ustawiana je s t w artość identyfikatora d an eg o p rocesu main_n parray[id x, :] = o s . g e t p i d ( )
W fu n k cji
main
z przykładu 9.30 realizowane są następujące trzy główne etapy:
1. Utworzenie w spółużytkowanego obiektu multiprocessing.Array i przekształcenie go w ta blicę narzędzia numpy. 2. Ustawienie wartości domyślnej w tablicy i uruchomienie czterech procesów w celu prze twarzania tablicy w sposób równoległy. 3. Sprawdzenie zawartości tablicy po zw róceniu wyniku przez procesy. Przykład 9.30. Funkcja if
240
|
main
konfigurująca tablice narzędzia numpy w celu współużytkowania
name == ' main ' : DEFAULT_VALUE = 42 NBR_OF_PROCESSES = 4 # utworzenie bloku bajtów i p rzekształcenie w lokaln ą tablicę narzędzia numpy NBR_ITEMS_IN_ARRAY = SIZE_A * SIZE_B shared_array_base = m u l ti p r o c e s s i n g .A r ra y ( c t y p e s .c _ d o u b l e , NBR_ITEMS_IN_ARRAY, lo ck =Fa ls e) main_nparray = np.f ro m b uff er (s hared _arra y_b ase , dtype=ctypes.c_double) main_nparray = main_nparray.reshape(SIZE_A, SIZE_B) # upewnienie się, że nie została utworzona żadna kop ia a s s e r t main_ npa rray .bas e.base i s shared_array_base p r i n t "Utworzono współużytkowaną t a b l i c ę , która zawiera { : , } nbajtów".format( main _n parray.nbytes) p r i n t " I d e n t y f i k a t o r współużytkowanej t a b l i c y to {} w i d e n t y f i k a t o r z e PID { } " .f o r m a t ( i d ( m a i n _ ^np array), os.getpid()) p rin t "Rozpoczęcie od t a b l i c y z wartościami zerowymi:" p r i n t main_nparray p rin t
Rozdział 9. Moduł multiprocessing
Zw ykle zostanie skonfigurowana tablica narzędzia numpy, a następnie będzie ona przetw arza na z wykorzystaniem jednego procesu. Prawdopodobnie będzie w ykonyw any kod podobny do następującego: arr = np.array((100, 5 ), dtype=np.float_). Choć w przypadku pojedynczego procesu nie stanow i to problem u, nie jest m ożliw e w spółużytkow anie tych danych m iędzy procesami zarówno w celu odczytu, jak i zapisu. Stosowany zabieg polega na utworzeniu w spółużytkowanego bloku bajtów. Jeden ze sposo bów sprowadza się do utworzenia obiektu multiprocessing.Array. Domyślnie obiekt Array jest opakowany za pomocą blokady, aby zapobiec jednoczesnym operacjom edycji. Taka blokada nie jest jednak potrzebna, ponieważ odpowiednio zadbamy o w zorce dostępu. A by było to oczywiste dla innych członków zespołu program istów, warto jaw nie ustaw ić lock=False. Jeśli nie zostanie ustawione lock=False, zam iast odwołania do bajtów będzie dostępny obiekt, a ponadto konieczne będzie w yw ołanie m etody .get_obj() w celu użycia bajtów . Poniew aż dzięki wywołaniu tej metody pomijana jest blokada, zachowanie jaw ności w przypadku blo kady nie daje żadnych korzyści. Następnym krokiem jest pobranie bloku współużytkowanych bajtów i opakowanie ich tablicą narzędzia numpy za pomocą obiektu frombuffer. Parametr dtype jest opcjonalny, ale ze względu na przekazywanie mu bajtów zawsze rozsądnym rozwiązaniem jest jawne określenie go. Dzięki funkcji reshape możliwe jest adresowanie bajtów jako tablicy dwuwym iarowej. Domyślnie wartości tablicy są ustawione na 0. Przykład 9.30 prezentuje pełną fu n k cję main . A by potwierdzić, że procesy działają na tym samym bloku danych, od którego zaczęto przetwarzanie, każdy element zostanie ustawiony na now ą w artość DEFAULT_VALUE. W artość ta w idoczna jest na początku przykładu 9.31 (używ ana je st w artość 42, czyli odpow iedź na „W ielkie pytanie o życie, w szechśw iat i całą resztę"). N astępnie tw orzona jest pula (obiekt Pool) procesów (w tym przypadku czterech), a ponadto wysyłane są porcje indeksów wiersza za pośrednictw em wywołania funkcji map. Przykład 9.31. Funkcja main służąca do współużytkowania tablic narzędzia numpy za pomocą modułu multiprocessing # m odyfikow anie danych za p o m o c ą lokaln ej tablicy narzędzia numpy main_nparray.fill(DEFAULT_VALUE) p r i n t "Oryginalna t a b l i c a wypełniona w ar to śc ią {}:".format(DEFAULT_VALUE) p r i n t main_nparray raw _inpu t( "N aci śn ij kla w isz, aby uruchomić procesy robocze za pomocą modułu m u l t i p r o c e s s i n g . . . " ) p rin t # tworzenie p u li procesów , które b ę d ą współużytkować blo k p a m ięci # g lo b a ln ej tablicy numpy, a także odw ołanie d o bazow ego bloku # danych, aby m ożliw e było utworzenie op akow an ia tablicy n arzędzia numpy w nowych p ro c esa c h pool = multiprocessing.Pool(processes=NBR_OF_PROCESSES) # zastosow anie odw zorow ania, w którym każdy indeks w iersza je s t przekazyw any j a k o p aram etr # fu n kcji w orker_fn pool.map(worker_fn, xrange(SIZE_A))
Po zakończeniu przetw arzania rów noległego następuje pow rót do procesu nadrzędnego w celu zweryfikowania wyniku (przykład 9.32). Krok weryfikacji korzysta ze „spłaszczonego" w idoku tablicy (zauw aż, że w idok nie tw orzy kopii, ale jedynie generuje jednow ym iarow y w idok iterow alny dla dw uw ym iarow ej tablicy), obliczając częstość w ystępow ania każdego identyfikatora PID. Na końcu przeprowadzane są sprawdzenia za pomocą funkcji assert w celu upewnienia się, że uzyskano oczekiwane ilości.
W spółużytkowanie danych narzędzia numpy za pomocą modułu multiprocessing
|
241
Przykład 9.32. Funkcja main weryfikująca współużytkowany wynik p r i n t "Weryfikacja - wyodrębnianie unikalnych wartości z elementów {:,}\n w t a b l i c y narzędzia numpy ^(może to być długotrwałe)...".format(NBR_ITEMS_IN_ARRAY) # m ain_nparray.flatprzeprow adza iterację dla zaw artości tablicy, a pon adto nie tworzy # kopii counter = Counter( main _nparray.flat) p r i n t "Unikalne wartości w o b ie k c ie main_nparray:" tb l = P r e t ty T a b l e ([ " P I D " , "L ic z b a "] ) f o r pid, count in c o u n t e r . i t e m s ( ) : tb l.a dd_r ow ([p id, count]) p rin t tbl to t a l _ it e m s _ s e t _ i n _ a r r a y = sum (co u nt er .valu es( )) # sprawdzenie, czy d la każdego elementu tablicy nie ustawiono w artości DEFA ULT_VALUE a s s e r t DEFAULT_VALUE not in c oun te r.k eys() # sprawdzenie, czy uwzględniono każdy elem ent tablicy a s s e r t t o t a l _ it e m s _ s e t _ i n _ a r r a y == NBR_ITEMS_IN_ARRAY # sprawdzenie, czy występuje liczba p rocesów (NBR_OF_PROCESSES) unikalnych kluczy w celu potw ierdzenia, że # każdy p ro c es w ykonał część o p eracji przetw arzania a s s e r t le n (c oun te r) == NBR_OF_PROCESSES ra w_inp ut(" Naciśn ij klaw isz, aby z a k o ń c z y ć .. ." )
Utworzono w łaśnie jednow ym iarową tablicę bajtów, przekształcono ją w tablicę dwuwymia rową, którą udostępniono czterem procesom, a ponadto umożliwiono im współbieżne przetwa rzanie w tym samym bloku pamięci. Takie rozwiązanie ułatwi przetwarzanie równoległe z wy korzystaniem wielu rdzeni. Zachowaj jednak ostrożność w przypadku współbieżnego dostępu do tych samych punktów danych. Konieczne będzie użycie blokad w m odule multiprocessing, aby uniknąć problem ów z synchronizacją, co zmniejszy szybkość wykonywania kodu.
Synchronizowanie dostępu do zmiennych i plików W zam ieszczonych dalej przykładach przyjrzym y się w ielu procesom , które w spółużytkują i modyfikują stan. W tym przypadku cztery procesy inkrem entują w spółużytkow any licznik ustaloną liczbę razy. Bez procesu synchronizacji operacja liczenia jest niepopraw na. Jeśli współużytkujesz dane w spójny sposób, zaw sze niezbędna będzie metoda synchronizowania odczytu i zapisu danych, ponieważ w przeciwnym razie w ystąpią błędy. M etody synchronizacji są zw ykle specyficzne dla używanego systemu operacyjnego. Często są one pow iązane ze stosowanym językiem. Przyjrzymy się synchronizacji opartej na plikach, która korzysta z biblioteki języka Python, a także współużytkow aniu obiektu całkowitoliczbowego między procesami kodu Python.
Blokowanie plików O peracje odczytyw ania i zapisyw ania pliku to zam ieszczony w tym podrozdziale przykład w spółużytkow ania danych, który cechuje się najm niejszą szybkością. W przykładzie 9.33 zaprezentowano pierwszą funkcję work. Funkcja dokonuje iteracji dla liczni ka lokalnego. W każdej iteracji funkcja otwiera plik i odczytuje istniejącą w artość, inkremen tuje ją o wartość jeden, a następnie zapisuje nową wartość w miejsce starej. Przy pierwszej ite racji plik będzie pusty lub nie będzie istnieć, dlatego funkcja zgłosi wyjątek, a ponadto przyjmie, że wartość powinna być zerowa.
242
|
Rozdział 9. Moduł multiprocessing
Przykład 9.33. Funkcja work bez blokady def wor k(filename, max_count): f o r n in range(max_count): f = open(file na m e, " r " ) try: nbr = i n t ( f . r e a d ( ) ) except ValueError as e r r : p r i n t " P l i k j e s t pusty. Rozpoczęcie l i c z e n i a od 0. Błąd: " + s t r ( e r r ) nbr = 0 f = open(file na m e, "w") f . w r i t e ( s t r ( n b r + 1) + '\ n') f.clo se ()
Zastosujmy ten przykładowy kod dla jednego procesu. Dane wyjściow e prezentuje przykład 9.34. Funkcja work jest wywoływana 1000 razy. Zgodnie z oczekiwaniami poprawnie wykonuje operację liczenia bez utraty żadnych danych. Przy pierwszym odczycie funkcja identyfikuje pusty plik. Pow oduje to w ygenerow anie błędu invalid lite r a l for in t() dotyczący funkcji in t() (z powodu w ywołania jej dla pustego łańcucha). Błąd ten w ystępuje tylko raz. Później zaw sze dostępna jest poprawna w artość do odczytania i przekształcenia w liczbę całkowitą. P r z y k ła d 9 .3 4 . O k r e ś la n ie c z a s u d la o p a r t e j n a p lik a c h o p e r a c ji lic z e n ia b e z b lo k a d y w p r z y p a d k u je d n e g o p r o c e s u $ python ex1_nolock.py Uruchamianie procesów, których l i c z b a to 1, w c elu o d l i c z a n i a do 1000 P l i k j e s t pusty. Rozpoczęcie l i c z e n i a od 0. Błąd: in v a l i d l i t e r a l f o r i n t ( ) with base 10: ' ' Oczekiwano wyświetlenia l i c z b y : 1000 P l i k c o u n t . t x t zawiera: 1000
Z ostanie teraz uruchom iona ta sam a funkcja work dla czterech w spółbieżnych procesów . Poniew aż nie jest używ any żaden kod blokujący, m ożna oczekiw ać dziw nych w yników . P r z e d p r z e a n a l i z o w a n i e m p o n i ż s z e g o k o d u z a s t a n ó w si ę , j a k i c h d w ó c h t y p ó w b ł ę d ó w m o ż n a o czekiw ać, g d y d w a p ro ces y jed n o cz eśn ie d o k o n u ją od czytu lu b zap isu d la tego sa m e g o pliku? P o m y śl o d w ó c h g łó w n y c h sta n a ch k o d u (p o cz ą tek w y k o t
n y w a n i a d la k a ż d e g o p r o c e s u o ra z s ta n z w y k ł e g o d z ia ła n ia k a ż d e g o z n ich ).
Aby zidentyfikować problemy, przyjrzyj się przykładowi 9.35. Po pierwsze, w momencie uru chamiania każdego procesu plik jest pusty. Z tego powodu procesy próbują rozpocząć licze nie od zera. Po drugie, gdy jeden proces dokonuje zapisu, drugi m oże odczytywać częściowo zapisany wynik, który nie może być analizowany. Powoduje to zgłoszenie wyjątku i ponow ne zapisanie zera. Z kolei to sprawia, że licznik będzie nieustannie resetowany! Czy widzisz, jak znaki /n oraz dwie wartości zostały zapisane przez dwa współbieżne procesy w tym samym otwartym pliku, co spowodowało odczytanie niepopraw nego w pisu przez trzeci proces? P r z y k ła d 9 .3 5 . O k re ś la n ie c z a s u d la o p a r te j n a p lik a c h o p e r a c ji lic z en ia b e z b lo k a d y w p r z y p a d k u cz te re c h p r o c e s ó w $ python ex1_nolock.py Uruchamianie procesów, których l i c z b a to 4 , w c elu o d l i c z a n i a do 4000 P l i k j e s t pusty. Rozpoczęcie l i c z e n i a od 0. Błąd: in v a l i d l i t e r a l f o r i n t ( ) with base 10: ' ' P l i k j e s t pusty. Rozpoczęcie l i c z e n i a od 0. Błąd: in v a l i d l i t e r a l f o r i n t ( ) with base 10: '1\n7\n' # w iele błędów p od obn y ch d o tych Oczekiwano wyświetlenia l i c z b y : 4000
Synchronizowanie dostępu do zmiennych i plików
| 243
P l i k c o u n t . t x t zawiera: 629 $ python -m tim e it - s "import ex1_nolock" "ex1_n olock .run _w orkers()" 10 loops , b est o f 3: 125 msec per loop
Przykład 9.36 prezentuje kod z modułem multiprocessing, który wywołuje funkcję work z czte rema procesami. Zauważ, że zamiast używania metody map tworzona jest lista obiektów Process. Choć w tym przypadku nie jest wykorzystywana funkcjonalność obiektu Process, zapewnia on duże możliwości introspekcji stanu każdego procesu. Zachęcamy do przeczytania dokumentacji (https://docs.python.Org/2/library/multiprocessing.html), aby dowiedzieć się, dlaczego m oże być w skazane zastosow anie obiektu Process. Przykład 9.36. Funkcja run_workers konfigurująca cztery procesy import m ulti proce ss ing import os MAX_COUNT_PER_PROCESS = 1000 FILENAME = " c o u n t . t x t " def run_workers(): NBR_PROCESSES = 4 to tal_ ex pe cte d_ coun t = NBR_PROCESSES * MAX_COUNT_PER_PROCESS p r i n t "Uruchamianie procesów, których l i c z b a to { } , w c elu od l i c z a n i a do {}".format(NBR_PROCESSES, ^ t o t a l_ e x p e c te d _ c o u n t) # resetow anie licznika f = open(FILENAME, "w") f.clo se () processes = [] f o r process_nbr in range(NBR_PROCESSES): p = m u lti p r oce ssing .P ro c ess(t arg e t= w o r k, args=(FILENAME, MAX_COUNT_PER_PROCESS)) p .start() processes.append(p) f o r p in pro cesse s: p .jo in () p r i n t "Oczekiwano wyświetlenia l i c z b y : {} " .f o r m a t(t o ta l _ e x p e c t e d _ c o u n t ) p r i n t " P l i k {} zawiera:".format(FILENAME) os. system('m ore ' + FILENAME) if name == " main ": run_workers()
Użycie modułu lockfile (https://pypi.python.org/pypi/lockfile) pozwala w prow adzić metodę syn chronizacji, dzięki czemu jednocześnie tylko jeden proces dokonuje zapisu, a pozostałe pro cesy czekają na swoją kolej. Choć ogólny proces przebiega trochę w olniej, nie w ystępują po myłki. Przykład 9.37 prezentuje poprawne dane wyjściowe. W internecie dostępna jest pełna dokumentacja pod adresem http://pythonhosted.org/llockfile/. M iej świadomość tego, że mecha nizm blokowania jest specyficzny dla języka Python, dlatego inne procesy sprawdzające plik blokady nie będą zw racać uwagi na jego skłonność do bycia „zablokowanym ". Przykład 9.37. Określanie czasu dla opartej na plikach operacji liczenia z blokadą w przypadku czterech procesów $ python ex1_lock .p y Uruchamianie procesów, których l i c z b a to 4 , w c elu o d l i c z a n i a do 4000 P l i k j e s t pusty. Rozpoczęcie l i c z e n i a od 0. Błąd: in v a l i d l i t e r a l f o r i n t ( ) with base 10: '' Oczekiwano wyświetlenia l i c z b y : 4000 P l i k c o u n t . t x t zawiera: 4000 $ python -m tim e it - s "import ex1_lo ck " "ex1_lo ck .ru n _w o rk ers()" 10 loops , b est o f 3: 401 msec per loop
244
|
Rozdział 9. Moduł multiprocessing
U życie m odułu lo c k file pow oduje dodanie tylko kilku w ierszy kodu. N ajpierw tw orzony jest obiekt FileLock. Nazwa pliku m oże być dowolna, ale zastosowanie takiej samej nazwy jak dla pliku, który ma zostać zablokowany, praw dopodobnie ułatwi debugowanie z poziomu wiersza poleceń. W m om encie zażądania blokady za pomocą m etody acquire obiekt FileLock otworzy nowy plik o takiej samej nazw ie z dołączonym rozszerzeniem .lock. M etoda acquire użyta bez żadnych argumentów spow oduje blokowanie przez nieokreślony czas aż do momentu udostępnienia blokady. Gdy to nastąpi, możliwe jest przetw arzanie bez żadnego ryzyka wystąpienia konfliktu. Po zakończeniu zapisu m ożesz zw olnić blokadę za pom ocą m etody release (przykład 9.38). Przykład 9.38. Funkcja work z blokadą def wor k(filename, max_count): l o ck = l o c k f i l e . F i l e L o c k ( f i l e n a m e ) f o r n in range(max_count): lock .acq uire () f = open(file na m e, " r " ) try: nbr = i n t ( f . r e a d ( ) ) except ValueError as e r r : p r i n t " P l i k j e s t pusty. Rozpoczęcie l i c z e n i a od 0. Błąd: " + s t r ( e r r ) nbr = 0 f = open(file na m e, "w") f . w r i t e ( s t r ( n b r + 1) + '\ n') f.clo se () lo ck .release()
M ożliwe jest użycie m enedżera kontekstu. W tym przypadku m etody acquire i release są za stępowane instrukcją with lock:. Choć pow oduje to nieznaczne w ydłużenie czasu działania, sprawia też, że kod staje się trochę bardziej czytelny. Przejrzystość jest zw ykle ważniejsza od szybkości w ykonywania. M ożliwe jest również zażądanie uzyskania blokady z limitem czasu za pomocą m etody acqu ire, a także sprawdzenie i usunięcie istniejącej blokady. Dostępnych jest kilka m echanizmów blokowania. Sensowne domyślne opcje wyboru dla każdej platform y są ukryte za interfejsem FileLock.
Blokowanie obiektu Value M oduł multiprocessing oferuje kilka opcji współużytkowania obiektów języka Python między procesami. M ożliwe jest w spółużytkow anie obiektów podstawowych przy niewielkim obcią żeniu komunikacyjnym, a także obiektów języka Python wyższego poziomu (np. słowników i list) za pom ocą obiektu Manager (zauważ jednak, że obciążenie zw iązane z synchronizacją znacznie spowolni współużytkow anie danych). W omawianym przykładzie zostanie użyty obiekt multiprocessing.Value (https://docs.python.org/ 2/library/multiprocessing.htmT) do współużytkowania liczby całkowitej między procesami. Choć obiekt Value ma blokadę, nie realizuje ona do końca tego, czego m ożna oczekiw ać. Blokada uniem ożliw ia jednoczesne odczyty lub zapisy, ale nie zapew nia niepodzielnego inkrem en towania. Zilustrowano to w przykładzie 9.39. W idać w nim, że ostatecznie uzyskuje się nie popraw ną liczbę. Przypom ina to przedstaw iony w cześniej przykład braku synchronizacji, w którym bazowano na plikach.
Synchronizowanie dostępu do zmiennych i plików
| 245
Przykład 9.39. Brak blokowania prowadzi do niepoprawnej liczby $ python ex2_nolock.py Oczekiwano wyświetlenia l i c z b y : 4000 Lic zen ie zakończono na: 2340 $ python -m tim e it - s "import ex2_nolock" "ex2_n olock .run _w orkers()" 100 loop s, b est o f 3: 12.6 msec per loop
Dane nie ulegają uszkodzeniu, ale pomijana jest część aktualizacji. Takie rozwiązanie może być przydatne przy zapisie do obiektu Value z jednego procesu i korzystaniu z tego obiektu (ale bez modyfikowania go) w innych procesach. Przykład 9.40 prezentuje kod współużytkowania obiektu Value. Konieczne jest określenie ty pu danych i w artości inicjalizacji. Za pom ocą kodu V a lu e("i", 0) żądana jest liczba całkowita ze znakiem o wartości domyślnej 0. Liczba jest przekazywana jako zw ykły argum ent obiek towi Process, który odpowiada za w spółużytkow anie w tle tego samego bloku bajtów między procesam i. A by uzyskać dostęp do obiektu podstaw ow ego utrzym yw anego przez obiekt Value, używany jest kod .value. Zauważ, że żądana jest operacja dodania lokalnego — ocze kiwano, że będzie to operacja niepodzielna, ale nie jest ona obsługiwana przez obiekt Value, dlatego końcowa liczba jest niższa od oczekiwanej. Przykład 9.40. Kod liczący bez obiektu Lock import m ultip roce ss in g def work(value, max_count): f o r n in range(max_count): v a lu e.v a lu e += 1 def run_w orkers() : value = m u l t i p r o c e s s i n g . V a l u e ( ' i ' , 0) f o r process_nbr in range(NBR_PROCESSES): p = m u l ti p r o c e ssi n g .P ro c e ss(t a rg e t= w o r k , arg s= (v alue , MAX_COUNT_PER_PROCESS)) p .start() processes.append(p )
M ożliw e jest dodanie obiektu Lock (https://docs.python.org/2/library/m ultiprocessing.htm l), który będzie działać bardzo podobnie jak w e w cześniej zam ieszczonym przykładzie z obiektem FileLock. W przykładzie 9.41 pokazano popraw nie synchronizow aną liczbę. Przykład 9.41. Użycie obiektu Lock do synchronizacji zapisów w obiekcie Value # blokad a aktualizacji, która nie je s t o p er a c ją n iepodzielną $ python ex2_lock .p y Oczekiwano wyświetlenia l i c z b y : 4000 Liczen ie zakończono na: 4000 $ python -m tim e it - s "import ex2_lo ck " "ex2_lock .ru n _w ork ers()" 10 loo ps , b est o f 3 : 2 2 .2 msec per loop
W przykładzie 9.42 użyto m enedżera kontekstu (z obiektem Lock) w celu uzyskania blokady. Jak w poprzednim przykładzie z obiektem Fi leLock, m enedżer oczekuje przez nieokreślony czas na pobranie blokady. Przykład 9.42. Uzyskiwanie blokady za pomocą menedżera kontekstu import m ultip roce ss in g def work(value, max_count, l o c k ) : f o r n in range(max_count): with lo c k : v a lu e .v a lu e += 1 def ru n_w orkers() :
246
|
Rozdział 9. Moduł multiprocessing
pro ces se s = [] lo ck = m u ltip r o ce ssin g .L o ck () value = m u l t i p r o c e s s i n g . V a l u e ( ' i ' , 0) f o r process_nbr in range(NBR_PROCESSES): p = m u l ti p r o c e ssi n g .P ro c e ss(t a rg e t= w o r k , a rg s= (v alu e , MAX_COUNT_PER_PROCESS, l o c k )) p .start() processes.append(p )
Jak w spomniano w przykładzie z obiektem FileLock, uniknięcie użycia m enedżera kontekstu pozwala trochę zw iększyć szybkość. Fragment kodu z przykładu 9.43 prezentuje sposób uzy skania (za pom ocą metody acquire) i zwolnienia (przy użyciu metody release) obiektu Lock. Przykład 9.43. Blokowanie lokalne zamiast używania menedżera kontekstu lock .acq uire () v alu e.v alu e += 1 l o c k . r e l e a s e ()
Ponieważ obiekt Lock nie oferuje żądanego poziomu szczegółowości, zapewniane przez niego podstawowe blokowanie niepotrzebnie powoduje wydłużenie czasu. Obiekt Value może zostać zastąpiony obiektem RawValue (https://docs.python.org/2/library/multiprocessing.html) (przykład 9.44), co pozwala uzyskać zwiększające się przyspieszenie. Aby zobaczyć kod bajtowy związany z tą zmianą, przeczytaj pośw ięcony temu post na blogu Eli Bendersky'ego (http://eli.thegreenplace.net/ 2012/01/04/shared-counter-with-pythons-multiprocessing/). Przykład 9.44. Dane wyjściowe konsoli prezentujące szybszą metodę użycia obiektów RawValue i Lock # ob iekt RawValue nie m a blokady $ python ex2_lock_raw value.py Oczekiwano wyświetlenia l i c z b y : 4000 Lic zen ie zakończono na: 4000 $ python -m tim e it - s "import ex2_lock_raw value" "ex2_lock_raw valu e.run _w orkers()" 100 loo ps, b est o f 3: 12 .6 msec per loop
Aby użyć obiektu RawValue, w ystarczy zastąpić nim obiekt Value (przykład 9.45). Przykład 9.45. Przykład użycia liczby całkowitej obiektu RawValue def run_w orkers() : lo ck = m u ltip r o ce ssin g .L o ck () value = m u l t i p r o c e s s i n g .R a w V a l u e ( 'i ', 0) f o r process_nbr in range(NBR_PROCESSES): p = m u l ti p r o c e ssi n g .P ro c e ss(t a rg e t= w o r k , a rg s = ( v a l ue, MAX_COUNT_PER_PROCESS, l o c k )) p .start() processes.ap pend(p )
Jeśli współużytkowano tablicę obiektów podstawowych, możliwe jest też zastosowanie obiektu RawArray w miejsce obiektu multiprocessing.Array. Przyjrzeliśmy się różnym sposobom podziału zadań między procesami w przypadku jednego komputera, a także współużytkow aniu flagi i synchronizowaniu współużytkowania danych między tymi procesami. Pamiętaj jednak, że współużytkow anie danych może być przyczyną frustracji. Jeśli to możliwe, próbuj tego unikać. Radzenie sobie z komputerem dla w szystkich skrajnych przypad ków w spółużytkow ania stanu m oże okazać się trudne. Przy pierw szej
Synchronizowanie dostępu do zmiennych i plików
| 247
próbie debugowania interakcji wielu procesów zrozum iesz, dlaczego akceptowanym rozwią zaniem jest unikanie w razie możliwości takiej sytuacji. Rozważ tworzenie kodu, który działa trochę w olniej, ale bardziej praw dopodobne będzie, że będzie zrozum iały dla reszty zespołu programistów. Użycie narzędzia zewnętrznego, takiego jak Redis, do w spółużytkow ania stanu, prow adzi do uzyskania system u, który m oże być sprawdzany w czasie działania przez osoby niebędące programistami. Jest to bardzo wygodny sposób, który pozw ala członkom zespołu być na bieżąco z tym , co dzieje się w system ach przetwarzania równoległego. Zdecydowanie pamiętaj o tym, że zmodyfikowany kod Python o dużej wydajności z mniej szym prawdopodobieństwem będzie zrozumiały dla mniej doświadczonych członków zespołu. Albo będą nim przerażeni, albo sprawią, że kod przestanie działać. Postaraj się unikać tego problemu (i akceptuj spadek szybkości), aby utrzym ać duże tempo pracy zespołu.
Podsumowanie W tym rozdziale omówiliśmy wiele zagadnień. Najpierw przyjrzeliśmy się dwóm problemom idealnie nadającym się do zastosowania przetwarzania równoległego. Pierwszy problem miał przew idyw alną złożoność, a drugi nie. Przykłady zam ieszczone w rozdziale zostaną wkrótce ponow nie w ykorzystane w przypadku w ielu kom puterów przy om aw ianiu klastrow ania w rozdziale 10. Dalej omówiliśmy obsługę obiektu Queue w m odule multiprocessing i zw iązane z nim obcią żenia. G eneralnie zalecam y użycie zew nętrznej biblioteki kolejek, aby stan kolejki był bar dziej transparentny. Najlepiej, zam iast z danych poddanych serializacji przez m oduł pickle, skorzystać z łatwego do odczytania formatu zadań, który ułatwia debugowanie. Omówienie komunikacji międzyprocesowej IPC miało uzmysłowić Ci, jak trudne jest efektywne korzystanie z niej. Sensowne może być zastosowanie jedynie naiwnego wariantu przetwarzania równoległego (bez komunikacji międzyprocesowej). Zakup szybszego komputera z większą licz bą rdzeni może okazać się znacznie bardziej pragmatycznym rozwiązaniem niż próba użycia komunikacji IPC na posiadanym komputerze. W spółużytkowanie macierzy narzędzia numpy w sposób równoległy bez tworzenia kopii jest istotne tylko dla niewielkiej grupy problemów. Jeśli masz do czynienia z jednym z nich, na prawdę się przydaje. Takie rozwiązanie wymaga kilku dodatkowych wierszy kodu, a ponadto trochę sprawdzania (w granicach rozsądku) w celu upewnienia się, że dane nie są w rzeczy w istości kopiowane między procesami. Na końcu zajęliśmy się stosowaniem blokad plików i pam ięci w celu uniknięcia uszkodzenia danych. To źródło subtelnych i trudnych do wykrycia błędów. Zaprezentowaliśmy kilka pew nych i prostych rozwiązań. W następnym rozdziale zajmiemy się klastrowaniem z wykorzystaniem języka Python. W przy padku klastra można w yjść poza przetwarzanie równoległe na jednym komputerze i skorzystać z procesorów znajdujących się w grupie komputerów. W prow adza to nas do now ego świata pełnego utrapień związanych z debugowaniem. Błędy mogą w ystępować nie tylko w kodzie, ale też na innych kom puterach (w ynikające ze złej konfiguracji lub uszkodzonego sprzętu). Pokażemy, jak przeprow adzić przetwarzanie równoległe dla przybliżenia liczby pi z w yko rzystaniem modułu Parallel Python, a także w jaki sposób uruchom ić kod analizujący w ob rębie powłoki IPython, używając klastra IPython.
248
|
Rozdział 9. Moduł multiprocessing
__________________ROZDZIAŁ 10.
Klastry i kolejki zadań
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Dlaczego klastry są przydatne? • Jakie są koszty zw iązane z zastosowaniem klastrowania? • Jak można przekształcić rozwiązanie bazujące na przetwarzaniu wieloprocesorowym w rozwiązanie klastrowe? • Jak działa klaster IPython? • W jaki sposób NSQ ułatwia tworzenie niezawodnych system ów produkcyjnych?
K laster jest pow szechnie definiow any jako zbiór kom puterów w spółpracu jących ze sobą w celu rozw iązania typow ego zadania. Na zew nątrz sieci klaster m oże w yglądać jak poje dynczy większy system. W latach 90. popularne stało się wykorzystyw anie do przetwarzania klastrowego klastra zło żonego ze zwykłych komputerów PC w obrębie sieci lokalnej. Taki klaster był określany mia nem klastra Beowulfa (http://pl.w ikipedia.org/w iki/Beow ulfJinformatyka)). Później platforma firm y Google (http://en.wikipedia.org/wiki/Google_platform) spowodowała, że rozwiązanie to było w jesz cze większym stopniu stosowane. Platforma bazująca na klastrach zw ykłych kom puterów PC była używana w centrach danych firmy Google, w szczególności do uruchamiania zadań plat form y M apReduce. Na drugim końcu skali znajduje się projekt serwisu TOP500 (http://pl. w ikipedia.org/w iki/TO P500), w ramach którego co roku tworzony jest ranking najbardziej wy dajnych systemów komputerowych. Zwykle bazują one na architekturze klastrowej, a wszystkie najszybsze komputery używają systemu Linux. Usługi AW S (Amazon Web Services) są pow szechnie używane zarówno na potrzeby inżynierii klastrów produkcyjnych w chm urze, jak i do budow ania klastrów na żądanie, które wyko nują krótkie zadania, takie jak w przypadku uczenia maszynowego. Usługi te umożliwiają w ydzierżaw ienie zestawów złożonych z ośmiu rdzeni Intel Xeon z 60 GB pamięci RAM przy koszcie wynoszącym 1,68 dolara za godzinę, a także komputery z 244 GB pamięci RAM i ukła dami GPU. Jeśli zamierzasz skorzystać z usług AW S w celu doraźnego zastosowania klastrów do realizowania zadań w ym agających dużej m ocy obliczeniowej, przeczytaj punkt „Użycie m odułu IPython Parallel do obsługi badań" oraz zaznajom się z pakietem StarCluster.
249
Różne zadania obliczeniowe wymagają klastrów o odmiennych konfiguracjach, wielkościach i możliwościach. W tym rozdziale zostanie zdefiniowanych kilka typowych scenariuszy. Zanim zajmiesz się zagadnieniem rozwiązania klastrowego, upewnij się, że zostały wykonane następujące czynności: • Przeprowadzono profilow anie systemu w celu poznania wąskich gardeł. • W ykorzystano rozwiązania do kompilowania, takie jak Cython. • W ykorzystano wiele rdzeni jednego komputera (może to być pokaźna maszyna z wieloma rdzeniami). • W ykorzystano techniki zapewniające mniejsze zużycie pamięci RAM. Ograniczenie systemu do jednego komputera (nawet jeśli pod tym określeniem kryje się na prawdę zaawansowany komputer z mnóstwem pamięci RAM i wieloma procesorami) ułatwi pracę. Zastosuj klaster, jeśli rzeczywiście potrzebujesz wielu procesorów , możliwości równo ległego przetwarzania danych z dysków lub pojawiły się w ym agania produkcyjne takie jak duża elastyczność i uzyskanie natychmiastowych odpowiedzi.
Zalety klastrowania N ajbardziej oczyw istą zaletą klastra jest to, że dzięki niem u z łatw ością m ożesz skalow ać wym agania obliczeniowe. Jeśli m usisz przetw orzyć więcej danych lub szybciej uzyskać od powiedź, m ożesz po prostu dodać więcej komputerów (lub „w ęzłów "). Dodanie komputerów pozwala też zwiększyć niezawodność. Z komponentami każdego kom putera związane jest określone ryzyko awarii. W przypadku dobrego projektu awaria kom ponentów nie spowoduje przerw y w działaniu klastra. Klastry są też używane do tworzenia systemów skalowanych dynamicznie. Typowym przy padkiem zastosow ania jest klaster złożony z zestawu serwerów, które przetw arzają żądania internetowe lub powiązane dane (np. zmiana wielkości zdjęć użytkowników, transkodowanie wideo lub transkrypcja mowy), a także aktywują w iększą liczbę serwerów, gdy w określonych porach dnia zwiększa się obciążenie. Skalowanie dynamiczne to bardzo ekonomiczny sposób radzenia sobie ze zm iennym i w zor cami wykorzystania, pod warunkiem że czas aktywowania komputera jest na tyle krótki, aby możliwe było nadążenie za zmieniającym się obciążeniem. Bardziej subtelną zaletą klastrowania jest to, że choć klastry m ogą być rozmieszczane w róż nych lokalizacjach geograficznych, m ożna je kontrolow ać w sposób scentralizow any. Jeśli w danym obszarze geograficznym w ystąpi przestój (np. przez pow ódź lub zanik zasilania), inny klaster może kontynuować działanie, być może z dodanym i dodatkowymi jednostkami obliczeniow ym i w celu obsługi zw iększonego obciążenia. Klastry um ożliw iają też urucha m ianie heterogenicznych środow isk oprogram ow ania (np. różnych w ersji system ów ope racyjnych i oprogram ow ania przetw arzającego), które mogą zw iększyć niezaw od ność ca łego system u. Zauw aż jed nak, że zdecyd ow anie je st to zagadnienie w ym agające w iedzy eksperckiej!
250
|
Rozdział 10. Klastry i kolejki zadań
Wady klastrowania Zastosowanie rozwiązania klastrowego wymaga zmiany sposobu myślenia. Jest to ciąg dalszy zmian, które zapoczątkowało przejście z kodu przetwarzania szeregowego na kod przetwa rzania rów noległego, o czym była m ow a w rozdziale 9. N agle trzeba się zastanow ić, co się dzieje w przypadku użycia w ięcej niż jednego kom putera. M iędzy kom puteram i w ystępuje opóźnienie. Niezbędne jest stwierdzenie, czy komputery działają, a także zapewnienie, że będą pracow ać pod kontrolą tej samej w ersji oprogram ow ania. Adm inistrow anie system em to praw dopodobnie najw iększe w yzw anie. Ponadto standardow o w ym agane jest uw zględnianie im plem entow anych algorytm ów , jak również tego, co się dzieje, gdy już dysponuje się w szystkim i dodatkow ym i, zm ieniającym i się komponentami, dla których może być konieczna synchronizacja. Zw iązane z tym dodat kowe planow anie może w ym agać sporego w ysiłku umysłow ego. Prawdopodobnie sprawi to, że trudno będzie skoncentrować się na głównym zadaniu. Gdy system stanie się w ystarczają co rozbudowany, konieczne będzie praw dopodobnie dołączenie do zespołu inżyniera, który zajmie się w yłącznie obsługą klastra.
^
P o w o d e m , d l a k t ó r e g o p o d j ę l i ś m y p r ó b ę s k o n c e n t r o w a n i a s i ę w tej k s i ą ż c e n a e f e k t y w n y m u ż y c i u j e d n e g o k o m p u t e r a , j e s t to, ż e w i e r z y m y , ż e ż y c i e s t a je s i ę p r o s t s z e , je śli z a jm u je s z się ty lk o je d n y m k o m p u te r e m , a n ie ich z b i o r e m (c h o ć p r z y z n a je m y , ż e e k s p e r y m e n t o w a n i e z k l a s t r e m m o ż e w p e w n y m s e n s ie d a ć w i ę c e j r a d o ś c i — d o m o m e n t u , g d y k la s te r p rz e s ta n ie d ziałać). Je śli m o ż liw e je st s k a lo w a n ie p io n o w e (dzięki n a b y ciu d o d a tk o w ej p a m ięc i R A M lub w iększej liczby p ro cesoró w ), w arto r o z w a ż y ć to r o z w ią z a n ie p o d k ą t e m k la stro w a n ia . O c z y w iś c ie w y m a g a n ia d o ty c z ą ce p rz e tw a rz a n ia m o g ą p rz e k ra cz a ć m o ż liw o ści s k a lo w a n ia p io n o w e g o lub solid n o ś ć k lastra m o ż e b y ć w a ż n ie js z a o d z a s t o s o w a n ia je d n e g o k o m p u te r a . Je śli je d n a k d a n y m z a d a n i e m z a jm u je się ty lk o je d n a o so b a , tr z e b a r ó w n ie ż m i e ć ś w ia d o m o ś ć tego, ż e u r u c h o m ie n ie klastra p o c h ło n ie troch ę czasu.
Podczas projektowania rozwiązania klastrowego trzeba pam iętać o tym, że konfiguracja każ dego komputera może być inna (poszczególne komputery będą w różnym stopniu obciążone i będą zaw ierać różne dane lokalne). W jaki sposób pobierzesz wszystkie w łaściw e dane na komputer, który realizuje przydzielone zadanie? Czy opóźnienie zw iązane z przemieszcza niem zadania i danych oznacza problem ? Czy częściow e w yniki m uszą być przekazyw ane między zadaniami? Co się stanie, gdy podczas w ykonyw ania kilku zadań proces przestanie działać bądź komputer lub jego urządzenie ulegnie awarii? Trzeba rozważyć te kwestie, by ograniczyć ryzyko w ystąpienia awarii. Pod uwagę należy też w ziąć to, że awarie mogą być akceptowane. Na przykład prawdopodob nie nie jest wymagana niezaw odność w ynosząca 99,999%, gdy obsługiwana jest usługa inter netowa bazująca na treści. Jeśli sporadycznie zadanie nie zostanie zrealizow ane (np. w ielkość obrazu nie zostanie w ystarczająco szybko zmieniona), a ponadto będzie w ym agane ponowne załadow anie strony przez użytkow nika, będzie to coś, z czym każdy m iał już do czynienia. Choć może nie być to rozwiązanie, jakie chciałoby się zaoferow ać użytkownikowi, zaakcep tow anie niew ielkiej liczby niepow odzeń operacji zm niejsza zw ykle o godną uw agi w artość koszty zw iązane z inżynierią i zarządzaniem . Jeśli natom iast aw arie w ystępują w bardzo aktywnym systemie obsługującym transakcje handlowe, koszt niepoprawnych transakcji może być znaczny!
Wady klastrowania
|
251
Utrzymanie niezmiennej infrastruktury może stać się kosztowne. Choć koszt zakupu kom puterów jest stosunkowo niewielki, mają one okropny zwyczaj sprawiania problemów. Auto matyczne aktualizacje oprogramowania mogą szwankować, karty sieciowe mogą ulegać awa riom, w przypadku dysków mogą występować błędy zapisu, zasilacze mogą powodować skoki napięcia, które uszkadzają dane, a prom ieniow anie kosm iczne m oże w płynąć negatyw nie na m oduł pam ięci RAM . Im w ięcej używ anych kom puterów , tym w ięcej czasu straconego na zajm ow anie się tym i problem am i. W cześniej czy później koniecznością będzie dołączenie do zespołu inżyniera systemowego, który będzie potrafił poradzić sobie z tymi problemami. Z tego powodu zwiększ budżet o dodatkowe 300 000 zł. Użycie klastra bazującego na chmu rze m oże zm inim alizow ać w iele spośród opisanych problem ów (jest to kosztow niejsze, ale eliminuje konieczność samodzielnego konserwowania sprzętu). Niektórzy dostawcy techno logii chmury oferują również rynek z ustalanymi cenami obszarów (http://aws.amazon.com/ec2/ purchasing-options/spot-instances/), który jest przeznaczony dla tanich, lecz tymczasowych za sobów obliczeniowych. Z pozoru niew innie w yglądający problem dotyczący klastra, który pow iększa się z czasem w sposób naturalny, polega na tym, że możliwe jest, że nikt nie udokum entuje m etody bez piecznego zrestartow ania klastra, gdy w szystko zostanie w yłączone. Jeśli nie istnieje opisany w dokumentacji plan restartu, należy przyjąć, że konieczne będzie stworzenie go w najgor szym możliwym m om encie (jeden z autorów książki został zaangażow any w W igilię Bożego Narodzenia w debugowanie tego rodzaju problemu — nie jest to w ym arzony prezent świą teczny!). W takich okolicznościach dowiesz się także, ile czasu zajm ie zaktualizow anie każdej części systemu. Ładowanie każdej części klastra i urucham ianie zadań procesów m oże zająć kilka minut, dlatego w przypadku 10 części, które kolejno są aktywowane, uruchom ienie od początku całego system u m oże zająć godzinę. W konsekw encji m oże się okazać, że pojaw ią się zaległe dane z takiego czasu. Czy dysponujesz wym aganym i zasobami, aby w tej sytuacji w odpowiednim czasie poradzić sobie z takimi zaległościami? Niedbałość może być przyczyną kosztownych pomyłek, a złożone i trudne do przewidzenia zachow anie m oże spow odow ać nieoczekiwane rezultaty oznaczające duże straty finansowe. Przyjrzyjm y się dwóm słynnym aw ariom klastrów i zastanów m y się, jakie w nioski można z nich wyciągnąć.
Strata o wartości 462 milionów dolarów na giełdzie Wall Street z powodu kiepskiej strategii aktualizacji klastra W roku 2012 firma Knight Capital (http://www.theregister.co.uk/2013/10/23/lone_sysadmin_caused_ 462_meeellion_wall_street_crash/) zajm ująca się obsługą bardzo szybkich transakcji papieram i w artościow ym i straciła 462 miliony dolarów w wyniku błędu, który pojawił się podczas ak tualizowania oprogram owania w klastrze. Oprogram ow anie w ygenerow ało w ięcej zleceń na akcje, niż złożyli klienci. W oprogram ow aniu obsługującym handel papieram i w artościow ym i starszej fladze została przypisana nowa funkcja. Aktualizacja została zastosowana na siedmiu z ośmiu aktywnych komputerów, ale ostatni z nich korzystał ze starszego kodu do obsługi flagi. Spowodowało to w ygenerow anie niepopraw nych transakcji. Kom isja Papierów W artościow ych i G iełd SEC (Securities and Exchange Commission) stwierdziła, że firma Knight Capital nie dokonała drugiego przeglądu technicznego aktualizacji, a ponadto nie istniał żaden proces przeglądu aktualizacji.
252
|
Rozdział 10. Klastry i kolejki zadań
Zasadniczy błąd w ydaje się m ieć dw ie przyczyny. Po pierw sze, w procesie projektow ania oprogramowania nie została usunięta przestarzała funkcja, dlatego nadal istniał nieaktualny kod. Po drugie, nie zastosowano żadnego procesu ręcznego przeglądu w celu potwierdzenia, że aktualizacja została pom yślnie zakończona. Zaniedbania o charakterze technicznym pow odują koszt, który ostatecznie musi zostać po niesiony. N ajlepiej usunąć te zaniedbania, gdy nie trzeba działać pod presją czasu. Zaw sze używ aj testów jednostkow ych, zarów no przy tw orzeniu, jak i refaktoryzow aniu kodu. Brak sporządzonej listy kontrolnej, która umożliwi sprawdzenie aktualizacji systemu, a także do datkowej kontroli przez drugą osobę, może zakończyć się kosztowną awarią. Z tego właśnie powodu piloci samolotów muszą poddać się kontroli zgodnie z listą kontrolną startu. Ozna cza to, że nikt nigdy nie przeoczy istotnych czynności, niezależnie od tego, ile razy wcześniej było to już powtarzane!
24-godzinny przestój usługi Skype w skali globalnej W roku 2010 usługa Skype doświadczyła 24-godzinnej awarii w skali globalnej (http://beeyeas. blogspot.co.uk/2014/01/cio-update-post-mortem-on-skype-outage.html). U sługa ta obsługiw ana jest przez sieć P2P (Peer-to-Peer). Nadm ierne obciążenie w jednej części systemu (używanego do przetw arzania w iadom ości błyskaw icznych w trybie offline) spow odow ało otrzym yw anie odpowiedzi z klientów Windows z opóźnieniem. Niektóre wersje tych klientów nie obsługiwały popraw nie opóźnionych odpowiedzi i zaw iesiły się. Ostatecznie w przybliżeniu 40% aktyw nych klientów uległo awarii, w tym 25% publicznych superw ęzłów . Superw ęzły odgryw ają kluczową rolę w routingu danych w sieci. Przy niedostępności 25% superwęzłów odpowiedzialnych za routing (ich działanie było przy wracane, lecz był to pow olny proces) cała sieć była znacznie obciążona. W ęzły klientów W in dows, które uległy aw arii, dodatkow o były ponow nie urucham iane i próbow ały ponow nie d ołączyć do sieci. Spow od ow ało to now y ruch sieciow y w ju ż p rzeciążon y m system ie. W przypadku w ystąpienia zbyt dużego obciążenia superw ęzły stosują procedurę w ycofy w ania, dlatego rozpoczęły proces w yłączania w odpow iedzi na zw iększający się stopniow o ruch sieciowy. Usługa Skype stała się w dużym stopniu niedostępna przez 24 godziny. Proces przywracania obejmował najpierw przygotow anie setek now ych bardzo w ydajnych superw ęzłów skonfi gurowanych do obsługi zw iększonego ruchu sieciowego, a następnie zastosow anie kolejnych tysięcy superwęzłów. W ciągu kilku kolejnych dni przywrócono sieć do pełnej sprawności. W ydarzenie to przysporzyło firmie Skype wielu kłopotów. Oczywiście sprawiło ono też, że przez kilka nerw ow ych dni firm a koncentrow ała się na ograniczeniu szkód. Klienci byli zm uszeni do poszukania alternatyw nych rozw iązań zapew niających połączenia głosowe. Praw dopodobnie dla konkurencji było to m arketingow e dobrodziejstwo. Biorąc pod uwagę złożoność sieci i eskalację awarii, które w ystąpiły, praw dopodobne jest, że taka niefortunna sytuacja była trudna do przewidzenia, trudne byłoby także przygotowanie planu aw aryjnego. Powodem, dla którego nie wszystkie w ęzły w sieci przestały działać, było to, że w ykorzystywane były różne w ersje oprogramowania i platformy. Użycie sieci hetero genicznej zam iast systemu homogenicznego zapewnia korzyść w postaci niezawodności.
Wady klastrowania
| 253
Typowe projekty klastrowe Częstą sytuacją jest rozpoczęcie od lokalnego klastra doraźnego złożonego z sensownej liczby jednakow ych komputerów. M ożesz się zastanawiać, czy możliwe jest dodanie starych kom puterów do sieci doraźnej. Ponieważ zwykle starsze procesory mają duży pobór mocy i działają bardzo wolno, ich wkład nie będzie taki, jakiego można oczekiwać w porów naniu z nowym komputerem o bardzo dobrych parametrach wydajnościowych. Klaster zlokalizowany w biurze wymaga obecności kogoś, kto może go serwisować. W przypadku klastra połączonego z usługą EC2 firmy Amazon (http://aws.amazon.com/ec2/purchasing-options/spot-instances/), z usługą Azure firmy M icrosoft (http://azure.microsoft.com/en-us/) lub zarządzanego przez instytucję akademicką odpowiedzialność za obsługę sprzętu spoczywa na zespole specjalistów dostawcy. Jeśli określono zrozum iałe wymagania dotyczące przetwarzania, sensow ne może być zapro jektowanie klastra niestandardowego. Być może będzie to klaster korzystający z bardzo szybkich wzajemnych połączeń InfiniBand, a nie z gigabitowego Ethernetu, albo klaster, który używa konkretnej konfiguracji napędów RAID spełniających wym agania dotyczące odczytu, zapisu lub niezaw odności. W przypadku niektórych komputerów możliwe jest połączenie proceso rów i układów GPU lub po prostu pozostanie przy domyślnym w ariancie z procesorami. Istnieje możliwość zastosowania klastra do przetwarzania w znacznie zdecentralizowany spo sób (tego typu klastry są wykorzystywane, począwszy od projektów SETI@home i Foding@home, a skończywszy na systemie Berkeley Open Infrastructure for N etw ork Computing (http://en. wikipedia.org/wiki/Berkeley_Open_Infrastructure_for_Network_Computing)). W takich rozwiązaniach w dalszym ciągu współużytkow any jest scentralizowany system koordynacji, ale węzły obli czeniowe są dołączane do projektu i usuw ane z niego w stylu ad hoc. Na bazie architektury sprzętowej możesz urucham iać różne architektury oprogramowania. Kolejki zadań są najczęściej stosow ane i najbardziej zrozum iałe. Zadania są zw ykle um iesz czane w kolejce i pobierane przez procesor. W ynik przetwarzania może trafić do innej kolejki w celu dalszego przetwarzania lub m oże stać się ostatecznym rezultatem (np. przez dodanie do bazy danych). Sposób działania system ów z przekazyw aniem kom unikatów jest trochę inny. Kom unikaty są um ieszczane w m agistrali kom unikatów , a następnie używ ane przez wiele komputerów. W przypadku bardziej złożonego systemu wykorzystuje się komunikację międzyprocesową. Ten w ariant m oże być traktowany jako konfiguracja ekspercka, ponieważ istnieje m nóstwo opcji niepoprawnego przeprowadzenia konfiguracji, co m oże rodzić fru strację. Na zastosowanie komunikacji IPC zdecyduj się tylko wtedy, gdy napraw dę wiesz, że tego potrzebujesz.
Metoda rozpoczęcia tworzenia rozwiązania klastrowego Najprostszym sposobem, by rozpocząć tworzenie systemu klastrowego, jest użycie najpierw jednego kom putera, na którym będzie działać zarów no serw er zadań, jak i procesor zadań (na każdy procesor przypada tylko jeden procesor zadań). Jeśli zadania są pow iązane z pro cesoram i, dla każdego z nich uruchom jeden procesor zadań. W przypadku pow iązania za dań z operacjami wejścia-wyjścia dla poszczególnych procesorów uruchom kilka procesorów zadań. Jeżeli zadania są powiązane z pam ięcią RAM, trzeba uważać, aby nie w ykorzystać jej
254
|Rozdział 10. Klastry i kolejki zadań
w całości. Najpierw zapewnij działanie rozwiązania bazującego na pojedynczym komputerze z jednym procesorem , a następnie dodaj więcej procesorów. W celu sprawdzenia, czy system jest solidny, spowoduj błąd w działaniu kodu na różne nieprzewidyw alne sposoby (np. w y konaj w kodzie operację 1/0, użyj polecenia k ill -9 dla procesu robo czego, odłącz cały komputer z zasilania, aby został w yłączony). Oczywiście w arto w ykonać bardziej złożone testy. D obrą propozycją jest pakiet testów jed nostkow ych zaw ierający m nóstw o testów błędów kodow ania i sztucznych w yjątków . Jeden z autorów lubi dodawać nieoczekiwane zdarzenia, takie jak w ym uszanie urucham iania przez procesor zestaw u zadań, gdy proces zew nętrzny system atycznie kończy działanie w ażnych procesów i potwierdza, że wszystkie one są poprawnie ponownie uruchamiane przez dowolny używany proces monitorowania. Po uruchomieniu procesora zadań dodaj kolejny. Sprawdź, czy nie jest w ykorzystyw ane zbyt wiele pam ięci RAM. Czy zadania są przetw arzane dwa razy szybciej niż wcześniej? Zastosuj drugi komputer z tylko jednym procesorem zadań oraz komputerem koordynującym bez żadnych procesorów zadań. Czy w tym przypadku zadania są przetwarzane równie szyb ko jak w sytuacji, gdy komputer koordynujący zawierał procesor zadań? Jeśli nie, jaka jest tego przyczyna? Czy problemem jest opóźnienie? Czy istnieją inne konfiguracje? Być może stosowa ny jest inny sprzęt (np. procesory) lub odmienne wielkości pamięci RAM i pamięci podręcznej? Dodaj dziewięć kolejnych komputerów i sprawdź, czy zadania są przetwarzane 10 razy szyb ciej niż wcześniej. Jeśli nie, dlaczego tak jest? Czy w tym przypadku występują kolizje w sieci, które zmniejszają ogólną szybkość przetwarzania? Aby pewnie uruchomić komponenty klastra podczas ładowania komputera, zwykle używane jest zadanie programu cron, program Circus (https://circus.readthedocs.org/en/latest/) lub system supervisord (http://supervisord.org/), a czasami program Upstart (http://pl.wikipedia.org/wiki/Upstart), zastępowany przez m enedżer systemd. Choć Circus jest nowszy niż supervisord, oba bazują na języku Python. Program cron jest stary, ale niezawodny w przypadku uruchamiania skryptów takich jak proces monitorowania, który w razie potrzeby może uruchamiać procesy podrzędne. Po uzyskaniu stabilnego klastra m ożesz zastosow ać narzędzie służące do kończenia działania losowych programów, takie jak Netflix ChaosM onkey (https://github.com/Netflix/SimianArmy), które celow o w yłącza części system u w celu przetestow ania ich pod kątem elastyczności. Procesy i sprzęt w końcu przestaną działać. Bądź świadom tego, że praw dopodobnie uda Ci się przynajmniej uporać z błędami, których możliw ość wystąpienia m ożesz przewidzieć.
Sposoby na uniknięcie kłopotów podczas korzystania z klastrów Szczególnie bolesne zdarzenie, jakiego doświadczył jeden z autorów, miało miejsce, gdy za wiesiła się grupa kolejek w systemie klastrowanym. Następne kolejki nie były używane, dla tego zostały w ypełnione. Ponieważ w niektórych kom puterach zabrakło pamięci RAM, ich procesy przestały działać. W cześniejsze kolejki były przetwarzane, ale nie m ogły przekazać swoich w yników do następnej kolejki, dlatego uległy awarii. Na końcu pierw sza kolejka zo stała wypełniona, lecz nie wykorzystana, więc przestała działać. Później zapłacono za dane od dostawcy, które ostatecznie zostały odrzucone. W arto rozw ażyć różne w arianty zawieszenia
Sposoby na uniknięcie kłopotów podczas korzystania z klastrów
| 255
klastra (mowa nie o opcji jeśli się zawiesi, lecz o opcji gdy się zawiesi), a także tego, co się wy darzy. Czy dane zostaną utracone (i czy stanow i to problem )? Czy zostanie utw orzony dziennik, którego przetwarzanie jest zbyt kłopotliwe? System, który można w prosty sposób debugować, prawdopodobnie będzie lepszy niż szybszy system. Czas zw iązany z inżynierią i koszt przestoju to prawdopodobnie największe koszty (nie jest to praw dą w przypadku używania programu obrony przeciwrakietowej, ale dla firmy rozpoczynającej działalność już tak). Zam iast zm niejszać w ielkość o kilka bajtów za pomocą niskopoziomowego, skompresowanego protokołu binarnego, podczas przekazywania komu nikatów rozważ zastosow anie tekstu w formacie JSON zrozum iałym dla użytkownika. Choć pow oduje to obciążenie zw iązane z w ysyłaniem kom unikatów i dekodow aniem ich, gdy zostaniesz z częściow ą bazą danych na skutek pożaru głów nego kom putera, będziesz za dowolony z możliw ości szybkiego wczytania ważnych komunikatów w trakcie przywracania działania systemu. Upewnij się, że w drażanie aktualizacji w systemie (zarówno aktualizacji systemu operacyjne go, jak i now ych w ersji oprogram ow ania) nie zajm uje w iele czasu i nie w iąże się z dużymi kosztam i finansow ym i. Każdorazow o, gdy cokolw iek zm ieni się w klastrze, ryzykujesz, że system zacznie reagować w dziwny sposób, jeśli jego stan nie będzie jednoznaczny. Upewnij się, że korzystasz z systemu wdrażania, takiego jak Fabric (http://www.fabfile.org/), Salt (https:// salt.readthedocs.org/en/latest/), Chef (http://www.getchef.com/) lub Puppet (http://puppetlabs.com/), albo z obrazu system u (np. w postaci pliku z rozszerzeniem .deb lub .rpm odpow iednio dla system ów Debian i RedH at lub urządzenia w irtualnego A m azon M achine Im age (http://en. w ikipedia.org/w iki/A m azon_M achine_Im age)). M ożliwość pewnego wdrażania aktualizacji do tyczącej całego klastra (z raportem wyszczególniającym wszelkie napotkane problemy) po zwoli Ci w ogromnym stopniu zm niejszyć stres towarzyszący trudnym chwilom. Pozytywne raportowanie jest przydatne. Codziennie wyślij do kogoś w iadom ość e-mail za wierającą szczegóły dotyczące wydajności klastra. Jeśli taka wiadom ość nie pojaw i się, będzie to cenna w skazów ka sugerująca, że w ydarzyło się coś złego. Praw dopodobnie w ym agane będą inne system y w czesnego ostrzegania, które będą szybciej pow iadam iać o problem ach. W tym przypadku szczególnie przydatne są system y Pingdom (https://w w w .pingdom .com /) i ServerDensity (https://www.serverdensity.com/). „Czuw ak", który reaguje na brak zdarzenia, to kolejne przydatne zabezpieczenie (np. produkt Dead Man's Switch (https://www.deadmansswitch.net/)). Bardzo korzystnym rozwiązaniem jest składanie członkom zespołu raportów o stanie klastra. Takie inform acje m ogą pojaw iać się na stronie adm inistracyjnej w obrębie aplikacji interne towej lub m ieć postać osobnego raportu. Znakom icie nadaje się do tego narzędzie Ganglia (http://ganglia.sourceforge.net/). Jeden z autorów spotkał się z interfejsem przypom inającym LCA RS z serialu Star Trek, który działał na dodatkow ym kom puterze PC w biurze. Po wy kryciu problem ów interfejs odtw arzał dźw ięk „stanu najw yższego pogotow ia". Takie roz w iązanie w yjątkow o skutecznie zw racało uw agę w szystkich pracow ników biura. M ieliśm y naw et okazję w idzieć analogow e przyrządy sterujące platform y A rduino przypom inające klasyczne w skaźniki ciśnienia w kotle (gdy igła się przem ieszcza, generują ładny dźw ięk!), które pokazywały obciążenie systemu. Tego rodzaju raportowanie jest ważne, aby każdy zro zumiał różnicę między stwierdzeniami „normalnie" i „to może zrujnować piątkowy wieczór!".
256
|
Rozdział 10. Klastry i kolejki zadań
Trzy rozwiązania klastrowe W zam ieszczonych dalej punktach zostaną zaprezentow ane rozw iązania Parallel Python, IPython Parallel i NSQ. Parallel Python ma interfejs bardzo podobny do wykorzystywanego w przypadku modułu multiprocessing. D ostosow anie rozw iązania opartego na tym m odule z jednego kom putera z wieloma rdzeniami do konfiguracji z wieloma komputerami sprowadza się przy w ykony w aniu czynności trw ających kilka m inut. Parallel Python ma niew iele zależności i w prosty sposób może zostać skonfigurowany pod kątem prac badaw czych z wykorzystaniem klastra lokalnego. Choć m oduł Parallel Python nie oferuje dużych możliw ości i jest pozbaw iony m e chanizmów komunikacyjnych, w bardzo łatwy sposób pozwala w ysyłać zadania nadające się idealnie do przetwarzania równoległego do niewielkiego klastra lokalnego. K lastry m odułu IPython Parallel m ogą być w bardzo prosty sposób stosow ane na jednym kom puterze z wieloma rdzeniami. Ponieważ wielu naukowców używa narzędzia IPython ja ko pow łoki, naturalne jest w ykorzystanie jej rów nież do kontrolow ania zadań przetw arza nych rów nolegle. Tw orzenie klastra w ym aga niew ielkiej w iedzy z dziedziny adm inistro w ania system am i. Ponadto zw iązane są z tym zależności (np. biblioteka ZeroM Q), dlatego konfiguracja jest trochę bardziej w ym agająca niż w przypadku m odułu Parallel Python. Ogromną zaletą modułu IPython Parallel jest to, że umożliwia zastosow anie klastrów zdal nych (np. klastrów Am azon AW S i EC2) z taką samą łatwością jak klastrów lokalnych. NSQ to system kolejkowania przygotow any do zastosow ań produkcyjnych, który używany jest w takich firmach jak Bitly. NSQ zapewnia trwałość (dzięki temu w przypadku awarii kom puterów zadania m ogą zostać ponow nie pobrane przez inny komputer) i solidne mechani zm y na potrzeby skalowalności. W iększe możliwości systemu NSQ pow odują trochę w iększe wymagania dotyczące umiejętności z zakresu inżynierii i administrowania systemami.
Użycie modułu Parallel Python dla prostych klastrów lokalnych M oduł Parallel Python (http://www.parallelpython.com/) (pp) um ożliw ia zastosow anie klastrów lokalnych procesów roboczych korzystających z interfejsu, który bardzo przypom ina ten obecny w m odule multiprocessing. O znacza to, że bardzo łatw e je st p rzekształcan ie kodu z w ersji bazującej na m odule multiprocessing używ ającym funkcji map w w ersję dla m odułu Parallel Python. Z rów ną łatw ością m ożesz uruchom ić kod za pom ocą jednego kom putera lub sieci typu ad hoc. Aby zainstalow ać m oduł Parallel Python, użyj polecenia pip in stall pp. Za pomocą modułu Parallel Python m ożliw e jest obliczenie liczby pi z wykorzystaniem me tody M onte Carlo, podobnie jak to pokazaliśm y w podrozdziale „Przybliżanie liczby pi za p om ocą procesów i w ątk ó w ", gdy użyto jed nego kom putera. W p rzykład zie 10.1 zw róć uwagę na to, w jakim stopniu interfejs jest podobny do użytego w e wcześniejszym przykła dzie z zastosowanym modułem multiprocessing. W obiekcie nbr_trials_per_process tworzona jest lista zadań, które są przekazyw ane do czterech procesów lokalnych. M ożliwe jest utw o rzenie dowolnej żądanej liczby elem entów zadań. Gdy procesy robocze zostaną zw olnione, zadania zostaną przez nie pobrane.
Trzy rozw iązania klastrowe
| 257
Przykład 10.1. Przykład lokalnego modułu Parallel Python import pp NBR_ESTIMATES = 1e8 def c a l c u l a t e _ p i ( n b r _ e s t i m a t e s ) : st e p s = x r a n g e ( i n t ( n b r _ e s t i m a t e s ) ) n br_ tria ls_ in _ u n it_ circle = 0 f o r st ep in s t e p s : x = random.uniform(0, 1) y = random.uniform(0, 1) i s _ i n _ u n i t _ c i r c l e = x * x + y * y <= 1.0 n b r _ t r i a l s _ i n _ u n i t _ c i r c l e += i s _ i n _ u n i t _ c i r c l e re tu rn n b r _ t r i a l s _ i n _ u n i t _ c i r c l e if name == " main ": NBR_PROCESSES = 4 j o b _ s e r v e r = pp.Server(ncpus=NBR_PROCESSES) p r i n t "Uruchamianie modułu pp z l i c z b ą procesów ro b o c z y c h :" , j o b _ s e r v e r . g e t _ n c p u s () n b r _ t r i a ls _ p e r _ p r o c e s s = [NBR_ESTIMATES] * NBR_PROCESSES jo b s = [] f o r inp ut_args in n b r _ t r i a ls _ p e r _ p r o c e s s : jo b = jo b _ s e r v e r . s u b m i t ( c a l c u l a t e _ p i , ( i n p u t _ a r g s , ) , ( ) , ("random",)) jo b s.ap p en d (j ob ) # każde zadanie je s t blokow ane d o momentu przygotow ania wyniku n b r _ i n _ u n i t _ c i r c l e s = [ j o b ( ) f o r jo b in jo b s ] p r i n t "L iczba z a d a ń : ", su m (nb r_ tri als_p er_p ro c ess) p r i n t su m ( n b r_ i n _ u n i t_ c i rc l e s ) * 4 / NBR_ESTIMATES / NBR_PROCESSES
W przykładzie 10.2 rozszerzono powyższy przykład. Tym razem wymagane będą 1024 zada nia 100 000 000 przybliżeń. Każde zadanie pow iązane jest z dynamicznie skonfigurowanym klastrem. Na komputerach zdalnych można wykonać polecenie python ppserver.py -w 4 -a -d. Zostaną uruchom ione serwery zdalne używ ające czterech procesów (w przypadku laptopa jednego z autorów domyślnie byłoby osiem procesów, ale rezygnujemy z czterech hiperwątków, pozostając tylko przy czterech procesorach), a także funkcji autom atycznego łączenia i dziennika debugow ania, który w yśw ietla na ekranie inform acje procesu debugow ania. Przydaje się to do sprawdzenia, czy zadanie zostało odebrane. Flaga automatycznego łączenia oznacza, że nie jest konieczne określanie adresów IP. M oduł pp ma m ożliw ość samodzielnego zgłoszenia i nawiązania połączenia z serwerami. Przykład 10.2. Moduł Parallel Python używany za pośrednictwem klastra NBR_JOBS = 1024 NBR_LOCAL_CPUS = 4 ppse rvers = ( " * " , ) # ustaw ianie listy adresów IP, które zostaną automatycznie wykryte j o b _ s e r v e r = pp .Server(p pse rv e rs = ppse rv e rs , ncpus=NBR_LOCAL_CPUS) p r i n t "Uruchamianie modułu pp z l i c z b ą lokalnych procesów ro b o c z y c h :" , j o b _ s e r v e r . g e t _ n c p u s () n b r _ t r i a ls _ p e r _ p r o c e s s = [NBR_ESTIMATES] * NBR_JOBS jo b s = [] f o r inp ut_args in n b r _ t r i a ls _ p e r _ p r o c e s s : jo b = jo b _ s e r v e r . s u b m i t ( c a l c u l a t e _ p i , ( i n p u t _ a r g s , ) , ( ) , ("random",)) jo b s.ap p en d (j ob )
Zastosowanie drugiego wydajnego laptopa pow oduje skrócenie czasu obliczeń w przybliże niu o połowę. Z kolei starszy laptop M acBook z jednym procesorem niewiele pomoże. Często będzie on przetw arzać jedno z zadań tak wolno, że szybki laptop pozostanie bezczynny, nie wykonując żadnych kolejnych zadań. A zatem ogólny czas zakończenia całości obliczeń będzie dłuższy niż w sytuacji, gdyby używany był wyłącznie szybki laptop.
258
|Rozdział 10. Klastry i kolejki zadań
Jest to bardzo korzystna m etoda rozpoczęcia tw orzenia klastra doraźnego do realizow ania prostych zadań obliczeniowych. Praw dopodobnie m etody tej nie użyjesz w środowisku pro dukcyjnym (rozwiązanie Celery lub GearMan to lepsza opcja), ale do przeprowadzania badań i zapewnienia łatwego skalowania podczas analizowania problemu nada się idealnie. M oduł pp nie będzie pomocny przy dystrybucji kodu lub danych statycznych do komputerów zdalnych. Konieczne będzie przeniesienie bibliotek zew nętrznych (np. w szystkiego, co mogło zostać skom pilow ane do postaci biblioteki statycznej) do tych kom puterów i zapew nienie w szystkich współużytkow anych danych. Moduł zajm uje się przygotowaniem kodu do uru chomienia, dodatkowymi im portami oraz danymi dostarczanymi z procesu kontrolera.
Użycie modułu IPython Parallel do obsługi badań Obsługa klastra IPython odbywa się za pośrednictwem narzędzia ipcluster (http://ipython.org/ ipython-doc/dev/parallel/). IPython staje się interfejsem dla lokalnych i zdalnych mechanizmów przetwarzania, między którymi mogą być przekazywane dane, a zadania mogą być kierowane do kom puterów zdalnych. M ożliw e jest zdalne debugow anie. O pcjonalnie obsługiw any jest interfejs M PI (M essage Passing Interface). Taki sam m echanizm komunikacji obsługuje interfejs środowiska IPython Notebook. Jest to znakomite rozwiązanie do zastosowań badawczych. Możliwe jest w trybie interaktyw nym przekazyw anie zadań do komputerów klastra lokalnego, prow adzenie interakcji i debugowanie po wystąpieniu problemu, kierowanie danych do komputerów i gromadzenie z po wrotem wyników. Zauważ również, że kompilator PyPy uruchamia powłokę IPython i moduł IPython Parallel. Takie połączenie może zapewnić bardzo duże możliwości (jeśli nie korzystasz z narzędzia numpy). W tle wykorzystywana jest biblioteka ZeroM Q w roli oprogramowania pośredniego służącego do przesyłania komunikatów. Oznacza to, że trzeba ją zainstalować. Jeśli klaster jest budowany w sieci lokalnej, możesz uniknąć uwierzytelniania opartego na protokole SSH. Jeżeli w ym a gane są zabezpieczenia, protokół ten jest w pełni obsługiwany, ale powoduje, że konfiguracja staje się trochę bardziej złożona. Zacznij od zaufanej sieci lokalnej i rozbudowuj ją po poznaniu zasad działania każdego komponentu. Projekt podzielono na cztery kom ponenty. M echanizm to synchroniczny interpreter języka Python, który uruchamia kod. W celu umożliwienia przetwarzania równoległego zostanie uru chomiony zestaw takich m echanizm ów . Kontroler zapewnia interfejs m echanizm om . Odpo wiada on za dystrybucję zadań oraz dostarcza bezpośredni interfejs i interfejs ze zrównoważonym obciążeniem, który zapewnia proces szeregujący zadania. Koncentrator śledzi mechanizmy, pro cesy szeregujące i klienty. Procesy szeregujące ukryw ają synchroniczność mechanizm ów i udo stępniają interfejs asynchroniczny. Za pom ocą polecenia ip clu ster s ta rt -n 4 na laptopie uruchom iono cztery m echanizm y. W przykładzie 10.3 uruchamiana jest powłoka IPython i sprawdzane jest, czy klient lokalny Client może wykryć cztery mechanizmy lokalne. Adresowanie wszystkich tych mechanizmów um ożliw ia łańcuch c [ :] . Dla każdego m echanizm u stosow ana jest funkcja apply_sync, która pobiera m ożliw y do w yw ołania obiekt. Z tego pow odu funkcji przekazyw any jest zerow y argum ent lambda, który zwróci łańcuch. Każdy z czterech mechanizmów lokalnych uruchom i jedną z tych funkcji, zw racając taki sam wynik.
Trzy rozw iązania klastrowe
| 259
Przykład 10.3. Sprawdzanie, czy mechanizmy lokalne są widoczne w powłoce IPython In [ 1 ] : from I P y th o n . p a r a l l e l import C lien t In [ 2 ] : c = Cl i e n t ( ) In [ 3 ] : p r i n t c . i ds [0 , 1, 2, 3] In [ 4 ] : c [ :] .a p p l y _ s y n c ( l a m b d a : " W i t a j c i e , e n t u z ja ś c i bardzo wydajnego kodu Python!") O u t[4]: [ ' W i t a j c i e , e n tu z ja ś c i bardzo wydajnego kodu P y th o n !', ' W i t a j c i e , e n tu z ja ś c i bardzo wydajnego kodu P y th o n !', ' W i t a j c i e , e n tu z ja ś c i bardzo wydajnego kodu P y th o n !', ' W i t a j c i e , e n tu z ja ś c i bardzo wydajnego kodu P ython!']
Po utworzeniu mechanizmów ich stan jest identyfikowany jako pusty. Jeśli moduły są impor towane lokalnie, nie zostaną zaim portow ane w mechanizm ach zdalnych. Prostą m etodą im portowania zarówno lokalnego, jak i zdalnego jest użycie menedżera kontekstów sync_imports. W przykładzie 10.4 zostanie zaimportowany moduł os za pomocą instrukcji import dla lokalnej powłoki IPython oraz czterech połączonych mechanizm ów. W celu pobrania identyfikatorów PID czterech mechanizm ów ponow nie wywoływana jest dla nich funkcja apply_sync. Jeśli nie zostałyby wykonane zdalne importy, wystąpiłby błąd NameError, ponieważ mechanizmy zdalne nie dysponow ałyby inform acjam i o m odule os. M ożliw e je st też użycie funkcji execute do zdalnego uruchomienia po stronie mechanizm ów dowolnego polecenia języka Python. Przykład 10.4. Importowanie modułów do mechanizmów zdalnych In [ 5 ] : dview=c[:] # je s t to bezpośredni w idok (a nie w idok ze zrównoważonym obciążeniem ) In [ 6 ] : with dv iew.s yn c_im po rts(): : import os importing os on en gin e(s ) In [ 7 ] : dv iew .ap p ly_sy nc (l a m b d a:o s.g etp id () ) O u tp ] : [1 5079, 15080, 15081, 15089] In [ 8 ] : dview.e xe cute("im port sy s" ) # inny sp osó b zdalnego wykonywania p o leceń
W ym agane będzie przekazanie danych do m echanizm ów . Polecenie push zaprezentow ane w przykładzie 10.5 umożliwia wysłanie słownika elementów, które są dodawane do globalnej przestrzeni nazw każdego mechanizmu. Istnieje odpowiednie polecenie pull służące do po bierania elem entów , którem u zapew niasz klucze. Polecenie zw róci odpow iednie w artości z każdego m echanizm u. Przykład 10.5. Przekazywanie współużytkowanych danych do mechanizmów In [ 9 ] : d v i e w . p u s h ( { 's h a r e d _ d a t a ': [ 5 0 , 10 0 ]} ) O u t[ 9]: In [ 1 0 ] : dv iew .a pp ly_s ync (la m bda :len (share d_da ta)) Out[ 10 ] : [2 , 2 , 2 , 2]
Dodajmy teraz do klastra drugi komputer. Najpierw zakończymy działanie utworzonych wcze śniej m echanizm ów ipengine i wyłączymy pow łokę IPython. Zaczniemy wszystko od nowa. Niezbędny będzie drugi komputer ze skonfigurowanym protokołem SSH w celu umożliwienia automatycznego logowania. W przykładzie 10.6 zostanie utworzony nowy profil dla klastra. Zestaw plików konfiguracyj nych znajduje się w katalogu /.ipython/profile_mycluster. Dom yślnie m echanizm y są skonfigurowane w celu akceptowania połączeń wyłącznie z komputera localhost, a nie z urządzeń zewnętrznych. Dokonaj edycji pliku ipengine_config.py, aby skonfigurować obiekt HubFactory pod kątem akceptowania połączeń zewnętrznych, zapisz zmiany, a następnie uruchom nowe pole cenie ipcluster, używając nowego profilu. Ponownie użyjemy czterech mechanizmów lokalnych.
260
|
Rozdział 10. Klastry i kolejki zadań
Przykład 10.6. Tworzenie lokalnego profilu, który akceptuje połączenia publiczne $ $ # $
ipython p r o f ile c re a te m ycluster -- p a r a l l e l gvim /h o m e/ia n /.ip y th o n /p ro file _ m y clu ste r/ip e n g in e _co n fig .p y d o d aj c.H ubFactory.ip = '*' w p obliżu początku pliku ip c lu s te r s t a r t -n 4 --p ro file = m y clu s te r
Konieczne jest następnie przekazanie tego pliku konfiguracyjnego do kom putera zdalnego. W przykładzie 10.7 użyto polecenia scp do skopiowania pliku ipcontroller-engine.json (utworzono go podczas uruchamiania narzędzia ipcluster) do katalogu .config/ipython/profile_default/security na komputerze zdalnym. Po zakończeniu kopiowania uruchom mechanizm ipengine na kom puterze zdalnym . M echanizm poszuka pliku ipcontroller-engine.json w katalogu domyślnym. Jeśli naw iązyw anie połączenia zakończy się pomyślnie, zostanie wyśw ietlony kom unikat po dobny do pokazanego poniżej. Przykład 10.7. Kopiowanie zmodyfikowanego profilu do komputera zdalnego i testowanie # Na kom puterze lokalnym $ scp /home/ia n /.ip y th o n /p r o file _ m y c lu s te r /s e c u r ity /ip c o n tro lle r -e n g in e .js o n i a n @ 1 9 2 .1 6 8 .0 . 1 6 : /h o m e /ia n /.co n fi g /ip y th o n /p ro file _ d e fa u lt/s e c u ri t y / # Na kom puterze zdalnym ian@ubuntu:~$ ipengine . . . U s i n g e x i s t i n g p r o f i l e d i r : u'/ h om e/ ia n / .config /ipyt ho n/pr ofile_ defau lt' . . . Loading u r l _ f i l e u'/home/ian/.config/i python/profi l e _ d e fa u l t / s e c u r i t y / ip c o n tro ller-en g in e .jso n ' . . . R e g i s t e r i n g with c o n t r o l l e r at t c p : / / 1 9 2 . 1 6 8 .0 .1 2 8 : 3 5 9 6 3 . . . S t a r t i n g to monitor the h eart b eat sign al from the hub every 3010 ms. . . . U s i n g e x i s t i n g p r o f i l e d i r : u '/ hom e/ ian/ .config/ ip yt ho n/ pro file_d ef au lt' ...C omp leted r e g i s t r a t i o n with id 4
Przetestujmy konfigurację. W przykładzie 10.8 zostanie uruchomiona lokalna powłoka IPython przy użyciu nowego profilu. Zostanie pobrana lista pięciu klientów (czterech lokalnych i jedne go zdalnego), a następnie utw orzone żądanie przekazania informacji o wersji języka Python. Możesz zauważyć, że na komputerze zdalnym używana jest dystrybucja Anaconda. Zostanie zastosowany tylko jeden dodatkowy mechanizm, ponieważ w tym przypadku komputer zdalny to jednordzeniowy MacBook. Przykład 10.8. Sprawdzanie, czy nowy komputer stanowi część klastra $ ipython --p ro file = m y clu s te r Python 2 . 7 . 5 + ( d e f a u l t , Sep 19 2013, 1 3 : 4 8 :4 9 ) Type "c o p y r i g h t " , " c r e d i t s " or " l i c e n s e " f o r more inf ormation . IPython 1.1 .0 -A n enhanced I n t e r a c t i v e Python. In [ 1 ] : from I P y th o n . p a r a l l e l import C lien t In [ 2 ] : c = C l i e n t ( ) In [ 3 ] : c . i d s Out[3] : [0 , 1, 2, 3, 4] In [ 4 ] : dview=c[:] In [ 5 ] : with dv iew.s yn c_im po rts(): ... : import sys In [ 6 ] : dv iew.a pp ly_sy nc (la m bda :sys.ver sion ) O u t[6]: [ ' 2 . 7 . 5 + ( d e f a u l t , Sep 19 2013, 1 3 : 4 8 :4 9 ) \n[GCC 4 . 8 . 1 ] ' , ' 2 . 7 . 5 + ( d e f a u l t , Sep 19 201 3, 1 3 : 4 8 :4 9 ) \n[GCC 4 . 8 . 1 ] ' , ' 2 . 7 . 5 + ( d e f a u l t , Sep 19 2013, 1 3 : 4 8 :4 9 ) \n[GCC 4 . 8 . 1 ] ' , ' 2 . 7 . 5 + ( d e f a u l t , Sep 19 2013, 1 3 : 4 8 :4 9 ) \n[GCC 4 . 8 . 1 ] ' , ' 2 . 7 . 6 |Anaconda 1 . 9 . 2 (6 4 - b it ) | ( d e f a u l t , Jan 17 2014, 1 0 : 1 3 :1 7 ) \n [GCC 4 . 1 . 2 20080704 (Red Hat 4 . 1 . 2 - 5 4 ) ] ' ]
Trzy rozw iązania klastrowe
|
261
Pora na połączenie w szystkich elementów. W przykładzie 10.9 zostanie użytych pięć mecha nizm ów do przybliżenia liczby pi, tak jak to m iało m iejsce w punkcie „Użycie m odułu Pa rallel Python dla prostych klastrów lokalnych". Tym razem zostanie zastosowany dekorator @require do zaim portowania modułu random w mechanizm ach. Bezpośredni w idok jest uży wany do wysyłania do nich zadań. Pow oduje to blokowanie do m omentu zwrócenia w szyst kich w yników . Gdy to nastąpi, dokonyw ane jest przybliżenie liczby pi w zaprezentow any wcześniej sposób. Przykład 10.9. Przybliżanie liczby pi za pomocą klastra lokalnego from I P y th o n . p a r a l l e l import C l i e n t , r e q u ire NBR_ESTIMATES = 1e8 @ re q uire ('ran dom ') def c a l c u l a t e _ p i ( n b r _ e s t i m a t e s ) : r e tu r n n b r _ t r i a l s _ i n _ u n i t _ c i r c l e i f __name__ == "__main__": c = C lien t() nbr_engines = l e n ( c . i d s ) p r i n t "L iczba używanych mechanizmów: { } " .f o r m a t(n b r _ e n g in e s ) dview = c [ : ] n b r _ i n _ u n i t _ c i r c l e s = d v ie w .a p p l y _ sy n c (c a l c u l a te _ p i, NBR_ESTIMATES) p r i n t "L iczba dokonanych p r z y b l i ż e ń : " , n b r _ i n _ u n i t _ c i r c l e s # przetw arzanie wyłącznie z w ykorzystaniem mechanizm ów nbr_jobs = l e n ( n b r _ i n _ u n i t _ c i r c l e s ) p r i n t su m ( n b r_ i n _ u n i t_ c i rc l e s ) * 4 / NBR_ESTIMATES / nbr_jobs
M oduł IPython Parallel oferuje znacznie więcej opcji, niż tutaj zaprezentowano. M ożliwe są oczywiście zadania asynchroniczne i odwzorowania z wykorzystaniem większych zakresów w ejściowych. M oduł zawiera też klasę CompositeError, czyli wyjątek wyższego poziomu opakow ujący ten sam w yjątek, który w ystąpił w wielu m echanizm ach (dzięki temu nie odbiera się w ielu identycznych w yjątków po w drożeniu niepopraw nego kodu!). Jest to w ygodne rozwiązanie w przypadku obsługi wielu mechanizmów 1. Funkcja modułu IPython Parallel o szczególnych możliwościach pozwala użyć większego śro dowiska klastrowania, w tym superkomputerów i usług chmury, takich jak EC2 firmy Amazon. W celu dodatkowego ułatwienia tworzenia tego rodzaju klastra dystrybucja Anaconda zapew nia obsługę narzędzia StarCluster. Na konferencji PyCon 2013 O livier Grisel przeprow adził znakom ity kurs dotyczący zaaw ansow anego uczenia m aszynow ego z w ykorzystaniem na rzędzi scik it-le a rn . W trakcie dw ugodzinnego pokazu zadem onstrow ał użycie narzędzia StarCluster do uczenia m aszynowego za pośrednictw em modułu IPython Parallel obecnego w instancjach obszaru danych usługi EC2 firmy Amazon.
Użycie systemu NSQ dla niezawodnych klastrów produkcyjnych W środowisku produkcyjnym niezbędne będzie rozw iązanie, które jest bardziej niezaw odne od innych dotychczas omówionych rozwiązań. W ynika to stąd, że podczas codziennej pracy klastra węzły mogą stawać się niedostępne, kod może ulec zawieszeniu, sieci m ogą paść ofia rą awarii lub może wystąpić jedna z tysięcy innych możliw ych kom plikacji. Problem polega 1 Więcej sz czegółów do st ępn ych je st p o d a d r e se m h ttp ://ipython .org/ipython -doc/dev/parallel/parallel_m u ltien gin e.htm l.
262
|
Rozdział 10. Klastry i kolejki zadań
na tym, że w e w szystkich w cześniej omaw ianych systemach znajdow ał się jeden komputer, na którym wykonywano polecenia, a także ograniczona i statyczna liczba komputerów wczy tujących i realizujących te polecenia. Wymagany jest natomiast system, w którym będzie istnieć w iele aktorów komunikujących się za pośrednictwem określonej magistrali komunikatów. Pozwoliłoby to na dysponow anie arbitralną i ciągle zmieniającą się liczbą twórców i konsu m entów komunikatów. Prostym rozwiązaniem tych problemów jest zastosowanie systemu NSQ (https://github.com/bitly/nsq), czyli bardzo wydajnej, rozproszonej platformy do przesyłania komunikatów. Ponieważ system napisano w języku GO, jest on całkowicie niezależny od formatu danych i języków. W rezulta cie istnieją biblioteki utw orzone za pom ocą wielu języków. Podstawowy interfejs dla systemu NSQ to interfejs API REST, który wymaga jedynie możliwości tworzenia wyw ołań HTTP. Co więcej, komunikaty mogą być wysyłane w dowolnym żądanym formacie (JSON, Pickle, msgpack itp.). Najistotniejsze jest jednak to, że system NSQ zapewnia zasadnicze gwarancje dotyczące dostarczania komunikatów, a ponadto w tym celu korzysta jedynie z dwóch prostych wzorców projektowych, czyli kolejek i publikatora/ subskrybentów.
Kolejki Kolejka jest typem bufora przeznaczonego dla kom unikatów . Każdorazow o, gdy kom unikat ma zostać w ysłany do innej części potoku przetw arzającego, jest on kierow any do kolejki. W niej kom unikat oczekuje do m om entu dostępności procesu roboczego, który go w czyta. K olejka przyd aje się najbard ziej w przypadku przetw arzania rozproszonego, gdy nie ma równowagi między generowaniem i pobieraniem. Jeśli taka nierów now aga się pojawi, m oż liwe jest po prostu zastosowanie skalowania poziom ego przez dodawanie kolejnych konsu mentów danych do momentu wyrównania szybkości generowania i pobierania komunikatów. Jeśli awarii ulegną także komputery odpowiedzialne za pobieranie komunikatów, komunikaty nie zostaną utracone i po prostu będą kolejkowane do chwili udostępnienia konsumenta. W ten sposób zapewniane jest dostarczanie komunikatów. Dla przykładu załóżmy, że nowe rekomendacje dla użytkownika m ają zostać przetworzone za każdym razem, gdy oceni on now y produkt w witrynie. Jeśli nie istniałaby kolejka, dzia łania oceniania spow odow ałyby bezpośrednie w yw ołanie działania ponownego przetw orze nia rekomendacji, niezależnie od tego, jak bardzo byłyby zajęte serwery, które zajm ują się re kom endacjam i. Jeśli nagle tysiące użytkow ników postanow ią ocenić jakiś produkt, serwery rekom endacji m ogą zostać tak bardzo obciążone żądaniam i, że zaczną przekraczać limity oczekiwania, pom ijać komunikaty i generalnie przestaną odpowiadać! Z kolei w przypadku użycia kolejki serwery rekomendacji zażądają dodatkowych zadań, gdy będą gotowe. Now e działania oceniania spowodują um ieszczenie nowego zadania w kolejce. Gdy serwer rekomendacji będzie gotowy do wykonania kolejnych zadań, pobierze je z kolejki i przetworzy. Jeśli w takim w ariancie więcej niż zwykle użytkowników zacznie oceniać pro dukty, kolejka zostanie wypełniona i będzie pełnić rolę bufora dla serwerów rekomendacji. Ich obciążenie nie zmieni się z tego powodu, dlatego w dalszym ciągu będą mogły przetwarzać komunikaty do momentu opróżnienia kolejki. Z tym rozwiązaniem zw iązany jest potencjalny problem. Polega on na tym, że jeśli kolejka zostanie całkowicie przeciążona zadaniami, będzie przechowywać sporą liczbę komunikatów. System NSQ radzi sobie z tym przez korzystanie z wielu serwerów magazynujących. Komu nikaty są przechow yw ane w pam ięci, gdy nie m a ich w iele. W m om encie pojaw ienia się większej liczby komunikatów zaczynają być one umieszczane na dysku.
Użycie systemu NSQ dla niezawodnych klastrów produkcyjnych
j
263
^
O g ó ln ie rzecz biorąc, w p rz y p a d k u k o rzy stan ia z sy ste m ó w k o lejk o w a n y c h w arto s p r ó b o w a ć z a g w a r a n t o w a ć , b y k o l e j n e s y s t e m y (n p. s y s t e m y r e k o m e n d a c j i w e w c z e śn iej p r z e d s t a w i o n y m p r z y k ł a d z i e ) m i a ł y p r z y n o r m a l n y m o b c i ą ż e n i u n i e w y k o r z y s ta n e 6 0 % z a s o b ó w . Je st to o d p o w ie d n i k o m p r o m is w p r z y p a d k u p r z y d z ie la n ia z b y t w ie lu z a s o b ó w n a p o tr z e b y d a n e g o p r o b le m u i z a p e w n ia n ia se r w e r o m d o d a tk o w ej, w y sta rcz a ją ce j m o c y o b licz e n io w e j, g d y liczb a z a d a ń p r z e k r o c z y n o r m a ln y p o z io m .
Publikator/subskrybent Publikator/subskrybent opisuje, kto i jakie uzyskuje kom unikaty. Publikator danych m oże przekazać dane do określonego tem atu, a subskrybenci danych m ogą subskrybow ać różne kanały inform acyjne danych. Każdorazow o, gdy publikator przekaże porcję inform acji, jest ona w ysyłana do w szystkich subskrybentów. Każdy z nich otrzym uje identyczną kopię ory ginalnej inform acji. M ożesz to porów nać do gazety: w iele osób m oże w ykupić subskrypcję konkretnej gazety. Za każdym razem, gdy pojawi się now e wydanie gazety, każdy subskry bent otrzyma jednakow y egzemplarz. Ponadto w ydawca gazety nie musi znać w szystkich osób, do których są w ysyłane jego gazety. Oznacza to, że publikatorzy i subskrybenci są od siebie oddzieleni. Dzięki temu system może być bardziej niezawodny, gdy w dalszym ciągu w środowisku produkcyjnym w sieci są dokonywane zmiany. Oprócz tego system NSQ wprowadza pojęcie konsumenta danych. Oznacza to, że wiele proce sów może być połączonych z tą samą subskrypcją danych. Każdorazowo, gdy zostanie prze kazana nowa porcja danych, każdy subskrybent uzyska ich kopię. Jednakże dane te są widocz ne tylko dla jednego konsumenta każdej subskrypcji. Korzystając z analogii do gazety, można to porów nać do w ielu osób w tym sam ym dom u, które czytają gazetę. W ydaw ca dostarczy do dom u jed n ą gazetę, poniew aż w ykupiono w nim jed n ą subskrypcję. O soba, która jako pierwsza odbierze gazetę, przeczyta jej treść. Konsumenci każdego subskrybenta przetwarzają kom unikat w ten sam sposób po uzyskaniu do niego dostępu. M ogą jednak znajdow ać się na wielu komputerach, a tym samym zw iększać całkowitą m oc obliczeniową całej puli. Na rysunku 10.1 opisano model złożony z publikatora, subskrybentów i konsumentów. Jeśli w temacie „kliknięcia" zostanie opublikowany nowy komunikat, wszyscy subskrybenci (w żar gonie systemu NSQ są oni określani mianem kanałów, takich jak „pom iary", „analiza_spamu" i „archiw um ") otrzymają kopię. Każdy subskrybent jest złożony z co najmniej jednego kon sum enta, który reprezentuje procesy reagujące na kom unikaty. W przypadku subskrybenta „pom iary" now y kom unikat zobaczy tylko jeden konsum ent. N astępny kom unikat trafi do kolejnego konsumenta itd.
Rysunek 10.1. Topologia systemu NSQ przypominająca hierarchię publikator/subskrybenty
264
|Rozdział 10. Klastry i kolejki zadań
Zaletą rozsyłania komunikatów w obrębie potencjalnie dużej puli konsumentów jest zasadniczo automatyczne równoważenie obciążenia. Jeśli przetwarzanie komunikatu zajmuje dość sporo czasu, dany konsument nie zgłosi systemowi NSQ gotowości przyjęcia kolejnych komunikatów, dopóki nie zakończy pracy związanej z tym komunikatem. Oznacza to, że inne konsumenty będą otrzy mywać większość przyszłych komunikatów (do momentu, aż pierwotny konsument będzie po nownie gotowy do przetwarzania). Dodatkowo takie rozwiązanie umożliwia rozłączenie ist niejącym konsumentom (albo świadomie, albo z powodu awarii), a nowym nawiązanie połączenia z klastrem przy jednoczesnym utrzymaniu mocy obliczeniowej w obrębie konkretnej grupy subskrypcji. Jeśli na przykład okaże się, że subskrybent „pomiary" potrzebuje trochę czasu na przetwarzanie i nie jest w stanie podołać zapotrzebowaniu, można po prostu dodać więcej pro cesów do puli konsumenta dla danej grupy subskrypcji. Dzięki temu zapewni się w iększą moc obliczeniową. Z kolei jeśli ewidentnie większość procesów jest bezczynna (czyli nie otrzymuje żadnych komunikatów), z łatwością można usunąć konsumenty z takiej puli subskrypcji. G odne uw agi jest też to, że dane m ogą być publikow ane przez cokolw iek. K onsum ent nie musi być po prostu konsumentem. M oże pobierać dane z jednego tematu, a następnie publi kow ać je w innym tem acie. O kazuje się, że w przypadku użycia tego m odelu do obliczeń rozproszonych taki łańcuch pełni rolę ważnego przepływu zadań. Konsumenty będą doko nyw ać odczytu z tem atu danych, transform ow ać je w określony sposób, a następnie publi kow ać w now ym tem acie, który m oże być dalej transform ow any przez inne konsum enty. Dzięki temu różne tem aty reprezentują różne dane, grupy subskrypcji reprezentują różne transformacje danych, a konsumenty są rzeczywistym i procesam i roboczymi, które dokonują transformacji poszczególnych komunikatów. Taki system zapewnia niesamowitą redundancję. Może istnieć wiele procesów nsqd, z którymi łączy się każdy konsument. Z konkretną subskrypcją może być połączonych wiele konsumen tów. Sprawia to, że nie w ystępuje pojedynczy punkt awarii, a system pozostanie niezawodny naw et wtedy, gdy niedostępnych będzie kilka komputerów. Na rysunku 10.2 widać, że jeśli nawet przestanie działać jeden z komputerów pokazanych na diagramie, system nadal będzie w stanie dostarczać i przetw arzać komunikaty. Co w ięcej, ze względu na to, że podczas w y łączania system NSQ zapisuje na dysku zaległe kom unikaty, o ile nie dojdzie do katastrofy w postaci utraty sprzętu, dane najprawdopodobniej w dalszym ciągu pozostaną nienaruszone i zostaną dostarczone. Jeżeli konsument zostanie wyłączony przed udzieleniem odpowiedzi na konkretny komunikat, system NSQ ponow nie w yśle ten komunikat do innego konsumenta. Oznacza to, że naw et w tedy, gdy konsumenty przestaną być dostępne, będzie w iadom o, że dla w szystkich komunikatów w temacie przynajmniej raz zostanie udzielona odpowiedź2.
Konsument
Konsument
Rysunek 10.2. Topologia połączeń systemu NSQ 2 M o ż e to b y ć d o ś ć k o r z y stn e p o d c z a s st o s o w a n ia usłu gi A W S , w p r z y p a d k u której p r o c e s y
nsqd m o g ą
dz iałać
w z a re z e rw o w a n e j ins tancji, a k o n s u m e n t y w kla st rz e ins tancji ob sz a ru da nyc h.
Użycie systemu NSQ dla niezawodnych klastrów produkcyjnych
| 265
Rozproszone obliczenia liczb pierwszych Kod używ any przez system NSQ jest generalnie asynchroniczny3 (w rozdziale 8. zamiesz czono pełne objaśnienie), choć niekoniecznie musi taki być. W poniższym przykładzie zostanie utworzona pula procesów roboczych dokonujących odczytu z tematu o nazwie liczby, w przy padku którego komunikaty to po prostu obiekty BLOB (Binary Large Object) w formacie JSON zaw ierające liczby. Konsumenty odczytają ten temat, stwierdzą, czy liczby są liczbami pierw szymi, a następnie zapiszą je w innym temacie, zależnie od tego, czy dana liczba jest liczbą pierwszą. W rezultacie powstaną dwa nowe tematy primes (liczby pierwsze) i non_primes (inne liczby), z którymi m ogą łączyć się inne konsumenty w celu wykonania dalszych obliczeń4. Jak wcześniej w spomniano, z realizowania w ten sposób zadań pow iązanych z procesorem wynika wiele korzyści. Przede wszystkim uzyskiw ane są pełne gwarancje niezawodności, co w przypadku omawianego projektu niekonieczne może być przydatne. Co jednak ważniejsze, zapewniane jest automatyczne równoważenie obciążenia. Oznacza to, że jeśli jeden konsu m ent uzyska liczbę, której przetw orzenie zajm uje szczególnie dużo czasu, inne konsumenty w ezm ą na siebie odpowiedzialność za resztę. Tworzenie konsumenta polega na utworzeniu obiektu nsq.Reader z określonym tematem i grupą subskrypcji (jest to w idoczne na końcu przykładu 10.10). Konieczne jest również określenie położenia działającej instancji procesu nsqd (lub instancji procesu nsqlookupd, który nie będzie omawiany w tym podrozdziale). Ponadto określamy procedurę obsługi, która jest po prostu funk cją wywoływaną dla każdego komunikatu z tematu. W celu utworzenia producenta tworzony jest obiekt nsq.Writer oraz podaw ane jest położenie co najmniej jednej instancji procesu nsqd, w której będzie mieć miejsce zapis. Umożliwia to asynchroniczny zapis w systemie nsq przez samo podanie nazwy tematu i komunikatu5. Przykład 10.10. Rozproszone obliczenia liczb pierwszych z wykorzystaniem systemu NSQ import nsq from tornado import gen from fu n c to o l s import p a r t i a l import u js o n as j s o n @gen.coroutine def w r ite _m ess a g e(t o p ic, d at a, w r i t e r ) : response = y i e l d g e n .T a sk (w rite r .p u b , t o p i c , data) # O 0 © i f isinstance(response, nsq.Error): p r i n t "Błąd dotyczący komunikatu: { } : { } " . f o r m a t ( d a t a , response) y i e l d write_m essa ge (d ata, w r i te r ) else: p r i n t "Opublikowany komunikat: " , data def c alcu late_ prim e( m es sa ge, w r i t e r ) : m essag e.ena ble_a sy nc () # 0 data = js on .loa d s( m es sa g e. bod y) prime = is_ prim e(data[ "n um ber" ])
3 T a k a a s y n c h r o n i c z n o ś ć w y n i k a z p r o t o k o ł u s y s t e m u N S Q słu ż ą c eg o d o w y s y ł a n ia k o m u n i k a t ó w d o k o n s u m e n t ó w , k t ó r y b a z u je n a m e c h a n i z m i e a k t y w n e g o p rze sy łan ia . W re z u lta c ie k o d m o ż e p o w o d o w a ć w y s tą p i e n i e w tle o d c z y t u a s y n c h r o n i c z n e g o z p o ł ą c z e n ia z s y s t e m e m N S Q , a p o n a d t o a k t y w o w a n i e w m o m e n c i e n a p o tk a n i a k o m u n i k a t u . 4 T e g o ro d z a ju tw o r z e n i e ła ń c u c h a an a li z y d a n y c h je st o k r e ś l a n e m i a n e m po to ko w a n ia . M o ż e to b y ć sk u tec z n a m e t o d a e f e k t y w n e g o p r z e p r o w a d z a n i a w ie lu t y p ó w an a li z y t y ch s a m y c h d a n y c h . 5 M o ż l i w e je st te ż r ę c z n e p u b l i k o w a n i e w p r o s t y s p o s ó b k o m u n i k a t u z a p o m o c ą w y w o ł a n i a H T T P . Je d n a k ż e o b iek t n sq .W riter u p r a s z c z a z n a c z n ą c z ę ś ć o b słu g i b łę d ó w .
266
|
Rozdział 10. Klastry i kolejki zadań
data[ "p rim e"] = prime i f prime: to p ic = ' primes' else: to p ic = 'non_primes' output_message = json.dumps(data) w r ite _m ess ag e(t o p ic, output_message, w r i te r ) m e s s a g e .f i n i s h ( ) # © if name == " main ": w riter = n s q .W r it e r ( [ '1 2 7 .0 .0 . 1 :4 1 5 0 ', ]) handler = p a r t i a l ( c a l c u l a t e _ p r i m e , w r ite r = w rite r) re ad er = nsq.Read er( message_handler = hand ler, nsqd_tcp_addresses = [ ' 1 2 7 . 0 . 0 . 1 : 4 1 5 0 ' , ] , to p ic = 'n um bers ', channel = 'worker_group_a', ) n sq .ru n()
O W ynik zostanie asynchronicznie zapisany w nowym temacie. Zapis zostanie ponowiony, jeśli nie powiedzie się z jakiegoś powodu. © Po włączeniu opcji async dla komunikatu podczas przetwarzania komunikatu można wy konyw ać operacje asynchroniczne. © W przypadku komunikatów z włączoną opcją async po zakończeniu przetwarzania ko m unikatu konieczne jest zasygnalizow anie tego systemowi NSQ. Aby skonfigurować środowisko systemu NSQ, zostanie uruchomiona instancja procesu nsdq na komputerze lokalnym: $ nsqd 2014/05/10 2014/05/10 2014/05/10 2014/05/10 2014/05/10
1 6 : 4 8 :4 2 1 6 : 4 8 :4 2 1 6 : 4 8 :4 2 1 6 : 4 8 :4 2 1 6 : 4 8 :4 2
nsqd v 0 . 2 . 2 7 ( b u i l t w/go1.2.1) worker id 382 NSQ: p e r s i s t i n g topic/channel metadata to nsq d.3 8 2.dat TCP: l i s t e n i n g on [ : : ] :4150 HTTP: l i s t e n i n g on [ : : ] : 4 1 5 1
M ożliwe jest teraz uruchom ienie żądanej liczby instancji kodu Python (przykład 10.10). Oka zuje się, że instancje te m ogą działać na innych kom puterach pod warunkiem, że nadal waż ne jest odwołanie do obiektu nsqd_tcp_addresses zw iązanego z tworzeniem instancji obiektu nsq.Reader. Konsumenty będą łączyć się z procesem nsdq i czekać na opublikow anie komuni katów w temacie numbers (liczby). Istnieje w iele sposobów publikow ania danych w tem acie numbers (liczby). Użyjem y do tego narzędzi wiersza poleceń, ponieważ wiele czasu zajmuje gruntowne poznanie systemu w celu stwierdzenia, jak poprawnie zająć się publikowaniem danych. Aby w temacie opublikować komunikaty, m ożemy po prostu użyć interfejsu HTTP: $ f o r i in 's e q 10000' > do > echo {\"number\": $ i } > done
| curl -d@- " http:// 12 7.0.0 .1:4 151 /p ub ?topi c=n um ber s"
Po rozpoczęciu działania tego polecenia w temacie numbers są publikowane komunikaty za w ierające różne liczby. W tym samym czasie w szystkie producenty rozpoczną zw racanie komunikatów statusu, które wskazują, że napotkały i przetw orzyły komunikaty. Dodatkowo liczby te są publikow ane w temacie primes lub non_primes. Pozwala to zastosow ać inne konsum enty danych, które łączą się z jednym z tych tem atów w celu uzyskania filtrow anego podzbioru oryginalnych danych. Na przykład aplikacja, która wymaga tylko liczb pierwszych,
Użycie systemu NSQ dla niezawodnych klastrów produkcyjnych
| 267
m oże po prostu połączyć się z tem atem primes i cały czas m ieć dostęp do now ych liczb pierw szych używ anych do sw oich obliczeń. Status obliczeń m ożna w yśw ietlić za pom ocą punktu końcow ego HTTP stats dla procesu nsqd: $ curl " h t t p : / / 1 2 7 .0 . 0 .1 : 4 1 5 1 / s t a t s " nsqd v 0 . 2 . 2 7 ( b u i l t w/go1.2.1) [numbers ] depth: 0 be-depth: 0 msgs: 3060 [worker_group_a ] depth: 1785 be-depth: 0 in flt: 1 re-q: 0 tim eou t: 0 msgs: 3060 [V2 muon:55915 ] s t a t e : 3 i n f l t : 1 rdy: 0 fin : re-q: 0 msgs: 1469 connected: 24s [primes ] depth: 195 be-depth: 0 msgs: 1274 [non_primes ] depth: 1274 be-depth: 0 msgs: 1274
e2e%: de f: 0 e2e%: 1469 e2e%: e2e%:
W tym przypadku w idoczne jest, że tem at numbers ma jedną grupę subskrypcji worker_group_a z jednym konsumentem. Ponadto grupa ta ma dużą głębokość wynoszącą 1785 komunikatów. Oznacza to, że komunikaty są umieszczane w systemie NSQ szybciej, niż m ogą być przetw a rzane. W skazyw ałoby to potrzebę dodania kolejnych konsum entów, aby zapew nić w iększą moc obliczeniową do przetworzenia większej liczby kom unikatów. Dodatkowo możliwe jest stwierdzenie, że konkretny konsum ent był połączony przez 24 sekundy, przetw orzył 1469 komunikatów i obecnie zajmuje się jednym komunikatem. Taki punkt końcowy statusu oferuje liczbę informacji wystarczającą do tego, aby debugować konfigurację systemu NSQ! Na końcu wyniku powyższego polecenia widoczne są tematy primes i non_primes, które nie mają subskry bentów lub konsumentów. Oznacza to, że komunikaty będą przechow yw ane do momentu pojawienia się subskrybenta żądającego danych. W s y s t e m a c h p r o d u k c y j n y c h m o ż l i w e j e s t u ż y c i e n a r z ę d z i a nsqadmin o j e s z c z e w i ę k sz y ch m o ż liw o ś c ia c h , k tó re u d o s tę p n ia in terfejs W W W z b a r d z o sz c z e g ó ło w y m i p rz e g lą d a m i w szy stk ich te m a tó w / su b s k ry b e n tó w oraz k o n su m e n tó w . D o d a tk o w o ^
um ożliw ia w prosty sposób w strzy m y w an ie i u su w an ie subskrybentów i tem atów .
A by w y św ietlić kom u nik aty , n ależy u tw orzyć now y kon su m en t dla tem atu primes (lub non_primes), który po prostu archiw izuje w yniki w pliku lub bazie danych. A lternatyw nie można zastosow ać narzędzie nsq_tail do przyjrzenia się danym i sprawdzenia, co zawierają: $ n sq _ ta il 2014/05/10 2014/05/10 2014/05/10
- - t o p i c primes --n s q d -tc p -a d d r e s s = 1 2 7 .0 .0 .1 :4 1 5 0 1 7 : 0 5 :3 3 s t a r t i n g Handler g o-rou tine 1 7 : 0 5 :3 3 [ 1 2 7 . 0 . 0 . 1 : 4 1 5 0 ] connectin g to nsqd 1 7 : 0 5 :3 3 [ 1 2 7 . 0 . 0 . 1 : 4 1 5 0 ] IDENTIFY response: {MaxRdyCount:2500 T L S v 1 :f a ls e D e f l a t e : f a l s e Sn ap p y :fa lse} { " p r i m e ":tr u e ,"n u m b e r ":5 } { " p r i m e ": tr u e , "n u m b e r ": 7 } {"pri m e": tr u e," nu m b er" :11} {"pri m e": tr u e," nu m b er" :13} {"pri m e": tr u e," nu m b er" :17}
Inne warte uwagi narzędzia klastrowania Systemy przetw arzające zadania, które korzystają z kolejek, są w użyciu od początku istnie nia branży informatycznej, gdy komputery były bardzo powolne, a niezbędne było przetw o rzenie wielu zadań. W efekcie dostępnych jest wiele bibliotek kolejek, spośród których spora część m oże być używ ana w konfiguracji klastra. Szczególnie nam aw iam y do w ybrania doj rzałej biblioteki, z którą związana jest aktywna społeczność. Biblioteka ta powinna obsługiwać ten sam zestaw wymaganych funkcji, a ponadto niezbyt wiele dodatkowych.
268
|Rozdział 10. Klastry i kolejki zadań
Im więcej funkcji biblioteka oferuje, tym więcej pojawi się sposobów na niepopraw ne skonfi gurowanie jej i stratę czasu na debugowanie. Prostota jest zwykle w łaściwym celem w przy padku korzystania z rozwiązań klastrowych. Oto kilka częściej stosowanych rozwiązań: • Celery (http://www.celeryproject.org/) (na licencji BSD) to pow szechnie używana asynchro niczna kolejka zadań, która korzysta z napisanej w języku Python architektury rozpro szonego przesyłania komunikatów. Kolejka obsługuje języki Python, PyPy i Jython. Choć zw ykle kolejka ta używa narzędzia RabbitM Q jako brokera komunikatów, obsługuje też systemy Redis, M ongoDB oraz inne systemy przechowywania. Kolejka Celery jest często używ ana w projektach zw iązanych z projektow aniem aplikacji internetow ych. Została omówiona przez Andrew Godwina w podrozdziale „Kolejki zadań w serw isie interne towym Lanyrd.com ". • Gearman (http://gearman.org/) (na licencji BSD) to wieloplatform owy system przetw arza jący zadania. Bardzo przydaje się w przypadku integrow ania przetw arzania zadań za pom ocą różnych technologii. Pow iązania są dostępne dla języków Python, PHP, C++, Perl i wielu innych. • PyRes (https://github.com/binarydud/pyres) to oparty na systemie Redis uproszczony menedżer zadań przeznaczony dla języka Python. Zadania są dodawane do kolejek w systemie Redis, a konsumenty są konfigurow ane do przetwarzania tych zadań i przekazują opcjonalnie wyniki z pow rotem do now ej kolejki. Jeśli w ym agania są niew ielkie, a jedyny używ any język to Python, rozpoczęcie pracy z tym systemem jest bardzo ułatwione. • Am azon Sim ple Queue Service (http://aws.amazon.com/sqs/) to system przetw arzający za dania zintegrow any z usługami Am azon W eb Services. Konsumenty i producenty zadań mogą znajdować się w obrębie usług AW S lub m ogą być zewnętrzne. Dzięki temu system SQS jest prosty do uruchomienia i obsługuje ułatw ioną m igrację do chmury. Zapewniono wsparcie bibliotek dla wielu języków. C hoć klastry m ogą być też używ ane na potrzeby rozproszonego przetw arzania z w yko rzystaniem narzędzia numpy, w świecie języka Python jest to stosunkowo now e rozwiązanie. Firmy Enthought i Continuum oferują takie rozwiązania za pośrednictwem pakietów distarray (https://github.com/enthought/distarray) i blaze (https://github.com/ContinuumIO/blaze). Zauważ, że te pakiety próbują samodzielnie radzić sobie ze złożonym i problem ami dotyczącymi syn chronizacji i lokalizacji danych (nie istnieje jedno u niw ersalne rozw iązanie), d latego m iej św iadom ość tego, że jeśli z nich skorzystasz, praw dopodobnie będziesz m usiał zastanow ić się, w jaki sposób dane są rozmieszczone i udostępniane.
Podsumowanie Do tej pory zajm owaliśm y się w tej książce profilowaniem w celu zrozum ienia mniej wydaj nych części kodu, kompilowaniem i stosowaniem narzędzia numpy do przyspieszenia działania kodu, a także różnymi metodami wykorzystyw anym i w przypadku wielu procesów i kom puterów. W przedostatnim rozdziale przyjrzym y się sposobom zmniejszania w ykorzystania pamięci RAM za pomocą różnych struktur danych i metod probabilistycznych. M oże to uła twić utrzymanie wszystkich danych na jednym komputerze, co pozwoli uniknąć konieczności uruchamiania klastra.
Podsumowanie
| 269
270
j
Rozdział 10. Klastry i kolejki zadań
________________________________________ ROZDZIAŁ 11.
Mniejsze wykorzystanie pamięci RAM
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Dlaczego należy używ ać mniej pam ięci RAM? • Dlaczego narzędzia numpy i array lepiej nadają się do przechowywania zbiorów liczb? • W jaki sposób zbiory tekstowe mogą być efektywnie przechowywane w pamięci RAM? • Jak przy użyciu tylko jednego bajta możliwe jest obliczanie (w przybliżeniu!) do war tości 1e77? • Czym są filtry Blooma i dlaczego mogą być potrzebne?
O używanej pamięci RAM rzadko się myśli do momentu, aż jej zabraknie. Jeśli dojdzie do te go podczas skalowania kodu, może się to okazać nagłym elementem blokującym. M ożliwość umieszczenia w pam ięci RAM komputera większej ilości danych oznacza mniej komputerów do zarządzania, a ponadto zapewnia w ytyczne do planow ania zasobów w przypadku więk szych projektów . Znajom ość przyczyn zużycia pam ięci RAM oraz uw zględnienie bardziej efektywnych metod wykorzystania tego cennego zasobu ułatwi Ci radzenie sobie z problemami związanymi ze skalowaniem. Innym sposobem zaoszczędzenia pamięci RAM jest zastosow anie kontenerów, które korzy stają z w łaściw ości danych w celu przeprow adzenia kom presji. W rozdziale przyjrzym y się drzewom trie (uporządkowanym, drzewiastym strukturom danych), a także grafom podsłów DAW G (Directed Acyclic Word Graph), które um ożliw iają skompresowanie zbioru łańcuchów o wielkości 1,1 GB do zaledw ie 254 M B przy niewielkim spadku wydajności. Trzecia metoda polega na uzyskaniu miejsca do magazynowania w zam ian za dokładność. W odniesieniu do tego przeanalizujem y obliczanie przybliżone i przynależność do zbioru przybliżonego, które korzystają ze znacznie mniejszej ilości pamięci RAM niż ich dokładne odpowiedniki. Przy rozważaniu wykorzystania pam ięci RAM pojaw ia się pojęcie m asy danych. Im większa masa danych, tym wolniej są one przemieszczane. Jeśli m ożesz w m niejszym stopniu wyko rzystywać pamięć RAM, dane zostaną prawdopodobnie pobrane szybciej, ponieważ w krótszym czasie będą przemieszczane w magistralach, a ponadto więcej danych zmieści się w pamięciach podręcznych o ograniczonej pojem ności. Jeśli dane w ym agają przechow yw ania w magazynie
271
typu offline (np. na dysku tw ardym lub w zdalnym klastrze danych), będą dostarczane do kom putera znacznie dłużej. Spróbuj w ybrać odpow iednie struktury danych, aby w szystkie używane dane mogły zostać utrzym ane na jednym komputerze. Operacja obliczania ilości pamięci RAM używanej przez obiekty języka Python jest zaskaku jąco złożona. N iekoniecznie w iadom o, jak obiekt jest reprezentow any w tle. Jeśli zażądam y od systemu operacyjnego inform acji o liczbie używ anych bajtów , poda on całkow itą ilość pamięci przydzielonej procesowi. W obu przypadkach nie jest m ożliwe dokładne określenie, w jakim stopniu każdy obiekt języka Python zwiększa całkowite w ykorzystanie pamięci. Ponieważ niektóre obiekty i biblioteki nie zgłaszają swojej pełnej, wewnętrznej alokacji bajtów (lub opakowują biblioteki zewnętrzne, które w ogóle nie informują o swojej alokacji), konieczne jest jedynie szacow anie z w ysokim praw dopodobieństw em . M etody objaśnione w rozdziale ułatw iają podjęcie decyzji dotyczącej najlepszego sposobu reprezentow ania danych tak, aby zużywały mniej pam ięci RAM.
Obiekty typów podstawowych są kosztowne Często stosow ane są kontenery, takie jak obiekt l i s t , które przechow ują setki lub tysiące elementów. Gdy tylko przechowywana jest duża liczba elementów, problemem staje się wy korzystanie pamięci RAM. Obiekt l i s t zawierający 100 000 000 elementów zużywa w przybliżeniu 760 MB pamięci RAM, jeśli elem enty są tym samym obiektem. W przypadku magazynowania 100 000 000 różnych ele m entów (np. unikalnych liczb całkowitych) można spodziew ać się użycia gigabajtów pamięci RAM! Każdy unikalny obiekt to koszt w postaci zużycia pamięci. W przykładzie 11.1 w obiekcie l i s t przechow yw anych jest wiele liczb całkowitych 0. Jeśli zo stałoby zapisanych 100 000 000 odwołań do dowolnego obiektu (niezależnie od wielkości jednej instancji takiego obiektu), w dalszym ciągu należałoby oczekiw ać w ykorzystania pam ięci w ynoszącego mniej więcej 760 M B. W ynika to stąd, że obiekt l i s t przechowuje odwołania do obiektu (a nie ich kopie). W podrozdziale „Użycie narzędzia mem ory_profiler do diagnozo wania wykorzystania pam ięci" wyjaśniono, jak korzystać z narzędzia memory_profiler. W tym miejscu narzędzie to jest ładowane w powłoce IPython jako nowa funkcja „magiczna" za po mocą instrukcji %load_ext memory_profiler. Przykład 11.1. Pomiar wykorzystania pamięci przez 100 000 000 elementów takiej samej liczby całkowitej obiektu list In [ 1 ] : %load_ext memory_profiler # ładow an ie fu n kcji „ m agicznej" %memit In [ 2 ] : %memit [ 0 ] * i n t ( 1 e 8 ) peak memory: 7 9 0.64 MiB, increment: 762.9 1 MiB
Na p otrzeby następ n eg o przy kład u zo stan ie u ru ch om ion a now a p ow łoka. Jak pokazu ją w yniki pierw szego w yw ołania funkcji memit w przykład zie 11.2, now a pow łoka IPython zużyw a w przybliżeniu 20 M B pamięci RAM. M ożem y następnie utw orzyć tymczasową listę zaw ierającą 100 000 000 unikalnych liczb. W sumie pow oduje to w ykorzystanie mniej więcej 3,1 GB pamięci.
272
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Przykład 11.2. Pomiar wykorzystania pamięci przez 100 000 000 różnych liczb całkowitych obiektu list # z pow odu użycia now ej p o w łok i IPython na p oczątku p a m ięć nie je s t zajm ow ana In [ 1 ] : %load_ext memory_profiler In [ 2 ] : %memit # inform acja o bieżącym zużyciu p a m ięci RAM przez dany p ro c es peak memory: 20 .0 5 MiB, increment: 0 .0 3 MiB In [ 3 ] : %memit [n f o r n in x r a n g e ( i n t ( 1 e 8 ) ) ] peak memory: 312 7. 53 MiB, increment: 3 106.96 MiB In [ 4 ] : %memit peak memory: 236 4. 81 MiB, increment: 0 . 0 0 MiB
P a m ię ć m o ż e b y ć b u f o r o w a n a w d z ia ła ją c y m procesie, d lateg o z a w s z e b e z p ie cz n ie j sze je st z a m k n ię c ie i p o n o w n e u r u c h o m ie n ie p o w ło k i ję z y k a P y th o n p o d c z a s u ż y w a n i a f u n k c j i memi t d o p r o f i l o w a n i a .
Po zakończeniu działania polecenia memit następuje dealokacja listy tym czasow ej. O statnie w ywołanie funkcji memit zapewnia informację, że wykorzystanie pam ięci pozostaje w przy bliżeniu na poziom ie 2,3 GB. P rz e d d a lsz ą lek tu rą o d p o w ie d z s o b ie n a n a stę p u ją c e p ytan ia: „D la c z e g o p ro c e s k o d u P y th o n w d a ls z y m c ią g u m o ż e z a jm o w a ć 2,3 G B p a m ię c i R A M ? C o je sz cz e po zo stało , n a w e t p o m i m o t e g o , ż e o b i e k t l i s t t r a fi ł d o p r o c e s u c z y s z c z e n i a p a m i ę c i ? " .
100 000 000 obiektów całkow itoliczbow ych zajm uje w iększość z 2,3 GB pamięci, naw et po m im o tego, że nie są już używane. Interpreter języka Python buforuje obiekty typu podsta w owego (np. liczby całkowite) w celu ich późniejszego wykorzystania. W systemie o ograni czonej ilości pam ięci RAM m oże to spow odow ać problem y. Z tego pow odu należy być świadomym tego, że takie obiekty m ogą zajm ow ać coraz więcej pamięci podręcznej. W przykładzie 11.3 po raz kolejny użyto funkcji memit w celu utworzenia drugiej listy liczącej 100 000 000 elementów, która w przybliżeniu zajmuje 760 MB. W efekcie sumaryczna alokacja po tym wywołaniu funkcji zw iększyła swoją wielkość do około 3,1 GB. Sam kontener zajmuje 760 MB, ponieważ bazowe, całkowitoliczbowe obiekty języka Python już istnieją. Znajdują się one w pamięci podręcznej, dlatego zostaną ponow nie wykorzystane. Przykład 11.3. Pomiar wykorzystania pamięci ponownie przez 100 000 000 różnych liczb całkowitych obiektu list In [ 5 ] : %memit [n f o r n in x r a n g e ( i n t ( 1 e 8 ) ) ] peak memory: 312 7. 52 MiB, increment: 76 2.7 1 MiB
W dalszej części rozdziału dowiesz się, że moduł array może zostać użyty do przechowywania 100 000 000 liczb całkowitych przy znacznie mniejszym zużyciu pamięci.
Moduł array zużywa mniej pamięci do przechowywania wielu obiektów typu podstawowego M oduł array efektywnie przechow uje obiekty typów podstawowych, takie jak liczby całko w ite, liczby zm iennoprzecinkow e i znaki, lecz nie liczby zespolone lub klasy. Do przecho wywania bazow ych danych moduł tworzy ciągły blok pam ięci RAM.
Obiekty typów podstawowych są kosztowne
| 273
W przykładzie 11.4 dokonano alokacji 100 000 000 liczb całkowitych (każda zajmuje osiem bajtów ) w ciągłym obszarze pam ięci. Proces w ykorzystuje w sum ie w przybliżeniu 760 MB. Różnica w zajmowanej pamięci w przypadku tej metody i wcześniej zaprezentowanej metody z listą unikalnych liczb całkowitych w ynosi 2300 M B-760 M B = 1,5 GB. Oznacza to ogromne zmniejszenie zużycia pamięci RAM. Przykład 11.4. Tworzenie tablicy zawierającej 100 000 000 liczb całkowitych, która zajmuje 760 MB pamięci RAM In [ 1 ] : %load_ext memory_profiler In [ 2 ] : import arr ay In [ 3 ] : %memit a r r a y . a r r a y ( ' l ' , x r a n g e ( i n t ( 1 e 8 ) ) ) peak memory: 78 1.0 3 MiB, increment: 7 6 0.98 MiB In [ 4 ] : a r r = a r r a y . a r r a y ( ' l ' ) In [ 5 ] : a r r . i t e m s i z e O u t[5]: 8
Zauważ, że unikalne liczby w przypadku modułu array nie są obiektami języka Python, lecz bajtam i. Jeśli dla dowolnej z tych liczb dokonano by dereferencji, zostałby utw orzony now y obiekt języka Python typu int. Jeżeli zamierzasz przeprow adzić dla nich obliczenia, nie wy stąpią żadne ogólne oszczędności miejsca. Jeśli jednak przekażesz tablicę do procesu zewnętrz nego lub użyjesz tylko części danych, powinno być możliwe uzyskanie sporych oszczędności dotyczących wykorzystania pamięci RAM w porównaniu z zastosowaniem obiektu l i s t za w ierającego liczby całkowite.
^
Je ś li w p r z y p a d k u n a r z ę d z i a C y t h o n u ż y w a s z d u ż e j t a b l i c y l u b m a c i e r z y liczb , a p o n a d t o c h c e s z u n i k n ą ć z e w n ę t r z n e j z a l e ż n o ś c i o d n a r z ę d z i a numpy, w a r t o w i e d z i e ć , ż e istn ieje m o ż l i w o ś ć p r z e c h o w y w a n i a d a n y c h w m o d u l e a rr a y i p r z e k a z a n i a g o d o p rz e tw a rz a n ia n a rz ę d z iu C y th o n b e z p o w o d o w a n ia ż a d n e g o d o d a tk o w e g o o b cią ż e n ia p am ięci.
Moduł array obsługuje ograniczony zestaw typów danych o zmiennej precyzji (przykład 11.5). W ybierz najm niejszą w ym aganą precyzję, aby zostało przydzielone tylko tyle pamięci RAM, ile jest potrzebne, nie więcej. Bądź świadom tego, że wielkość podana w bajtach zależy od plat formy. Wymienione tutaj wielkości odnoszą się do platformy 32-bitowej (informuje ona o mini malnej wielkości), przykłady są natomiast wykonywane z wykorzystaniem 64-bitowego laptopa. Przykład 11.5. Typy podstawowe zapewniane przez moduł array In [ 5 ] : array? # „ m ag iczn a” fu n kcja p o w łok i IPython p o d o b n a d o help(array) Type: module S t r i n g Form: D ocstri n g: T his module defin es an o b j e c t type which can e f f i c i e n t l y re pre se nt an ar ray o f b a s i c v a l u e s : c h a r a c t e r s , i n t e g e r s , f l o a t i n g point numbers. Arrays are sequence types and behave very much l i k e l i s t s , except th a t the type o f o b j e c t s st or e d in them i s c o n st ra in e d . The type i s s p e c i f i e d a t o b j e c t c r e a t i o n time by using a type code, which i s a s i n g l e c h a r a c t e r . The fo llowin g type codes are defined: Type code C Type Minimum s i z e in bytes c' character 1 b' signed i n t e g e r 1 B' unsigned i n t e g e r 1 u' Unicode c h a r a c t e r 2 h' signed i n t e g e r 2
274
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
H' i' I' l' L' f' d' The c o n s t r u c t o r array(ty pe code
unsigned i n t e g e r signed i n t e g e r unsigned i n t e g e r sign ed i n t e g e r unsigned i n t e g e r f l o a t i n g point f l o a t i n g point is: [, i n i t i a l i z e r ] ) - -
2 2 2 4 4 4 8 c r e a t e a new arr ay
N arzędzie numpy korzysta z tablic, które m ogą przechow yw ać szerszą gam ę typów podsta wowych. Dysponujesz większą kontrolą nad liczbą bajtów przypadających na element, a po nadto możesz używać liczb zespolonych i obiektów datetime. Obiekt complex128 zajmuje 16 baj tów dla elementu: każdy element jest parą 8-bajtowych liczb zmiennoprzecinkowych. Choć obiekty compl ex nie mogą być przechowywane w tablicy języka Python, w przypadku narzędzia numpy są one dostępne bez żadnych wymagań. Aby przypom nieć sobie informacje dotyczące tego narzędzia, zajrzyj do rozdziału 6. W przykładzie 11.6 zaprezentow ano dodatkow ą funkcję tablic narzędzia numpy. Um ożliw ia ona tw orzenie zapytań dotyczących liczby elem entów , w ielkości każdego obiektu podsta w ow ego oraz łącznej w ielkości bazow ego bloku pam ięci RAM . Zauw aż, że nie obejm uje to m iejsca zajm ow anego przez obiekt języka Python (zw ykle jest to niew iele w porów naniu z wielkością danych przechow yw anych w tablicach). P r z y k ł a d 1 1 .6 . P r z e c h o w y w a n i e b a r d z i e j z ło ż o n y c h t y p ó w w t a b l i c y n a r z ę d z ia n u m p y In [ 1 ] : %load_ext memory_profiler In [ 2 ] : import numpy as np In [ 3 ] : %memit a r r = n p . z e r o s ( 1 e 8 , np.complex128) peak memory: 1552 .4 8 MiB, increment: 1525.75 Mi B In [ 4 ] : a r r . s i z e # to sam o co len(arr) O u t[ 4]: 100000000 In [ 5 ] : a r r .n b y t e s O u t[ 5]: 1600000000 In [ 6 ] : a r r . n b y t e s / a r r . s i z e # bajty p rzy p ad ające na elem ent O u t[ 6]: 16 In [ 7 ] : a r r . i t e m s i z e # inny s p osó b spraw dzania O u t[ 7]: 16
W porów naniu z obiektem array zastosow anie zw ykłego obiektu l i s t do przechow yw ania w ielu liczb jest znacznie m niej efektyw ne w odniesieniu do pam ięci RAM . Konieczna jest większa liczba alokacji pamięci. Każda taka operacja zajm uje czas. Dla większych obiektów m ają też m iejsce obliczenia. Takie obiekty są mniej przyjazne, jeśli chodzi o użycie pam ięci podręcznej, a ponadto ogólnie zużyw ana jest w iększa ilość pam ięci RAM , a tym sam ym mniej jest jej dostępnej dla innych programów. Jeśli jednak w jakikolw iek sposób przetw arzasz zaw artość obiektu array w kodzie Python, obiekty podstaw ow e zostaną praw dopodobnie przekształcone w obiekty tym czasow e, co spowoduje zaprzepaszczenie korzyści zapewnianych przez te obiekty. Użycie obiektów pod staw ow ych do przechow yw ania danych podczas kom unikacji z innym i procesam i stanow i znakomity przykład wykorzystania obiektu array. Jeśli wykonujesz dowolne obciążające operacje num eryczne, tablice narzędzia array są prawie na pew no lepszym w ariantem , poniew aż dzięki nim dostępnych będzie w ięcej opcji typów danych oraz w iele w ysp ecjalizow anych i szybkich funkcji. Jeśli w projekcie pożądana jest
Obiekty typów podstawowych są kosztowne
| 275
mniejsza liczba zależności, możesz zdecydow ać się na zrezygnow anie z narzędzia numpy, choć narzędzia Cython i Pythran sprawdzają się równie dobrze w przypadku tablic modułu array i narzędzia numpy. Narzędzie Numba obsługuje wyłącznie tablice narzędzia numpy. Jak się okaże w następnym podrozdziale, język Python zapewnia kilka innych narzędzi, które umożliwiają przeanalizowanie poziomu wykorzystania pamięci.
Analiza wykorzystania pamięci RAM w kolekcji Możesz się zastanawiać, czy m ożliw e jest zażądanie od interpretera języka Python informacji o pam ięci RAM używanej przez każdy obiekt. Dzięki wywołaniu funkcji sy s.g etsizeof(ob j) tego języka dowiesz się czegoś o pamięci w ykorzystywanej przez obiekt (pozwala na to więk szość obiektów, lecz nie wszystkie). Jeśli do tej pory nie korzystałeś z tej funkcji, bądź świadom tego, że metoda ta nie zapewni odpowiedzi, jakiej oczekiwano by dla kontenera! Zacznijmy od sprawdzenia niektórych typów podstawowych. Obiekt typu int języka Python to obiekt o zmiennej wielkości. Początkowo jest to zw ykła liczba całkowita, która przyjm uje postać długiej liczby całkow itej, jeśli obliczenia spow odują przekroczenie stałej sys.maxint (w przypadku 64-bitowego laptopa jednego z autorów jest to liczba 9 223 372 036 854 775 807). Zwykła liczba całkowita zajmuje 24 bajty (obiekt powoduje duże zużycie pamięci), długa liczba całkowita natomiast wymaga 36 bajtów: In [ 1 ] : O u tp ]: In [ 2 ] : O u t[2]: In [ 3 ] : In [ 4 ] : O u t[4]:
sys.getsizeo f ( i n t ( ) ) 24 sys.getsizeof(1) 24 n=sys.maxint+1 sys.getsizeof(n) 36
Takie sam o spraw dzenie m ożna p rzeprow ad zić dla łańcuchów bajtow ych. Pusty łańcuch zajm uje 37 bajtów , a każdy dodatkow y znak zw iększa zużycie pam ięci o 1 bajt: In [ 2 1 ] : O u t[ 21]: In [ 2 2 ] : O u t[ 22]: In [ 2 3 ] : O u t[ 23]: In [ 2 6 ] : O u t[ 26]:
sys.getsizeof(b "") 37 sys.getsizeof(b "a") 38 sys.getsizeof(b"ab") 39 sys.getsizeof(b "cde") 40
W przypadku korzystania z listy ma miejsce coś innego. Funkcja getsizeof nie oblicza wiel kości zawartości listy, ale jedynie wielkość samej listy. Pusta lista zajm uje 72 bajty, a każdy jej element wymaga kolejnych ośmiu bajtów w przypadku 64-bitowego laptopa: # zam iast In [ 3 6 ] : O u t[ 36]: In [ 3 7 ] : O u t[ 37]: In [ 3 8 ] : O u t[ 38]:
276
|
oczekiw anych skoków 24-bajtow ych m ają m iejsce 8-bajtow e sko k i w ielkości używanej p am ięci! sy s.g e tsizeo f([]) 72 sy s.g e tsizeo f([1]) 80 sy s.g e tsizeo f([1,2]) 88
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Jest to bardziej oczywiste, gdy używ ane są łańcuchy bajtowe. M ożna by oczekiwać znacznie większego zużycia pamięci, niż w skazuje funkcja getsizeof: In [ 4 0 ] : O u t[ 40]: In [ 4 1 ] : O u t[4l]: In [ 4 2 ] : O u t[ 42]:
sy s.g etsizeo f([b ""]) 80 s y s.g e tsiz e o f([b "a b c d e fg h ijk lm "]) 80 s y s . g e t s i z e o f ( [ b " a " , b "b " ] ) 88
Funkcja ta inform uje jedynie o części w ykorzystanej pam ięci, nierzadko tylko dla obiektu nadrzędnego. Jak w cześniej zaznaczono, funkcja nie jest zaw sze im plem entow ana, dlatego m oże m ieć ograniczoną przydatność. Trochę lepszym narzędziem jest funkcja asizeof (http://code.activestate.com/recipes/546530/), która przechodzi hierarchię kontenera, a następnie dokonuje najlepszego możliw ego przybliżenia w ielkości każdego napotkanego obiektu, dodając kolejne w ielkości do łącznej sum y. Trzeba wiedzieć, że jest to dość powolny proces. Oprócz tego, że funkcja polega na przypuszczeniach i założeniach, nie pozw ala także obli czyć wielkości pamięci przydzielonej w tle (np. moduł, który opakowuje bibliotekę języka C, m oże nie poinform ow ać o przydzielonych w niej bajtach). Najlepiej posługiw ać się tym jako wskazówką. Preferowane jest użycie funkcji memit, ponieważ zapewnia ona informacje o do kładnej wielkości wykorzystanej pamięci danego komputera. Funkcja asizeof jest stosowana w następujący sposób: In [ 1 ] : %run a s i z e o f . p y In [ 2 ] : a s i z e o f ( [ b " a b c d e f g h i j k l m " ] ) O u t[ 2]: 136
Przybliżony w ynik zwracany przez tę funkcję można spraw dzić dla dużej listy. W tym przy padku zostanie użyta lista zawierająca 10 000 000 liczb całkowitych: # uruchom ienie zajm uje 30 sekund! In [ 1 ] : a s i z e o f ( [ x f o r x in x r a n g e (1 0000000 )] ) O u t [ l ] : 321528064
# liczby całkow ite 1e7
Funkcja memit pozw ala spraw dzić popraw ność uzyskanego szacunkow ego w yniku w celu przekonania się, jak bardzo proces zw iększył zużycie pam ięci. W tym przypadku liczby są bardzo zbliżone: In [ 2 ] : %memit([x f o r x in x r an ge (1 0000000)] ) peak memory: 3 3 0 .6 4 MiB, increment: 310 .6 2 MiB
Choć przeważnie proces realizowany przez funkcję asizeof jest wolniejszy od przeprowadzanego przez funkcję memit, funkcja asizeof może okazać się przydatna podczas analizowania niewielkich obiektów. Funkcja memit będzie prawdopodobnie bardziej przydatna w przypadku prawdziwych aplikacji, ponieważ faktyczne wykorzystanie pamięci przez proces jest mierzone, a nie szacowane.
Bajty i obiekty Unicode Jednym z w ażnych pow odów przejścia na język Python w w ersji 3.3 lub now szej jest to, że przechow yw anie obiektów Unicode powoduje znacznie mniejsze wykorzystanie pamięci niż w przypadku języka Python 2.7. Jeśli obsługiw ane są głów nie zbiory łańcuchów , które zu żywają mnóstwo pamięci RAM, zdecydow anie rozważ korzystanie z języka Python w wersji 3.3 lub nowszej. Całkowicie za darmo zapewnisz sobie m niejsze zużycie pam ięci RAM.
Bajty i obiekty Unicode
| 277
W przykładzie 11.7 tworzona jest sekwencja złożona ze 100 000 000 znaków jako kolekcja baj tów (odpowiada to zwykłemu obiektowi str języka Python 2.7) oraz w postaci obiektu Unicode. W drugim w ariancie w ykorzystyw ane jest cztery razy więcej pamięci RAM . Każdy znak Uni code pow oduje takie samo większe obciążenie, niezależnie od liczby bajtów w ymaganych do reprezentowania bazow ych danych. Przykład 11.7. Obiekty Unicode wymagają wiele pamięci w przypadku języka Python 2.7 In [ 1 ] : %load_ext memory_profiler In [ 2 ] : %memit b"a" * i n t ( 1 e 8 ) peak memory: 100.9 8 MiB, increment: 80 .9 7 MiB In [ 3 ] : %memit u"a" * i n t ( 1 e 8 ) peak memory: 3 8 0.98 MiB, increment: 360.9 2 MiB
Kodow anie UTF-8 obiektu U nicode używ a jednego bajta na znak ASCII, a ponadto więcej bajtów w przypadku rzadziej spotykanych znaków . W języku Python 2.7 w ykorzystyw ana jest jednakow a liczba bajtów dla znaku Unicode, niezależnie od pow szechności znaku. Jeśli nie masz pewności w odniesieniu do kodowania Unicode w przypadku obiektów Unicode, obejrzyj prezentację Neta Batcheldera zatytułowaną Pragmatic Unicode, or, How Do I Stop the Pain? (Pragmatyczny Unicode albo jak pozbywam się bólu?) (http://nedbatchelder.com/text/unipain.html). Począwszy od wersji 3.3 języka Python, dzięki dokumentowi PEP 393 (https://www.python.org/ dev/peps/pep-0393/) dostępna jest elastyczna reprezentacja Unicode. Jej działanie polega na ob serwacji zakresu znaków w łańcuchu i w razie możliwości użyciu najmniejszej liczby bajtów do reprezentowania znaków niższego stopnia. Przykład 11.8 pokazuje, że jednakow e jest w ykorzystanie pamięci przez znak ASCII w przy padku w ersji bazującej na bajtach i obiektach Unicode. Ponadto zastosowanie znaku innego niż znak A SC II (sigma) pow oduje jedynie podw ojenie zużycia pam ięci — w dalszym ciągu jest to lepszy rezultat od uzyskanego dla języka Python 2.7. Przykład 11.8. Obiekty Unicode zajmują znacznie mniej pamięci w przypadku języka Python w wersji 3.3 lub nowszej Python 3 . 3 . 2 + ( d e f a u l t , Oct 9 2013 , 1 4 : 5 0 :0 9 ) IPython 1 . 2 . 0 - - An enhanced I n t e r a c t i v e Python. In [ 1 ] : %load_ext memory_profiler In [ 2 ] : %memit b"a" * i n t ( 1 e 8 ) peak memory: 91 .7 7 MiB, increment: 71 .4 1 MiB In [ 3 ] : %memit u"a" * i n t ( 1 e 8 ) peak memory: 9 1 . 5 4 MiB, increment: 7 0 . 9 8 MiB In [ 4 ] : %memit u" " * i n t ( 1 e 8 ) peak memory: 174.72 MiB, increment: 153.7 6 MiB
M ając na uwadze to, że obiekty Unicode są domyślnie stosowane w języku Python w wersji 3.3 lub nowszej, jeśli przetwarzasz zbiory danych łańcuchowych, praw ie na pewno skorzy stasz z aktualizacji do tych wersji języka. Brak magazynu łańcuchów w początkow ym okresie istnienia języka Python w w ersji 3.1 lub nowszej, który nie pow oduje dużego wykorzystania pamięci, był przeszkodą dla części osób. Jednakże obecnie dzięki dokumentowi PEP 393 nie stanowi to absolutnie żadnego problemu.
278
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Efektywne przechowywanie zbiorów tekstowych w pamięci RAM Typowym problemem związanym z tekstem jest to, że zajm uje on w iele pamięci RAM. Jeśli jednak ma zostać sprawdzone, czy wcześniej napotkano łańcuchy, lub ma zostać obliczona częstość ich występowania, w ygodne będzie umieszczenie ich w pamięci RAM, a nie stoso w anie stronicow ania z dysku lub na dysk. Choć zw ykłe przechow yw anie łańcuchów w iąże się z dużym wykorzystaniem pamięci, drzewa trie i grafy podsłów DAW G mogą być używa ne do kom presow ania reprezentacji łańcuchów przy dalszym um ożliw ianiu w ykonyw ania szybkich operacji. Takie bardziej zaawansowane algorytmy pozwalają zaoszczędzić znaczną ilość pamięci RAM. Oznacza to, że może nie być konieczne rozszerzanie konfiguracji sprzętowej o dodatkowe serwery. W przypadku systemów produkcyjnych oszczędności miejsca w pamięci mogą być ogromne. W tym punkcie zajmiemy się kompresowaniem za pom ocą drzewa trie zbioru łań cuchów o wielkości 1,1 GB do rozmiaru 254 M B. Spowoduje to tylko nieznaczny spadek wy dajności. Na potrzeby prezentow anego przykładu zostanie użyty zbiór tekstowy utworzony na pod stawie częściowego zrzutu z serwisu W ikipedia. Zbiór zawiera 8 545 076 unikalnych tokenów z porcji anglojęzycznego serw isu W ikipedia. Na dysku zbiór ten zajm uje 111 707 546 bajtów (111 MB). Tokeny są w yodrębniane z ich oryginalnych artykułów przy użyciu znaków spacji. Tokeny m ają zm ienną długość, a ponadto zaw ierają znaki U nicod e i liczby. M ają one n astępu jącą postać: faddishness 'm e l a n e s i a n s ' Kharalampos PizzaInACup™ url = " http:/ /en .wikipedia.org/wi ki?curid =363886" V IIIa), Superbagneres.
Pow yższa próbka tekstu posłuży do spraw dzenia, jak szybko m ożna zbudow ać strukturę danych, która przechow uje jedno wystąpienie każdego unikalnego słowa. Następnie dowiesz się, jak szybko można utw orzyć zapytanie dotyczące znanego słowa (zostanie użyte rzadkie słowo Zwiebel, czyli nazwisko malarza Alfreda Zwiebla). Pozwala to zadać następujące pyta nie: „Czy spotkałeś się w cześniej ze słow em Z w iebel?". W yszukiw anie tokenów to typow y problem, dlatego istotna jest możliw ość szybkiego w ykonania tej operacji.
^
Je śli p r ó b u je s z u ż y ć ta k ich k o n te n e r ó w w p r z y p a d k u r o z w ią z y w a n y c h p rz e z siebie p r o b l e m ó w , m i e j ś w i a d o m o ś ć te g o , ż e p r a w d o p o d o b n i e z a u w a ż y s z i n n e z a c h o w a n i a . K a ż d y k o n te n e r tw o rzy w łasn e struktury w e w n ętrz n e n a różn e sposoby. P rzek azy w a n ie ró ż n y ch ty p ó w to k en a p ra w d o p o d o b n ie b ęd z ie m ie ć w p ły w n a czas b u d o w a n ia struktury, a ró żn e dłu go ści to k en a w p ły n ą n a czas zap ytan ia. Z a w s z e p rz e p ro w a d z a j test w s p o s ó b m e to d y c z n y .
Efektywne przechowywanie zbiorów tekstowych w pamięci RAM
| 279
Zastosowanie metod dla 8 milionów tokenów Na rysunku 11.1 pokazano plik tekstow y zaw ierający 8 m ilionów tokenów (w ielkość nie przetworzonych danych wynosi 111 M B), który jest przechow yw any z wykorzystaniem kilku kontenerów om aw ianych w tym punkcie. Oś x prezentuje w ykorzystanie pam ięci RAM dla każdego kontenera. Oś y śledzi czas zapytania. W ielkość każdego punktu odnosi się do czasu, jaki zajm uje zbudow anie struktury (większy punkt oznacza dłuższy czas).
Działanie kontenerów w przypadku 8 545 076 tokenów Wielkość reprezentuje czas budowania - im mniejsza wielkość, tym lepiej 0,025
list b ite ct
•
“
Drzewo trie Marisa
Drzewo trie HAT - G r a f słów DAWG
•
•
Kt
0,000 0
200
400
600
800
1000
1200
Wielkość używanej pamięci RAM (MB) mniejsze zużycie oznacza lepszy wynik R y s u n e k 1 1 .1 . P o r ó w n a n ie g r a f u p o d s ł ó w D A W G i d r z e w tr ie z w b u d o w a n y m i k o n t e n e r a m i
Jak w idać na diagramie, dla przykładów użycia funkcji set i grafu DAW G zużywa się dużo pam ięci RAM . W przykładzie bazującym na obiekcie l i s t w ykorzystuje się w iele pam ięci RAM, a ponadto proces jest powolny. Przykłady użycia drzew trie M arisa i HAT okazują się najbardziej efektyw ne w przypadku rozpatryw anego zbioru danych. W ich przypadku zu żywana jest jedna czwarta pamięci RAM wykorzystywanej w innych metodach przy niewiel kiej zm ianie szybkości wyszukiwania. Na rysunku nie pokazano czasu wyszukiw ania dla prostego obiektu l i s t bez m etody sorto wania (zostanie w krótce zaprezentow ana), poniew aż zajm uje ona znacznie więcej czasu. Na w ykresie nie uw zględniono przykładu użycia narzędzia datrie, poniew aż zgłosiło ono błąd segmentacji (w przeszłości mieliśmy problem y z tym narzędziem przy okazji innych zadań). Gdy narzędzie działa, cechuje się szybkością i zwięzłością, ale może pow odow ać niewłaściwe czasy budowania, dla których trudno znaleźć uzasadnienie. W arto w ykorzystać to narzędzie, ponieważ może okazać się szybsze niż inne metody, ale oczyw iście w skazane będzie dokład nie przetestow anie tego dla używanych danych.
280
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Bądź św iadom tego, że konieczne jest spraw dzenie problem u dla różnych kontenerów , po nieważ z każdym związane są różne kompromisy, dotyczące m.in. czasu tworzenia i elastycz ności interfejsu API. Utworzym y proces w celu sprawdzenia działania każdego kontenera.
list Zacznijmy od najprostszej metody. Tokeny zostaną załadow ane do obiektu li s t , a następnie odpytane za pom ocą wyszukiwania liniowego O(n). N ie jest m ożliwe przeprow adzenie tego dla dużego przykładu, o którym już wspom niano, ponieważ operacja wyszukiw ania zajmuje zbyt wiele czasu. Z tego powodu zademonstrujemy metodę z wykorzystaniem znacznie mniej szego (zawierającego 499 048 tokenów). W każdym z zam ieszczonych dalej przykładów używ any jest generator text_example.readers, który w yodrębnia po jednym tokenie U nicode z pliku w ejściow ego naraz. Oznacza to, że proces odczytu zużywa jedynie niew ielką ilość pamięci RAM: t1 = t i m e . ti m e () words = [w f o r w in te xt _ex am ple .r e aders] p r i n t "Ładowanie {} słów ".f orm at(l en (w ord s)) t2 = t i m e . ti m e () p r i n t "Zużycie pamięci RAM po utworzeniu obiektu l i s t : format(m emory_profiler .memory_usage()[0], t2 - t1 )
{ : 0 . 1 f } MiB. Czas trwania:
{:0 .1 f}
s".
Interesuje nas to, jak szybko może zostać odpytany ten obiekt l i s t . W idealnej sytuacji pożą dane będzie znalezienie kontenera, który będzie przechow yw ać tekst, a ponadto umożliwi odpytyw anie i m odyfikow anie kontenera bez pow odow ania spadku w yd ajności. W celu w ysłania zapytania dotyczącego tekstu za pom ocą funkcji ti mei t kilka razy szukane jest znane słowo: a s s e r t u'Zw iebel' in words tim e_ cost = su m ( ti m e i t.re p e a t(st m t= "u 'Z w ie b e l ' in words", setup="from main import words", number=1, re p eat= 10000)) p r i n t "Łączny czas szukania słowa: { : 0 . 4 f } s " . f o r m a t ( t i m e _ c o s t )
Skrypt testowy informuje, że do przechowania jako listy oryginalnego pliku o wielkości 5 M B zostało w ykorzystane w przybliżeniu 59 M B, a ponadto że czas szukania wyniósł 86 sekund: Z a ję t a pamięć RAM przy uruchamianiu: 10.3 MiB Ładowanie 499048 słów Zużycie pamięci RAM po utworzeniu obiektu l i s t : Łączny czas szukania słowa: 86.1757 s
5 9 . 4 MiB. Czas trwania: 1.7 s
Przechowywanie tekstu przy użyciu obiektu l i s t bez sortowania to oczywiście kiepski pomysł. Czas wyszukiwania O(n) jest długi, duże jest też wykorzystanie pamięci. Jest to najgorszy ze wszystkich możliw ych wariantów! Czas wyszukiw ania można popraw ić przez sortowanie obiektu l i s t i zastosow anie szukania binarnego za pośrednictwem modułu bisect (https://docs.python.org/2/library/bisect.html). W przy padku przyszłych zapytań zapewnia to zauważalnie mniejsze ograniczenie. W przykładzie 11.9 mierzony jest czas operacji sortowania listy za pom ocą m etody sort. W tym przypadku użyto większego zbioru, który liczy 8 545 076 tokenów.
Efektywne przechowywanie zbiorów tekstowych w pamięci RAM
|
281
P r z y k ł a d 1 1 .9 . P o m i a r c z a s u d la o p e r a c j i s o r t o w a n i a w y k o n y w a n e j w c e lu p r z y g o t o w a n i a z b io r u d o u ż y c ia m o d u łu b is e c t t1 = t i m e . ti m e () words = [w f o r w in te xt _ex am ple .r e aders] p r i n t 11 Ładowanie {} słów ".f orm at(l en (w ord s)) t2 = t i m e . ti m e () p r i n t "Zużycie pamięci RAM po utworzeniu obiektu l i s t : { : 0 . 1 f } MiB. Czas trwania: format(m emory_profiler .memory_usage()[0], t2 - t1) p r i n t " L i s t a zawiera {} s łów ".f or m at(l en (w o rd s)) w o r d s .s o rt () t3 = t i m e . ti m e () p r i n t "Czas trwania sortowania l i s t y : { : 0 . 1 f } s " . f o r m a t ( t 3 - t2 )
{:0 .1 f}
s"
W dalszej kolejności przeprow adzane jest to samo w yszukiw anie co w cześniej, lecz z dodaną m etodą index, która korzysta z modułu bisect: import b i s e c t def ind ex(a, x ) : 'Znajdowanie położonej n a jb a r d z i e j na lewo wartości odpowiadającej dokładnie x' i = b i s e c t . b i s e c t _ l e f t ( a , x) i f i != l e n ( a ) and a [ i ] == x: re tu rn i r a i s e ValueError tim e_ cost = su m (ti m eit. re pe at( stm t= "i nd ex(w ord s, u ' Z w i e b e l ' ) " , setup="from main import words, index ", number=1, repeat=10 000 ))
W przykładzie 11.10 widać, że wykorzystanie pamięci RAM jest znacznie większe niż wcześniej, ponieważ ładowano dużo więcej danych. Operacja sortowania trwała dodatkowe 16 sekund, a łączny czas wyszukiwania wyniósł 0,02 sekundy. P r z y k ł a d 1 1 .1 0 . C z a s p o m i a r u w p r z y p a d k u u ż y c ia m o d u łu b is e c t d la s o r t o w a n e j l is t y $ python te x t_ e x a m p le _ lis t_ b is e c t.p y Z a ję t a pamięć RAM przy uruchamianiu: 10.3 MiB Ładowanie 8545076 słów Zużycie pamięci RAM po utworzeniu obiekt u l i s t : 932 .1 MiB. Czas trwania : 3 1 .0 s L i s t a zawiera 8545076 słów Czas trwania sortowania l i s t y : 16.9 s Łączny czas wyszukiwania słowa: 0.0 201 s
Dysponujemy obecnie rozsądną linią bazow ą do pomiaru czasu operacji wyszukiw ania łań cuchów. W ykorzystanie pamięci RAM m usi być m niejsze niż 932 MB, a łączny czas w yszu kiwania pow inien być lepszy niż 0,02 sekundy.
Funkcja set Użycie wbudowanej funkcji set m oże w ydać się najbardziej oczywistym sposobem na zreali zow anie zadania. W przykładzie 11.11 funkcja set przechow uje każdy łańcuch w strukturze z mieszaniem (jeśli musisz sobie przypomnieć to zagadnienie, zajrzyj do rozdziału 4.). Metoda ta pozw ala szybko spraw dzić przynależność, ale każdy łańcuch m usi być przechow yw any osobno, co oznacza duże w ykorzystanie pam ięci RAM. P r z y k ł a d 1 1 .1 1 . U ż y c ie f u n k c j i s e t d o p r z e c h o w y w a n i a d a n y c h words_set = se t ( t e x t _ e x a m p le .r e a d e r s )
282
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Jak widać w przykładzie 11.12, funkcja set zużywa więcej pamięci RAM niż obiekt l i st. Jednak że zapewnia bardzo krótki czas wyszukiwania bez wymogu stosowania dodatkowej funkcji index lub pośredniej operacji sortowania. P r z y k ł a d 1 1 .1 2 . U r u c h a m ia n ie s k r y p t u p r z y k ła d o w e g o z b io r u $ python text_exam p le_set.p y Z a ję t a pamięć RAM przy uruchamianiu: 10.3 MiB Zużycie pamięci RAM po utworzeniu zb io ru : 11 22.9 MiB. Czas trw ania : 3 1 . 6 s Zbiór zawiera 8545076 słów Łączny czas wyszukiwania słowa: 0.0 033 s
Jeśli wykorzystanie pamięci RAM nie ma kluczowego znaczenia, może to być najbardziej roz sądna metoda. Na tym etapie zostało jed nak utracone uporządkowanie oryginalnych danych. Jeśli nie jest to istotna kwestia, zauważ, że m ożesz przechow yw ać łańcuchy jako klucze w słowniku, gdzie każda w artość jest indeksem pow iązanym z uporządkow aniem z chwili pierw otnej operacji odczytu. Dzięki temu m ożliw e będzie spraw dzenie w słowniku, czy klucz jest obecny, a po nadto zażądanie jego indeksu.
Bardziej efektywne struktury drzew a W prowadźmy zbiór algorytmów, które do reprezentowania łańcuchów bardziej efektywnie używają pamięci RAM. Na rysunku 11.2 pochodzącym z serwisu Wikimedia Commons (http://commons.wikimedia.org/wiki/ Main_Page) p okazano różnice w reprezentacji czterech słów tap, taps, top i tops w przypadku użycia drzewa trie i grafu słów DAW G1. DAFSA to inna nazwa grafu słów DAW G. W przy padku obiektu l i s t lub funkcji set każde z tych słów byłoby przechow yw ane jako osobny łańcuch. Zarówno graf słów DAWG, jak i drzewo trie współużytkują części łańcuchów. Z tego powodu zużywane jest mniej pamięci RAM.
R y s u n e k 1 1 .2 . S t r u k t u r y d r z e w a t r ie i g r a f u s łó w D A W G ( il u s t r a c ja a u t o r s t w a C h k n o [ C C B Y - S A 3 .0 ])
1 P r z y k ł a d p o c h o d z i z a r t y k u ł u s e r w i s u W i k i p e d i a p o ś w i ę c o n e g o s t r u k t u r z e d a n y c h D A F S A (D e te rm in is tic A cy clic F in ite S tate A u to m a to n ) (h ttp ://en .w ik ip ed ia .o rg /w ik i/D eterm in istic_ a cy clic_ fin ite_ sta te_ au to m a to n ). D A F S A to i n n a n a z w a g r a fu słó w D A W G . D o łą c z o n a ilust ra cja p o c h o d z i z se r w i s u W i k i m e d i a C o m m o n s .
Efektywne przechowywanie zbiorów tekstowych w pamięci RAM
| 283
Podstaw ow ą różnicą m iędzy tym i strukturam i jest to, że drzew o trie w spółużytkuje tylko wspólne przedrostki, graf słów DAWG natomiast wspólne przedrostki i przyrostki. W językach z wieloma wspólnymi przedrostkami i przyrostkami słów (np. w angielskim) m oże to pozwo lić na wyeliminowanie wielu powtórzeń. To, jakie dokładnie będzie działanie pamięci, zależy od struktury danych. Zw ykle graf słów DAW G nie może przypisać w artości do klucza z pow odu w ielu ścieżek m iędzy początkiem i końcem łańcucha, ale zaprezentowana tutaj wersja grafu m oże zaakceptować odwzorowanie wartości. Drzewa trie również m ogą akceptować odwzorowanie w artości. Niektóre struktury muszą być tworzone na początku, a inne m ogą być w dowolnym m omencie aktualizowane. Dużą zaletą niektórych z tych struktur jest to, że zapewniają wyszukiwanie wspólnych przedrostków. Oznacza to, że możliwe jest zapytanie dotyczące wszystkich słów, które współużytkują podany przedrostek. Dla przykładowej listy liczącej cztery słowa wynikiem wyszukiwania przedrostku ta byłyby słowa tap i taps. Ponadto, ponieważ wykrywanie tych słów odbywa się z wykorzy staniem struktury grafu, takie wyniki są uzyskiwane bardzo szybko. Jeśli na przykład zajmu jesz się kodem DNA, kom presow anie m ilionów krótkich łańcuchów za pom ocą drzewa trie może być efektywnym sposobem zmniejszenia wykorzystania pam ięci RAM. W dalszych punktach przyjrzymy się bliżej grafom słów DAWG, drzewom trie oraz ich użyciu.
Graf słów DAWG (Directed Acyclic Word Graph) N arzędzie Directed Acyclic Word Graph (https://github.com/kmike/DAW G) (na licencji M IT) po dejm uje próbę efektyw nego reprezentow ania łańcuchów , które w spółużytkują przedrostki i przyrostki. W przykładzie 11.13 widoczna jest bardzo prosta konfiguracja dla grafu słów DAWG. W przy padku tej implementacji graf DAWG nie może być modyfikowany po utworzeniu. Graf wczy tuje jednokrotnie iterator, aby samemu się utworzyć. W przypadku konkretnego zastosowania brak aktualizacji po utworzeniu może być czynnikiem eliminującym takie rozwiązanie. Jeśli tak jest, może być konieczne rozważenie użycia drzewa trie. Graf DAWG obsługuje rozbudo w ane zapytania, w tym w yszukiw ania przedrostków . Um ożliw ia też zapew nienie trwałości i przechow yw anie indeksów całkow itoliczbow ych jako w artości w raz z w artościam i bajtów i rekordów . Przykład 11.13. Użycie grafu DAWG do przechowywania danych import dawg words_dawg = dawg.DAWG(text_example.readers)
Jak w idać w przykładzie 11.14, dla tego samego zbioru łańcuchów graf DAW G używa tylko trochę m niej pam ięci RAM , niż m iało to m iejsce w przedstaw ionym w cześniej przykładzie w ykorzystania funkcji set. Tekst w ejściow y o jeszcze większym stopniu podobieństw a spo woduje w iększą kompresję. Przykład 11.14. Uruchamianie skryptu przykładu z grafem słów DAWG $ python text_example_dawg.py Z a ję t a pamięć RAM przy uruchamianiu: 10.3 MiB Zużycie pamięci RAM po utworzeniu grafu DAWG: 96 8 .8 MiB. Czas trwania: 63.1 s Łączny czas wyszukiwania słowa: 0 . 0049 s
284
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Drzewo trie Marisa D r z e w o t r i e M a r i s a (h t t p s : / / g i t h u b . c o m / k m i k e / m a r i s a - t r i e ) ( p o d w ó j n a l i c e n c j a L G P L i B S D ) t o s t a ty c z n e d r z e w o tr ie u ż y w a ją c e p o w ią z a ń k o m p ila to r a C y t h o n d o b ib lio t e k i z e w n ę tr z n e j. Z p o w o d u s ta ty c z n o ś c i d r z e w o to n ie m o ż e b y ć m o d y fik o w a n e p o u tw o r z e n iu . P o d o b n ie d o g r a f u D A W G , d r z e w o t r ie o b s ł u g u je p r z e c h o w y w a n i e in d e k s ó w
c a ł k o w it o lic z b o w y c h ja k o
w a r t o ś c i, a t a k ż e w a r t o ś c i b a jt o w y c h i w a r t o ś c i r e k o r d u . K lu c z m o ż e b y ć u ż y w a n y d o w y s z u k iw a n ia w a r to ś c i i o d w r o tn ie . W s z y s tk ie k lu c z e w s p ó ł u ż y t k u ją c e t e n s a m p r z e d r o s t e k m o g ą b y ć e f e k t y w n i e z n a jd o w a n e . Z a w a r t o ś ć d r z e w a tr ie m o ż e b y ć u t r w a la n a . P r z y k ła d 1 1 .1 5 ilu s t r u je z a s t o s o w a n ie d r z e w a tr ie M a r is a d o p r z e c h o w y w a n ia p r z y k ła d o w y c h d a n y c h . P r z y k ł a d 1 1 .1 5 . U ż y c ie d r z e w a tr ie M a r i s a d o p r z e c h o w y w a n i a d a n y c h import m a ris a _ tr ie w o rd s_trie = m a ris a _ tr ie .T r ie (te x t_ e x a m p le .r e a d e r s ) W p r z y k ł a d z i e 1 1 .1 6 w id a ć , ż e w p o r ó w n a n i u z p r z y k ł a d e m z a s t o s o w a n ia g r a f u D A W G m a m i e js c e z n a c z n e z m n i e js z e n i e w y k o r z y s t a n ia p a m i ę c i R A M , a le o g ó ln y c z a s w y s z u k iw a n ia je s t tr o c h ę d łu ż s z y . P r z y k ł a d 1 1 .1 6 . U r u c h a m ia n ie s k r y p t u p r z y k ła d u z d r z e w e m tr ie M a r i s a $ python te x t_ e x a m p le _ trie .p y Z a ję ta pamięć RAM przy uruchamianiu: 1 1 .0 Mi B Zużycie pamięci RAM po utworzeniu drzewa t r i e : 3 0 4 .7 MiB. Czas trw an ia: 5 5 .3 s Drzewo t r i e zawiera 8545076 słów Łączny czas wyszukiwania słow a: 0 .0 1 0 1 s
Drzewo datrie S t r u k t u r a d r z e w a d a t r i e (h t t p s : / / g i t h u b . c o m / k m i k e / d a t r i e ) ( d o u b l e - a r r a y t r i e ; l i c e n c j a L G P L ) u ż y w a w s t ę p n ie w b u d o w a n e g o a lfa b e t u d o e f e k t y w n e g o p r z e c h o w y w a n ia k lu c z y . T o d r z e w o tr ie m o ż e b y ć m o d y f ik o w a n e p o u tw o r z e n iu , a le ty lk o z w y k o r z y s ta n ie m te g o s a m e g o a lfa b e tu . S t r u k tu r a ta m o ż e te ż z n a le ź ć w s z y s tk ie k lu c z e , k t ó r e w s p ó łu ż y t k u ją p r z e d r o s te k u d o s tę p n io n e g o k lu c z a , a p o n a d t o u m o ż liw ia u tr w a la n ie . W r a z z d r z e w e m tr ie H A T d r z e w o d a t r ie o f e r u je je d n e z n a jk r ó t s z y c h c z a s ó w w y s z u k iw a n ia .
^
W p r z y p a d k u z a s t o s o w a n ia d r z e w a d a t r ie d la p r z y k ł a d o w y c h d a n y c h z s e r w is u W ik i p e d i a , a t a k ż e d la r e a li z o w a n y c h p r z e z n a s w p r z e s z ł o ś c i d z i a ł a ń z w ią z a n y c h z r e p r e z e n ta c ja m i k o d u D N A , d r z e w o d a tr ie m ia ło n ie w ła ś c iw y c z a s tw o rz e n ia . W p o r ó w n a n iu z in n y m i s t r u k tu r a m i d a n y c h , k t ó r y c h b u d o w a n ie z a k o ń c z y ło s ię p o k ilk u s e k u n d a c h , d o w y g e n e r o w a n ia r e p r e z e n t a c ji k o d u D N A d r z e w o d a tr ie m o g ło w y m a g a ć w ie lu m in u t lu b g o d z in .
W p r z y p a d k u d r z e w a d a tr ie k o n ie c z n e je s t z a p r e z e n to w a n ie a lfa b e tu k o n s tr u k to r o w i. D o z w o lo n e s ą ty lk o k lu c z e , k t ó r e u ż y w a ją te g o a lfa b e t u . W p r z y p a d k u p r z y k ła d o w y c h d a n y c h z s e r w is u W i k i p e d i a o z n a c z a to , ż e d la n ie p r z e t w o r z o n y c h d a n y c h n ie z b ę d n e s ą d w a p r z e jś c i a . P r e z e n t u je to p r z y k ł a d 1 1 .1 7 . W p ie r w s z y m p r z e jś c iu t w o r z o n y je s t a l f a b e t z n a k ó w w p o s t a c i f u n k c ji s e t , a w d r u g im p r z e jś c i u b u d o w a n e je s t d r z e w o t r ie . W o ln ie js z y p r o c e s b u d o w a n ia p o z w a la u z y s k a ć k r ó ts z e c z a s y w y s z u k iw a n ia .
Efektywne przechowywanie zbiorów tekstowych w pamięci RAM
| 285
P r z y k ł a d 1 1 .1 7 . U ż y c ie s t r u k t u r y d a t r ie d o p r z e c h o w y w a n i a d a n y c h import d a tr ie ch ars = s e t ( ) f o r word in te x t_ e x a m p le .re a d e rs: chars.update(w ord) t r i e = d a tr ie .B a s e T r ie (c h a r s ) # wykorzystanie g en eratora w pierwszym p rzejściu (chars), który je s t niezbędny d o utworzenia now ego gen eratora read ers = text_example.read_words(text_example.SUMMARIZED_FILE) # nowy gen erator f o r word in re a d ers: trie[w o rd ] = 0
Niestety dla przykładowego zbioru danych struktura d a t r i e spowodowała błąd segmentacji, dlatego nie m ożem y zaprezentow ać inform acji o czasach. Zdecydow aliśm y się na uw zględ nienie tej struktury, ponieważ w innych testach była zwykle trochę szybsza (lecz mniej efek tywna pod w zględem w ykorzystania pam ięci RA M ) niż om ówiona dalej struktura drzewa t r i e HAT. Ponieważ drzewa t r i e HAT używaliśmy z powodzeniem przy przeszukiwaniu kodu DNA, jeśli zajmujesz się statycznym problemem, dla którego ta struktura działa, m ożesz być pewien, że struktura d a t r i e również zadziała. Jeśli jednak masz do czynienia z problemem, w któ rym występują zmienne dane wejściowe, struktura d a t r i e może nie być odpowiednią opcją.
Drzewo trie HAT (h t t p s : / / g i t h u b . c o m / k m i k e / h a t - t r i e ) (licencja MIT) używa reprezentacji przyjaznej dla pam ięci podręcznej, aby dla now oczesnych procesorów osiągać bardzo krótkie czasy wy szukiwania. Drzewo to może być m odyfikowane po utworzeniu, ale poza tym oferuje bardzo ograniczony interfejs API. D r z e w o tr ie H A T
Przy prostych zastosow aniach struktura zapew nia znakom itą w ydajność, ale ograniczenia interfejsu API (np. brak wyszukiwań przedrostków) m ogą sprawić, że w przypadku konkret nego zastosow ania może ona okazać się mniej przydatna. Przykład 11.18 demonstruje użycie drzewa t r i e HAT dla przykładowego zbioru danych. P r z y k ł a d 1 1 .1 8 . U ż y c ie d r z e w a t r ie H A T d o p r z e c h o w y w a n i a d a n y c h import h a t_ tr ie w o rd s_ trie = h a t _ t r i e .T r i e ( ) f o r word in te x t_ e x a m p le .re a d e rs: w ords_trie[w ord] = 0
Jak w idać w przykładzie 11.19, drzewo t r i e HAT oferuje najkrótsze czasy wyszukiw ania spo śród now ych struktur danych, a ponadto znakom ite w yniki dotyczące wykorzystania pam ię ci RAM. Ograniczenia interfejsu API drzewa t r i e HAT oznaczają, że korzystanie z niego jest ograniczone, ale jeśli niezbędne są jedynie krótkie czasy wyszukiwania dużej liczby łańcuchów, może to być odpowiednie rozwiązanie. P r z y k ł a d 1 1 .1 9 . U r u c h a m ia n ie s k r y p t u p r z y k ł a d u z d r z e w e m tr ie H A T $ python te x t_ e x a m p le _ h a ttrie .p y Z a ję ta pamięć RAM przy uruchamianiu: 9 .7 MiB Zużycie pamięci RAM po utworzeniu drzewa t r i e : 2 5 4 .2 MiB. Czas trw an ia: 4 4 .7 s Drzewo t r i e zawiera 8545076 słów Łączny czas wyszukiwania słow a: 0 .0 0 5 1 s
286
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Użycie drzew trie (i grafów słów DAWG) w systemach produkcyjnych Struktury danych drzewa trie i grafu słów DAW G oferują odpowiednie korzyści, ale w dal szym ciągu konieczne jest przeprow adzanie dla nich testów porów naw czych w przypadku rozpatryw anego problem u, poniew aż nie pow inny być stosow ane bez spraw dzenia. Jeśli w łańcuchach w ystępują nakładające się sekw encje, praw dopodobne jest, że zm niejszy się w ykorzystanie pam ięci RAM. Drzewa trie i grafy słów DAWG są mniej znane, ale w systemach produkcyjnych mogą za pewnić znaczne korzyści. W podrozdziale „Analiza serwisu społecznościowego o dużej skali w firmie Sm esh" z rozdziału 12. przedstawiono historię, która zakończyła się spektakularnym sukcesem. Jamie Matthews z firmy DapApps (producent oprogramowania bazującego na kodzie Python z siedzibą w Wielkiej Brytanii) również opisał zdarzenie dotyczące użycia drzew tries w systemach klienckich w celu zapewnienia klientom bardziej efektywnych i tańszych wdrożeń: W f ir m ie D a b A p p s c z ę s to p r ó b u je m y z a jm o w a ć s ię z ło ż o n y m i p r o b le m a m i te c h n ic z n y m i d o ty c z ą c y m i a r c h ite k tu r y , d z ie lą c je n a n ie w ie lk ie , n ie z a le ż n e k o m p o n e n ty , k t ó r e z w y k le k o m u n ik u ją s i ę z e s o b ą z a p o ś r e d n i c t w e m s i e c i o p a r t e j n a p r o t o k o l e H T T P . T a k i e r o z w i ą z a n i e ( o k r e ś la n e m ia n e m a r c h ite k tu r y „ z o rie n to w a n e j n a u s łu g i" lu b „ m ik r o u s łu g i" ) z a p e w n ia w s z e lk ie g o ro d z a ju k o r z y ś c i, w ty m m o ż liw o ś ć p o n o w n e g o z a s to s o w a n ia lu b w s p ó łu ż y tk o w a n ia fu n k c ji je d n e g o k o m p o n e n tu w w ie lu p ro je k ta c h . J e d n y m z z a d a ń c z ę s to w y m a g a n y c h w p r o je k t a c h o p r o g r a m o w a n ia k lie n c k ie g o , k t ó r e o b e jm u ją in t e r a k c ję z k o n s u m e n t e m , je s t g e o k o d o w a n ie k o d ó w p o c z to w y c h . Z a d a n ie to p o le g a n a p r z e k s z t a łc a n iu p e łn e g o k o d u p o c z to w e g o z o b s z a r u W ie lk ie j B r y ta n ii (n a p r z y k ła d B N 1 1 A G ) w p a r ę w s p ó łr z ę d n y c h o k r e ś la ją c y c h s z e r o k o ś ć i d łu g o ś ć g e o g r a fic z n ą . M a to n a c e lu u m o ż liw ie n ie a p lik a c ji p r z e p r o w a d z a n ia o b lic z e ń g e o p r z e s t r z e n n y c h , ta k ic h ja k p o m i a r o d le g ło ś c i. W s w o je j n a jp r o s ts z e j p o s t a c i b a z a d a n y c h g e o k o d o w a n ia to p r o s te o d w z o r o w a n ie ła ń c u c h ó w . Z n a c z e n i o w o b a z a t a k a m o ż e b y ć r e p r e z e n t o w a n a ja k o s ło w n ik . K l u c z a m i s ł o w n i k a s ą k o d y p o c z to w e p r z e c h o w y w a n e w p o s t a c i z n o r m a liz o w a n e j (B N 1 1 A G ), a w a r to ś c i to r e p r e z e n t a c ja w s p ó łr z ę d n y c h ( u ż y liś m y k o d o w a n i a g e o h a s h , a le d la u p r o s z c z e n ia w y o b r a ź s o b ie p a r ę r o z d z ie lo n ą p r z e c in k ie m , ta k ą ja k 5 0 ,8 2 2 9 2 1 , - 0 ,1 4 2 8 7 1 ) . W W ie lk ie j B r y ta n ii z n a jd u je s ię w p r z y b liż e n iu 1 ,7 m ilio n a k o d ó w p o c z to w y c h . Z w y k łe z a ła d o w a n ie w w y ż e j o p is a n y s p o s ó b p e łn e g o z b io r u d a n y c h d o s ło w n ik a o p a r te g o n a ję z y k u P y th o n s p o w o d u je z a ję c ie s e t e k m e g a b a jtó w p a m ię c i. U t r w a la n ie ta k ie j s t r u k tu r y d a n y c h n a d y s k u z a p o m o c ą w b u d o w a n e g o fo r m a t u s e r ia liz a c ji ję z y k a P y th o n w y m a g a n ie m o ż liw e g o d o p r z y ję c ia , p o k a ź n e g o m ie js c a d o p r z e c h o w y w a n ia . W ie d z ie liś m y , ż e m o ż e m y to z r o b ić le p ie j. P o e k s p e r y m e n to w a liś m y z k ilk o m a r ó ż n y m i fo r m a t a m i s e r ia liz a c ji o r a z z m e to d a m i p r z e c h o w y w a n i a n a d y s k u i w p a m ię c i, u w z g l ę d n i a ją c m a g a z y n o w a n ie d a n y c h n a z e w n ą t r z w ta k ic h b a z a c h d a n y c h ja k R e d is i L e v e lD B , a ta k ż e k o m p r e s o w a n ie p a r z ło ż o n y c h z k lu c z a i w a r to ś c i. O sta te c z n ie w p a d liś m y n a p o m y s ł u ż y c ia d r z e w a t r ie . T a k ie d r z e w a są n ie z w y k le e fe k ty w n e w p rz y p a d k u re p re z e n to w a n ia d u ż y c h lic z b ła ń c u c h ó w w p a m ię c i. P o n a d to d o s tę p n e b ib lio te k i o p e n so u rc e (z d e c y d o w a liś m y się n a m a risa -tr ie ) s p r a w ia ją , ż e k o r z y s ta n ie z d r z e w tr ie je s t b a r d z o ła tw e . U z y s k a n a a p lik a c ja , w ty m n ie w ie lk i i n t e r n e t o w y in t e r fe js A P I w b u d o w a n y w ś r o d o w is k o F la s k , d o r e p r e z e n t o w a n ia c a łe j b a z y d a n y c h k o d ó w p o c z to w y c h d la o b s z a r u W ie lk ie j B r y ta n ii w y k o rz y s tu je z a le d w ie 3 0 M B p a m ię c i, a p o n a d t o m o ż e w y d a jn ie o b s łu g iw a ć d u ż ą lic z b ę ż ą d a ń d o ty c z ą c y c h w y s z u k iw a n ia k o d ó w p o c z to w y c h . K o d je s t p ro s ty . U s łu g a c e c h u je s ię w y ją tk o w ą p r o s to tą o r a z b e z p r o b le m o w y m w d r a ż a n ie m i u r u c h a m ia n ie m n a d a r m o w e j p la tf o r m ie h o s tin g o w e j (n p . H e ro k u ) b e z ż a d n y c h z e w n ę trz n y c h w y m a g a ń lu b z a le ż n o śc i o d b a z d a n y ch . U ż y ta p rz e z n a s im p le m e n ta c ja to im p lem en ta c ja o p e n so u rce. D o stę p n a je st p o d a d re se m https://githu b.com /i4m ie/postcodeserver/. — Ja m ie M a tt h e w s D y r e k to r d s . te c h n ic z n y c h w f ir m ie D a b A p p s .c o m (W ie lk a B r y ta n ia )
Efektywne przechowywanie zbiorów tekstowych w pamięci RAM
| 287
Wskazówki dotyczące mniejszego wykorzystania pamięci RAM Ogólnie rzecz biorąc, jeśli m ożesz uniknąć umieszczania czegoś w pamięci RAM, postąp tak. Wszystko, co jest ładowane, powoduje zużycie dostępnej pamięci RAM. Można załadować tylko części danych (na przykład za pomocą modułu mmap (https://docs.python.org/2/library/mmap.html) obiektów pliku odwzorowywanej pamięci). Alternatywnie m ożesz skorzystać z generatorów, aby załadow ać tylko część danych, które są niezbędne do częściow ych obliczeń, zam iast od razu ładować je w całości. Jeśli zajmujesz się danymi num erycznym i, praw ie na pewno będzie wskazane zastosowanie tablic narzędzia numpy, które oferuje w iele szybkich algorytm ów korzystających bezpośrednio z bazow ych obiektów podstaw ow ych. W porów naniu z listam i liczb oszczędności pam ięci RAM m ogą być ogromne, a skrócenie czasu również m oże okazać się zdum iewające. Zauważysz, że w książce zamiast funkcji range przeważnie używana jest funkcja xrange. Wynika to (w przypadku języka Python 2.x) po prostu z tego, że funkcja xrange to generator, funkcja range natom iast buduje całą listę. Przesadą jest tworzenie listy zawierającej 100 000 000 liczb całkowitych tylko w celu wykonania odpowiedniej liczby iteracji. Zw iązane z tym wykorzy stanie pamięci RAM jest duże i zupełnie niepotrzebne. W języku Python 3.x funkcję range prze kształcono w generator, dlatego dokonywanie takiej zmiany nie jest już konieczne. Jeśli przetwarzasz łańcuchy i używasz języka Python 2.x, a ponadto chcesz zaoszczędzić pa mięć RAM, spróbuj pozostać przy funkcji str, a nie funkcji unicode. Prawdopodobnie zwykła aktualizacja do języka Python w w ersji 3.3 lub now szej zapew ni lepszy kom fort pracy, jeśli w program ie w ymaganych jest wiele obiektów Unicode. Jeżeli w statycznej strukturze prze chowujesz dużą liczbę obiektów Unicode, pewnie postanowisz sprawdzić struktury grafu słów DAWG i drzewa trie, które zostały wcześniej omówione. Jeśli m asz do czynienia z w ielom a łańcucham i bitow ym i, spraw dź narzędzie numpy i pakiet bitarray (https://pypi.python.org/pypi/bitarray/0.8.1). Oba zapew niają efektyw ne reprezentacje bitów w postaci bajtów . Korzyści m oże też przynieść przyjrzenie się systemowi Redis, który zapewnia wydajne przechow yw anie wzorców bitowych. W ramach projektu implementacji PyPy prowadzone są eksperymenty z bardziej efektywnymi reprezentacjam i hom ogenicznych struktur danych. Oznacza to, że długie listy tego samego typu podstaw ow ego (np. liczby całkow ite) m ogą pow odow ać znacznie m niejsze obciążenie w im plem entacji PyPy niż odpow iednie struktury w im plem entacji CPython. Projekt M icro Python (http://micropython.org/) będzie interesujący dla każdego, kto pracuje z systemami wbu dowanymi. Jest to zajmująca niewielką ilość pamięci implementacja języka Python, która ma być zgodna z językiem Python 3. Nie trzeba (prawie!) wspominać, że oczywista jest konieczność przeprow adzania testów po rów naw czych przy próbach optym alizacji w ykorzystania pam ięci RAM . Ponadto bardzo opłaci się zastosow anie pakietu testów jednostkow ych przed w prow adzeniem zm ian algo rytmicznych. Po dokonaniu przeglądu sposobów efektywnego kom presowania łańcuchów i przechow y wania liczb zajmiemy się zagadnieniem pośw ięcania dokładności na rzecz miejsca do prze chowywania.
288
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Probabilistyczne struktury danych Probabilistyczne struktury danych pozw alają na kom promisy, w przypadku których kosztem dokładności uzyskuje się ogromne zm niejszenie wykorzystania pamięci. Dodatkowo, w po rów naniu z funkcją s e t lub drzew em trie, w przypadku takich struktur znacznie bardziej ograniczona jest liczba operacji m ożliw ych do w ykonania. Na przykład dla pojedynczej struktury HyperLogLog++ używającej 2,56 kB możliwe jest określenie liczby unikalnych ele m entów do m aksym alnej liczby w ynoszącej w przybliżeniu 7 900 000 000 elem entów przy błędzie o wartości 1,625%. Oznacza to, że jeśli próbujem y określić liczbę unikalnych num erów tablic rejestracyjnych sa mochodów, a licznik struktury HyperLogLog++ podał liczbę tablic w ynoszącą 654 192 028, będziemy pewni, że rzeczywista liczba zawiera się w przedziale od 664 822 648 do 643 561 407. Co w ięcej, jeśli taka dokładność jest niew ystarczająca, m ożna po prostu dodać więcej pam ięci do struktury, która będzie w efekcie lepiej działać. Przydzielenie strukturze 40,96 kB zasobów spow oduje zmniejszenie błędu z 1,625% do 0,4%. Jednakże przechow yw anie takich danych z wykorzystaniem funkcji set będzie w ym agać 3,925 GB, i to naw et przy założeniu, że nie wystąpi nadm ierne obciążenie! Z kolei struktura HyperLogLog++ byłaby jedynie w stanie określić liczbę tablic rejestracyjnych w ich zbiorze i scalić go z innym zbiorem. Na przykład dla każdego stanu m oże istnieć jedna struktura. W tym przypadku można określić, ile unikalnych tablic rejestracyjnych ma poszczególne stany, a następnie scalić je wszystkie w celu uzyskania liczby dla całego kraju. Jeśli otrzymalibyśmy tablicę, nie bylibyśmy w stanie stwierdzić z bardzo dużą dokładnością, czy widzieliśmy ją wcze śniej, a ponadto nie moglibyśmy przekazać próbki już widzianych tablic rejestracyjnych. Probabilistyczne struktury danych są fantastyczne, gdy dysponuje się czasem potrzebnym na zrozum ienie problemu, a ponadto niezbędne jest wprow adzenie czegoś do produkcji, co m o że pozwolić udzielić odpowiedzi na bardzo niew ielki zbiór pytań dotyczących bardzo duże go zbioru danych. Z każdą odm ienną strukturą zw iązane są różne pytania, na które można odpow iedzieć z różną dokładnością. Z tego pow odu znajdow anie w łaściw ej odpow iedzi sprowadza się po prostu do zrozum ienia w łasnych wymagań. W praw ie każdym przypadku działanie probabilistycznych struktur danych polega na znaj dowaniu alternatywnej reprezentacji danych, która jest bardziej zwięzła i zawiera odpowiednie informacje pozwalające udzielić odpowiedzi na określone pytania. Może to być potraktowane jako typ stratnej kompresji, w której mogą zostać utracone niektóre aspekty danych, ale zacho wywane są niezbędne elementy. Ponieważ dopuszczamy utratę danych, które niekoniecznie są istotne w przypadku określonego zbioru interesujących nas pytań, tego rodzaju stratna kom presja może być znacznie bardziej efektywna niż kompresja bezstratna, o której była mowa w cześniej, podczas prezentowania drzew trie. W ynika to stąd, że dość duże znaczenie ma to, jaka probabilistyczna struktura danych zostanie użyta. Pożądane jest wybranie takiej struktury, która w konkretnym przypadku użycia spowoduje zachowanie właściwych informacji! Zanim bliżej zajm iem y się tym zagadnieniem , należy zw rócić uwagę n a to, że w szystkie „współczynniki błędów " są tutaj definiowane w kategoriach odchyleń standardowych. Jest to pojęcie związane z opisywaniem rozkładów Gaussa, które określa, jak bardzo funkcja jest roz łożona względem wartości środkowej. Gdy odchylenie standardowe zwiększa się, rośnie liczba wartości oddalonych od punktu centralnego. W przypadku probabilistycznych struktur danych w spółczynniki błędów są formułowane w ten sposób, ponieważ wszystkie zw iązane z nim i analizy mają charakter probabilistyczny. Oznacza to, że gdy na przykład mówimy, że algorytm
Probabilistyczne struktury danych
| 289
H yperLogLog++ ma błąd o w artości
b łą d
1,04
= —= ■ , przez 66% czasu błąd będzie m niejszy od
w artości b ł ą d , przez 95% czasu będzie m niejszy do w artości dzie m niejszy niż w artość 3 * b ł ą d 2.
2 * b łą d ,
a przez 99,7% czasu bę
Obliczenia o bardzo dużym stopniu przybliżenia z wykorzystaniem jednobajtowego licznika Morrisa Zaprezentujemy zagadnienie obliczeń probabilistycznych przeprowadzanych za pom ocą jed nego z najstarszych liczników probabilistycznych, czyli licznika M orrisa (stworzonego przez Roberta M orrisa związanego z Agencją Bezpieczeństwa Narodowego i firm ą Bell Labs). Za stosowania obejmują liczenie milionów obiektów w środowisku z ograniczoną w ielkością pamięci RAM (np. w przypadku wbudowanego komputera), analizowanie dużych strumieni danych oraz rozwiązywanie problem ów związanych ze sztuczną inteligencją, takich jak roz poznaw anie obrazów i mowy. Licznik M orrisa m onitoruje w ykładnik i m odeluje obliczany stan jako 2 wyMadmk (zam iast po dawania poprawnej liczby). Licznik zapewnia przybliżenie r z ę d u w i e l k o ś c i , które jest aktuali zowane za pom ocą reguły probabilistycznej. Na początku w ykładnik jest ustaw iony na zero. Po zażądaniu w a r t o ś c i licznika uzyskam y pow(2,exponent)=1 (czujny czytelnik zauw aży, że m a tu m iejsce przekroczen ie o jed en — w spomnieliśmy w cześniej, że jest to licznik p r z y b l i ż e n i a !). Jeśli zażądamy, aby licznik dokonał inkrementacji samego siebie, w ygeneruje on liczbę losową (używając rozkładu jednostajnego) i spraw dzi w arunek random(0, 1) <= 1/pow(2,exponent), który zaw sze będzie praw dziw y (pow(2,0) == 1). L icznik je st inkrem entow any, a w ykład nik ustaw iany na 1. Gdy drugi raz zażądamy, aby licznik dokonał inkrementacji samego siebie, sprawdzi warunek random(0, 1) <= 1/pow(2,1). Będzie on prawdziwy w przypadku 50% czasu. Jeśli test pow ie dzie się, w ykładnik jest inkrementowany. W przeciwnym razie nie ma miejsca inkrementacja licznika dla tego żądania inkrementacji. W tabeli 11.1 przedstaw iono praw dopodobieństw o inkrem entacji w ystępującej dla każdego z pierw szych w ykładników . T a b e la 1 1 .1 . S z c z e g ó ł y l i c z n ik a M o r r i s a exponent (wykładnik)
pow(2,exponent)
P(increment)
0
1
1
1
2
0,5
2
4
0,25
3
8
0,125
4
16
0,0625
254
2,894802e+ 76
3,454467e-77
2 P o d a n e w a rto ści w y n ik a ją z re g u ły 6 6 -95-99 z w ią z a n ej z ro z k ła d a m i G au ssa. W ięcej in fo rm a c ji m o ż n a zn a le ź ć w a rty k u le se rw isu W ik ip e d ia d o stę p n y m p o d n a s tę p u ją c y m a d resem : h ttp ://p l.w ik ip e d ia .o r g /w ik i/O d ch y len ie_ s ta n d a rd o w e - D la _ r o z k .C 5 .8 2 a d u _ n o r m a ln e g o .
290
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
M aksymalna liczba możliwa do obliczenia w przybliżeniu, gdy dla wykładnika używany jest pojedynczy bajt bez znaku, w ynosi math.pow(2,255) == 5e76. Błąd pow iązany z rzeczyw istą liczbą będ zie dość duży, p o n iew aż zw iększają się liczby. Jed n akże uzyska się ogrom ne oszczędności pam ięci RAM, gdyż zamiast 32 bajtów bez znaku, które musiałyby zostać użyte w przeciwnym razie, zostanie zastosowany tylko 1 bajt. Przykład 11.20 prezentuje prostą im plem entację licznika M orrisa. P r z y k ł a d 1 1 .2 0 . P r o s t a im p l e m e n t a c j a lic z n ik a M o r r i s a from random import random c la s s M o rris C o u n te r(o b je ct): cou n ter = 0 def a d d (s e lf , * a r g s ) : i f random() < 1 .0 / (2 * * s e l f .c o u n t e r ) : s e l f .c o u n t e r += 1 def len (s e lf): re tu rn 2 * * s e lf .c o u n t e r
W przykładzie 11.21 widać, że gdy używa się tej przykładowej implementacji, pierw sze żą danie inkrementacji licznika kończy się pom yślnie, a drugie już nie3. P r z y k ł a d 1 1 .2 1 . P r z y k ł a d b ib l i o t e k i li c z n i k ó w M o r r i s a In In 1 .0 In In 2 .0 In In 2 .0
[ 2 ] : mc = M orrisC ounter() [ 3 ] : p rin t len (mc) [ 4 ] : m c.add() # praw dopodobieństw o P(1) w ykonania dodaw ania [ 5 ] : p rin t len (mc) [ 6 ] : m c.add() # praw dopodobieństw o P(0,5) w ykonania dodaw ania [ 7 ] : p rin t len(m c) # dodaw anie nie występuje w tej p r ó b ie
Czarna, gruba linia na rysunku 11.3 wskazuje standardowe inkrementowanie liczb całkowitych w każdej iteracji. W przypadku kom putera 64-bitow ego jest to 8-bajtow a liczba całkow ita. Ewolucję trzech 1-bajtowych liczników M orrisa zaprezentowano za pom ocą linii przerywa nych. Oś y pokazuje ich w artości, które w przybliżeniu reprezentują praw dziw ą liczbę dla każdej iteracji. Trzy liczniki mają na celu zobrazowanie ich różnych trajektorii i ogólnego trendu. Liczniki te są od siebie całkowicie niezależne. Powyższy diagram pozwala zorientow ać się w tym, jakiego błędu należy oczekiwać w przy padku korzystania z licznika Morrisa. Dodatkowe szczegóły dotyczące błędów są dostępne pod adresem h t t p : / / g e o m b l o g . b l o g s p o t . c o .u k / 2 0 1 1 / 0 6 / b o b - m o r r i s - a n d - s t r e a m - a l g o r i t h m s .h t m l .
Wartości k-minimum W przypadku licznika M orrisa tracone są w szelkiego rodzaju informacje o wstaw ianych ele mentach. Oznacza to, że stan w ewnętrzny licznika jest taki sam, niezależnie od tego, czy zo stanie zastosowana instrukcja .add("micha"), czy .add("ian"). Takie dodatkow e informacje są przydatne, a jeśli zostaną w łaściw ie użyte, m ogą ułatw ić spraw ienie, że liczniki będą liczyć w yłącznie unikalne elem enty. Dzięki temu w yw ołanie instrukcji .add("micha") tysiące razy spow odow ałoby tylko jednorazow e zw iększenie licznika. 3 B a r d z ie j k o m p le tn a im p le m e n ta c ja k o r z y s ta ją c a z o b ie k tu a rr a y b a jtó w d o tw o rz e n ia w ie lu lic z n ik ó w je s t d o s tę p n a p o d a d r e s e m h ttp s://g ith u b .c o m /ia n o z sv a ld /m o r r is_ c o u n ter .
Probabilistyczne struktury danych
|
291
Działanie liczników Morrisa 140 000
120000
Licznik liczb całkowitych (z 4 bajty) - - Licznik Morrisa nr 0(1 bajt) - * Licznik Morrisa nr 1 (1 bajt) ■ * Licznik Morrisa nr 2 (1 bajt)
100000
o o
20000
40000
60000
80000
100 000
Iteracja R y s u n e k 1 1 .3 . T r z y 1 - b a jt o w e l i c z n i k i M o r r i s a w z e s t a w i e n i u z 8 - b a jt o w ą lic z b ą c a łk o w it ą
Aby zaim plem entow ać taki sposób działania, w ykorzystam y w łaściw ości funkcji m ieszania (w punkcie „Funkcje mieszania i entropia" bardziej szczegółowo omówiono funkcje mieszania). Podstawowa właściwość, jaką chcemy wykorzystać, określa, że funkcja mieszania pobiera dane wejściowe i dokonuje ich j e d n o l i t e g o rozkładu. Dla przykładu załóżmy, że istnieje funkcja mie szania, która pobiera łańcuch i zwraca liczbę z przedziału od 0 do 1. Jednolitość w przypadku tej funkcji oznacza, że po przekazaniu jej łańcucha rów nie praw dopodobne jest uzyskanie w artości 0,5, jak i w artości 0,2 lub dowolnej innej. W skazuje to również na to, że jeśli funkcji zostanie przekazanych wiele wartości łańcuchowych, można oczekiwać, że zostaną one sto sunkow o rów nom iernie rozłożone. Pam iętaj, że jest to argum ent probabilistyczny: w artości nie zaw sze będą równomiernie rozłożone, ale jeśli dla wielu łańcuchów zostanie podjęta próba wielokrotnego przeprowadzenia takiego eksperymentu, wartości zostaną raczej równomiernie rozłożone. Załóżm y, że pobrano 100 elem entów i zapisano ich w artości m ieszania (są to liczby z prze działu od 0 do 1). Równomierne rozłożenie oznacza, że zam iast stwierdzić „Istnieje 100 ele m entów ", można powiedzieć: „Dla każdego elementu występuje odległość równa 0,01". Tu po jawia się wreszcie algorytm K-Minimum Values4. Jeśli zachowamy k najmniejszych unikalnych w artości m ieszania, które w ystąpiły, m ożemy dokonać aproksymacji ogólnego rozłożenia dla w artości m ieszania, a także wywnioskować, jaka jest całkowita liczba elementów. Na rysun ku 11.4 zaprezentowano stan struktury K-M inimum Values (stosowany jest też skrót KMV), gdy dodawanych jest coraz więcej elementów. Ponieważ początkowo nie istnieje wiele wartości
4 K . B e y er, P . J. H a a s, B. R e in w a td , Y . S ism a n is i R . G e m u lla , d o k u m e n t O n sy n o p ses f o r d istin ct-v a lu e estim ation u n d er m u ltiset o p era tio n s, P ro c e e d in g s o f th e 2 0 0 7 A C M S IG M O D In te rn a tio n a l C o n fe re n ce o n M a n a g e m e n t o f D a ta - S IG M O D '0 7 , 2 0 0 7 r.: 1 9 9 -2 1 0 , d o i:1 0 .1 1 4 5 / 1 2 4 7 4 8 0 .1 2 4 7 5 0 4 .
292
I
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
R y s u n e k 1 1 .4 . M a g a z y n y w a r t o ś c i w s t r u k t u r z e K M V w p r z y p a d k u d o d a w a n i a k o l e j n y c h e le m e n t ó w
mieszania, największa z nich, która została zachowana, jest dość duża. Wraz z dodawaniem kolej nych elementów największa wartość spośród k zachowanych wartości mieszania będzie stawać się coraz mniejsza. Używając tej metody, możemy uzyskać współczynniki błędu z czasem wynoszącym ( i— ^ O %( k -
2
)
Im w iększe k , tym bardziej praw dopodobne będzie to, że używana funkcja mieszania nie za pewni całkowitej jednolitości w przypadku konkretnych danych w ejściow ych i niefortunnych w artości m ieszania. Przykładem takich w artości byłoby w ykonanie operacji m ieszania dla zbioru ['A ', 'B ', 'C'] i uzyskanie w artości [0.01, 0.02, 0.03]. W raz z rozpoczęciem m iesza nia coraz większej liczby w artości, coraz mniej praw dopodobne będzie, że będą one tworzyć zw arte grupy. Co więcej, ponieważ zachowywane są tylko najmniejsze u n i k a l n e w artości mieszania, struktura danych uwzględnia jedynie unikalne dane wejściowe. Z łatwością można to stwierdzić, gdyż jeśli w ystępuje stan, w którym przechow yw ane są tylko trzy najm niejsze wartości mieszania, a obecnie jest to zbiór [0 .1 , 0 .2 , 0 .3 ], po dodaniu elementu z w artością m ieszania 0.4 stan ten nie zm ieni się. Jeśli zostanie dodanych więcej elem entów z w artością m ieszania 0.3, stan również nie ulegnie zmianie. Właściwość taka jest określana mianem i d e m p o t e n t n o ś c i . Oznacza to, że jeśli w iele razy dla tej struktury w ykonyw ana jest taka sama operacja z użyciem iden tycznych danych w ejściow ych, stan nie będzie się zmieniać. Zupełnie inaczej jest na przykład przy operacji append w przypadku obiektu l i s t , która zaw sze zm ieni sw oją w artość. Pojęcie id em potentności dotyczy w szystkich struktu r danych om ów ionych w tym podrozdziale z w yjątkiem licznika Morrisa.
Probabilistyczne struktury danych
| 293
P r z y k ł a d 1 1 .2 2 p r e z e n t u j e b a r d z o p r o s t ą i m p l e m e n t a c ję s t r u k t u r y K - M i n i m u m V a l u e s . G o d n e u w a g i je s t u ż y c i e m o d u łu s o r t e d s e t, k t ó r y p o d o b n ie d o f u n k c ji s e t , m o ż e z a w i e r a ć t y lk o u n i k a ln e e le m e n ty . T a k a u n ik a ln o ś ć b e z ż a d n y c h k o s z tó w z a p e w n ia id e m p o te n tn o ś ć s tr u k tu r z e K M inV alues. A b y s i ę o t y m p r z e k o n a ć , p r z e a n a l i z u j k o d : g d y t e n s a m e l e m e n t z o s t a n i e d o d a n y w ię c e j n iż r a z , w ł a ś c i w o ś ć d a ta n ie z m ie n i s ię . P r z y k ł a d 1 1 .2 2 . P r o s t a i m p le m e n t a c ja s t r u k t u r y K M i n V a lu e s import mmh3 from b l i s t import so rte d s e t c la s s K M in V alues(object): d ef in it ( s e l f , num_hashes): self.num _hashes = num_hashes s e l f .d a t a = s o r t e d s e t( ) def a d d (s e lf , ite m ): item_hash = mmh3.hash(item) se lf.d a ta .a d d (ite m _ h a sh ) i f l e n ( s e l f .d a t a ) > self.n u m _h ash es: s e lf.d a t a .p o p () def _ _ len _ _ ( s e l f ) : i f l e n ( s e l f .d a t a ) <= 2: re tu rn 0 re tu rn (self.n um _h ash es - 1) * (2 * * 3 2 -1 ) / f l o a t ( s e l f . d a t a [ - 2 ] + 2**31 - 1) U ż y w a ją c i m p l e m e n t a c ji s t r u k t u r y KM inValues w p a k i e c i e countmemaybe (h t t p s : / / g i t h u b . c o m / m y n a m e ^ń s f i b e r / c o u n t m e m a y b e ) j ę z y k a P y t h o n ( p r z y k ł a d 1 1 . 2 3 ) , m o ż e m y z a c z ą ć z a u w a ż a ć p r z y d a t n o ś ć t a k i e j s t r u k t u r y d a n y c h . I m p l e m e n t a c ja t a b a r d z o p r z y p o m i n a u ż y t ą w p r z y k ł a d z i e 1 1 . 2 2 , a l e w p e ł n i w y k o r z y s t u je in n e o p e r a c je n a z b io r a c h , t a k ie ja k s u m a i ilo c z y n . Z a u w a ż te ż , ż e p o ję c i a „ w i e l k o ś c i " i „ l i c z n o ś c i " s ą s t o s o w a n e w y m i e n n i e ( t e r m i n „ l i c z n o ś ć " w y w o d z i s i ę z t e o r i i z b io r ó w i u ż y w a n y je s t ra c z e j w a n a liz ie p r o b a b ilis ty c z n y c h s tr u k tu r d a n y c h ). W ty m p r z y p a d k u w i d o c z n e je s t , ż e n a w e t d la s t o s u n k o w o n ie w ie lk ie j w a r t o ś c i k m o ż liw e je s t p r z e c h o w a n ie 5 0 0 0 0 e le m e n tó w i o b lic z e n ie lic z n o ś c i w ie lu o p e r a c ji n a z b io r a c h z r e la t y w n ie m a ły m b łę d e m . P r z y k ł a d 1 1 .2 3 . I m p l e m e n t a c j a s t r u k t u r y K m i n V a l u e s z p a k i e t e m c o u n t m e m a y b e >>> >>> >>> >>>
from countmemaybe import KMinValues kmv1 = KMinValues(k=1024) kmv2 = KMinValues(k=1024) f o r i in x r a n g e (0 ,5 0 0 0 0 ): # O k m v 1 .a d d (str(i))
>>> f o r i in x ra n g e (2 5 0 0 0 , 7 5 0 0 0 ): # 0 k m v 2 .a d d (str(i)) >>> p rin t len(kmv1) 50416 >>> p rin t len(kmv2) 52439 >>> p rin t k m v 1 .ca rd in a lity _ in te rsectio n (k m v 2 ) 25900.2862992 >>> p rin t km v1.cardinality_union(km v2) 75346.2874158
O
1 5 0 0 0 0 e l e m e n t ó w u m i e s z c z a n y c h j e s t w z b i o r z e kmv1.
0
Z b i ó r kmv2 r ó w n i e ż u z y s k u j e 5 0 0 0 0 e l e m e n t ó w , z k t ó r y c h 2 5 0 0 0 j e s t t a k i c h s a m y c h j a k w z b i o r z e kmv1.
294
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
p r z y p a d k u te g o r o d z a ju a lg o r y tm ó w w y b ó r fu n k c ji m ie s z a n ia m o ż e m ie ć z n a c z n y w p ły w n a ja k o ś ć p r z y b liż e ń . W o b u p r z e d s ta w io n y c h im p le m e n ta c ja c h u ż y to m o d u łu mmh3, c z y li im p le m e n ta c ji ję z y k a P y th o n m o d u łu mumurhash3, k t ó r y o fe r u je c ie k a w e w ła ś c iw o ś c i n a p o trz e b y m ie s z a n ia ła ń c u ch ó w . M o ż liw e je s t je d n a k z a s to s o w a n ie in n y c h fu n k c ji m ie s z a n ia , je ś li o k a ż ą się b a rd z ie j w y g o d n e d la k o n k r e tn e g o z b io r u d a n y c h .
Filtry Blooma Czasem niezbędna jest możliwość wykonania innych typów operacji na zbiorach, które wyma gają wprow adzenia now ych typów probabilistycznych struktur danych. Filtry Blooma5 zostały stworzone jako odpowiedź na pytanie dotyczące tego, czy dany element wcześniej wystąpił. Działanie filtrów Blooma polega na użyciu wielu wartości m ieszania w celu reprezentowania w artości jako w ielu liczb całkow itych. Jeśli później zostanie coś zauw ażone z tym samym zbiorem liczb całkowitych, słusznie można być pewnym tego, że jest to ta sama wartość. Aby to zrealizować w sposób pozwalający na efektywne wykorzystanie dostępnych zasobów, liczby całkow ite są niejaw nie kodow ane jako indeksy listy. M ożna to potraktow ać jako listę wartości typu bool, które początkow o są ustaw iane na w artość False. Jeśli zostanie zażądane dodanie obiektu z w artościam i m ieszania [10, 4, 7], w artość True zostanie ustaw iona dla dziesiątego, czwartego i siódmego indeksu listy. Jeśli w przyszłości pojawi się pytanie doty czące tego, czy wcześniej napotkano konkretny element, po prostu zostaną znalezione jego wartości mieszania, a ponadto zostanie sprawdzone, czy dla wszystkich odpowiednich miejsc na liście wartości typu bool ustawiono wartość True. M etoda ta nie pow oduje żadnych fałszywie negatywnych wyników, a oprócz tego pozwala uzyskać możliwą do kontrolowania liczbę wyników fałszywie pozytywnych. Oznacza to tyle, że jeśli filtr Blooma informuje, że wcześniej nie napotkał danego elementu, można być całkowicie pewnym tego, że ten element wcześniej nie wystąpił. Z kolei jeśli filtr Blooma wskazuje, że dany element wystąpił wcześniej, istnieje prawdopodobieństwo tego, że w rzeczywistości jest inaczej i po prostu mamy do czynienia z błędnym rezultatem. Wynika to z faktu, że będą mieć miejsce kolizje w artości mieszania, a czasem wartości dla dwóch obiektów będą takie same, naw et je śli same obiekty nie są identyczne. W praktyce jednak filtry Blooma są tak ustawiane, aby ich w spółczynniki błędu nie przekraczały 0,5%, a taki błąd m oże być do przyjęcia.
^
M o ż liw e je s t s y m u lo w a n ie u ż y c ia d o w o ln e j ż ą d a n e j lic z b y fu n k c ji m ie s z a n ia . W ty m c e lu w y s ta r c z y z a s t o s o w a ć d w ie fu n k c je m ie s z a n ia , k tó r e s ą n ie z a le ż n e o d s ie b ie . M e t o d a ta je s t o k r e ś la n a m ia n e m „ p o d w ó jn e g o m i e s z a n ia " . J e ś li is tn ie je fu n k c ja m ie sz a n ia , k tó r a z a p e w n ia d w ie n ie z a le ż n e w a r to ś c i m ie s z a n ia , m o ż liw e je s t z a s to s o w a n ie n a s tę p u ją c e g o k o d u : d ef m u lti_h ash (key, num_hashes): h a sh l, hash2 = h ashfu n ctio n(key ) f o r i in xrange(num _hashes): y ie ld (h ash l + i * hash2) % (2C32 - 1) O p e r a c ja m o d u lo z a p e w n ia , ż e w y n ik o w e w a r to ś c i m ie s z a n ia s ą 3 2 -b ito w e (w p r z y p a d k u 6 4 -b ito w y c h fu n k c ji m ie s z a n ia o p e r a c ja m o d u lo z o s ta ła b y w y k o n a n a z w y k o r z y s ta n ie m 2^64 - 1).
5 B. H . B lo o m , d o k u m en t S p ace/tim e trade-offs in h ash co d in g w ith allow able errors, „ C o m m u n ica tio n s o f th e A C M ", 13:7, 1970 r.: 4 2 2 -4 2 6 , d o i:1 0 .1 1 4 5 / 3 6 2 6 8 6 .3 6 2 6 9 2 .
Probabilistyczne struktury danych
| 295
D okładna w ym agana długość listy w artości typu bool oraz liczba w artości m ieszania przy padających na element będą ustalane na podstawie wielkości i żądanego współczynnika błędu. W przypadku dość prostych argumentów statystycznych6 stwierdzamy, że idealne wartości są następujące: ,\ o g ( b łq d ) lic z b a _ b it ó w = - w i e l k o ś ć -
log(2)2
lic z b a _ f u n k c j i _ m ie s z a n ia = lic z b a _ b it ó w
log(2) w ie lk o ś ć
Oznacza to, że jeśli miałoby być przechow yw anych 50 000 obiektów (niezależnie od tego, jak są duże) przy współczynniku fałszywie pozytywnych wyników wynoszącym 0,05% (co wska zuje, że stwierdzono, że przez 0,05% czasu wcześniej napotkano obiekt, choć w rzeczywistości tak nie było), wymagałoby to 791 015 bitów do przechowywania oraz 11 funkcji mieszania. Aby dodatkowo popraw ić efektyw ność pod względem wykorzystania pamięci, możliwe jest użycie pojedynczych bitów do reprezentowania wartości typu bool (ten wbudowany typ zaj muje w rzeczywistości 4 bity). W łatwy sposób można to osiągnąć za pomocą modułu bitarray. Przykład 11.24 prezentuje prostą implementację filtru Blooma. P r z y k ł a d 1 1 .2 4 . P r o s t a i m p le m e n t a c ja f i l t r u B l o o m a import b ita r r a y import math import mmh3 c la s s B lo o m F ilte r (o b je c t): d ef in it ( s e l f , c a p a c ity , e r r o r = 0 .0 0 5 ): In icjow anie filtru B loom a dla danej w ielkości i współczynnika fałszyw ie pozytywnych wyników s e l f .c a p a c i t y = c a p a c ity s e lf .e r r o r = erro r s e lf.n u m _ b its = i n t ( - c a p a c i t y * m a th .lo g (e rr o r) / m a th .lo g (2 )* *2 ) + 1 self.num _hashes = in t(s e lf .n u m _ b its * m a th .lo g (2 ) / f l o a t ( c a p a c i t y ) ) + 1 s e l f .d a t a = b ita r r a y .b it a r r a y (s e lf .n u m _ b its ) d ef _ in d e x e s ( s e lf , key): h1, h2 = mmh3.hash64(key) fo r i in x ra n g e (self.n u m _ h a sh es): y ie ld (h1 + i * h2) % s e lf.n u m _ b its d ef a d d (s e lf , k e y ): fo r index in s e lf._ in d e x e s (k e y ): s e lf.d a t a [in d e x ] = True d ef _ _ c o n t a in s _ _ ( s e lf, key): re tu rn a l l ( s e l f .d a t a [ i n d e x ] f o r index in s e lf._ in d e x e s (k e y )) d ef _ _ len _ _ ( s e l f ) : num_bits_on = s e lf.d a ta .c o u n t(T r u e ) re tu rn - 1 .0 * s e lf.n u m _ b its * m a th .lo g (1 .0 - num_bits_on / f lo a t ( s e lf .n u m _ b it s ) ) / f l o a t ( s e l f . num_hashes) @staticm ethod def union(bloom _a, bloom_b): a s s e r t b lo o m _a.cap acity == b lo o m _ b .ca p a city , "W ielkości muszą być równe" a s s e r t b lo o m _a.erro r == b lo o m _ b .erro r, "W spółczynniki błędu muszą być równe" bloom_union = B lo o m F ilte r(b lo o m _ a .c a p a c ity , b lo om _a.error) bloom _union.data = bloom _a.data | bloom _b.data re tu rn bloom union
6 N a stro n ie serw isu W ik ip e d ia p o św ię co n ej filtro m B lo o m a (http://en .w ikip ed ia.org /w iki/B loom _ filter) z n a jd u je się b a rd z o p ro sty d o w ó d d la ich w łaściw o ści.
296
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Co się stanie, gdy zostanie w staw ionych więcej elementów, niż określono dla wielkości filtru Blooma? W przeciwnym w ariancie wszystkie elementy listy typu bool zostaną ustawione na w artość True. W tym przypadku zaś stwierdzane jest, że napotkano każdy element. Oznacza to, że filtry Blooma są bardzo w rażliw e na to, jaką ustawiono dla nich wielkość początkową. M oże to być dość irytujące, jeśli ma się do czynienia ze zbiorem danych o nieznanej wielkości (na przykład ze strumieniem danych). Jeden ze sposobów poradzenia sobie z tym problem em polega na użyciu w ariantu filtrów Blooma nazyw anego s k a l o w a l n y m i filtrami Blooma7. Działanie takich filtrów polega na połą czeniu ze sobą w ielu filtrów Blooma, których w spółczynniki błędu różnią się w specyficzny sposób8. W ten sposób można zagwarantować ogólny współczynnik błędu i po prostu dodać now y filtr Bloom a, gdy w ym agana będzie w iększa w ielkość. A by spraw dzić, czy wcześniej napotkano element, po prostu iterowane są wszystkie podfiltry Blooma do momentu znalezie nia obiektu lub osiągnięcia końca listy. Przykład 11.25 prezentuje implementację takiej struktury, gdzie na potrzeby bazow ej funkcjonalności używana jest poprzednia im plem entacja filtrów Blooma. Ponadto stosowany jest licznik w celu ułatwienia rozpoznania momentu, w którym ma zostać dodany now y filtr Blooma. P r z y k ł a d 1 1 .2 5 . P r o s t a im p l e m e n t a c j a s k a l o w a l n e g o f i l t r u B l o o m a from b lo o m filte r import B lo o m F ilter c la s s S c a lin g B lo o m F ilte r ( o b je c t) : def in it ( s e l f , c a p a c ity , e r ro r= 0 .0 0 5 , m a x _ fill= 0 .8 , e r r o r _ tig h te n i n g _ r a tio = 0 .5 ): s e l f .c a p a c i t y = c a p a c ity s e l f .b a s e _ e r r o r = e r r o r s e l f .m a x _ f i l l = m a x _ fill s e l f .i t e m s _ u n t i l_ s c a l e = i n t (c a p a c ity * m a x _ fill) s e lf.e r r o r _ t i g h t e n i n g _ r a t i o = e r r o r _ tig h te n in g _ r a tio s e l f .b l o o m _ f i lt e r s = [] s e lf.c u rr e n t_ b lo o m = None self._ a d d _ b lo om () def _ a d d _ b lo o m (se lf): new_error = s e l f .b a s e _ e r r o r * s e lf .e r r o r _ t i g h t e n i n g _ r a t i o * * l e n ( s e l f .b l o o m _ f i lt e r s ) new_bloom = B lo o m F ilte r (s e lf .c a p a c ity , new_error) self.b lo o m _ filters.a p p en d (n ew _ b lo o m ) s e lf.c u rr e n t_ b lo o m = new_bloom re tu rn new_bloom def a d d (s e lf , k e y ): i f key in s e l f : re tu rn True se lf.c u rre n t_ b lo o m .a d d (k e y ) s e l f .i t e m s _ u n t i l_ s c a l e -= 1 i f s e l f .i t e m s _ u n t i l_ s c a l e == 0: bloom _size = le n (s e lf.c u rr e n t_ b lo o m ) bloom_max_capacity = in t(s e lf .c u r r e n t_ b lo o m .c a p a c ity * s e l f .m a x _ f i l l ) # Poniew aż d o filtru B loo m a m ogło zostać dodanych w iele zduplikowanych wartości, # kon ieczne je s t spraw dzenie, czy rzeczywiście niezbędne je s t skalow an ie lub czy n adal # dostępne je s t m iejsce i f bloom _size >= bloom_m ax_capacity: self._ a d d _ b lo om ()
7 P. S. A lm eid a , C . B aq u ero , N . P reg u iça i D . H u tch iso n , d o k u m en t Scalable B loom Filters, „In fo rm atio n P ro cessin g L e tte rs", 101: 2 5 5 -2 6 1 , d o i:10.1016/ j.ip l.2006.10.007. 8 W a rto śc i b łę d u z m n ie jsz a ją się w rz e cz y w isto śc i p o d o b n ie d o sz ereg ó w g e o m e try cz n y c h . D z ię k i te m u p o z a sto so w a n iu ilo cz y n u w s z y stk ic h w s p ó łc z y n n ik ó w b łę d u ilo cz y n z m ie rz a d o ż ą d a n e g o w s p ó łc z y n n ik a b łęd u .
Probabilistyczne struktury danych
| 297
s e l f .i t e m s _ u n t i l_ s c a l e = bloom_max_capacity e ls e : s e l f .i t e m s _ u n t i l_ s c a l e = int(bloom _m ax_capacity - bloom _size) re tu rn F alse def c o n ta in s ( s e l f , key): re tu rn any(key in bloom fo r bloom in s e l f .b l o o m _ f i lt e r s ) def len (s e lf): re tu rn sum(len(bloom) fo r bloom in s e l f .b l o o m _ f i lt e r s )
Inny sposób poradzenia sobie z tym problemem sprowadza się do użycia m etody określanej mianem czasowych filtrów Blooma. Ten wariant umożliwia unieważnianie elementów w struktu rze danych, a tym samym zw alnianie dodatkow ego m iejsca dla w iększej liczby elem entów. Jest to szczególnie przydatne przy przetw arzaniu strumieni, poniew aż elementy m ogą zostać unieważnione na przykład po godzinie, a w ielkość można ustawić na tyle dużą, aby została obsłużona ilość danych, jaka pojawiła się w ciągu godziny. Użycie filtru Blooma w ten sposób pozwoli zapewnić wygodny wgląd w to, co wydarzyło się przez ostatnią godzinę. Zastosowanie tej struktury danych będzie bardziej przypominać użycie obiektu set. W przed stawionej poniżej interakcji użyto skalow alnego filtru Bloom a do dodania kilku obiektów, spraw dzenia, czy zostały wcześniej napotkane, a następnie podjęcia próby eksperym ental nego znalezienia w spółczynnika fałszyw ie pozytyw nych wyników: >>> bloom = B lo o m F ilte r(1 0 0 ) >>> f o r i in x ra n g e (5 0 ): b lo o m .a d d (s tr(i)) >>> ”2 0 ” in bloom True >>> ”2 5 ” in bloom True >>> ”5 1 ” in bloom F alse >>> n u m _ fa lse_ p o sitiv es = 0 >>> num _true_negatives = 0 >>> # Żadna z poniższych liczb nie pow inna być w filtrze B loom a >>> # J e ś li taka liczba zostanie znaleziona w filtrze B loom a, będzie to fałszyw ie pozytywny wynik >>> fo r i in x ra n g e (5 1 ,1 0 0 0 0 ): i f s t r ( i ) in bloom: n u m _ fa lse_ p o sitiv es += 1 e ls e : num _true_negatives += 1 >>> n u m _ fa lse_ p o sitiv es 54 >>> num _true_negatives 9895 >>> fa ls e _ p o s itiv e _ r a te = n u m _ fa lse_ p o sitiv es / flo a t( 1 0 0 0 0 - 51) >>> f a ls e _ p o s itiv e _ r a te 0.005427681173987335 >>> b lo o m .erro r 0.005
W przypadku filtrów Bloom a m ożliw e jest też stosow anie sum zbiorów do łączenia wielu zbiorów elem entów : >>> bloom_a = B lo o m F ilte r(2 0 0 ) >>> bloom_b = B lo o m F ilte r(2 0 0 ) >>> f o r i in x ra n g e (5 0 ): ...: b lo o m _ a .a d d (s tr(i)) >>> f o r i in x ra n g e (2 5 ,7 5 ):
298
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
...:
b lo o m _ b .a d d (str (i))
>>> bloom = B lo o m Filter.u n io n (b lo o m _a, bloom_b) >>> ”5 1 ” in bloom_a # O Out[9] : F a lse >>> ”2 4 ” in bloom_b # 0 O u t[1 0 ]: Fal se >>> ”5 5 ” in bloom # © O u t[1 1 ]: True >>> ”2 5 ” in bloom O u t[1 2 ]: True
O W artość '51' nie znajduje się w filtrze bloom_a. 0
Podobnie w artość '24' nie znajduje się w filtrze bloom_b.
© Jednakże obiekt bloom zawiera w szystkie obiekty w filtrach bloom_a i bloom_b! Zw iązane jest z tym takie zastrzeżenie, że sum a dw óch filtrów Bloom a m ożliw a jest tylko dla filtrów z tą samą wielkością i współczynnikiem błędu. Co więcej, wielkość końcowego filtru Blooma może być nawet równa sumie wielkości dwóch filtrów Blooma połączonych w celu utworzenia tego filtru. Oznacza to tyle, że możliwe jest rozpoczęcie od dwóch filtrów Blooma, która są wypełnione trochę więcej niż w połowie, a po ich zsumow aniu zostanie uzyskany nowy filtr Blooma, który ma nadm ierną w ielkość i nie jest wiarygodny!
Licznik LogLog Liczniki typu LogLog (http://algo.inria.fr/flajolet/Publications/DuFl03-LNCS.pdf) bazują na tym, że poszczególne bity funkcji m ieszania m ogą być też rozpatryw ane jako losow e. Oznacza to, że praw dopodobieństw o, iż pierw szy bit w artości m ieszania to 1, pierw sze dwa bity to 01, a pierw sze trzy bity to 001, w ynosi odpowiednio 50%, 25% i 12,5%. Znając te wartości, a po nadto zachow ując w artość m ieszania z najw iększą liczbą zer na początku (czyli najm niej praw dopodobną w artość m ieszania), można określić przybliżenie dotyczące liczby napotka nych do tej pory elementów. D obrą analogią do tej m etody jest podrzucanie m onet. W yobraźm y sobie, że chcielibyśm y podrzucić m onetę 32 razy i za każdym razem uzyskać orzełka. Liczba 32 w ynika z tego, że używ am y 32-bitow ych funkcji m ieszania. Jeśli podrzucim y m onetę raz i w ypadnie reszka, zostanie zapisana liczba 0, ponieważ w najlepszej próbie nie uzyskano żadnego orzełka. Jako że znane są nam w artości praw dopodobieństw a zw iązane z podrzucaniem monety, a po nadto m ożliw e jest stwierdzenie, że najdłuższa seria nie zakończyła się w yrzuceniem żadne go orzełka (0), m ożesz oszacow ać, że próba przeprow adzenia tego eksperym entu została podjęta jeden raz (2^0 = 1). Jeśli w dalszym ciągu m oneta będzie podrzucana i uda się uzy skać 10 orzełków przed w yrzuceniem reszki, zostanie zapisana liczba 10. Stosując taką samą logikę, m ożesz oszacować, że próba wykonania tego eksperymentu została podjęta 1024 razy (2^10 = 1024). W przypadku takiego systemu największą możliw ą do obliczenia liczbą byłaby maksymalna liczba podrzuceń monety (dla 32 podrzuceń jest to liczba 2^32 = 4 294 967 296). Aby zakodować tę logikę przy użyciu liczników typu LogLog, dla binarnej reprezentacji warto ści mieszania danych wejściowych ustalane jest, ile zer w ystępuje przed pierw szą jedynką. W artość m ieszania może być traktowana jako seria 32 podrzuceń monety, gdzie 0 oznacza wyrzucenie orzełka, a 1 wyrzucenie reszki (np. wartość 000010101101 oznacza wyrzucenie czte rech orzełków przed uzyskaniem pierwszej reszki, a wartość 010101101 wskazuje na wyrzucenie
Probabilistyczne struktury danych
| 299
jednego orzełka przed uzyskaniem pierw szej reszki). Pozw ala to zorientow ać się, ile prób miało miejsce przed otrzymaniem takiej wartości m ieszania. Obliczenia m atematyczne zw ią zane z tym systemem są prawie takie same jak w przypadku licznika M orrisa z jednym za sadniczym wyjątkiem: zamiast używania generatora liczb losowych wartości „losowe" są uzy skiwane przez spraw dzenie rzeczyw istych danych w ejściow ych. Oznacza to, że jeśli nadal będzie dodawana identyczna w artość do licznika LogLog, jego stan w ewnętrzny nie zmieni się. Przykład 11.26 prezentuje prostą im plementację licznika LogLog. P r z y k ł a d 1 1 .2 6 . P r o s t a i m p le m e n t a c ja l ic z n ik a L o g L o g import mmh3 def tra ilin g _ z e ro s(n u m b e r): Zw raca indeks p ierw szeg o bitu ustaw ionego na 1, począw szy o d p raw ej strony 32-bitow ej liczby całkow itej > > > trailing_zeros(0) 32 > > > trailing_zeros(0b1000) 3 > > > trailing_zeros(0b10000000) 7 i f not number: re tu rn 32 index = 0 w hile (number >> index) & 1 == 0: index += 1 re tu rn index c la s s L o g L o g R e g iste r(o b je ct): coun ter = 0 def a d d (s e lf , ite m ): item_hash = m m h3.hash (str(item )) re tu rn se lf._ a d d (ite m _ h a sh ) def _ a d d (s e lf , item _hash): b it_ in d e x = tr a ilin g _ z e r o s (ite m _ h a s h ) i f b it_ in d e x > s e l f .c o u n t e r : s e lf .c o u n t e r = b it_ in d e x d ef _ _ l e n _ _ ( s e l f ) : re tu rn 2 * * s e lf .c o u n te r
N ajw iększą w ad ą tej m etody jest to, że m ożliw e jest uzyskanie w artości m ieszania, która zwiększa licznik od samego początku, powodując wypaczanie oszacowań. Przypominałoby to wyrzucenie 32 reszek w pierwszej próbie. Aby temu zaradzić, w tym samym czasie wiele osób m usiałoby w yrzucić m onety i połączyć uzyskane dla nich wyniki. Prawo dotyczące du żych liczb głosi, że w raz z dodawaniem kolejnych osób rzucających m onetą łączne statystyki w m niejszym stopniu stają się zależne od nietypow ych prób przeprow adzonych przez po szczególne osoby. Konkretny sposób łączenia wyników stanowi podstawę różnic między me todami typu LogLog (klasyczny LogLog, SuperLogLog, HyperLogLog, HyperLogLog++ itd.). W celu uzyskania takiej metody z „wieloma osobami rzucającym i m onetą" należy użyć kilku pierw szych bitów w artości m ieszania do określenia, które z tych osób uzyskały konkretny w ynik. Jeśli w ykorzystano pierw sze cztery bity w artości m ieszania, oznacza to, że zaanga żow ano 16 osób rzucających m onetą (2^4 = 16). Poniew aż w tym przypadku użyto pierw szych czterech bitów , pozostało jedynie 28 bitów (odpowiadających 28 rzutom m onetą przez jed n ą osobę). O znacza to, że każdy licznik m oże prow ad zić obliczenia jed yn ie do liczby 2^28 = 268 435 456. Ponadto istnieje stała (alfa), która zależy od liczby zaangażow anych osób.
300
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
P o w o d u j e t o n o r m a l i z o w a n i e o s z a c o w a n i a 9. W s z y s t k o t o z a p e w n i a a l g o r y t m 1 ,0 5 .— , g d z i e m o z n a c z a l i c z b ę z a a n g a ż o w a n y c h r e j e s t r ó w
o d o k ła d n o ś c i
(lu b o s ó b r z u c a ją c y c h m o n e tą ).
4m
P r z y k ła d 1 1 .2 7 p r e z e n t u je p r o s t ą im p le m e n t a c ję a lg o r y t m u L o g L o g . P r z y k ł a d 1 1 .2 7 . P r o s t a im p l e m e n t a c j a a lg o r y t m u L o g L o g from l l r e g i s t e r import LL R egister import mmh3 c la s s L L (o b je c t): def in it ( s e l f , p ): s e lf.p = p s e lf.n u m _ re g is te r s = 2**p s e l f . r e g i s t e r s = [L L R eg ister() fo r i in x r a n g e (in t (2 * * p ) )] s e l f .a l p h a = 0 .7 2 1 3 / (1 .0 + 1 .0 7 9 / s e lf.n u m _ r e g is te r s ) def a d d (s e lf , ite m ): item_hash = m m h3.hash (str(item )) r e g is te r_ in d e x = item_hash & (s e lf.n u m _ r e g is te r s - 1) re g is te r_ h a s h = item_hash >> s e l f . p s e l f . r e g i s t e r s [ r e g i s te r_ in d e x ]._ a d d (r e g is te r_ h a s h ) def len (s e lf): reg ister_ su m = sum (h.counter f o r h in s e l f . r e g i s t e r s ) re tu rn s e lf.n u m _ re g is te r s * s e l f .a l p h a * 2 * * (flo a t(r e g is te r _ s u m ) / s e lf.n u m _ re g is te r s ) O p r ó c z te g o , ż e a lg o r y tm
te n d e d u p lik u je p o d o b n e e le m e n t y , u ż y w a ją c w a r t o ś c i m ie s z a n ia
ja k o in d y k a t o r a , o f e r u je o n m o ż liw y d o m o d y f ik a c ji p a r a m e t r , k t ó r y m o ż e p o s ł u ż y ć d o o k r e ś la n ia k o m p r o m is u m ię d z y d o k ła d n o ś c ią i m ie js c e m d o p r z e c h o w y w a n ia . W m e t o d z ie _ _ le n _ _ u ś r e d n ia n e s ą o s z a c o w a n ia u z y s k a n e d la w s z y s t k ic h p o s z c z e g ó ln y c h r e je s t r ó w w p r z y p a d k u a l g o r y t m u L o g L o g . N i e j e s t t o j e d n a k n a j b a r d z i e j e f e k t y w n y s p o s ó b ł ą c z e n ia d a n y c h ! W y n ik a to s tą d , ż e m o ż n a u z y s k a ć n ie f o r t u n n e w a r to ś c i m ie s z a n ia , k tó r e s p r a w i ą , ż e o k r e ś l o n y r e je s t r o s i ą g n i e m a k s y m a l n e w a r t o ś c i , i n n e n a t o m i a s t n a d a l b ę d ą m i e ć n ie w i e l k i e w a r t o ś c i. Z te g o p o w o d u m o ż l iw e je s t je d y n i e o s ią g n ię c ie w s p ó łc z y n n ik a b łę d u O(
1 ,3 0 — ), g d z i e m t o l i c z b a u ż y w a n y c h r e je s t r ó w .
4m
J a k o r o z w i ą z a n i e t e g o p r o b l e m u o p r a c o w a n o a l g o r y t m S u p e r L o g L o g 10. W j e g o p r z y p a d k u d o s z a c o w a n i a w i e lk o ś c i u ż y w a n y c h je s t t y lk o 7 0 % n a jm n i e js z y c h r e je s t r ó w . I c h w a r t o ś ć je s t o g r a n ic z o n a p r z e z m a k s y m a ln ą w a r to ś ć o k r e ś lo n ą p r z e z r e g u łę o g r a n ic z a n ia . D z ię k i te m u w s p ó łc z y n n i k b łę d u z o s t a ł z m n i e js z o n y d o w a r t o ś c i O (
1 ,0 5 ,— ). J e s t t o s p r z e c z n e z i n t u i c ją , p o -
4m
n i e w a ż l e p s z e o s z a c o w a n i e z o s t a ł o u z y s k a n e p r z e z z i g n o r o w a n i e i n f o r m a c ji !
9 P e łn y o p is p o d sta w o w y c h a lg o ry tm ó w L o g L o g i S u p e rL o g L o g d o stę p n y je s t p o d a d re se m h ttp ://a lg o .in ria .fr/ fla jo let/P u b lica tio n s/D u F l0 3 .p d f. 10 M . D u ra n d i P . F la jo le t, d o k u m e n t L o g L o g C o u n tin g o f L a rg e C ard in a lities, „ P ro c e e d in g s o f E S A 2 0 0 3 " , 2832 (200 3 r.): 6 0 5 -6 1 7 , d o i:1 0 .1 0 0 7 / 9 7 8 -3 -5 4 0 -3 9 6 5 8 -1 _ 5 5 .
Probabilistyczne struktury danych
| 301
W 2007 r. pojaw ił się algorytm H yperLogLog11, zapew niający dalszą popraw ę dokładności. Zostało to osiągnięte przez zm ianę m etody uśredniania poszczególnych rejestrów : zam iast zw ykłego uśredniania użyto schem atu uśredniania sferycznego, który w specjalny sposób uw zględnia także różne przypadki brzegow e, jakie m ogą dotyczyć struktury. Dzięki temu uzyskano najlepszy obecnie w spółczynnik błędu wynoszący O(
1 ,0 4
om
,— ). Ponadto takie sfor
mułow anie wyelim inow ało operację sortowania niezbędną w przypadku algorytmu Super LogLog. M oże to znacznie zw iększyć w ydajność struktury danych, gdy podejm ow ana jest próba wstawiania elementów przy ich dużej liczbie. Przykład 11.28 prezentuje prostą im ple m entację algorytmu HyperLogLog. P r z y k ł a d 1 1 .2 8 . P r o s t a i m p le m e n t a c ja a l g o r y t m u H y p e r L o g L o g from l l import LL import math c la s s HyperLogLog(LL): d ef len (s e lf): in d ic a to r = sum (2**-m .counter fo r m in s e l f . r e g i s t e r s ) E = s e lf .a lp h a * (s e lf.n u m _ r e g is te r s * * 2 ) / f l o a t ( i n d i c a t o r ) i f E <= 5 .0 / 2 .0 * s e lf.n u m _ r e g is te r s : V = sum(1 fo r m in s e l f . r e g i s t e r s i f m .counter == 0) i f V != 0: E s ta r = s e lf.n u m _ r e g is te r s * m a th .lo g (s e lf.n u m _ re g is te rs / (1 .0 * V ), 2) e ls e : E s ta r = E e ls e : i f E <= 2 **3 2 / 3 0 .0 : E s ta r = E e ls e : E s ta r = -2 * *3 2 * m ath .lo g (1 - E / 2 * * 3 2 , 2) re tu rn E s ta r if name == ” main ” : import mmh3 h ll = HyperLogLog(8) f o r i in x ra n g e (1 0 0 0 0 0 ): h ll.a d d (m m h 3 .h a sh (str(i) ) ) p rin t l e n ( h l l )
Dodatkowe zwiększenie dokładności struktury danych zostało zapewnione tylko przez algo rytm H yperLogLog++, który osiągnął to dla stosunkow o pustej struktury. Po w staw ieniu większej liczby elementów schem at ten zaczyna przypom inać schem at standardowego algo rytmu HyperLogLog. Jest to w rzeczywistości dość przydatne, ponieważ do zapewnienia do kładności statystyk liczników typu LogLog w ym aganych jest w iele danych. D ostępność schematu oferującego lepszą dokładność przy mniejszej liczbie elementów znacznie zwiększa użyteczność takiej m etody. Jeszcze w iększa dokład ność jest osiągana z w ykorzystaniem mniejszej, ale dokładniejszej struktury HyperLogLog, która później m oże zostać przekształ cona w większą, pierwotnie żądaną strukturę. Poza tym istnieją określone stałe używane przy szacowaniu wielkości, które eliminują obciążenia.
11 P. F lajo le t, E. F u sy , O . G a n d o u et i in n i, d o k u m en t H yperL ogL og: the analysis o f a near-optim al cardin ality estim ation a lgorith m , „ P ro ceed in g s o f th e 2 0 0 7 In tern a tio n a l C o n feren ce o n A n a ly sis o f A lg o rith m s", (2 0 0 7 r.): 1 2 7 -1 4 6 .
302
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Praktyczny przykład Aby lepiej zrozum ieć struktury danych, utworzyliśmy najpierw zbiór danych z wieloma uni kalnym i kluczam i, a następnie zbiór ze zduplikow anym i elem entam i. Rysunki 11.5 i 11.6 p rezentują w yniki dla sytuacji, w której takie klucze przekazano w cześniej om aw ianym strukturom danych, a ponadto okresowo żądano odpowiedzi na następujące pytanie: „Ile zo stało napotkanych unikalnych elem entów ?". W idoczne jest, że struktury danych zaw ierające bardziej stanowe zmienne (np. struktury HyperLogLog i KM inValues) w ypadają lepiej, po niew aż lepiej radzą sobie ze złym i statystykam i. Z kolei licznik M orrisa i pojedynczy rejestr LogLog m ogą szybko osiągnąć bardzo w ysokie współczynniki błędu, jeśli w ystąpi jedna nie fortunna liczba losowa lub w artość mieszania. Jednakże w przypadku większości algorytmów w iadom o, że liczba zmiennych stanowych jest bezpośrednio pow iązana z gwarancjami doty czącymi błędów, dlatego ma to sens. 60 000 elementów z duplikatami 35 000
Liczba dodanych elementów R y s u n e k 1 1 .5 . P o r ó w n a n i e r ó ż n y c h p r o b a b i l i s t y c z n y c h s t r u k t u r d a n y c h d la p o w t a r z a j ą c y c h s ię d a n y c h
Przyjrzenie się probabilistycznym strukturom danych o najlepszej w ydajności (i tym, z któ rych praw dopodobnie skorzystasz) pozwala dokonać podsum ow ania dotyczącego ich uży teczności i przybliżonego w ykorzystania pam ięci (tabela 11.2). Z ależnie od pytań, jakie chcemy zadać, w idoczna jest ogromna różnica w w ykorzystaniu pamięci. Zwraca to uwagę na fakt, że przy korzystaniu z probabilistycznej struktury danych przed dalszymi działania m i konieczne jest najpierw zastanow ienie się, jakie pytania dotyczące zbioru danych rzeczy w iście w ym agają udzielenia odpowiedzi. Zauważ też, że tylko wielkość filtru Blooma zależy od liczby elementów. W ielkości struktur HyperLogLog i KM inValues są zależne jedynie od współczynnika błędu.
Probabilistyczne struktury danych
| 303
R y s u n e k 1 1 .6 . P o r ó w n a n i e r ó ż n y c h p r o b a b i l i s t y c z n y c h s t r u k t u r d a n y c h d la u n ik a ln y c h d a n y c h T a b e la 1 1 .2 . P o r ó w n a n i e g ł ó w n y c h p r o b a b i l i s t y c z n y c h s t r u k t u r d a n y c h Wielkość HyperLogLog
Tak(O(
KMinValues Tak ( o
Filtr Blooma
Tak(O(
1 0 4 ))
í \1 2
1
Sumaa
Iloczyn
Zawiera
Wielkośćb
Tak
Niec
Nie
2,704 MB
Tak
Tak
Nie
20,372 MB
Tak
Niec
Tak
197,8 MB
) m{m - 2) J
° 7 8 )) -ym
a Operacja sumy występuje bez zwiększania współczynnika błędu. b Wielkość struktury danych w przypadku zastosowania współczynnika błędu równego 0,05% , 100 000 000 unikalnych elementów oraz 64-bitowej funkcji mieszania. c Operacje te są m ożliw e do wykonania, ale powodują znaczne zmniejszenie dokładności.
W ramach kolejnego bardziej realistycznego testu zdecydow aliśm y się na użycie zbioru da nych uzyskanego z tekstu pochodzącego z serwisu W ikipedia. Uruchom iliśm y bardzo prosty skrypt, aby w yodrębnić wszystkie jednow yrazow e tokeny liczące co najmniej pięć znaków ze w szystkich artykułów , a następnie zapisaliśm y je w pliku z separatorem w postaci znaku nowego wiersza. Zadano następujące pytanie: „Ile w ystępuje unikalnych tokenów ?". W yniki zaprezentow ano w tabeli 11.3. Ponadto podjęto próbę udzielenia odpow iedzi na to samo pytanie, używ ając struktury drzewa d a t r i e opisanej w punkcie „Drzewo datrie" (ten w ariant drzewa t r i e został wybrany, a nie inne, ponieważ oferuje dobrą kom presję przy zachowaniu w dalszym ciągu solidności wystarczającej do poradzenia sobie z całym zbiorem danych).
304
|
Rozdział 11. Mniejsze w ykorzystanie pamięci RAM
Podstaw ow ą korzyścią z takiego eksperym entu jest to, że jeśli masz m ożliw ość dokonania specjalizacji kodu, m ożesz u zyskać niesam ow itą szybkość i zm niejszenie w ykorzystania pam ięci. Tak było w poprzednich rozdziałach książki: gdy dokonyw ano specjalizacji kodu w punkcie „Optymalizacje selektywne: znajdowanie tego, co wymaga popraw ienia", podob nie możliwe było uzyskanie w zrostu szybkości. Probabilistyczne struktury danych to algorytmiczny sposób dokonywania specjalizacji kodu. Przechowywane są jedynie dane, które są niezbędne do udzielenia odpowiedzi na konkretne pytania przy danych granicach błędu. Dzięki konieczności zajmowania się wyłącznie podzbiorem danych informacji nie tylko m oż na uzyskać znacznie mniejsze wykorzystanie pamięci, ale też szybciej w ykonyw ać większość operacji dla struktury (widoczne jest to w tabeli 11.3 w przypadku czasu operacji wstawiania wykonanej dla drzewa d a t r i e , który jest dłuższy niż dla dowolnej probabilistycznej struktury danych). T a b e la 1 1 .3 . O s z a c o w a n ie w i e l k o ś c i d l a k ilk u u n i k a ln y c h s ł ó w z s e r w i s u W ik ip e d ia Liczba elementów
Błąd względny
Czas przetwarzania9
Wielkość strukturyb
1 073 741 824
6,52%
751 s
5 bitów
Rejestr LogLog
1 048 576
78,84%
1690 s
5 bitów
LogLog
4 522 232
8,76%
2112 s
5 bitów
HyperLogLog
4 983 171
-0 ,5 4 %
2907 s
40 kB
KMinValues
4,912,818
0,88%
3503 s
256 kB
Licznik Morrisac
Skalowalny filtr Blooma
4 949 358
0,14%
10392 s
11509 kB
Drzewo datrie
4 505 514d
0,00%
14620 s
114 068 kB
Rzeczywista wartość
4 956 262
0,00%
49 558 kBe
a Czas przetwarzania został skorygowany w celu usunięcia czasu wczytywania zbioru danych z dysku. Ponadto używane są proste implementacje, które wcześniej udostępniono do testowania. b Wielkość struktury jest teoretyczna dla danej wielkości danych, ponieważ nie przeprowadzono optymalizacji użytych implementacji. c Ponieważ licznik Morrisa nie deduplikuje danych wejściowych, wielkość i błąd względny podano w odniesieniu do całkowitej liczby wartości. d Z powodu problemów z kodowaniem drzewo datrie nie mogło załadować wszystkich kluczy. e Zbiór danych ma wielkość 49 558 kB w przypadku tylko unikalnych tokenów lub wielkość 8,742 GB dla wszystkich tokenów.
W efekcie, niezależnie od tego, czy używane są probabilistyczne struktury danych, czy nie, zaw sze należy pamiętać, jakie pytania dotyczące danych zostaną zadane, a także jak najbar dziej efektywnie można przechow yw ać te dane tak, by było m ożliwe zadanie tych specjali stycznych pytań. M oże się to sprow adzić do użycia określonego typu listy zam iast innego, zastosowania konkretnego typu indeksu bazy danych lub być m oże naw et probabilistycznej struktury danych do wyelim inow ania w szystkich danych oprócz tych, które nas interesują!
Probabilistyczne struktury danych
| 305
306
|
Rozdział 11. Mniejsze wykorzystanie pamięci RAM
_________________________ROZDZIAŁ 12.
Rady specjalistów z branży
Pytania, na jakie będziesz w stanie udzielić odpowiedzi po przeczytaniu rozdziału • Jak z powodzeniem rozpocząć przetwarzanie dużych wolumenów danych i zająć się uczeniem maszynowym? • Jakie technologie m onitorow ania i w drażania pozw alają zachow ać stabilność sys tem ów ? • Do jakich wniosków dotyczących używanych technologii i prowadzonych zespołów doszli szefowie działów informatycznych, którzy odnieśli sukces? • W jakim stopniu może być w drażana im plementacja PyPy?
W tym rozdziale zebrano historie firm, które odniosły sukces. W ich przypadku język Python jest wykorzystywany do przetwarzania pokaźnych zbiorów danych oraz w sytuacjach, w któ rych szybkość ma kluczowe znaczenie. Historie zostały opisane przez kluczowe osoby w każ dej firmie, która m ają w ieloletnie doświadczenie. Osoby te nie tylko podzieliły się tym, jakich dokonały w yborów odnośnie do technologii, ale też przekazały w iedzę zdobytą ciężką pracą.
Narzędzie Social Media Analytics (SoMA) firmy Adaptive Lab Ben Jackson (adaptivelab.com) Adaptive Lab to firma zajmująca się wprowadzaniem innowacji i projektow aniem produk tów , która sw oją siedzibę m a w rejonie Shoreditch londyńskiego Tech City. O pracow aną przez nas metodę projektowania i dostarczania produktów, która cechuje się prostotą i ukie runkowaniem na użytkowników, w drażam y w e w spółpracy z wieloma firmami, począwszy od małych, początkujących firm, a skończywszy na dużych korporacjach. YouGov to firma zajmująca się badaniem globalnego rynku, której am bicją jest zapewnianie aktywnego strumienia ciągłych i precyzyjnych danych oraz wglądu w to, o czym myślą i czym się zajm ują ludzie na całym świecie. W łaśnie za zapew nienie tego byliśm y odpowiedzialni.
307
Firma Adaptive Lab opracowała metodę pasywnego słuchania rzeczywistych dyskusji odby wających się na platform ach społecznościowych i analizowania odczuć użytkow ników zwią zanych z możliwym do dostosowania zakresem tematów. Zbudowaliśm y skalowalny system, który był w stanie przechw ytyw ać dużą ilość danych strumieniowych, przetw arzać je, prze chowywać bezterminowo i prezentow ać w czasie rzeczywistym za pośrednictwem możliwe go do filtrow ania interfejsu zapew niającego dużą funkcjonalność. System został utw orzony z w ykorzystaniem języka Python.
Język Python w firmie Adaptive Lab Język Python to jed na z naszych podstaw ow ych technologii. U żyw am y go w aplikacjach, w przypadku których wydajność ma kluczowe znaczenie, a także zawsze podczas współpracy z klientami z w łasnym personelem z doświadczeniem z zakresu języka Python. Oznacza to, że wyniki naszej pracy klienci m ogą dalej przetw arzać w e w łasnym zakresie. Język Python nadaje się idealnie do tworzenia niewielkich, autonomicznych i długo działają cych demonów. Sprawdza się też znakomicie w przypadku elastycznych i bogatych w funkcje środowisk do projektowania aplikacji internetowych, takich jak Django i Pyramid. Społecz ność związana z językiem Python doskonale się rozwija. Oznacza to, że dostępna jest ogrom na biblioteka narzędzi open source, które um ożliw iają nam szybkie tw orzenie z poczuciem pewności. Dzięki temu możemy skoncentrować się na nowych rozwiązaniach, które pozwalają nam radzić sobie z problemami użytkowników. W ramach w szystkich projektów realizowanych w firmie Adaptive Lab wielokrotnie wyko rzystywanych jest kilka narzędzi bazujących na języku Python, które jednak mogą być używa ne w sposób niezależny od języka. Na przykład używamy narzędzia SaltStack do przygoto wywania serwerów do pracy oraz narzędzia M ozilla Circus do zarządzania długotrwałymi procesam i. K orzyścią w ynikającą z faktu, że dane narzędzie jest udostępniane na zasadach open source, a ponadto zostało napisane w znanym nam języku, jest to, że w przypadku napo tkania jakichkolw iek problem ów m ożemy je sami rozwiązać, a następnie zająć się dodatkowo tymi rozwiązaniami, na czym skorzysta cała społeczność.
Projekt narzędzia SoMA Narzędzie SoMA (Social Media Analytics) wymagało poradzenia sobie z dużą przepustowością danych społecznościow ych oraz przechow yw aniem i pobieraniem dużej ilości inform acji w czasie rzeczyw istym . Po spraw dzeniu różnych m agazynów danych i m echanizm ów w y szukiwania wybraliśmy m echanizm Elasticsearch na nasz m agazyn dokum entów dostępnych w czasie rzeczywistym. Jak sugeruje nazwa m echanizmu (elastyczne w yszukiwanie), cechuje się on dużą skalowalnością. Ponadto jest bardzo prosty w użyciu, a także oferuje bardzo duże możliwości w zakresie udostępniania odpowiedzi statystycznych i wyszukiwania. Jest to ideal ne narzędzie w naszym przypadku. Sam mechanizm Elasticsearch zbudowano z wykorzy staniem języka Java, ale podobnie do dow olnego w łaściw ie zaprojektow anego kom ponentu nowoczesnego systemu zapewnia on dobry interfejs API, a ponadto jest odpowiednio przy gotowany do zastosow ania dzięki bibliotece języka Python i kursom. Zaprojektowany przez nas system używa kolejek z wykorzystaniem narzędzia Celery, które są przechow yw ane w systemie Redis, tak by duży strumień danych mógł być szybko prze kazywany do dowolnej liczby serwerów w celu niezależnego przetwarzania i indeksowania.
308
|
Rozdział 12. Rady specjalistów z branży
Każdy komponent złożonego systemu został zaprojektowany tak, aby był mały i prosty, a po nadto miał m ożliw ość pracy w izolacji. Poszczególne komponenty zajm ują się jednym zada niem, takim jak analizowanie rozmowy pod kątem emocji lub przygotow yw anie dokumentu do indeksowania w mechanizmie Elasticsearch. Kilka spośród tych komponentów zostało skon figurowanych za pomocą narzędzia M ozilla Circus tak, by działały jako demony. Dzięki temu w szystkie procesy są konfigurow ane i uruchamiane, więc mogą być skalowane dla poszcze gólnych serwerów. System SaltStack służy do definiowania i udostępniania złożonego klastra, a ponadto obsługuje konfigurację w szystkich bibliotek, języków , baz danych i m agazynów dokum entów . Korzy stamy również z narzędzia Fabric bazującego na języku Python, które uruchamia opcjonalne zadania z poziomu wiersza poleceń. Definiowanie serwerów w kodzie ma wiele zalet, takich jak: pełna zgodność ze środowiskiem produkcyjnym, kontrola wersji konfiguracji oraz um iej scowienie w szystkich elementów w jednym miejscu. Takie rozwiązanie zapewnia też doku m entację konfiguracji i zależności w ymaganych przez klaster.
Zastosowana metodologia projektowa Dążymy do tego, aby nowa osoba dołączająca do projektu miała jak najbardziej ułatwioną pracę, by mogła szybko i z poczuciem pewności rozpocząć dodaw anie kodu i wdrażanie. Używamy oprogramowania Vagrant do lokalnego definiowania złożoności systemu w obrębie maszyny w irtualnej, która jest całkow icie zgodna ze środow iskiem produkcyjnym . Zw ykłe polecenie v a g r a n t up jest wszystkim, czego nowa osoba dołączająca do projektu m usi użyć, by zapoznać się ze w szystkimi zależnościami niezbędnymi do dalszej pracy. Działamy elastycznie, planujemy razem, omawiamy decyzje dotyczące architektury i osiągamy konsensus odnośnie do szacow anego czasu realizacji zadań. W przypadku narzędzia SoMA podjęliśm y decyzję o uw zględnieniu w ram ach każdego spotkania co najm niej kilku zadań rozpatryw anych jako popraw ki zw iązane z „zaległościam i technicznym i". D ołączono także zadania związane z dokumentacją systemu (ostatecznie stworzyliśm y stronę wiki jako repo zytorium całej wiedzy dotyczącej tego ciągle rozwijającego się projektu). Po każdym zrealizo wanym zadaniu członkowie zespołu wzajemnie oceniają kod (w granicach rozsądku), wymie niają opinie i analizują nowy kod, który zostanie dodany do systemu. D obry p akiet testów przyczynił się do zw iększenia pew ności odnośnie do tego, że żadne zm iany nie spowodują, że przestaną działać istniejące funkcje. Testy integracyjne są kluczo we w systemie takim jak SoMA, który jest złożony z wielu dynamicznych części. Środowisko pom ostow e oferuje m etodę testow ania w ydajności now ego kodu. W szczególności w przy padku system u SoM A odbyw ało się to w yłącznie z w ykorzystaniem dość dużych zbiorów danych używ anych w środowisku produkcyjnym, które m ogą pow odow ać problem y w ym a gające rozwiązania. W zw iązku z tym często konieczne było odtworzenie odpowiedniej ilości danych w osobnym środowisku. Umożliwiła nam to technologia Elastic Compute Cloud (EC2) firmy Amazon oferująca dużą elastyczność.
Serwisowanie systemu SoMA System SoM A działa w trybie ciągłym , a ilość używ anych przez niego inform acji zw iększa się każdego dnia. Konieczne jest uwzględnienie szczytowych obciążeń w strumieniu danych, problem ów z siecią oraz występujących po stronie dowolnego zewnętrznego dostawcy usług,
Narzędzie Social Media Analytics (SoMA) firmy Adaptive Lab
| 309
na których system bazuje. W celu ułatw ienia sobie zadania system SoM A zaprojektow ano tak, aby w razie m ożliw ości sam rozw iązyw ał problem y. Dzięki narzędziu Circus procesy, które ulegną aw arii, zaczną działać, w znaw iając sw oje zadania od m iejsca, w którym prze rwały ich realizowanie. Zadanie będzie kolejkowane do m omentu pobrania go przez proces. W kolejce dostępna jest ilość miejsca wystarczająca do gromadzenia zadań, gdy trwa przywra canie systemu do normalnego trybu działania. Do monitorowania wielu serwerów systemu SoMA używ am y narzędzia Server Density. Jest ono bardzo proste do skonfigurow ania, ale jednocześnie oferuje duże m ożliw ości. Inżynier w yznaczony do jego obsługi m oże odebrać za pom ocą telefonu w iadom ość w staw ianą od ra zu, gdy w ystąpi praw dopodobieństw o pojawienia się problem u. Dzięki temu m oże zareago w ać na czas, aby problem nie wystąpił. W przypadku tego narzędzia bardzo ułatwione jest również tworzenie w języku Python niestandardowych wtyczek, które na przykład umożliwiają ustawienie natychm iastow ych alertów dotyczących różnych aspektów działania mechanizmu Elasticsearch.
Rada dla inżynierów z branży Przede wszystkim Ty i członkowie Twojego zespołu m usicie m ieć poczucie pewności i kom fortu odnośnie do tego, że to, co ma zostać w drożone w środow isku produkcyjnym , będzie działać bezbłędnie. Aby to osiągnąć, m usisz działać wstecznie, poświęcając czas na wszystkie kom ponenty system u, które pozw olą Ci zyskać to poczucie. Dbaj o to, aby w drożenie było proste i niezaw odne. Zastosuj środow isko pom ostow e, by testow ać w ydajność przy użyciu rzeczywistych danych. Upewnij się, że dysponujesz dobrym i solidnym pakietem testów o du żym pokryciu. Zaimplementuj proces wprowadzania nowego kodu do systemu. Zadbaj o to, aby „zaległości techniczne" zostały jak najwcześniej zrealizowane. Im bardziej będziecie dbać o infrastrukturę techniczną i ulepszanie procesów, z tym większym zadowoleniem i powodze niem członkowie zespołu będą opracowywać właściwe rozwiązania. Jeśli zam iast solidnego fundamentu w postaci kodu i „ekosystem u" pojaw i się presja bizne sowa zw iązana z w prow adzaniem rozw iązań w życie, spow oduje to jed yn ie pow stanie oprogram ow ania spraw iającego problem y. Na Tobie będzie spoczyw ać odpow iedzialność za w ydłużenie i zaplanow anie czasu stopniow ych ulepszeń kodu, a także testów i operacji związanych z wdrożeniem produktów.
Technika głębokiego uczenia prezentowana przez firmę RadimRehurek.com Radim R ehufek (radimrehurek.com) Gdy Ian poprosił mnie o napisanie tekstu do tej książki zawierającego rady specjalisty z branży dotyczące kodu Python i optym alizacji, od razu przyszła m i do głow y następująca myśl: „Powiedz im, jak sprawiłeś, że port kodu Python działał szybciej niż oryginalny kod C firmy G oogle!". Jest to inspirująca historia tworzenia algorytmu uczenia m aszynowego, który sta nowi sztandarowe rozwiązanie firmy Google z zakresu techniki głębokiego uczenia. Algorytm ten jest 12 tysięcy razy szybszy niż zw ykła im plem entacja oparta na kodzie Python. Każdy może stworzyć kiepski kod, a następnie rozgłaszać informacje o dużych w zrostach szybkości.
310
|
Rozdział 12. Rady specjalistów z branży
Jednakże zoptymalizow any kod Python również działa, co zadziwiające, praw ie czterokrot nie szybciej niż oryginalny kod napisany przez zespół program istów firmy Google! A więc cztery razy szybciej niż zagmatwany, mocno profilowany i zoptym alizowany kod C. Zanim jednak przedstaw ię rady dotyczące optymalizacji „na poziom ie m aszynow ym ", za prezentuję ogólne zalecenia związane z optymalizacjami „na poziom ie ludzkim ".
Strzał w dziesiątkę Prowadzę niew ielką firmę konsultingową, która koncentruje się w yłącznie na uczeniu m a szynowym. W raz ze współpracownikami ułatwiam firmom znalezienie sensu w burzliwym świecie analizy danych, aby mogły w ygenerować zyski lub uzyskać oszczędności (lub to i to). Pom agamy naszym klientom projektow ać i budow ać niezwykłe system y służące do przetw a rzania danych, zwłaszcza danych tekstowych. W śród naszych klientów są zarówno duże koncerny międzynarodowe, jak i dopiero powstałe m ałe firmy. Choć każdy projekt jest inny i w ym aga innego stosu technologicznego, a także podłączenia do istniejących przepływ ów i potoków danych klienta, język Python jest oczywi ście naszym faworytem . N ie przesadzając ze zbytnim w ychw alaniem , zw iązana z językiem Python pozbaw iona absurdu filozofia projektow ania, jego elastyczność i bogactw o bibliotek sprawiają, że jest on idealną opcją wyboru. Na początek kilka przemyśleń „branżow ych" w postaci następujących kwestii: • K o m u n ik a c ja , k o m u n ik a c ja i je s z c z e ra z k o m u n ik a c ja .
Choć jest to oczywiste, warto dalej pow tarzać. Przed podjęciem decyzji o sposobie postępow ania, zrozum problem przed stawiony przez klienta na w yższym poziom ie biznesowym. Usiądź z przedstawicielami klienta i omów z nim i, jakie elem enty uw ażają za potrzebne (bazując na ich częściowej wiedzy na tem at tego, co jest możliwe, i (lub) tego, co znaleźli przed spotkaniem za po m ocą w yszukiw arki G oogle). Kontynuuj rozm ow y do m om entu w yraźnego określenia, co uważają za naprawdę niezbędne, bez kierowania się negatywnymi zaszłościami i z góry określonymi osądami. Uzgodnij wcześniej metody sprawdzania poprawności rozwiązania. Lubię wizualizację tego procesu jako długiej, wijącej się drogi, która ma zostać zbudowana. Właściwie ustal linię początkową (definicja problemu, dostępne źródła danych) i końcową (ocenianie, priorytety rozwiązań) oraz ścieżkę przebiegającą między nimi.
• O b se rw u j o b ie c u ją c e te c h n o lo g ie .
Kształtująca się technologia, która jest dość dobrze rozumiana i solidna, zyskuje coraz większe zainteresowanie, a przy tym w dalszym ciągu w branży jest stosunkowo mało znana, może m ieć dla klienta (lub Ciebie) ogromną w ar tość. Przykładowo kilka lat temu mechanizm Elasticsearch był mało znanym lub w pew nym sensie niedopracowanym projektem open source. Jednakże oceniłem jego rozwiązania jako solidne (oparty na bibliotece Apache Lucene, oferujący replikację, tworzenie instancji partycji w klastrze itp.) i poleciłem go klientowi. W konsekwencji zbudow aliśm y system w yszukiw ania z m echanizm em Elasticsearch jako rdzeniem . Dzięki temu klient zaosz czędził znaczne kwoty na licencjonowaniu, projektow aniu i serwisowaniu w porównaniu z rozważanymi alternatywami (duże komercyjne bazy danych). Co ważniejsze, używając nowej, elastycznej technologii o dużych możliwościach, udało się zapewnić produktowi ogromną przewagę nad konkurencją. Obecnie mechanizm Elasticsearch pojawił się w seg mencie rynku dla przedsiębiorstw i nie daje już żadnej przewagi nad konkurencją, ponie waż każdy go zna i korzysta z niego. W łaściw e w yczucie chw ili jest tym, co nazyw am strzałem w dziesiątkę, który pozwala zm aksym alizow ać stosunek w artości do kosztu.
Technika głębokiego uczenia prezentowana przez firmę RadimRehurek.com
|
311
• P rz e d e w s z y s tk im n ie k o m p lik u j.
Jest to kolejna prosta myśl. Najlepszy kod to taki, który nie wymaga pisania i utrzym yw ania. Zacznij od prostego kodu, ulepszaj go i iteruj, gdy zaistnieje taka potrzeba. Preferuję narzędzia, które są zgodne z filozofią uniksową: „wy konaj jedną rzecz i zrób to dobrze". W ielkie środowiska program istyczne m ogą być ku szące, ponieważ oferują w jednym miejscu w szystko (porządnie do siebie dopasowane), co m ożna sobie w yobrazić. Jednakże nieuchronne jest to, że w cześniej czy później bę dziesz potrzebow ać czegoś, co w wielkim środowisku nie zostało przewidziane. W takiej sytuacji nawet modyfikacje, które wydają się proste (pod względem pojęciowym), zamie niają się w koszm ar (pod w zględem program istycznym ). W ielkie projekty i ich wszystko obejmujące interfejsy API uginają się zw ykle pod w łasnym ciężarem. Używaj modular nych, wyspecjalizowanych narzędzi komunikujących się za pomocą interfejsów API, które są jak najm niejsze i najprostsze. W ybieraj formaty tekstowe, które mogą być sprawdzane z wykorzystaniem prostych m etod w izualnych, chyba że z uwagi na w ydajność musisz w ybrać coś innego.
p rz y p a d k u p o to k ó w d a n y c h k o rz y sta j z ro z s ą d n y c h , rę c z n y c h m e to d s p r a w d z a n ia . Podczas optymalizowania systemów przetwarzania danych łatwo pozostać w trybie „binar nego nastawienia", używając „wąskich" potoków, efektywnych, binarnych formatów danych i skompresowanych danych operacji wejścia-wyjścia. Gdy dane są przesyłane w systemie jako niejawne i niespraw dzone (być może z wyjątkiem ich typu), pozostaną niewidoczne do momentu, aż coś jaw nie przestanie działać. W takiej sytuacji rozpoczyna się debugowanie. Jestem orędownikiem stosowania w kodzie kilku prostych komunikatów dziennika, które informują o wyglądzie danych na różnych wewnętrznych etapach przetwarzania. Jest to dobra praktyka, lecz w żadnej wyszukanej formie, a jedynie analogicznej do sposobu działania uniksowego polecenia head , które w ybiera kilka punktów danych i dokonuje ich wizualizacji. N ie tylko ułatwia to w spom niane debugowanie, ale m ożliw ość zobaczenia danych w formacie zrozum iałym dla człowieka prowadzi do zaskakujących m om entów „aha!" nawet wtedy, gdy wszystko wydaje się dobrze przebiegać. Dziwna dedukcja! Obie cali, że dane wejściowe zawsze będą kodowane w formacie l a t i n l ! W jaki sposób trafił tam dokument w tym języku? Pliki obrazów trafiły do potoku, który oczekuje plików teksto wych i analizuje je! Często są to spostrzeżenia, które w ykraczają poza te, które w ywołuje sprawdzanie zautom atyzowane lub ustalony test jednostkowy. Pozwalają zw rócić uwagę na kw estie, które w ykraczają poza te zw iązane z kom ponentam i. Rzeczyw iste dane są nieuporządkow ane. W ychw yć w cześnie naw et takie elem enty, które niekoniecznie do prow adziłyby do w ystąpienia w yjątków lub oczyw istych błędów . W arto przesadzać ze szczegółowością.
• W
• O s tro ż n ie p o d c h o d ź d o f a n a b e r ii.
Tylko to, że klient ciągle słyszy o produkcie X i twierdzi, że musi go mieć, nie oznacza, że rzeczywiście go potrzebuje. Ponieważ m oże to być raczej problem natury marketingowej niż technologicznej, postaraj się rozróżnić te dwie kwestie i odpowiednio reagować. Produkt X zmienia się z upływem czasu wraz z pojawianiem się i przemijaniem medialnego szumu związanego z danym rozwiązaniem. W ostatnim czasie symbol X może oznaczać dane o dużej pojemności.
Dobrze, wystarczy już biznesow ych wywodów. Pora opisać, jak udało mi się sprawić, że al gorytm word2vec napisany w języku Python zadziałał szybciej niż kod C.
312
|
Rozdział 12. Rady specjalistów z branży
Rady dotyczące optymalizacji word2vec (https://code.google.com /p/w ord2vec/) to algorytm techniki głębokiego uczenia, który umożliwia wykrywanie podobnych słów i fraz. Biorąc pod uwagę interesujące zastosowania z dziedziny analizy tekstu i optymalizacji wyszukiwarek (SEO), a także powiązanie z „lśniącą" marką Google, w iele nowych i już istniejących firm zdecydow ało się na skorzystanie z tego nowego narzędzia. N iestety jedyny dostępny kod został stw orzony przez samą firm ę G oogle. Jest to napisane w języku C narzędzie open source działające w trybie wiersza poleceń systemu Linux. Narzę dzie zostało dobrze zoptymalizowane, ale raczej trudno go użyć jako im plementacji. Podsta w ow ym pow odem , dla którego zdecydow ałem się na przeniesienie narzędzia word2vec do kodu Python, była m ożliw ość wykorzystania go na innych platformach. Dzięki temu narzę dzie będzie łatwiejsze do integracji i rozszerzania dla klientów. Choć w tym miejscu szczegóły nie m ają znaczenia, należy w spomnieć, że narzędzie word2vec wymaga fazy uczenia z wieloma danymi wejściow ymi, aby m ogło w ygenerować przydatny model podobieństw. Na przykład pracownicy firmy Google uruchomili to narzędzie dla swoje go zbioru danych usługi GoogleNews, przeprow adzając uczenie z wykorzystaniem około 100 m iliardów słów. Oczywiście tej wielkości zbiory danych nie zm ieszczą się w pamięci RAM, dlatego musi zostać zastosow ana metoda pozwalająca efektywnie używ ać pamięci. Stw orzyłem bibliotekę uczenia m aszynow ego gensim (http://radim rehurek.com /gensim /), która przeznaczona jest właśnie do tego rodzaju problem u optymalizacji pamięci. M owa o zbiorach danych, które nie są już trywialne (tak określa się zbiory w całości mieszczące się w pamięci RAM), a przy tym nie są aż tak duże, aby wym agać klastrów kom puterów platform y M apReduce z pojem nościam i w yrażanym i w petabajtach. Taki „terabajtow y" problem pasuje do zaska kująco dużej liczby rzeczywistych przypadków, w tym do narzędzia word2vec. Co praw da szczegóły zostały opisane na m oim blogu (http://radim rehurek.com /2013/09/deeplearning-w ith-w ord2vec-and-gensim /), ale w tym miejscu należy przyw ołać kilka następujących wniosków: • S tru m ie n iu j d a n e i m o n ito r u j p a m ię ć .
Pozwól na to, aby dane wejściowe były używane i przetwarzane po jednym punkcie danych naraz. Zapewni to niewielkie, ciągłe obciążenie pamięci. W celu zw iększenia wydajności strumieniowane punkty danych (w przypadku narzędzia word2vec są to zdania) m ogą być wew nętrznie grupowane w większe porcje (np. przetw arzanych m oże być jednocześnie 100 zdań), ale wysokopoziom owy, strumie niow any interfejs A PI okazał się elastyczną abstrakcją o dużych m ożliw ościach. Język Python obsługuje ten wzorzec w bardzo naturalny i elegancki sposób za pom ocą swoich w budow anych generatorów (napraw dę piękne dopasow anie problem u do technologii). Unikaj decydowania się na algorytmy i narzędzia, które ładują wszystko do pamięci RAM, chyba że wiesz, że dane zaw sze pozostaną małe, albo nie masz nic przeciwko późniejszej ponownej im plementacji wersji produkcyjnej w e w łasnym zakresie.
. Na początku utw orzyłem czytelny i przejrzysty port narzędzia word2vec za pomocą biblioteki numpy. Choć bibliotekę omówiono obszernie w rozdziale 6., w ramach krótkiego przypomnienia należy podkre ślić, że jest to znakom ita biblioteka, kam ień m ilow y społeczności naukow ej zw iązanej z językiem Python, a także de facto standard obliczeń liczbow ych na bazie tego języka. Zastosow anie bogatych w m ożliw ości interfejsów tablic tej biblioteki, w zorców dostępu do pamięci i opakowanych procedur BLAS do wykonywania wyjątkowo szybkich i typowych
• S k o rz y s ta j z ro z b u d o w a n e g o „ e k o s y s te m u " ję z y k a P y th o n
Technika głębokiego uczenia prezentowana przez firmę RadimRehurek.com
|
313
operacji w ektorow ych pozw ala uzyskać zw ięzły, przejrzysty i szybki kod. Taki kod jest setki razy szybszy od naiw nego kodu Python. Norm alnie na tym bym poprzestał, ale „setki razy szybszy" kod nadal jest 20 razy wolniejszy niż zoptymalizowana wersja kodu C. Z tego powodu nie ustawałem w wysiłkach. • P ro filu j i k o m p ilu j n a jb a rd z ie j w y m a g a ją c e p o rc je k o d u .
Narzędzie word2vec to typowa aplikacja obliczeniowa o dużej wydajności. W jej przypadku kilka w ierszy kodu w jednej pętli w ew nętrznej odpow iada za 90% całego czasu działania zw iązanego z uczeniem . Ponownie w języku C napisałem program dla jednego rdzenia (w przybliżeniu 20 wierszy kodu), używ ając w roli łącznika zewnętrznej biblioteki języka Python, czyli Cython. Choć pod względem technicznym jest znakomita, jeśli chodzi o koncepcję, nie uważam tej bi blioteki za szczególnie wygodne narzędzie. Zasadniczo korzystanie z niej można porów nać do nauki kolejnego języka. Biblioteka Cython, z którą zw iązane są zastrzeżenia i oso bliw ości, stanow i pozbaw ione intuicyjności połączenie języka Python, biblioteki numpy i języka C. Dopóki jednak nie dojrzeją technologie kom pilacji JIT (Just in Time) języka Python, biblioteka Cython to praw dopodobnie najlepsza propozycja. Po uzyskaniu klu czow ej porcji kodu w postaci skom pilow anej za pom ocą biblioteki Cython w ydajność portu narzędzia word2vec bazującego na kodzie Python odpowiada osiąganej w przypadku oryginalnego kodu C. Dodatkową korzyścią wynikającą z rozpoczęcia od wersji opartej na bibliotece numpy jest to, że uzyskuje się darmowe testy popraw ności przez porównanie z w olniejszą, lecz popraw ną wersją.
• P o z n a j u ż y w a n e p ro ce d u ry B L A S .
Pożyteczną funkcją biblioteki numpy jest to, że, gdy to jest możliwe, wewnętrznie opakowuje procedury BLAS (Basic Linear Algebra Subprograms). Są to zestawy niskopoziomowych procedur zoptymalizowanych bezpośrednio przez pro ducentów procesorów (Intel, AM D itp.) w asemblerze, języku Fortran lub języku C. Proce dury te mają za zadanie uzyskać maksymalną wydajność w przypadku konkretnej archi tektury procesora. Na przykład wywołanie procedury BLAS axpy pow oduje wykonanie obliczenia dla operacji v e c t o r _ y += s c a l a r * v e c t o r _ x szybciej od zw ykłego kom pilatora użytego dla odpowiadającej tej operacji jawnej pętli f o r . Przeprow adzenie uczenia narzę dzia word2vec przy użyciu operacji BLAS spow odow ało dodatkow e czterokrotne przy spieszenie i zdystansow anie pod w zględem w ydajności w ersji narzędzia napisanej w ję zyku C. Zwycięstwo! N ależy jednak w spom nieć o tym, że kod C rów nież m oże zostać pow iązany z procedu rami BLAS, dlatego nie jest to korzyść m ożliw a do osiągnięcia w yłącznie w przypadku języka Python. Biblioteka numpy po prostu realizuje to w w yróżniający się sposób, a po nadto ułatwia skorzystanie z tej możliwości.
• P r z e tw a rz a n ie ró w n o le g łe i w ie le r d z e n i.
Biblioteka gen sim zawiera im plementacje kilku algorytmów dla klastra rozproszonego. W przypadku narzędzia word2vec zdecydowałem się na użycie wielow ątkowości na jednym komputerze z powodu dużej dokładności po w iązanego z nią algorytm u uczenia. Użycie w ątków pozw ala też uniknąć problem ów interfejsu POSIX związanych z rozwidlaniem bez w ykonyw ania, które pow oduje prze tw arzanie w ieloprocesorow e oparte na kodzie Python, zw łaszcza w połączeniu z okre ślonymi bibliotekam i BLAS. Ponieważ procedura obsługi rdzeni znajduje się już w bi bliotece Cython, m ożem y pozw olić sobie na zw olnienie blokady GIL (Global Interpreter Lock; zajrzyj do podrozdziału „Przetwarzanie równoległe rozwiązania na jednym kom puterze z wykorzystaniem interfejsu O penM P" w rozdziale 7.) interpretera języka Python, co norm alnie powoduje, że wielow ątkowość staje się bezużyteczna w przypadku zadań intensywnie korzystających z procesora. Uzyskano dodatkow e trzykrotne przyspieszenie na komputerze z czterema rdzeniami.
314
|
Rozdział 12. Rady specjalistów z branży
• A lo k a c je p a m ię c i s ta ty c z n e j.
Na tym etapie w ciągu sekundy przetw arzane są dziesiątki tysięcy zdań. Uczenie przebiega tak szybko, że naw et niewielkie operacje, takie jak two rzenie nowej tablicy narzędzia numpy (oznacza to wywołanie funkcji m a llo c dla każdego strumieniowanego zdania), pow odują zmniejszenie wydajności. Rozw iązanie polega na w stępnej alokacji statycznej pam ięci „roboczej" i przekazyw aniu jej w starym , dobrym stylu języka Fortran. W zruszyłem się. W ynikający z tego w niosek jest taki, że w jak naj większym możliwym stopniu należy obsługiwać rejestrowanie i logikę aplikacji w postaci czystego kodu Python, a ponadto dbać o to, aby zoptym alizowane, najbardziej w ym aga jące porcje kodu były gotowe do efektywnej pracy.
• O p ty m a liz a c je s p e c y fic z n e d la p r o b le m u .
Oryginalna im plementacja w postaci kodu C zawierała specyficzne mikrooptymalizacje, takie jak dostosowanie tablic do konkretnych limitów wielkości pamięci lub wykonanie wstępnych obliczeń dla określonych funkcji w celu uzyskania tabel w yszukiw ania pam ięci. Choć budzi to nostalgiczne w spom nie nia, przy obecnych złożonych potokach instrukcji procesora, hierarchiach pamięci pod ręcznej i koprocesorach takie optym alizacje nie są już pewnym zwycięzcą. Staranne pro filowanie może dać kilkuprocentowy w zrost wydajności, co m oże nie być w arte kosztu w postaci złożoności dodatkowego kodu. W niosek jest taki, że należy użyć narzędzi do tworzenia adnotacji i profilowania w celu zidentyfikowania kiepsko zoptymalizowanych porcji kodu. Skorzystaj z wiedzy z tej dziedziny, aby zastosow ać aproksymacje algoryt miczne, które kosztem dokładności zw iększą wydajność (lub odwrotnie). Nigdy jednak nie przyjmuj niczego na wiarę. Przeprowadzaj profilowanie, jeśli to m ożliwe, używając rzeczywistych danych produkcyjnych.
Podsumowanie W ykonuj optym alizację, o ile zachodzi taka potrzeba. Z doświadczenia wiem, że nigdy nie należy ograniczać kom unikacji z klientem , aby w pełni ustalić zakres problem u, priorytety i powiązanie z celami biznesowymi klienta (jest to też określane mianem optymalizacji „na po ziomie ludzkim"). Upewnij się, że zajmujesz się problemem w sposób zapewniający konkretne korzyści, a nie zatracasz się dla zasady w „sprawach technicznych tylko dla znaw ców ". Zabie rając się do pracy, rób to z sensem!
Uczenie maszynowe o dużej skali gotowe do zastosowań produkcyjnych w firmie Lyst.com Sebastjan Trepca (lyst.com) Firma Lyst.com z siedzibą w Londynie oferuje mechanizm rekomendacji związany ze światem mody. Miesięcznie korzystają z niego ponad 2 miliony użytkowników, którzy dowiadują się o nowościach w modzie dzięki realizowanym przez firmę Lyst procesom modelowania, oczyszczania i wyodrębniania danych z witryn internetowych. Firma założona w 2010 r. po zyskała środki inwestycyjne w wysokości 20 milionów dolarów. Przy tworzeniu firmy Sebastjan Trepca zajmował się kwestiami technicznymi. Obecnie jest w niej dyrektorem ds. technicznych. Stworzył witrynę, korzystając z narzędzia Django. Język Python ułatwił zespołowi szybkie sprawdzanie nowych pomysłów.
Uczenie maszynowe o dużej skali gotowe do zastosowań produkcyjnych w firmie Lyst.com
|
315
Rola języka Python w witrynie Lyst Język Python i narzędzie Django stanowiły fundament witryny internetowej Lyst od momentu jej powstania. Wraz z powiększaniem się projektów wewnętrznych część komponentów kodu Python zastąpiono innym i narzędziam i i językam i, aby dopasować je do rosnących potrzeb systemu.
Projekt klastra Klaster bazuje na technologii Am azon EC2. W sumie znajduje się w nim około 100 kom pute rów, w tym now sze instancje C3 oferujące dobrą w ydajność procesorów. System Redis jest używ any do kolejkowania z wykorzystaniem narzędzia PyRes i przecho wywania metadanych. Aby dane były zrozumiałe dla użytkowników, dominującym formatem jest format JSON. Aktyw ność procesów zapewnia demon supervisord. M echanizm Elasticsearch i narzędzie PyES służą do indeksow ania w szystkich produktów . Klaster mechanizmu Elasticsearch przechowuje 60 milionów dokumentów przy użyciu siedmiu komputerów. Sprawdzano platformę Solr, ale zrezygnow ano z niej z powodu braku funkcji aktualizowania w czasie rzeczywistym.
Ewolucja kodu w szybko rozwijającej się nowej firmie Lepiej napisać kod, który może zostać szybko zaimplementowany w celu umożliwienia spraw dzenia pomysłu biznesowego, niż pośw ięcać w iele czasu na podejm owanie prób utworzenia „doskonałego kodu" przy pierwszym podejściu. Jeśli kod okaże się przydatny, może być refaktoryzowany. Jeśli pomysł powiązany z kodem jest kiepski, usunięcie go i zrezygnowanie z danej funkcji nie będzie kosztowne. M oże to doprowadzić do powstania złożonej bazy kodu z wie loma przemieszczanymi obiektami, ale jest to do zaakceptow ania, pod w arunkiem że człon kowie zespołu znajdą czas na refaktoryzowanie kodu, który zapewni korzyści biznesowe. W witrynie Lyst intensywnie wykorzystyw ane są notki dokumentacyjne. W ypróbowano ze wnętrzny system dokumentowania Sphinx, ale zrezygnowano z niego na rzecz zwykłego czy tania kodu. Serwis wiki służy do dokumentowania procesów i większych systemów. Zaczęliśmy również tworzyć bardzo małe usługi, zamiast łączyć wszystkie elementy w jednej bazie kodu.
Budowanie mechanizmu rekomendacji Początkow o kod m echanizm u rekom endacji tw orzono w języku Python, w ykorzystując bi blioteki numpy i scipy do przeprow adzania obliczeń. Później za pom ocą kom pilatora Cython przyspieszono części mechanizmu, w przypadku których kluczowe znaczenie ma w ydajność. Podstaw ow e operacje faktoryzacji m acierzy napisano całkow icie w języku Cython, dzięki czemu uzyskano przyspieszenie o rząd wielkości. W ynikało to głównie z możliwości tworze nia w kodzie Python w ydajnych pętli z w ykorzystaniem tablic biblioteki numpy. W czystym kodzie Python takie rozw iązanie jest w yjątkow o w olne, a ponadto m a kiepską w ydajność podczas wykonywania wektoryzacji, ponieważ wymaga tworzenia kopii tablic biblioteki numpy. Jest to spowodowane wyszukanym indeksowaniem używanym przez tę bibliotekę, w ramach którego zaw sze tworzona jest kopia danych dzielonej tablicy: jeśli kopia danych nie jest po trzebna lub w skazana, pętle języka Cython będą znacznie szybsze.
316
|
Rozdział 12. Rady specjalistów z branży
Z czasem komponenty sieciowe systemu (odpowiedzialne za obliczanie rekomendacji w mo mencie pojaw ienia się żądania) zostały zintegrow ane z kom ponentem w yszukiw ania, czyli m echanizmem Elasticsearch. W ramach realizowanego procesu kom ponenty te zostały prze kształcone w kod Java, aby możliwa była pełna integracja z tym m echanizmem . Podstawo wym powodem była nie w ydajność, lecz wykorzystanie integracji komponentu rekom endu jącego z pełnymi możliw ościam i w yszukiw arki. Umożliwia to łatwiejsze zastosowanie reguł biznesowych dla udostępnianych rekomendacji. Sam kom ponent Java jest w yjątkowo prosty i implementuje przede wszystkim efektywne iloczyny wewnętrzne wektorów rzadkich. Bar dziej złożony kom ponent niesieciow y nad al tw orzony je st w języku Python przy użyciu standardowych komponentów bazującego na języku Python stosu do zastosowań naukowych (głównie kod Python i Cython). Z naszego doświadczenia wiemy, że język Python okazuje się bardziej przydatny niż tylko jako język prototypowania: dostępność takich narzędzi jak numpy, Cython i weave (a w ostatnim czasie Numba) pozwoliła nam osiągnąć bardzo dobrą w ydajność w porcjach kodu, w przy padku których jest ona kluczowa. Jednocześnie zachowano przejrzystość i w yrazistość kodu Python tam, gdzie optymalizacja niskopoziomowa byłaby nieefektywna.
Raportowanie i monitorowanie Narzędzie Graphite jest używ ane do raportowania. Obecnie spadki wydajności m ogą być ob serwowane po wdrożeniu metodą wizualną. Ułatwia to analizowanie szczegółowych raportów dotyczących zdarzeń lub stosowanie powiększenia w celu wyświetlenia ogólnego raportu działania witryny. W razie potrzeby możliwe jest dodawanie i usuw anie zdarzeń. Zajmujemy się projektowaniem większej infrastruktury służącej do testowania wydajności. Aby było możliwe właściwe testowanie nowych wersji witryny, będzie ona uwzględniać dane reprezentatywne i przypadki użycia. Stosowana jest też w itryna pomostowa, by umożliwić niewielkiej grupie rzeczywistych użyt kowników zobaczenie najnowszej wersji wdrożenia. Jeśli zostanie znaleziony błąd lub stwier dzony spadek wydajności, będzie to dotyczyć wyłącznie mniejszości odwiedzających, a po nadto taką wersję będzie można szybko wycofać. Dzięki temu zajmowanie się błędami będzie znacznie mniej kosztowne i problematyczne. Narzędzie Sentry służy do rejestrowania i diagnozowania śladów stosu języka Python. N arzędzie Jenkins używ ane jest na potrzeby ciągłej integracji z konfiguracją bazy danych um ieszczoną w pam ięci. Umożliwia to równoległe testowanie, dzięki któremu sprawdzenia szybko informują programistę o wszelkich błędach.
Rada N aprawdę ważne jest korzystanie z dobrych narzędzi, które m onitorują efektyw ność tego, co tworzysz. Poza tym na początku pracy jest to w yjątkowo praktyczne. Nowe firmy ciągle się zmieniają, a inżynieria rozwija się: zaczynasz od rozbudowanej fazy przygotowawczej, cały czas budujesz prototypy i usuwasz kod do momentu trafienia na żyłę złota, a następnie prze chodzisz do dalszego etapu, ulepszając kod, zwiększając w ydajność itp. Do tej chwili w szyst ko sprowadza się do szybkich iteracji oraz dobrego monitorowania/analizy. Przypuszczam, że jest to napraw dę standardowa rada, która była wiele razy powtarzana, ale uważam, że w iele osób w rzeczywistości nie uświadamia sobie jej szczególnej rangi.
Uczenie maszynowe o dużej skali gotowe do zastosowań produkcyjnych w firmie Lyst.com
|
317
Poniew aż nie sądzę, że obecnie technologie m ają aż tak duże znaczenie, użyj tych, które uznasz za najwłaściwsze. Zastanowiłbym się jednak dwa razy przed podjęciem decyzji o za stosowaniu takich środowisk udostępnianych jak AppEngine lub Heroku.
Analiza serwisu społecznościowego o dużej skali w firmie Smesh Alex Kelly (sme.sh) W firmie Smesh tworzymy oprogramowanie, które pobiera dane z różnorodnych interfejsów API obecnych w witrynach internetowych, filtrach i procesach. Dane są agregowane, a na stępnie używ ane do budow ania zam ów ionych aplikacji dla różnych klientów . Na przykład zapew niam y technologię, która obsługuje filtrow anie i strum ieniow anie w pisów na m ikroblogu w aplikacji telewizyjnej serwisu Beamly z funkcją drugiego ekranu, platform ę m onito rowania m arek i kampanii sieci mobilnej EE oraz dla firmy Google grupę projektów związa nych z analizą danych usługi Adwords. Aby to było m ożliw e, stosujem y różne usługi strum ieniow ania i odpytyw ania, które często odpytują serwisy Twitter, Facebook, YouTube oraz m nóstwo innych usług w celu uzyskania treści i przetwarzania dziennie kilku milionów w pisów na mikroblogu.
Rola języka Python w firmie Smesh Język Python jest przez nas intensywnie wykorzystywany. Za pomocą tego języka zbudowano większość naszej platformy i usług. Dostępnych jest wiele różnych bibliotek, narzędzi i środo wisk, które umożliwiają wszechstronne zastosowanie języka Python w większości przypadków. Opisana różnorodność zapewnia nam m ożliw ość (taką m am y nadzieję) wybrania właściwego narzędzia do realizacji konkretnego zadania. Na przykład stworzyliśmy aplikacje, korzystając z narzędzi Django, Flask i Pyramid. Każde z nich ma swoje zalety. M ożem y w ybrać to, które będzie odpow iednie do danego zadania. Do w ykonyw ania zadań używ am y narzędzia Ce lery. Narzędzie Boto umożliwia interakcję z usługą AW S. Narzędzia PyMongo, MongoEngine, redis-py, Psycopg i w iele innych pozw alają spełnić w szystkie nasze w ym agania dotyczące danych. Lista ta cały czas się rozszerza.
Platforma N asza głów na platform a składa się z centralnego m odułu z kodem Python, który zapewnia m echanizm y um ożliw iające w prow adzanie danych, filtrow anie, agregacje i przetw arzanie, a także różne inne funkcje podstawowe. Kod właściwy dla projektu im portuje funkcje z tego m odułu, a następnie im plem entuje bardziej specyficzne przetw arzanie danych i logikę w y świetlania, jakie są wymagane przez poszczególne aplikacje. Jak dotąd, takie rozwiązanie świetnie się sprawdzało. Umożliwia ono budow anie dość zło żonych aplikacji, które pobierają i przetw arzają dane z wielu różnych źródeł bez nadm ierne go duplikowania działań. Rozw iązanie to nie jest jednak pozbaw ione wad — każda aplikacja jest zależna od wspólnego modułu podstawowego. Na skutek tego realizowanie procesu aktu alizowania kodu w tym m odule i dbanie o aktualność w szystkich używ ających go aplikacji staje się poważnym wyzwaniem.
318
|
Rozdział 12. Rady specjalistów z branży
O becnie realizujem y projekt m ający na celu przebudow anie tego podstaw ow ego oprogra m ow ania tak, aby w w iększym stopniu bazow ało na architekturze SoA (Service-O riented Architecture). W ydaje się, że określenie odpowiedniego momentu na w prow adzenie tego ro dzaju zm iany architektury jest jednym z w yzw ań, przed którym i staje w iększość zespołów tw orzących oprogram ow anie, gdy platform a się rozrasta. Z budow aniem kom ponentów w postaci osobnych usług zw iązany jest dodatkowy nakład pracy. Często obszerna wiedza specjalistyczna niezbędna do zbudow ania każdej usługi jest zdobyw ana tylko przez w stępne pow tarzanie procesu projektowania, w którym obciążenie związane z architekturą stanowi przeszkodę na drodze do szybkiego rozwiązania rzeczywistego problemu. M iejm y nadzieję, że w ybraliśm y w łaściw y m om ent na ponow ne przejrzenie w cześniej zastosow anych rozwią zań architektonicznych i ich rozwinięcie. Czas pokaże.
Dopasowywanie łańcuchów w czasie rzeczywistym z dużą wydajnością Z interfejsu API Tw itter Streaming pobieranych jest m nóstw o danych. W trakcie strumienio wania w e wpisach na mikroblogu wejściowe łańcuchy są dopasowywane do zbioru słów klu czowych. Dzięki temu wiemy, z jakim i spośród śledzonych terminów powiązany jest każdy w pis na mikroblogu. N ie stanowi to dużego problemu przy niewielkiej liczbie wejściowych łańcuchów lub małym zbiorze słów kluczowych, ale dopasowywanie w przypadku setek wpi sów na m ikroblogu w ciągu sekundy z w ykorzystaniem setek lub tysięcy m ożliw ych słów kluczowych zaczyna się stawać złożone. Aby w szystko jeszcze bardziej skom plikow ać, nie jesteśm y zainteresow ani tylko tym, czy łańcuch słowa kluczowego istnieje w e w pisie na mikroblogu, ale również bardziej złożonym dopasowywaniem wzorców przy użyciu ograniczeń słów, początku i końca wiersza, a także opcjonalnie znaków # i @, które poprzedzają łańcuch. Najbardziej efektywnym sposobem zde finiowania takich w ym agań dotyczących dopasowywania jest użycie w yrażeń regularnych. Jednakże stosowanie tysięcy w zorców w yrażeń regularnych dla setek w pisów na mikroblogu w ciągu sekundy w ym aga dużej m ocy obliczeniow ej. W cześniej byliśm y zm uszeni do uru cham iania w ielu w ęzłów procesów roboczych w klastrze kom puterów , aby zagw arantow ać niezaw odne dopasowywanie w czasie rzeczywistym. Ponieważ wiedzieliśmy, że w systemie było to główne wąskie gardło ograniczające wydajność, próbowaliśmy następujących metod w celu poprawienia wydajności systemu dopasowywania: upraszczania w yrażeń regularnych, uruchamiania liczby procesów wystarczającej do zapew nienia w ykorzystania w szystkich rdzeni serw erów , zagw arantow ania, że w szystkie w zorce wyrażeń regularnych są w łaściw ie kompilowane i buforowane, wykonywania zadań dopa sowywania z wykorzystaniem kompilatora PyPy, a nie CPython itp. Każde z tych rozwiązań spowodowało niew ielki w zrost wydajności, ale było oczywiste, że umożliwi to jedynie ułam kow e skrócenie czasu przetw arzania. O czekiw aliśm y przyspieszenia o rząd w ielkości, a nie o ułamek. Jasne było, że zam iast próbow ać popraw ić w yd ajność każdego dopasow ania, m usieliśm y zm niejszyć skalę problemu przed rozpoczęciem dopasowywania wzorców . W związku z tym konieczne było zm niejszenie liczby w pisów do przetw orzenia na m ikroblogu lub liczby w zorców w yrażeń regularnych niezbędnych do dopasow yw ania w pisów . O graniczenie liczby w ejściow ych w pisów na m ikroblogu nie w chodziło w grę, poniew aż takie dane nas
Analiza serwisu społecznościowego o dużej skali w firmie Smesh
|
319
interesowały. Z tego powodu zajęliśm y się szukaniem metody zredukow ania liczby wzorców w ym aganych do porów nania w ejściow ego w pisu na m ikroblogu w celu przeprow adzenia dopasowywania. Zaczęliśm y się przyglądać różnym strukturom drzew trie, aby m ieć m ożliw ość bardziej efektyw nego dopasow yw ania w zorców dla zbiorów łańcuchów . N atknęliśm y się na algo rytm dopasowywania łańcuchów Aho-Corasick. Okazał się on idealny w naszym przypadku. Słownik, za pom ocą którego budow ane jest drzewo trie, musi być statyczny. N ie jest możliwe dodawanie do drzewa trie nowych elementów po zakończeniu działania automatu. Nie sta nowi to jednak dla nas problemu, ponieważ zbiór słów kluczow ych nie zmienia się w czasie trwania sesji strumieniowania z serwisu Twitter. Po zm ianie śledzonych terminów musimy w ykonać operację rozłączenia i połączenia z interfejsem API, dlatego w tym sam ym czasie możemy ponow nie zbudow ać drzewo trie algorytmu Aho-Corasick. Przetw arzanie danych w ejściow ych dla łańcuchów przy użyciu algorytm u A ho-C orasick pow oduje jed noczesne znajdow anie w szystkich m ożliw ych dopasow ań. Proces polega na krokowym analizowaniu łańcucha wejściowego po jednym znaku naraz i znajdowaniu pa sujących węzłów na następnym niżej położonym poziom ie drzewa trie (może się okazać, że taki poziom nie w ystąpi). Dzięki temu możemy bardzo szybko stwierdzić, jakie terminy słów kluczow ych m ogą istnieć w e w pisie na m ikroblogu. W dalszym ciągu nie m am y całkow itej pewności, gdyż dopasowywanie dwóch łańcuchów algorytmu Aho-Corasick w czystej posta ci nie umożliwia zastosow ania żadnej bardziej złożonej logiki, która jest zawarta w e wzor cach w yrażeń regu larn y ch . M ożem y jed n ak u żyć tego algorytm u jako filtru w stępn ego. Słow a kluczow e, które nie w ystępu ją w łańcuchu, nie m ogą zostać dopasow ane, dlatego wiemy, że m usimy jedynie sprawdzić niewielki podzbiór w szystkich wzorców w yrażeń re gularnych, bazując na słowach kluczowych pojaw iających się w tekście. Zam iast analizować setki lub tysiące w zorców w yrażeń regularnych dla każdego łańcucha w ejściow ego, w y kluczam y w iększość z nich i m usim y tylko przetw orzyć niew ielką ich liczbę dla każdego wpisu na mikroblogu. Zm niejszając do niew ielkiej grupy liczbę w zorców , jakie próbujem y dopasow ać dla każde go w ejściow ego w pisu na m ikroblogu, jesteśm y w stanie osiągnąć żądane przyspieszenie. Zależnie od złożoności drzewa trie i średniej długości w ejściow ych w pisów na m ikroblogu system dopasow yw ania słów kluczow ych m a obecnie w ydajność 10 - 100 razy w iększą niż w przypadku oryginalnej im plementacji naiwnej. Jeśli przetwarzasz wiele wyrażeń regularnych lub przeprowadzasz innego rodzaju dopasowy wanie wzorców, szczególnie polecam przyjrzenie się różnym wariantom drzew przedrostków i przyrostków, które m ogą okazać się pomocne przy szukaniu w yjątkowo szybkiego rozwią zania problemu.
Raportowanie, monitorowanie, debugowanie i wdrażanie Utrzym ujem y grupę różnych systemów z uruchomionym oprogramowaniem Python, a także resztę infrastruktury, która wszystko to obsługuje. Pozostawanie w stanie aktywności bez wy stępowania przerw m oże być trudnym zadaniem. Poniżej zaprezentowano kilka wniosków, do których doszliśmy w trakcie pracy.
320
|
Rozdział 12. Rady specjalistów z branży
N aprawdę duże korzyści zapewnia m ożliw ość stwierdzenia, co zachodzi w używ anych sys temach, zarówno w czasie rzeczywistym, jak i na podstawie danych historycznych, niezależ nie od tego, czy dotyczy to naszego w łasnego oprogramowania, czy infrastruktury, na której je uruchomiono. Narzędzie Graphite wraz z demonami c o l l e c t d i s t a t s d um ożliwia nam ob serwowanie tego, co się dzieje, na ładnych wykresach. Zapewnia to m etodę identyfikowania trendów oraz retrospektywnego analizowania problem ów w celu znalezienia ich podstawo wej przyczyny. Choć nie zaim plem entow aliśm y go jeszcze, narzędzie Etsy Skyline rów nież nadaje się znakomicie do wykrywania nieoczekiwanych zdarzeń, gdy używ anych jest więcej pomiarów, niż jesteśm y w stanie śledzić. Kolejne przydatne narzędzie to Sentry, czyli znako mity system rejestrowania zdarzeń i śledzenia wyjątków zgłaszanych w klastrze komputerów. W drożenie m oże być kłopotliwe, niezależnie od tego, co zostanie użyte do jego przeprow a dzenia. Korzystaliśm y z narzędzi Puppet, Ansible i Salt. W szystkie m ają swoje zalety i wady, ale żadne z nich nie sprawi, że problem ze złożonym wdrożeniem w magiczny sposób zniknie. Aby utrzym ać wysoką dostępność części naszych systemów, stosujemy wiele rozproszonych geograficznie klastrów infrastruktury, w przypadku której jeden system pozostaje aktywny, a pozostałe pełnią rolę dynamicznie podłączanych klastrów zapasow ych z operacją przełą czania realizowaną za pom ocą aktualizacji systemu DNS z ustaw ionym i małym i wartościami TTL (Time to Live). Oczywiście nie zaw sze jest to proste, zwłaszcza w sytuacji, gdy spójność danych wymaga restrykcyjnych ograniczeń. Na szczęście nie utrudnia nam to zbytnio pracy, dzięki czemu całe rozwiązanie jest stosunkowo przejrzyste. Zapewnia ono również dość bez pieczną strategię wdrażania, aktualizując jeden z zapasow ych klastrów i przeprowadzając te stowanie przed uaktywnieniem go i zaktualizowaniem pozostałych klastrów. W szyscy jesteśm y napraw dę podekscytow ani perspektyw ą m ożliw ości, jakie daje nam na rzędzie D ocker (http://w w w .docker.com /). W dalszym ciągu jesteśm y dopiero na etapie eks perym entow ania z tym narzędziem. Eksperymenty m ają nam pozw olić stwierdzić, jak spra w ić, by stało się ono częścią naszych procesów w drażania. Jednakże m ożliw ość szybkiego w drażania oprogram ow ania w prosty i pow tarzalny sposób ze w szystkim i jego binarnym i zależnościami i dołączonymi bibliotekami systemowym i w ydaje się bardzo bliska. Na poziomie serwera realizowanych jest wiele rutynowych zadań, które po prostu ułatwiają pracę. Narzędzie M onit znakom icie nadaje się do automatycznego monitorowania działania system ów . N arzęd zie U p start i dem on s u p e r v i s o r d spraw iają, że urucham ianie usług jest mniej kłopotliw e. N arzędzie M unin pozw ala w szybki i prosty sposób w yśw ietlać w ykresy na poziom ie systemu, jeśli nie jest używana pełna konfiguracja złożona z narzędzia Graphite i demona c o l l e c t d . Z kolei narzędzie Corosync lub Pacemaker może być dobrym rozwiąza niem w przypadku uruchamiania usług w klastrze w ęzłów (na przykład w sytuacji, gdy ko nieczne jest uruchom ienie zestawu usług w jakim ś miejscu, lecz nie wszędzie). Próbowałem tu nie tyle jedynie w ym ienić kilka modnych terminów, ile zw rócić uwagę na używane przez nas na co dzień oprogramowanie, które napraw dę wpływa na to, jak efek tywnie m ożemy w drażać i urucham iać systemy. Jeśli słyszałeś już o tych w szystkich narzę dziach, jestem pewien, że możesz się podzielić całą m asą innych przydatnych rad. Jeśli tak, proszę o kontakt w celu przekazania nam jakichś w skazówek. W przeciwnym razie sprawdź te narzędzia. Mam nadzieję, że niektóre z nich okażą się tak bardzo przydatne jak w naszym przypadku.
Analiza serwisu społecznościowego o dużej skali w firmie Smesh
|
321
Interpreter PyPy zapewniający powodzenie systemów przetwarzania danych i systemów internetowych Marko Tasic (https://github.com/mtasic85) Ponieważ wcześnie miałem okazję zdobyć duże doświadczenie z zakresu implementacji PyPy języka Python, zdecydowałem się na używanie jej wszędzie tam, gdzie było to możliwe. Z im plementacji PyPy korzystałem zarówno w niew ielkich i prostych projektach, gdzie kluczowe znaczenie miała szybkość, jak i w projektach średniej wielkości. Pierwszym projektem, w któ rym jej użyłem, była im plementacja protokołów M odbus i DNP3. Później język PyPy zasto sowałem na potrzeby im plementacji algorytmu kompresji. W szyscy byli zadziw ieni jej szyb kością. Jeśli dobrze pam iętam , w ersja 1.2 języka PyPy ze standardow ym kom pilatorem JIT była pierwszą, jakiej użyłem w środowisku produkcyjnym. Przy okazji pojawienia się wersji 1.4 byliśmy pewni, że język PyPy był przyszłością wszystkich naszych projektów, ponieważ usu nięto z niego w iele błędów, a ponadto jeszcze bardziej zw iększono szybkość. Byliśm y zasko czeni tym, jak wykonanie zostało przyspieszone 2 - 3 razy tylko przez zaktualizow anie in terpretera języka PyPy do następnej wersji. Omówię dwa osobne, lecz ściśle pow iązane ze sobą projekty, w których 90% kodu jest iden tyczne. Aby jednak nie utrudniać lektury, w odniesieniu do obu projektów będę posługiwał się określeniem „projekt". Projekt polegał na utworzeniu systemu, który gromadzi gazety, czasopisma i blogi, w razie po trzeby stosuje technikę optycznego rozpoznawania tekstu OCR (Optical Character Recognition), klasyfikuje je, tłumaczy, używa analizy sentymentu, bada strukturę dokumentów i indeksuje je w celu późniejszego wyszukiwania. Użytkownicy mogą wyszukiwać słowa kluczowe w dowol nym z dostępnych języków i pobierać informacje o zindeksow anych dokumentach. Ponieważ wyszukiwanie obejmuje wiele języków, użytkownicy mogą wpisywać słowa po angielsku i uzy skiwać wyniki w języku francuskim. Ponadto użytkownicy będą otrzymywać artykuły i słowa kluczow e w yróżnione na stronie dokum entu z inform acjam i o zajm ow anym m iejscu i cenie publikacji. Bardziej zaaw ansow any w ariant użycia uw zględniałby generow anie raportów , w przypadku których użytkow nicy m ogliby w yśw ietlić tabelaryczny w idok w yników ze szczegółowymi informacjami dotyczącymi nakładów finansowych poniesionych przez kon kretną firmę na reklamę zam ieszczoną w monitorowanych gazetach, czasopismach i blogach. O prócz tego system m oże też „zgadyw ać", czy artykuł jest płatny lub obiektyw ny, a także określać jego w ydźwięk.
Wymagania wstępne Oczywiście język PyPy był naszą ulubioną im plementacją języka Python. Na potrzeby bazy danych użyliśmy produktów Cassandra i Elasticsearch. Serwery buforujące korzystały z syste mu Redis. W roli rozproszonej kolejki zadań (procesy robocze) zastosow aliśm y narzędzie Celery, a dla jej brokera wykorzystaliśmy narzędzie RabbitMQ. W yniki były przechow yw ane w zapleczu systemu Redis. Później kolejka Celery używała systemu Redis niemal wyłącznie na potrzeby brokerów i zaplecza. Użyliśmy mechanizmu rozpoznawania OCR Tesseract. W roli serwera i m echanizmu tłumaczenia języków zastosow aliśm y narzędzie M oses. Do przeszu kiwania witryn internetowych użyliśmy narzędzia Scrapy. W celu rozproszonego blokowania w całym systemie stosujemy serwer ZooKeeper, ale początkowo wykorzystyw aliśmy do tego
322
|
Rozdział 12. Rady specjalistów z branży
system Redis. Aplikacja internetowa bazuje na znakomitym środowisku Flask do projektowa nia oprogramowania internetowego oraz na wielu jego rozszerzeniach, takich jak Flask-Login, Flask-Principal itp. Aplikacja Flask była udostępniana przez serwery Gunicorn i Tornado na każdym serwerze W W W . Narzędzie nginx było używ ane jako serwer proxy odwrotnego dla serwerów WW W . Reszta kodu została napisana przez nas. Jest to czysty kod Python bazujący na języku PyPy. Cały projekt jest udostępniany w naszej lokalnej, prywatnej chm urze OpenStack. Zależnie od wymagań, które m ogą zm ieniać się dynamicznie, w chmurze wykonyw anych jest od 100 do 1000 instancji systemu ArchLinux. Co 6 - 12 m iesięcy cały system w ykorzystuje maksymalnie 200 TB przestrzeni dyskowej, zależnie od w spom nianych wymagań. Całe przetw arzanie jest realizowane przez nasz kod Python, z wyjątkiem m echanizmu OCR i tłumaczenia.
Baza danych Stworzyliśmy pakiet kodu Python, który ujednolica klasy modeli dla narzędzi Cassandra, Elasticsearch i Redis. Jest to proste m apow anie obiektow o-relacyjne ORM (O bject Relational M apper), które m apuje wszystko do postaci słownika lub listy słowników w przypadku, gdy z bazy danych pobieranych jest wiele rekordów. Ponieważ narzędzie Cassandra 1.2 nie obsługiwało złożonych zapytań dla indeksów, w celu zapewnienia ich obsługi użyliśmy zapytań przypom inających złączenia. Jednakże um ożliw i liśmy wykonyw anie złożonych zapytań dotyczących niew ielkich zbiorów danych (o m aksy malnej wielkości wynoszącej 4 GB), gdyż spora ich część znajdowała się w pam ięci i w ym a gała przetwarzania. Implem entacja PyPy była urucham iana w sytuacjach, gdy implementacja CPython nie mogła naw et załadow ać danych do pamięci, co wynikało z jej strategii stosowa nych w zględem hom ogenicznych list w celu dodatkowego zmniejszenia ich wielkości w pa m ięci. Inną zaletą im plem entacji PyPy jest to, że jej kom pilacja JIT jest stosowana w pętlach, w przypadku których miała miejsce modyfikacja lub analiza danych. Napisaliśmy kod w taki sposób, aby typy pozostały statyczne w ew nątrz pętli, poniew aż w łaśnie w ich obrębie kod kompilowany za pomocą kompilatora JIT szczególnie dobrze się sprawdza. N arzędzie Elasticsearch było używ ane do indeksow ania i szybkiego w yszukiw ania doku mentów. Okazuje się ono bardzo elastyczne w przypadku złożonych zapytań, dlatego nie mie liśmy z nim żadnych pow ażniejszych problemów. Jeden z problemów, które wystąpiły, doty czył aktualizowania dokumentów. Narzędzie to nie jest odpowiednie dla szybko zmieniających się dokumentów. Z tego powodu dla tej części dokumentów musieliśmy użyć narzędzia Cassandra. Inne ograniczenie było związane z fasetami i pamięcią wymaganą w instancji bazy danych, ale zostało ono wyeliminowane dzięki zastosowaniu mniejszych zapytań, a następnie ręczne mo dyfikowanie danych w procesach roboczych kolejki Celery. Nie wystąpiły żadne większe pro blemy między interpreterem języka PyPy i biblioteką PyES używaną do interakcji z pulami serwera Elasticsearch.
Aplikacja internetowa Jak wcześniej w spomniano, używaliśmy środowiska Flask z jego zewnętrznymi rozszerze niami. Początkowo zaczęliśmy wszystko w środowisku Django, ale zastąpiliśm y je środowi skiem Flask z powodu szybkich zm ian w wymaganiach. Nie oznacza to, że środowisko to jest lepsze od środowiska Django. Po prostu uznaliśm y, że w tym drugim łatwiej będzie nam
Interpreter PyPy zapewniający powodzenie systemów przetwarzania danych i systemów internetowych
| 323
analizować kod, gdyż układ projektów w środowisku Flask jest bardzo elastyczny. Produkt Gunicorn został zastosowany jako serwer HTTP W SGI (Web Server Gateway Interface), a jego pętla wejścia-wyjścia była w ykonywana przez narzędzie Tornado. Pozwoliło to nam uzyskać dla jednego serw era W W W m aksym alnie 100 jed noczesnych połączeń. Taka liczba była m niejsza od oczekiw anej, poniew aż w iele zapytań użytkow ników m oże zająć dużo czasu — w przypadku takich żądań ma m iejsce w iele operacji analizow ania, a dane są zw racane w ramach interakcji z użytkownikami. Początkow o aplikacja internetow a była zależna od biblioteki PIL (Python Im aging Library) w zakresie wyróżniania artykułów i słów. M ieliśmy problemy z biblioteką PIL i interpreterem języka PyPy, ponieważ w tamtym czasie z biblioteką pow iązanych było wiele „w ycieków " pamięci. W rezultacie zaczęliśmy korzystać z narzędzia Pillow, które było częściej aktualizowane. Ostatecznie stworzyliśmy bibliotekę, która prowadziła interakcję z pakietem GraphicsM agick za pośrednictwem modułu podprocesu. Im plem entacja PyPy działa dobrze, a w yniki są porów nyw alne z w ynikam i im plem entacji CPython. W iąże się to z tym, że zw ykle aplikacje internetowe są zależne od operacji wejściaw yjścia. Jednakże wraz z rozwijaniem techniki STM (Software Transactional Memory) w języku PyPy m ieliśmy nadzieję na uzyskanie w krótce skalowalnej obsługi zdarzeń na poziom ie in stancji z wieloma rdzeniami.
Mechanizm OCR i tłumaczenie N apisaliśm y biblioteki z czystym kodem Python dla narzędzi Tesseract i M oses, poniew aż m ieliśm y problem y z rozszerzeniam i zależnym i od interfejsu API CPython. Im plem entacja PyPy oferuje dobrą obsługę interfejsu API CPython z wykorzystaniem narzędzia CPyExt, ale w ym agaliśm y w iększej kontroli nad tym , co zachodzi w ew nątrz. W efekcie uzyskaliśm y rozwiązanie zgodne z im plem entacją PyPy, którego kod jest trochę szybszy niż w przypadku im plem entacji CPython. Przyczyną m niejszej w ydajności dla tej im plem entacji było to, że w iększość przetw arzania była realizow ana przez kod C/C++ narzędzi Tesseract i M oses. M ogliśmy jedynie przyspieszyć przetw arzanie danych w yjściow ych i budow anie struktury dokumentów z wykorzystaniem kodu Python. Na tym etapie nie występowały żadne większe problemy ze zgodnością implementacji PyPy.
Dystrybucja zadań i procesy robocze Kolejka Celery zapewniła możliwości pozwalające na uruchomienie wielu zadań w tle. Typowe zadania to rozpoznaw anie tekstu z wykorzystaniem m echanizm u OCR, tłum aczenie, analiza itp. Choć całość procesu m ogłaby zostać zrealizow ana za pom ocą środow iska Hadoop for M apReduce, zdecydow aliśm y się na narzędzie Celery, poniew aż w iedzieliśm y, że często mogą zm ieniać się wym agania projektowe. U żyw aliśm y około 20 procesów roboczych, z których każdy zaw ierał od 10 do 20 funkcji. Prawie wszystkie funkcje m iały pętle lub w iele pętli zagnieżdżonych. Dbaliśmy o to, aby ty py pozostały statyczne. Dzięki temu kompilator JIT mógł realizow ać swoje zadania. Końco wym rezultatem była 2 - 5 razy w iększa szybkość w porównaniu z im plem entacją CPython. Pow odem , dla którego nie uzyskaliśm y lepszych przyspieszeń, było to, że używ ane przez nas pętle były stosunkowo małe (zawierały od 20 do 100 tysięcy iteracji). W niektórych sytu acjach, w których konieczna była analiza na poziom ie słów, wykonyw anych było ponad m i lion iteracji. W tym przypadku osiągnęliśmy ponad 10-krotne przyspieszenie.
324
|
Rozdział 12. Rady specjalistów z branży
Podsumowanie Implementacja PyPy to znakomita propozycja dla każdego projektu z czystym kodem Python, który jest zależny od szybkości wykonywania czytelnego i możliwego do zarządzania kodu źródłowego o dużej wielkości. Stwierdziliśmy też, że ta im plementacja jest bardzo stabilna. Poniew aż w szystkie nasze program y długo działały z typam i statycznym i i (lub) hom oge nicznym i wewnątrz struktur danych, kompilator JIT mógł realizow ać swoje zadania. Gdy te stow aliśm y cały system dla im plem entacji C Python, w yniki nie były dla nas zaskakujące: w porów naniu z im plem entacją C Python w przypadku im plem entacji PyPy uzyskaliśm y w przybliżeniu dwukrotne przyspieszenie. Z punktu widzenia naszych klientów oznaczało to dwa razy lepszą wydajność za tę samą cenę. M amy nadzieję, że oprócz w szystkich korzyści, jakie dotąd zapewniła nam im plementacja PyPy, jej wersja techniki STM umożliwi nam ska lowalne wykonyw anie równoległe w przypadku kodu Python.
Kolejki zadań w serwisie internetowym Lanyrd.com Andrew Godwin (lanyrd.com) Lanyrd to w itryna internetow a oferująca znajdow anie konferencji z w ykorzystaniem sieci społecznościow ych. Po zalogow aniu naszym użytkow nikom sugerow ane są odpow iednie konferencje. W tym celu korzystam y z w ykresów bazujących na sieciach społecznościow ych i danych znajom ych użytkowników, a także z innych wskaźników, takich jak branża, z jaką są związani użytkownicy, lub ich lokalizacja geograficzna. Głównym zadaniem witryny jest w ydobycie z nieprzetw orzonych danych czegoś, co można zaprezentow ać użytkow nikom . D okładniej rzecz biorąc, jest to lista konferencji w postaci rankingu. Zadanie to musi być realizowane w trybie offline, ponieważ lista rekomendowanych konferencji jest odśw ieżana co kilka dni, a ponadto korzystam y z zew nętrznych interfejsów A PI, które często są pow olne. Używ am y rów nież kolejki zadań Celery na potrzeby innych czasochłonnych zadań, takich jak pobieranie m iniatur dla odnośników udostępnianych przez internautów oraz wysyłanie wiadomości e-mail. Codziennie w kolejce znajduje się sporo ponad 100 000 zadań, a czasami znaczenie więcej.
Rola języka Python w serwisie Lanyrd Od początku istnienia serwis Lanyrd był tworzony z wykorzystaniem języka Python i narzę dzia Django. Niemal każdy element serwisu napisano przy użyciu języka Python — samą w itrynę internetową, składniki przetwarzania w trybie offline, używane przez nas narzędzia statystyczne i analizy, serwery zaplecza dla urządzeń przenośnych oraz system wdrażania. Jest to bardzo w szechstronny i dojrzały język, który pozw ala w niezw ykle prosty sposób szybko tworzyć kod. W ynika to głównie z dostępności dużej liczby bibliotek. Ponadto język ten cechuje się składnią o dużej przejrzystości i zwięzłości. Oznacza to, że kod Python może być łatwo aktualizowany i refaktoryzowany, a także, w fazie początkow ej, tworzony. Gdy stwierdziliśmy, że niezbędna będzie kolejka zadań (miało to miejsce w e wstępnej fazie), narzędzie Celery było już dojrzałym projektem. Ponieważ reszta serwisu Lanyrd bazowała już na języku Python, kolejka Celery wpasowała się w całość w naturalny sposób. W raz z rozwo jem witryny pojaw iła się potrzeba zm iany w spierającej ją kolejki (ostatecznie została zastą piona narzędziem Redis), która generalnie oferowała bardzo dobre możliwości skalowania.
Kolejki zadań w serwisie internetowym Lanyrd.com
| 325
Na etapie rozpoczynania działalności m usieliśm y pozw olić sobie na pew ne techniczne nie dociągnięcia, aby m ożliw y był postęp. Coś takiego jest po prostu niezbędne. Jeśli tylko w ia domo, jakie są z tym zw iązane problemy, a ponadto kiedy m ogą się pojawić, niekoniecznie jest to zła praktyka. W tym przypadku elastyczność języka Python jest fantastyczna. Ogólnie rzecz biorąc, w tym języku stawia się na luźne pow iązanie kom ponentów . Oznacza to, że często z łatwością można udostępnić coś z „wystarczająco dobrą" im plementacją, a następnie w późniejszym czasie bez trudu dokonać refaktoryzacji w celu ulepszenia kodu. Każdy elem ent o krytycznym znaczeniu, tak jak w przypadku kodu obsługującego płatności, był w pełni objęty testami jednostkowymi. Jednakże w odniesieniu do innych elementów wi tryny i przepływu kolejki zadań (a zw łaszcza kodu pow iązanego z wyświetlaniem danych) w szystko zmieniało się często zbyt szybko, aby tworzenie testów jednostkow ych miało sens (byłyby one zbyt wrażliwe). Zamiast tego zastosowaliśmy bardzo poręczne rozwiązanie, dzięki któremu uzyskaliśm y dwuminutowy czas wdrażania oraz znakom ity m echanizm śledzenia błędów. Jeśli został wykryty błąd, często w ciągu pięciu m inut mogliśm y stworzyć poprawkę i w drożyć ją.
Zapewnianie odpowiedniej wydajności kolejki zadań W przypadku kolejki zadań głów nym problem em jest przepustow ość. Jeśli w ystąpią w niej zaległe zadania, w itryna internetowa kontynuuje działanie, ale w tajem niczy sposób jej treść zaczyna tracić na aktualności — listy nie są aktualizowane, treść stron jest niewłaściwa, a wia domości e-mail nie są wysyłane przez wiele godzin. Na szczęście jednak kolejki zadań zapewniają też architekturę o bardzo dużej skalowalności. Dopóki centralny serwer przesyłający komunikaty (w naszym przypadku system Redis) może obsługiwać obciążenie zw iązane z przesyłaniem komunikatów generowanych przez żądania zadań i odpow iedzi, na potrzeby samego przetw arzania m ożliw e jest dow olne zw iększenie liczby demonów procesów roboczych do obsługi obciążenia.
Raportowanie, monitorowanie, debugowanie i wdrażanie Korzystaliśmy z monitorowania obejmującego długość kolejki. Jeśli zaczynała się zwiększać jej długość, m ogliśm y po prostu w drożyć kolejny serwer z większą liczbą demonów procesów roboczych. Kolejka Celery sprawia, że jest to bardzo prosta operacja. Używany przez nas sys tem w drażania dysponow ał m echanizm am i, które um ożliw iały zw iększenie liczby w ątków procesów roboczych (jeśli wykorzystanie procesora nie było optymalne). Ponadto w ciągu 30 m inut potrafiły one z łatwością zm ienić now y serwer w serwer obsługiwany przez narzędzie Celery. Nie jest tak, że czasy odpowiedzi witryny internetowej drastycznie się w ydłużą. Jeśli kolejki zadań zostaną nagle maksymalnie obciążone, pozostanie trochę czasu na wprowadzenie poprawki. Jeśli pozostawiono w ystarczającą ilość wolnych zasobów, zw ykle poprawka wy starczy, by poradzić sobie z dużym obciążeniem.
Rada dla programistów z branży Przede w szystkim radziłbym , aby jak najszybciej w jak najw iększym stopniu w ykorzystać kolejkę zadań (lub podobną luźno pow iązaną architekturę). Choć początkowo będzie to wy m agać nakładu pracy ze strony inżynierów , wraz z rozw ojem działalności operacje, które zajmowały pół sekundy, m ogą trwać pół minuty. W takiej sytuacji będziesz zadowolony, że
326
|
Rozdział 12. Rady specjalistów z branży
nie blokują one głównego w ątku przetwarzania. Gdy kolejka zadań będzie już odpowiednio w ykorzystyw ana, pam iętaj o uw ażnym m onitorow aniu średniego opóźnienia kolejki (czas, jaki upływa od wprow adzenia zadania do momentu jego zakończenia), a także upewnij się, że dostępne są wolne zasoby w sytuacji, gdy zwiększy się obciążenie. Bądź też św iadom tego, że sensow nym rozw iązaniem jest zastosow anie w ielu kolejek dla różnych priorytetów zadań. W ysyłanie wiadomości e-mail nie ma bardzo dużego priorytetu. Ludzie są przyzw yczajeni do tego, że dostarczenie wiadomości e-mail zajm uje w iele minut. Jeśli jednak renderujesz miniaturę w tle, a w trakcie trwania tej operacji pokazujesz obracający się element graficzny, pożądane będzie, aby zw iązane z tym zadanie miało w ysoki priorytet. W przeciwnym razie użytkow nik może się zirytować. Z pewnością nie chcesz, aby operacja w ysłania wiadomości e-mail do 100 000 osób spowodowała, że wyśw ietlanie w szystkich m i niatur w witrynie będzie opóźnione przez następne 20 minut!
Kolejki zadań w serwisie internetowym Lanyrd.com
| 327
328
|
Rozdział 12. Rady specjalistów z branży
Skorowidz
A
b io p y t h o n , 2 7 B L A S , 2 7 , 1 6 2 , 1 69
a d n o ta c je ty p u , 1 4 4
b y tes, 2 7
a k t u a liz o w a n ie
c o lle c t io n s , 2 7
k la s tr a , 2 5 2
d if fu s io n .s o , 1 6 5
s ia tk i, 1 0 9
g e v e n t, 1 8 1 - 1 8 3 , 1 8 7 , 1 93 g re q u ests, 183
a lg o r y tm H y p erL o g L o g , 302
ite r to o ls , 9 7
H y p erL o g L o g + + , 2 9 0
L A P A C K , 1 69
L o g L o g , 301
L IB L IN E A R , 26
o d c h y le n ia s t a n d a r d o w e g o , 9 8
L IB S V M , 26
o n lin e , 9 8
m a th , 2 7
s o r t o w a n ia , 72
num py, 27
te c h n ik i g łę b o k ie g o u c z e n ia , 3 1 3
O p en C V , 28
w o rd 2v ec, 313
p a n d a s, 27, 28
w y s z u k iw a n ia , 72
P r e t ty T a b le , 2 3 8 s c ik it-le a r n , 2 7
a lo k a c ja , 1 0 9 p a m ię c i, 1 1 0 , 1 1 1 , 1 1 3
s c ip y , 2 7 , 1 2 9 , 1 3 0
p a m ię c i s t a ty c z n e j, 3 1 5
s q lite 3 , 2 7 to r n a d o , 2 7
a n a liz o w a n ie
u n ic o d e , 2 7
b lo k u k o d u , 1 3 8 , 1 4 1
V ie n n a C L , 1 6 2
k o d u F o r tr a n , 1 7 0 s e r w is u s p o łe c z n o ś c io w e g o , 3 1 8
b ib lio t e k i a s y n c h r o n ic z n e , 181
te k s tu , 3 1 3
b lo k a d a G IL , 1 5 3 , 1 6 0 , 1 9 7 , 2 0 6
w y k o r z y s ta n ia p a m ię c i, 2 7 6
b lo k o w a n ie in t e r p r e t e r a , 2 6
z b io r u d a n y c h , 9 7 A O T , A h e a d o f T im e , 1 3 6
lo k a ln e , 2 4 7
a p lik a c je in t e r n e t o w e , 2 7 , 3 2 3
o b ie k t u V a lu e , 2 4 5
a p r o k s y m a c ja r ó ż n ic s k o ń c z o n y c h , 1 0 5
p lik ó w , 2 4 2
a s y n c h r o n ic z n a k o le jk a z a d a ń , 2 6 9
b łą d , 2 4 3 , 2 9 0 , 2 9 6 , 3 0 2
a s y n c h r o n ic z n e d o d a w a n ie z a d a ń , 2 2 0
b łę d y z a o k r ą g la n ia , 6 7
a tr y b u c ja , 13
B P y th o n , 28
A W S , A m a z o n W e b S e r v ic e s , 2 4 9
B S B , B a c k s id e B u s , 2 1
C
B b aza d an y ch , 27, 190, 323
c e n tr a ln a je d n o s tk a o b lic z e n io w a , 16
b ib lio te k a
c h r o n o lo g ia ż ą d a ń , 1 8 0 , 1 8 4 , 1 8 7 , 1 9 0
array , 27
c ią g F ib o n a c c ie g o , 9 6
a s y n c io , 1 7 8 , 1 8 8 , 1 9 3
C P U , C e n t r a l P r o c e s s in g U n it, 1 6
329
tr ie H A T , 2 8 6
czas
tr ie M a r is a , 2 8 5
d z ia ła n ia fu n k c ji, 1 1 8 d z ia ła n ia p ę tli, 81
d u ż e o p e r a c je m a c ie r z o w e , 1 2 7
d z ia ła n ia s c h e m a tó w , 1 2 9
d y fu z ja , 1 0 4 , 1 1 9
tw o r z e n ia in s ta n c ji, 7 8
d w u w y m ia r o w a , 1 0 7 - 1 1 0 , 1 6 4 , 1 6 7
w y k o n y w a n ia p r o g r a m u , 5 1
je d n o w y m ia r o w a , 1 0 6 , 1 0 7
w y k o n y w a n ia w y r a ż e ń , 4 9
d y s k tw a r d y S S D , 2 0
w y s z u k iw a n ia , 7 1 , 2 8 1 , 2 8 2
d y s tr y b u c ja z a d a ń , 3 2 4
c z ę s to ś ć w y s tę p o w a n ia d z ie ln ik ó w , 2 2 2
d z ia ła n ie filtr ó w B lo o m a , 2 9 5
c z y s z c z e n ie fla g i, 2 2 8
E
p a m ię c i, 5 3 , 1 5 8
e fe k t y w n e p r o f ilo w a n ie , 3 0
D
e n tr o p ia , 8 6 , 8 7 e t y k ie t a c a lc u la t e _ o u t p u t , 5 5
dane lo s o w e , 19
F
s ie c io w e , 1 7 9 , 1 8 3 - 1 8 5 , 1 8 8 w y jś c io w e , 5 7 , 6 5 , 1 4 1 , 2 3 7 d e b u g o w a n ie , 3 2 0 d e b u g o w a n ie id e n t y fik o w a n y c h ty p ó w , 1 5 5 d e f in ic ja e n tr o p ii, 8 7 d e fin io w a n ie
fa łs z y w a s k a l a s z a r o ś c i, 3 2 , 3 6 filtr la p la c e , 1 3 0 filtr y B lo o m a , 2 9 5 , 2 9 7 fir m a A d a p tiv e L a b , 3 0 8
d e k o ra to ra , 38
L y s t.c o m , 3 1 5
k la s y , 8 6
R a d im R e h u r e k .c o m , 3 1 0
d e k la r o w a n ie w e k to r ó w , 1 7 0 d e k o r a to r , 3 7 @ jit, 1 5 5 @ p r o file , 6 4
Sm esh , 318 fla g a , 2 2 8 - 2 3 3 C H E C K _E V E R Y , 234 FL A G _C L E A R , 228
@ r e q u ir e , 2 6 2
F L A G _SE T , 228, 235
@ tim e f n , 3 9
p r z e n o ś n o ś c i, 4 0
czasu , 30 d e k o r a t o r y f ik c y jn e , 4 7 d e z a s e m b la c ja , 9 0
fo r m a t JS O N , 2 5 6 f r a g m e n ta c ja , 11 5 f r a g m e n ta c ja p a m ię c i, 111
d ia g n o z o w a n ie w y k o r z y s ta n ia p a m ię c i, 51
F S B , F r o n t s id e B u s , 2 1
d o d a w a n ie
fu n k c ja
a d n o ta c ji, 1 5 7 a d n o ta c ji ty p u , 1 4 3 e ty k ie t, 5 5 f la g k o m p ila to r a , 1 5 3 o p e r a to r a p r a n g e , 1 5 3 ty p ó w p o d s ta w o w y c h , 1 4 3 d o k u m e n ta c ja , 3 9
% tim e it, 6 2 b u iltin
, 65
h ash
, 86
m a in
, 149, 241
ab s, 33, 137 a n o m a lo u s _ d a te s .n e x t , 1 0 0 a p p ly _ s y n c , 2 5 9
k o m p ila to r a C y th o n , 141
a s iz e o f, 2 7 7
m o d u łu b is e c t, 7 2
a s s e r t, 2 4 1
m o d u łu lo c k file , 2 4 4 m o d u łu m u ltip r o c e s s in g , 2 4 4 d o k u m e n ty P E P , 1 8 9
c a lc _ p u r e _ p y th o n , 3 7 , 4 3 , 5 7 c a lc u la t e _ z , 1 5 0 , 1 5 7 c a lc u la t e _ z _ s e r ia l_ p u r e p y t h o n , 4 2 , 5 1 , 5 4
d o s t a r c z a n ie k o m u n ik a t ó w , 2 6 3
c h a in , 9 7
d r z e w a tr ie
c h e c k _ a n o m a ly , 9 9 , 1 0 0
d rzew o , 271
c h e c k _ p r im e , 2 1 3 , 2 2 6
d a tr ie , 2 8 5 , 3 0 4 tr ie , 2 8 0 , 2 8 3
330
|
Skorowidz
c h e c k _ p r im e _ in _ r a n g e , 2 3 0 c y c le , 9 7
d ay _g ro u p er, 100
g łó w n e u p o r z ą d k o w a n ie
d e fin it e _ p r im e s _ q u e u e , 2 1 7 e v o lv e , 1 0 8 , 1 1 0 , 1 5 6 , 1 6 5
k o lu m n o w e , 1 7 0 w ie r s z o w e , 1 7 0
f e e d _ n e w _ jo b s , 2 2 0
G P U , G r a p h ic s P r o c e s s in g U n it, 1 6 , 1 6 2
f ib o n a c c i_ tr a n s f o r m , 9 6
g r a f, 2 7 1
f n _ e x p r e s s iv e , 6 3
g r a f s łó w D A W G , 2 7 9 , 2 8 3 , 2 8 4
fn _ te r s e , 6 3
g r a fic z n e je d n o s tk i o b lic z e n io w e , 16
g e ts iz e o f, 2 7 6
g r e e n le t, 181
g e v e n t.iw a it, 1 8 2
g r u p o w a n ie d a n y c h , 9 8
h a sh , 83 h p y .s e tr e lh e a p , 5 8
H
id , 8 6 is lic e , 9 7
h ip e r w ą tk i, 2 1 6
iw a it, 1 8 5
h ip o te z a , 5 0
la m b d a , 9 8 la p la c e , 1 3 0
I
la p la c ia n , 1 2 4 - 1 3 0 m a in , 3 6 , 2 0 3
ID E , I n t e g r a te d D e v e lo p m e n t E n v ir o n m e n t, 2 8
m e m it, 2 7 7 n o rm _sq u are_n u m p y , 117 o rd , 8 4
id , 1 2 2
ro ll, 1 1 9
P ID , 2 3 7
r o ll_ a d d , 1 2 5
id e n t y fik o w a n ie w ą s k ic h g a r d e ł, 4 6
s e t, 2 8 2
ilo c z y n s k a la r n y , 11 8
ta k e w h ile , 9 7
im p le m e n ta c ja
tim e .tim e () , 3 0
a lg o r y tm u H y p e r L o g L o g , 3 0 2
tim e fn , 3 8
a lg o r y tm u L o g L o g , 3 0 1
tim e it, 2 8 1 add
id e m p o t e n tn o ś ć , 2 9 3 id e n t y fik a t o r
ra n g e , 3 6 , 5 5 , 95
to ta l.
id e a ln a fu n k c ja m ie s z a n ia , 8 8
filtr u B lo o m a , 2 9 6
, 63
lic z n ik a L o g L o g , 3 0 0
tw o le tt e r _ h a s h , 8 9
lic z n ik a M o r r is a , 2 9 1
w a it, 1 8 1
s t r u k tu r y K M in V a lu e s , 2 9 4
w ork , 243, 244
im p o r to w a n ie
w o r k e r _ fn , 2 3 8 , 2 4 0
k o d u F o r tr a n , 1 6 9
x ra n g e , 55, 93, 95
m o d u łó w , 2 6 0
y ie ld , 9 4
m o d u łu , 1 4 0 , 1 48
fu n k c je
m o d u łu z e w n ę tr z n e g o , 1 48
m a g ic z n e , 3 9
in f e r e n c ja ty p ó w , 1 48
m ie s z a n ia , 8 4 , 8 6 , 8 8
in f o r m a c je o
p o w ią z a n e z p r o c e s o r e m , 3 6
e le m e n c ie w y w o łu ją c y m , 4 4
w b u d o w an e, 62, 97
p r o f ilo w a n iu , 3 8 ty p ie , 7 0 , 1 3 6 , 1 5 5
G g e n e r a to r , 9 3 , 9 5 a k ty w n e g o w y k re su , 58 k o n g r u e n c ji lin io w e j, 8 3 lic z b lo s o w y c h , 2 1 0 te x t_ e x a m p le .r e a d e r s , 2 8 1 z b io r u Ju lii, 3 4 G IL , G lo b a l I n t e r p r e t e r L o c k , 1 5 3 g lo b a ln a b lo k a d a in t e r p r e t e r a G IL , 19 g łę b o k ie u c z e n ie , 3 1 0
in ic ja liz a c ja d y fu z ji d w u w y m ia r o w e j, 1 09 n a r z ę d z ia c ffi, 1 6 7 in s p e k c ja o b ie k t ó w , 5 6 s te r ty , 5 6 in s ta lo w a n ie m o d u łó w , 15 9 m o d u łu P a r a lle l P y th o n , 2 5 7 n a r z ę d z ia lin e _ p r o file r , 4 7 n a r z ę d z ia p ip , 1 59 n a r z ę d z ia r u n s n a k e , 4 6
Skorowidz
| 331
i n s tr u k c ja
K
im p o r t, 1 4 8 p r in t, 3 7
k a t a lo g b in , 1 59
S IM D , 1 7
k la s a
y ie ld , 1 7 8
A sy n c B a tch e r, 191, 192
in t e r fe js
C o m p o s ite E r r o r , 2 6 2
A P I, 2 8 , 1 3 4 , 1 5 2
P o in t, 8 7
A P I H T T P , 190
k la s te r , 2 4 9 , 2 5 0
C U D A , 162
B e o w u lf a , 2 4 9
F ile L o c k , 2 4 5
IP y th o n , 2 5 9
I P y th o n , 2 5 9
k la s t r o w a n ie
m e m o r y v ie w , 151
w ad y, 251
M P I, 2 2 2 , 259
z a le t y , 2 5 0
O p e n C L , 162
k la s tr y
O p en M P , 1 5 2 -1 5 4 , 162, 196
lo k a ln e , 2 5 7 , 2 6 2
p o w ło k i, 2 8
m o d u łu I P y th o n P a r a lle l, 2 5 7
S tr ic tR e d is , 2 3 0
p r o d u k c y jn e , 2 6 2
in t e r fe js y fu n k c ji z e w n ę tr z n y c h , 1 6 3
k lu c z , 8 1 , 8 3 , 8 5 , 9 8
in t e r p r e t e r , 2 5
kod
C P y th o n , 10
a s y n c h r o n ic z n y , 1 78
G IL , 2 6
b a jt o w y , 6 2
P y P y , 1 5 7 -1 6 0 , 322
C , 142, 164
IP C , I n s tr u c t io n s P e r C y c le , 1 6
d e te r m in is ty c z n y , 3 6
IP C , I n t e r p r o c e s s C o m m u n ic a t io n , 1 9 9
d y fu z ji, 1 19
I P y th o n , 2 8
F o r tr a n , 1 69
IP y th o n N o te b o o k , 2 8
k o d o w a n ie U T F -8 , 2 7 8
ite r a to r , 9 3 , 9 6
k o le jk a , 2 2 1 , 2 6 3
it e r a t o r ifilte r , 1 0 0
C e le r y , 2 6 9
ite r o w a n ie , 81
F IF O , 1 9 8 , 2 1 8 P y R es, 221
J je d n o s t k a M iB , 5 3 je d n o s tk a o b lic z e n io w a , 16
Q u eue, 220 k o le jk i za d a ń , 249, 325 z a d a ń ro b o czy ch , 217
C P U , 16
k o le k c ja , 2 7 6
G P U , 16
k o le k c ja u n ik a ln y c h k lu c z y , 81
IP C , 1 6
k o liz je w a r to ś c i m ie s z a n ia , 8 5
je d n o s tk i p a m ię c i, 19
k o m p ila to r A O T , 133, 136
ję z y k C , 133
C y th o n , 1 3 9 , 1 4 6 , 1 5 1 , 161
C + + , 138
g+ + , 137
F o r tr a n , 1 3 3 , 1 6 9
gcc, 137
G O , 263
JIT , 1 3 3 , 1 3 6 , 1 5 4
Ja v a , 1 9 9
k o d u C , 137
P y th o n 2 .7 , 11
L L V M , 133, 154
P y th o n 3 , 12
N u it k a , 1 62
T h e an o , 162
N u m b a , 1 3 4 , 1 5 4 , 1 61
JI T , J u s t in T im e , 1 0 , 1 3 3 , 1 3 6
P a r a k e e t, 1 62 P y P y , 10, 1 4 9 , 161 P y s to n , 1 6 2 F y th o n - C + + , 1 4 7 , 1 5 5 P y th r a n , 1 3 4 , 1 55 S h e d S k in , 1 4 7 , 1 5 0 , 161
332
I
Skorowidz
k o m p ila to r y ję z y k a C , 1 6 2
p a g e -fa u lts , 1 2 6
k o m p ilo w a n ie , 1 3 3 , 1 3 9
s t a lle d -c y c le s , 1 1 5
kom ponent
s t a lle d -c y c le s -b a c k e n d , 1 1 4
c ty p e s , 1 9 8
s t a lle d -c y c le s -f r o n te n d , 1 1 4
M a n a g e r, 198
lic z n ik i w y d a jn o ś c i, 1 1 3 , 1 2 0 - 1 3 0
P ip e , 1 9 8
lis ta , 6 9 , 7 3 , 1 0 7
P o o l, 1 9 8 , 1 9 9
k o m p ila to r ó w , 1 62
P ro ce ss, 198
n e w _ g r id , 1 1 0
Q u eu e , 198
lo s o w o ś ć s e k w e n c ji z a d a ń , 2 1 5
k o m p r e s ja , 2 7 1 k o m p u te r z d a tn y , 2 6 1
M
k o m u n ik a c ja m ię d z y p r o c e s o w a , 1 9 9 , 2 1 7 , 2 2 1 , 2 4 8 k o m u n ik a t y , 2 6 3
m a c ie r z e , 1 03
k o m u n ik a t y M P I , 2 2 2
m a g a z y n d a n y c h N A S , 21
k o n fig u r o w a n ie p r o c e s ó w , 2 4 4 k o n g r u e n c ja lin io w a , 8 3
m a g is tr a la B S B , 1 6 , 21
k o n s tr u k c ja fu tu r e , 1 7 7 , 1 7 8 , 181 ko n su m en t dan ych , 264
F S B , 16 , 21 m a p o w a n ie o b ie k t o w o -r e la c y jn e , 3 2 3
k o n tr o la p o p r a w n o ś c i, 3 6 k o n tr o le r , 2 5 9
m a s k a , 8 2 , 88 m a s z y n a w ir tu a ln a , 2 3 , 2 5
k o n w e r te r
m a s z y n a w ir tu a ln a P y P y , 1 3 3
F o r tr a n -P y t h o n , 1 3 4
M a tla b , 2 8
P y th o n -C , 1 3 4
m e b ib a jt , 5 3
k o p ie p a m ię c i, 1 5 0
m e c h a n iz m
k o p io w a n ie p r o f ilu , 2 6 1
O CR, 324
k o s z t fu n k c ji, 4 4
r e k o m e n d a c ji, 3 1 6
k o s z t k o p io w a n ia d a n y c h , 1 5 0 k r o tk i, 6 9 , 7 3 , 7 7
s o n d o w a n ia , 8 3 m enedżer k o n te k s tu , 5 5 , 2 4 5 - 2 4 7
L
z a d a ń P y R es, 269 m e to d a
le n iw e w c z y t y w a n ie d a n y c h , 9 8
E u le r a , 1 05
lic e n c ja C r e a tiv e C o m m o n s , 13
M o n te C a r lo , 1 9 9 , 2 0 0
lic z b a in s tr u k c ji, 1 2 0
m e to d o lo g ia p r o je k t o w a , 3 0 9 m e to d y
p r z y d z ia łó w p a m ię c i, 1 2 2 , 1 2 3
p o m ia r u c z a s u , 3 7
tr a n s fe r ó w , 1 1 5
s y n c h r o n iz a c ji, 2 4 2
w s p ó łb ie ż n y c h ż ą d a ń , 181
ty p u L o g L o g , 3 0 0
z e s p o lo n a , 3 3 , 1 3 7
m ie s z a n ie , h a s h a b le , 7 9
z m ie n n o p r z e c in k o w a , 1 3 7
m oduł
ż ą d a ń w s p ó łb ie ż n y c h , 1 8 2 lic z b y
a rra y , 116, 151, 2 7 4 a s y n c io , 1 8 9
F ib o n a c c ie g o , 9 6
b is e c t, 7 2 , 2 8 2
lo s o w e , 2 0 8
c o n c u r r e n t.fu tu r e s , 1 9 9
p ie r w s z e , 2 1 1 , 2 2 7
c P r o file , 4 1
lic z n ik
c ty p e s , 1 6 4 , 1 65
b ra n c h e s, 115
d is , 6 0
b r a n c h - m is s , 1 1 5
d is tu tils , 1 73
c o n t e x t -s w it c h e s , 1 1 3
f2 p y , 1 6 9 , 1 7 0
C P U -m ig r a t io n s , 1 1 3
I P y th o n P a r a lle l, 2 5 9 , 2 6 2
in s tr u c t io n s , 1 1 4 , 1 2 6
ite r to o ls , 9 8
LogLog, 299, 300
lo c k file , 2 4 4 , 2 4 5
M o r r is a , 2 9 0 , 2 9 1
m e m o r y _ p r o file r , 5 2 , 5 3
Skorowidz
| 333
n u m p y , 74, 103, 1 1 6 -1 2 8 , 151, 170, 199, 209, 2 36,
m oduł
241, 248, 275
m m ap , 232, 233 m u ltip r o c e s s in g , 1 9 2 , 1 9 5 - 2 4 8
p e r f, 1 1 2 , 1 1 3 , 1 15
k o m p o n e n ty , 1 9 8
p ip , 1 5 9
o g r a n ic z e n ia , 2 0 0
p r o f ile , 4 2
s t r a te g ie u ż y c ia , 2 1 5
P y P y , 134, 157
w s p ó łu ż y t k o w a n ie d a n y c h , 2 3 6
P y th r a n , 1 3 4 , 1 55
n a r z ę d z ia C P y th o n , 1 7 1 , 1 7 2
ru n sn a k e , 4 6
n u m e x p r, 127, 128
S h e d S k in , 1 3 4 , 1 3 7 , 1 4 7
n u m p y .a r r a y , 151
SoM A , 307
P a r a lle l P y th o n , 2 5 7 , 2 5 8
S y s te m M o n it o r , 4 0
p ic k le , 2 1 9
U p sta rt, 2 5 5
p p , 259
w o rd 2v ec, 313, 314
r e q u e s ts , 1 7 9
N A S , N e t w o r k A t ta c h e d S to r a g e , 2 1
s tr u c t, 1 6 6
n a w ia s k w a d r a t o w y , 5 4
tim e it, 3 8 , 3 9
n ie ja w n a p ę t la , 1 18
m o d y f ik o w a n ie k o d u ź r ó d ło w e g o , 5 3
n is k o p o z io m o w e ty p y n u m e r y c z n e , 1 1 8
m o n it o r o w a n ie , 3 1 7 , 3 2 0
n o tk a d o k u m e n ta c y jn a , 3 8
m o n it o r o w a n ie p a m ię c i, 3 1 3 M P I, M e s s a g e P a s s in g In te r f a c e , 2 2 2 , 2 5 9
N n a r z ę d z ia
O o b ie k t _ d if fu s io n , 1 65 BLO B, 266
k la s t r o w a n ia , 2 6 8
d eq u e, 100
p r o f ilu ją c e , 4 2
F ile L o c k , 2 4 5 , 2 4 7
n a r z ę d z ie C ir c u s , 2 5 5
H u b F a cto ry , 2 6 0 lis t, 2 8 1
c ffi, 1 6 6 , 1 6 7 , 1 6 8
L ock, 246, 247
c P r o file , 4 2
M a n a g e r .V a lu e , 2 2 8
c p y e x t, 1 5 9
m u ltip r o c e s s in g .V a lu e , 2 4 5
C p y th o n , 3 1 , 6 0 , 1 5 1 , 1 6 4
m u ltp r o c e s s in g .A r r a y , 2 3 9
cro n , 2 5 5
P o o l, 2 0 3 , 2 1 6 , 2 2 5 , 2 2 7
C y th o n , 2 8 , 1 3 3 , 1 3 7 , 1 3 9
P ro cess, 2 4 4
d iff, 6 7
Q u eu e, 197, 216, 219
D o ck er, 321
R a w V a lu e , 2 3 2 , 2 4 7
d o w ser, 58, 67
U n ic o d e , 2 7 8
d o zer, 67
V a lu e , 2 4 5
G a n g lia , 2 5 6 G r a p h it e , 3 1 7
v e cto r, 117 o b ie k t y
h eap y, 30, 56
ję z y k a P y th o n , 2 0 1
h o ts h o t, 4 2
lis ty , 1 51
h to p , 2 3 7
P y th o n , 2 0 8
I P y th o n , 1 5 9 , 2 5 7
ty p ó w p o d s ta w o w y c h , 2 7 2
jit v ie w e r , 1 6 0
U n ic o d e , 2 7 7
jo b lib , 2 1 6
o b ie tn ic a w y n ik u , 17 8
lin e _ p r o file r , 3 0 , 4 6 , 6 4 , 1 0 9
o b lic z a n ie
ls p r o f, 4 4
d y fu z ji d w u w y m ia r o w e j, 1 0 7
m a k e , 149
lic z b y z e s p o lo n e j, 3 3
m e m o r y _ p r o file r , 3 1 , 5 1 , 6 4 , 65
lis ty o u tp u t, 1 5 0
m p ro f, 54, 55
p o ch o d n y ch , 119
n o s e te s ts , 6 4 , 6 5
z b io r u Ju lii, 3 4
N u m b a, 133, 154
334
|
Skorowidz
o b lic z e n ia
p a r a m e tr c h u n k s iz e , 2 1 3 - 2 1 6
m a c ie r z o w e , 1 0 3
P E P , P y th o n E n h a n c e m e n t P r o p o s a ls , 18 9
p r o b a b ilis ty c z n e , 2 9 0
p ę t la
ro z p ro sz o n e, 2 6 6
fo r , 6 3 , 9 3 , 9 4
w e k to r o w e , 1 0 3
w h ile , 4 9 , 5 0 , 1 4 5
o b r a z s y s te m u , 2 5 6
z d arzeń , 176, 186
o b r ó t, 1 2 5
z w r o tn a , 2 3 1
o d c h y le n ie s t a n d a r d o w e , 9 8 o d c z y t s e k w e n c y jn y , 19
p ę tle n ie ja w n e , 1 18 w e w n ę tr z n e , 81
o d n o ś n ik i T R A C E , 6 0 o d t w a r z a n ie fla g i, 2 3 3
P G O , P r o file -G u id e d O p t im iz a t io n , 1 5 0
o g r a n ic z a n ie lic z b y o p e r a c ji, 1 3 3
p ie r w ia s te k k w a d r a t o w y , 1 3 7 , 1 45
o p c je k o m p ila to r ó w , 1 6 0
p lik
O p e n M P , O p e n M u lt i- P r o c e s s in g , 1 5 2
c d iff u s io n .s o , 1 7 4
o p e r a c ja
c o m p lic a te d .h , 1 68
F M A , 16
c y th o n fn .h tm l, 1 41
n u m p y .d o t , 1 1 7
c y th o n fn .p y x , 1 3 9 , 1 4 0
r e s iz e , 7 5
d if fu s io n .s o , 1 7 0
s q rt, 1 4 5
d if fu s io n _ n u m p y .s o , 1 5 6
o p e r a c je
ip c o n t r o lle r - e n g in e .js o n , 2 6 1
d o łą c z a n ia , 7 6
ju lia 1 .p y , 1 39
m a c ie r z o w e , 1 2 7
M a k e file , 1 49
w e jś c ia -w y jś c ia , 1 7 5 , 1 8 8
s e tu p .p y , 1 3 9 , 1 7 3
w e w n ę tr z n e , 1 2 1 - 1 2 4
s h e d s k in f n .c p p , 1 49
o p e r a to r cm p
s h e d s k in f n .h p p , 1 49 , 86
s h e d s k in f n .s o , 1 4 9
ap p en d , 77
s h e d s k in f n .s s .p y , 1 49
p ran g e, 153, 154
p lik i
o p ó ź n ie n ie , 19
.d e b , 2 5 6
o p r o g r a m o w a n ie M a tla b , 2 8
.lp r o f, 4 7
o p t y m a liz a c ja , 6 4 , 1 3 2 , 1 4 6 , 3 1 3
.p y x , 1 4 6
o p e r a c ji m a c ie r z o w y c h , 1 2 7
.rp m , 2 5 6
o p e r a c ji w e k to r o w y c h , 1 2 7
.so , 13 9
P G O , 150
s ta ty s ty k , 4 6 , 5 4
s e le k ty w n a , 1 2 4
p o b ie r a n ie , 8 2
w y s z u k iw a r e k , 3 1 3
p o d e jm o w a n ie d e c y z ji, 11 5
o b c ią ż a ją c e j lo g ik i, 2 3 4
p o le c e n ie
o p t y m a ln a fu n k c ja m ie s z a n ia , 8 8
g rep , 239
O R M , O b je c t R e la tio n a l M a p p e r , 3 2 3
ip c lu s te r , 2 5 9
o s z c z ę d z e n ie p a m ię c i, 2 7 1
m e m it, 2 7 3 p m ap, 239
P p a k ie t c o u n tm e m a y b e , 2 9 4 d is ta r r a y , 2 6 9 gu ppy, 56 ja v a .u til.c o n c u r r e n t , 1 9 9 to r n a d o , 1 8 5 - 1 8 7 w x P y th o n , 4 6 p a m ię ć p o d ręcz n a , 16, 2 0 R A M , 16, 2 0 , 5 3, 2 0 3 , 271
p r in t, 4 2 p s, 239 p u sh , 260 r e d is -c li, 2 3 1 s h e d s k in , 1 49 p o le c e n ie tim e , 4 0 p o łą c z e n ie g e n e r a to r ó w , 9 9 p o m ia r cz a su , 3 7 , 4 0 , 4 3 , 88 c z a s u s o r t o w a n ia , 2 8 2 w ie r s z y k o d u , 4 6 w y k o r z y s ta n ia p a m ię c i, 2 7 2 , 2 7 3
Skorowidz
| 335
p o r ó w n a n ie g r a fu i d r z e w , 2 8 0
p r z e s tr z e n ie n a z w , 8 9 p r z e s z u k iw a c z , 1 8 6
k o m p ila to r ó w J I T i A O T , 1 3 6
p r z e s z u k iw a c z s z e r e g o w y , 1 7 9
lis t i k r o te k , 7 3
p r z e tw a r z a n ie
p r o b a b ilis ty c z n y c h s t r u k tu r d a n y c h , 3 0 3 , 3 0 4
k la s tr o w e , 2 4 9
p ro g ra m ó w , 176
r ó w n o le g łe , 1 5 2 , 1 9 6 , 2 0 8 , 3 1 4
p o rt n u m p y p y , 159 p o t o k o w a n ie , 1 1 2 , 2 6 6
szereg o w e, 225 w y id e a liz o w a n e , 2 3 , 2 4 p r z e w id y w a n ie r o z g a łę z ie ń , 1 1 2
p o w ło k a in t e r a k ty w n a , 2 8
p r z y b liż a n ie lic z b y p i, 2 0 0 - 2 1 0 , 2 6 2
I P y th o n , 2 6 0
p r z y d z ia ły p a m ię c i, 121
p r a w o A m d a h la , 1 8 , 1 9 6
p r z y d z ie la n ie d la lis t, 7 5
p r o b a b ilis ty c z n e s t r u k tu r y d a n y c h , 2 8 9 , 3 0 3 - 3 0 5
p r z y s p ie s z e n ie
p r o b le m
d z ia ła n ia k o d u , 1 3 2 , 1 3 5 , 1 9 6 , 2 1 0
C a u c h y 'e g o , 1 0 5 z a lo k a c ją , 1 0 9 p ro ced u ra BLA S, 314
k o d u a s y n c h r o n ic z n e g o , 1 9 2 p se u d o k o d , 106 p u b lik a t o r , 2 6 4 p u n k t y s k o k u , 61
o b s łu g i, 2 6 6 p ro ces, 20 2 , 205, 206
R
n a d rzę d n y , 239 n sd q , 267 p r o c e s o r C P U , 16 p ro cesy
R A ID , 2 5 4 R A M , R a n d o m A c ce ss M e m o ry , 16 r a p o r t n a r z ę d z ia m e m o r y _ p r o file r , 5 4 - 5 6
ro b o cz e, 2 1 5 , 3 2 4 s z e r e g u ją c e , 2 5 9 w s p ó łb ie ż n e , 2 4 3 p r o f ilo w a n ie , 2 9 , 4 2 , 6 6 p r o f ilo w a n ie d y fu z ji, 1 0 9 w ie r s z y , 1 1 1 , 1 2 4 p r o g r a m , P a tr z n a r z ę d z ie p r o g r a m o w a n ie a s y n c h r o n ic z n e , 1 7 6 p ro g ram y szereg o w e, 176 w s p ó łb ie ż n e , 1 7 6 p r o je k t
r a p o r to w a n ie , 3 1 7 , 3 2 0 r e d u k o w a n ie m o c y , 1 45 r e d u n d a n c ja , 2 6 5 r e k o m e n d a c je , 3 1 6 r o z b ic ie p ę t l i w h ile , 4 8 te s tó w je d n o s tk o w y c h , 5 3 r o z k ła d G a u s s a , 2 9 0 r o z p r o s z o n e o b lic z e n ia , 2 6 6 r o z w ią z a n ia k la s tr o w e , 2 5 7 r o z w ija n ie fu n k c ji, 3 4 r o z w ija n ie fu n k c ji a b s , 1 45
k la s tr a , 2 5 4 , 3 1 6 M ic r o P y th o n , 2 8 8
r ó w n a n ie d y fu z ji, 1 0 4 - 1 0 7 r ó w n o w a ż e n ie o b c ią ż e n ia , 2 1 1
M P I4 P Y , 222 n a r z ę d z ia S o M A , 3 0 8
S
p ro to k ó ł T C P , 231 p r z e c h o w y w a n ie ty p ó w , 2 7 5 ty p ó w p o d s ta w o w y c h , 151 z b io r ó w te k s to w y c h , 2 7 9 p r z e g lą d s k o m p ilo w a n e g o k o d u , 1 5 5 p r z e k a z y w a n ie o b ie k tu , 2 2 8 , 2 2 9 w s p ó łu ż y t k o w a n y c h d a n y c h , 2 6 0 p r z e łą c z a n ie k o n te k s tu , 1 7 6 p r z e n o s z e n ie k o d u , 3 5 p r z e p u s t o w o ś ć in t e r fe js ó w , 2 3 p r z e s tó j u s łu g i, 2 5 3
s e k w e n c y jn e p r z e c h o w y w a n ie d a n y c h , 1 1 6 s e m a fo r y s y n c h r o n iz u ją c e , 1 98 s e r ia liz a c ja , 2 1 9 s e r w is L a n y r d .c o m , 3 2 5 s e r w is o w a n ie s y s te m u S o M A , 3 0 9 s k a lo w a ln y filtr B lo o m a , 2 9 7 s k a lo w a n ie d y n a m ic z n e , 2 5 0 s k r y p t, P a t r z ta k ż e p lik c o v e r a g e .p y , 6 4 k e r n p r o f.p y , 4 7 - 5 0 , 1 0 9 s e tu p .p y , 1 4 0 , 1 5 3 s ło w n ik , 7 9 , 8 9
336
|
Skorowidz
s ło w n ik g lo b a ls () , 8 9
T
s ło w o k lu c z o w e c d e f, 1 4 3 , 1 4 4 s o n d o w a n ie , 8 3
t a b e la m ie s z a ją c a , 8 2 , 8 4
s o r to w a n ie , 4 3 , 7 2 , 2 8 2
u s u w a n ie w a r to ś c i, 85
s p r a w d z a n ie
z m i a n a w ie lk o ś c i, 8 5
d a n y c h w y jś c io w y c h , 5 7 , 1 4 9
ta b lic a , 6 9
k o d u b a jt o w e g o , 6 0
ta b lic a lo c a ls ( ), 8 9
w s p ó łu ż y t k o w a n y c h d a n y c h , 2 2 2
ta b lic e
s t a c ja r o b o c z a , 1 6
d y n a m ic z n e , 7 3 , 7 4
s ta ła s y s .m a x in t , 2 7 6
s t a ty c z n e , 7 3 , 7 7
s te r ta , 5 6
w s p ó łu ż y t k o w a n e , 2 3 8
s tr a te g ie p r o f ilo w a n ia , 6 6
te c h n ik a g łę b o k ie g o u c z e n ia , 3 1 0
s t r u k tu r a
te s to w a n ie s z y b k o ś c i, 6 2
d r z e w a tr ie , 2 8 3
te s ty je d n o s tk o w e , 6 4 - 6 7
g r a fu D A W G , 2 8 3
tłu m a c z e n ie , 3 2 4
H y p erL o g L o g + + , 2 89
to k e n y , 2 8 0 , 2 8 1
K - M in im u m V a lu e s , 2 9 2 , 2 9 4
to p o lo g ia
P o in t, 1 6 8
p o łą c z e ń , 2 6 5
s tr u m ie n io w a n ie d a n y c h , 3 1 3
sy ste m u N S Q , 2 6 4
su b sk ry b e n t, 2 6 4
T T L , T im e to L iv e , 3 2 1
s y m b o l > > , 61
t w ie r d z e n ie P it a g o r a s a , 2 0 1
s y n c h r o n ic z n y in t e r p r e t e r , 2 5 9
tw o r z e n ie
s y n c h r o n iz a c ja , 1 9 8 , 2 1 9
a d n o ta c ji, 14 5
z a p is ó w , 2 4 6
d w ó c h k o le je k , 2 1 8
danych, 222
fu n k c ji o b r o tu , 1 2 5
d o s tę p u , 2 4 2
h ip o te z y , 4 2
s y s te m
lo k a ln e g o p r o f ilu , 2 6 1
G earm an , 269
m o d u łu r o z s z e r z e n ia , 14 8
k o m p u te r o w y , 15
n o r m w e k to r a , 1 1 7
N SQ , 2 6 2 -2 6 7
o p e r a c ji w e k to r o w y c h , 1 6 9
p r z e tw a r z a ją c y z a d a n ia , 2 6 9
p ro cesu sz ereg o w eg o , 196
p r z e tw a r z a n ia d a n y c h , 3 2 2
s y s t e m u k la s t r o w e g o , 2 5 4
R e d is , 2 2 4 , 2 2 9
ta b e li m ie s z a ją c e j, 82
S a ltS ta c k , 3 0 9
ta b lic y , 7 0
SoM A , 309
ty p
SQ S, 269
a rra y , 116
w d r a ż a n ia
d o u b le c o m p le x , 1 4 4
C h e f, 2 5 6
F u tu r e , 1 78
F a b r ic , 2 5 6
in t, 1 4 4
P u p p e t, 2 5 6
s tr u c t, 1 6 6 , 1 6 8
S a lt, 2 5 6
u n s ig n e d in t, 1 4 4
s z e r e g n ie s k o ń c z o n y , 9 6
ty p y p o d s ta w o w e , 2 7 4
s z y b k o ś c i p o łą c z e ń in t e r fe js ó w , 2 3 s z y b k o ś ć z e g a r a , 17
U Ś
u c z e n ie m a s z y n o w e , 3 1 5 u k ła d y g r a fic z n e G P U , 1 62
ś r e d n ia o n lin e , 9 8 ś r o d o w is k o
u r u c h a m ia n ie in t e r p r e t e r a P y P y , 15 9
C an op y, 28
m o d u łu tim e it, 3 9
EP D , 28
n a rz ę d z ia d o w se r, 59
Sage, 28
serw era W W W , 59
s y s te m u N S Q , 2 6 7
sk ry p tu , 140, 283
Skorowidz
| 337
u r z ą d z e n ie ty p u S S , 19
s y s t e m u R e d is , 2 2 9
u s łu g a
w ą tk u , 2 2 0
A W S, 249
w y k o n y w a n ia s z e r e g o w e g o , 2 0 2
EC 2, 254 Skyp e, 253
W
u s u w a n ie e le m e n tu , 8 5 u tra ta d a n y ch , 2 5 6 u ż y c ie
w a r s tw y k o m u n ik a c ji, 21 w a r to ś c i k - m in im u m , 2 9 1
a d n o ta c ji, 1 7 0 a d n o ta c ji k o m p ila to r a , 141
w a r to ś c io w a n ie le n iw e g e n e r a to r a , 9 7 w a ru n k i b rz e g o w e, 106
d e k o ra to ra , 38
w ą s k ie g a r d ło , 2 9
d e k o r a t o r a @ p r o file , 6 4
w ą tk i, 1 9 7 , 2 0 2 , 2 0 6 - 2 0 8
d r z e w tr ie , 2 8 7 d w ó c h k o le je k , 2 1 7 , 2 1 9 filtr u B lo o m a , 2 9 8
w d r a ż a n ie , 3 2 0 w d r a ż a n ie a k t u a liz a c ji, 2 5 6 w e k to r y , 1 0 3
filtr u la p la c e , 1 3 0
w e k to r y z a c ja , 1 7 , 1 1 9
fla g i p r z e n o ś n o ś c i, 4 0
w e r y f ik o w a n ie
fu n k c ji % tim e it, 6 2 fu n k c ji s e t, 2 8 2 fu n k c ji w b u d o w a n y c h , 6 2 g e n e r a to r ó w , 9 7 g r a fu D A W G , 2 8 4 ilo c z y n u s k a la r n e g o , 1 1 8 in t e r a k ty w n e g o d e b u g e r a , 6 0 k o d u F o r tr a n , 1 6 9 k o m p ila to r a k o d u C , 1 3 7 m e n e d ż e r a k o n te k s tó w , 5 5 m e n e d ż e r a k o n te k s tu , 2 4 5 m o d u łu c P r o file , 4 1 m o d u łu c ty p e s , 1 6 4
lic z b p ie r w s z y c h , 2 2 1 o p t y m a liz a c ji, 1 29 w y n ik u , 2 3 8 w ie lo w ą tk o w o ś ć w s p ó łb ie ż n a , 18 w ie r s z p o le c e ń , 3 9 w ir tu a ln y p r o c e s o r , 18 w iz u a liz a c ja d a n y c h w y jś c io w y c h , 4 6 p lik u p r o f ilo w a n ia , 4 7 w c z a s ie r z e c z y w is ty m , 6 7 w ła ś c iw o ś ć le n g th , 9 5 n e x t( ), 9 8
m o d u łu d is , 6 0 , 63
w s k a ź n ik , 1 12
m o d u łu I P y th o n P a r a lle l, 2 5 9
w s k a ź n ik M a jo r p a g e fa u lts , 4 1
m o d u łu lo c k file , 2 4 4 , 2 4 5 m o d u łu m m a p , 2 3 2 , 2 3 3 m o d u łu n u m e x p r , 1 2 7
w s p ó łb ie ż n e p r o c e s y , 2 4 3 w s p ó łb ie ż n o ś ć , 1 7 5 w s p ó łc z y n n ik b łę d u , 3 0 1
m o d u łu P a r a lle l P y th o n , 2 5 7
w s p ó łp r o g r a m y , c o r o u tin e , 1 78
n a r z ę d z ia c ffi, 1 6 7
w s p ó łp r o g r a m y g r e e n le t, 181
n a r z ę d z ia d o w s e r , 5 8 n a r z ę d z ia h e a p y , 5 7 n a r z ę d z ia lin e _ p r o file r , 4 6 n a r z ę d z ia m e m o r y _ p r o file r , 5 1 n a r z ę d z ia n u m p y , 1 1 9 , 2 0 9 n a r z ę d z ia r u n s n a k e , 4 6 o b ie k tu L o c k , 2 4 6 o b ie k tu M a n a g e r .V a lu e , 2 2 8 o b ie k tu R a w V a lu e , 2 3 2 p ro cesó w , 210 p r o f ilo w a n ia , 2 9 s e r w e r a R e d is , 2 3 0 s k r y p tu k e r n p r o f.p y , 4 8 s ło w n ik a , 8 0 s t r u k tu r y d a tr ie , 2 8 6 s y s te m u N S Q , 2 6 2
338
|
Skorowidz
w s p ó łr z ę d n e z e s p o lo n e , 3 5 w s p ó łu ż y t k o w a n ie danych, 236, 260 ta b lic y , 2 3 9 , 2 4 0 z ad ań , 197 w s ta w ia n ie , 82 w s ta w ia n ie z k o liz ja m i, 8 4 w y b ó r s t r u k tu r y d a n y c h , 7 3 w y d a jn o ś ć a lg o r y tm u , 72 i n s tr u k c ji w h ile , 1 45 k o le jk i z a d a ń , 3 2 6 w y s z u k iw a ń , 8 7 w y ją t e k N a m e E r r o r , 6 4 w y k o n y w a n ie s z e r e g o w e , 2 0 9 w y k o r z y s ta n ie p a m ię c i, 2 3 7 , 2 7 2 , 2 7 6 , 2 8 8
w y k r e s z b io r u Ju lii, 3 2 , 3 7
z m ia n a
w y k r y w a n ie n ie p r a w id ło w o ś c i, 9 9
fu n k c ji, 5 6
w y łą c z a n ie w e k to r y z a c ji, 121
sz y b k o ści z eg a ra , 17
w y o d rę b n ia n ie d a n y c h sie c io w y c h , 179, 1 8 3 -1 8 5 , 188 w y s z u k iw a n ie , 8 0
w ie lk o ś c i lis ty , 7 6 z m ie n n e g lo b a ln e , 1 0 8
b in a r n e , 7 2
z m n ie js z a n ie
e f e k t y w n e , 71
lic z b y a lo k a c ji p a m ię c i, 1 1 0 - 1 1 3
lin io w e , 8 0
lic z b y p r z y d z ia łó w p a m ię c i, 1 23
lin io w e lis ty , 71
z n a jd o w a n ie
p o w o ln e , 9 1
e le m e n tu , 85
w p r z e s tr z e n ia c h n a z w , 9 0
lic z b p ie r w s z y c h , 2 1 1 , 2 1 2
w s ło w n ik u , 8 3 , 8 7
n ie p r a w id ło w o ś c i, 101
w y ś w ie t la n ie lic z b y in s tr u k c ji, 63 w y w o ła n ia z w r o tn e , 1 7 7 , 1 8 6
u n ik a ln y c h im io n , 81 w a r to ś c i, 7 2
w y w o ły w a n ie n a r z ę d z ia d o w s e r , 5 9
z r ó w n o w a ż o n e o b c ią ż e n ie , 2 5 9
w z r o s ty s z y b k o ś c i, 1 3 4
z w ię k s z a n ie w y d a jn o ś c i, 1 45
Z z a s t o s o w a n ie , P a tr z u ż y c ie
ż ą d a n ia w s p ó łb ie ż n e , 1 8 2
z b io r y , 7 9
ż ą d a n y w s p ó łc z y n n ik b łę d u , 2 9 6
z b ió r Ju lii, 3 1 , 3 4 , 1 3 8 , 1 5 2 z e s p o lo n y w a r u n e k p o c z ą tk o w y , 3 5 z e s t a w ie n ie p r z y s p ie s z e ń , 1 3 2 z e w n ę tr z n y s y s t e m k o le je k , 2 2 1 z in te g r o w a n e śro d o w isk o p ro g ra m isty cz n e , ID E , 2 8 z ło ż o n o ś ć , 6 2 , 7 2 , 8 0
Skorowidz
| 339