Tak, Python jest wolny, i co z tego?
Robię przerwę w dyskusji na temat asyncio
Pythona, żeby porozmawiać o czymś, co zaprząta mi głowę od pewnego czasu: szybkości Pythona. Nie wszyscy wiedzą o tym, że jestem jego fanem i z uporem maniaka używam go wszędzie gdzie się da. Jednym z największych zażaleń, jakie ludzie kierują w stronę Pythona, jest to, że jest zbyt wolny. Niektórzy nie chcą go nawet wypróbować, twierdząc, że jest wolniejszy niż język X. Oto moje przemyślenia o tym, dlaczego powinieneś spróbować Pythona, mimo że jest wolny.
Szybkość nie ma już znaczenia
Dawniej programy bardzo długo się uruchamiały. CPU i pamięć były drogie. Czas wykonania danego programu stanowił istotny wskaźnik. Komputery były bardzo drogie, tak samo, jak elektryczność potrzebna do ich uruchomienia. Optymalizacja powyższych powodowana była odwiecznym prawem biznesowym:
Optymalizuj najdroższe zasoby
Najdroższym spośród nich był run-time komputera, co doprowadziło do rozwoju informatyki skupiającej się na wydajności różnych algorytmów. Ale to już nieprawda, bo krzem stał się tani. Naprawdę tani. Czas wykonania przestał być drogim zasobem. Najdroższym zasobem każdej firmy jest teraz czas pracownika. Lub innymi słowy – ty. Ważniejsze stało się wykonywanie określonych zadań, niż czas, w którym się to osiąga. Według mnie jest to tak ważne, że napiszę to jeszcze raz, jakby to był cytat (dla tych, którzy tylko przeglądają):
Ważniejsze stało się wykonywanie określonych zadań, niż czas, w którym się to osiąga.
Możesz powiedzieć, że „nasza firma dba o szybkość, buduję aplikację webową i wszystkie odpowiedzi muszą być szybsze niż x milisekund.”, albo że „niektórzy klienci rezygnują z naszych usług, bo uważają, że aplikacje są zbyt wolne.” Nie mówię, że prędkość w ogóle nie ma znaczenia. Próbuję tylko powiedzieć, że nie jest już rzeczą najważniejszą. Nie jest najdroższym zasobem.
Tylko prędkość ma znaczenie
Kiedy mówisz o prędkości w kontekście programowania, generalnie masz na myśli wydajność a.k.a cykle CPU. Kiedy twój CEO mówi o prędkości w kontekście programowania, chodzi mu o tempo biznesu. Najważniejszą miarą jest czas od momentu powstania koncepcji produktu aż do wprowadzenia go na rynek. Ostatecznie nie ma znaczenia, jak szybki jest Twój produkt, w jakim języku został napisany. Bez znaczenia jest nawet to, ile kosztuje uruchomienie go. Jedyne, co pozwoli przetrwać twojej firmie to czas „od pomysłu do rąk konsumenta”. Trzeba tworzyć produkty szybciej niż konkurencja. Musisz wprowadzić produkt na rynek jako pierwszy. Jeśli zwolnisz, przegrasz.
Jedyny sposób na przetrwanie w biznesie to tworzenie szybciej niż konkurencja.
Przypadek Mikroserwisów
Firmy takie jak Amazon, Google lub Netflix rozumieją wagę szybkiego działania. Stworzyły one system, w którym mogą szybko działać i jeszcze szybciej wprowadzać innowacje. Mikroserwisy są rozwiązaniem ich problemu. Z tego artykułu nie dowiesz się, czy powinieneś korzystać z mikroserwisów. Zaakceptuj tylko fakt, że Amazon i Google uważają, że powinny ich używać.
Mikroserwisy są z natury wolne. Całym konceptem mikroserwisów jest rozdzielanie granic przez wywołania sieciowe. To znaczy, że wywołanie funkcji (kilka cykli CPU) zmieniasz na wywołanie sieciowe. W kwestii wydajności gorzej zrobić się nie da. Wywołania sieciowe są bardzo wolne w porównaniu do CPU. Tymczasem duże firmy wciąż wybierają mikroserwisy. Naprawdę, nie znam wolniejszej architektury niż mikroserwisy. Ich głównym minusem jest wydajność, a największym plusem krótki czas „od pomysłu do rąk konsumenta”. Tworzenie teamów wokół małych projektów i programów pozwala firmie na znacznie szybszą iterację i innowację. To pokazuje, że nie tylko małym startupom zależy na prędkości dostarczania produktów konsumentowi.
CPU nie jest wąskim gardłem
Jeśli piszesz aplikację webową, jest duża szansa, że czas CPU nie jest zatorem. Obsługując żądanie, Twój serwer prawdopodobnie wykonuje kilka wywołań sieciowych – do bazy danych, do serwera cache takiego jak Redis. Mimo że te serwisy są szybkie, to samo wywołanie sieciowe do nich jest wolne. Tutaj znajdziecie całkiem niezły artykuł o różnicach w szybkości różnych operacji. Jego autor skaluje czas cykli CPU do bardziej ludzkiego czasu. Jeśli jeden cykl CPU jest odpowiednikiem 1 sekundy, to wywołanie sieciowe z Kalifornii do Nowego Yorku trwałby 4 lata. Są one aż tak wolne. Można oszacować, że normalne wywołanie sieciowe w obrębie jednej bazy danych trwa 3 milisekundy. To oznacza 3 miesiące w „ludzkiej skali”.
A teraz wyobraź sobie, że twój program bardzo obciąża CPU. Odpowiedź na pojedyncze wywołanie zajmuje 100 000 cykli, czyli równoważnik trochę ponad 1 dnia. Jeśli posługujesz się 5 razy wolniejszym językiem, to cały proces trwa teraz 5 dni. Porównując to do trzymiesięcznego wywołania sieciowego i różnicy 4 dni nie ma to w zasadzie znaczenia. Jeśli ktoś musi czekać na paczkę 3 miesiące, to nie sądzę, żeby dodatkowe 4 dni oczekiwania robiły mu jakąkolwiek różnicę.
Ostatecznie oznacza to, że nawet jeśli Python jest wolny, to nie ma to znaczenia. Prędkość języka (lub czasu CPU) prawie nigdy nie jest problemem. Pracownicy Google'a zrobili badania dotyczące tego zagadnienia i napisali na ten temat pracę. Skupia się ona na tworzeniu systemu o dużej wydajności. W podsumowaniu mówią oni, że:
Używanie interpretowanego języka w przepustowym środowisku może wydawać się paradoksalne, ale dowiedzieliśmy się, że czas CPU bardzo rzadko jest ograniczającym czynnikiem; ekspresywność języka oznacza, że w większości programy są małe i spędzają gros czasu na wykonaniu kodu natywnego i I/O. Ponadto, elastyczność implementacji w języku interpretowanym jest pomocna. Zarówno, gdy chodzi o eksperymentowanie na poziomie lingwistycznym, jak i w szukaniu sposobów na dystrybuowanie obliczeń na wiele maszyn.
Albo, żeby podkreślić:
Czas CPU bardzo rzadko jest ograniczającym czynnikiem.
A co, jeśli czas CPU jest problemem?
Może powiesz „Super, ale miałem problemy, kiedy to CPU mnie ograniczało i bardzo spowalniało aplikację webową.”, albo, że „Język x potrzebuje do uruchomienia serwera o wiele mniej sprzętu niż język y.” To wszystko może być prawdą. Piękno serwerów webowych polega na tym, że możesz rozpraszać ich obciążenie prawie w nieskończoność. Innymi słowy: załatw więcej sprzętu. Jasne, Python może potrzebować lepszego sprzętu niż inne języki, na przykład C. Po prostu rozwiąż problem z CPU sprzętem. Jest o wiele tańszy niż Twój czas. Kiedy oszczędzisz kilka tygodni produktywności w ciągu roku, to z nawiązką zwróci się wydatek na sprzęt.
No to czy Python jest szybszy?
Przez cały czas mówiłem o tym, że najważniejszy jest czas rozwoju. Pozostaje zatem pytanie: czy Python jest szybszy niż język x pod względem czasu rozwoju? Żartobliwie, ja, Google i kilku innych możemy ci powiedzieć, że Python jest bardziej produktywny. Upraszcza dla ciebie wiele rzeczy. Pomaga się skupić na tym, co tak naprawdę chcesz kodować, bez tracenia czasu na zbędne dylematy, np. czy powinieneś użyć wektora czy tablicy. Jeśli nie przekonują Cię opinie innych ludzi, popatrzmy na empiryczne dane.
Właściwie, to cała dyskusja na temat tego, czy Python jest produktywny, sprowadza się do różnicy między skryptowymi (albo językami dynamicznymi) a tymi statycznie typowanymi. Myślę, że powszechnie uważa się, że statyczne języki są mniej produktywne. Tutaj znajdziesz dobry tekst, który wyjaśnia, dlaczego tak jest. Jeśli chodzi o Pythona, to warto zajrzeć do podsumowania badania, które sprawdzało jak długo zajmie napisanie kodu do przetwarzania ciągów znaków w poszczególnych językach.
Python okazał się ponad 2 razy bardziej produktywny niż Java. Przeprowadzono też inne badania, które wskazują ten sam wniosek. Rosetta Code przeprowadziła w miarę dogłębne badanie różnic między językami programowania. W tekście porównują Pythona do innych skryptowych/interpretowanych języków i mówią, że:
Python jest bardziej zwięzły, nawet w porównaniu do funkcyjnych języków (średnio 1,2 – 1,6 razy krótszy.
Zgodnie z popularnym trendem w Pythonie pisze się mniej kodu. Linijki kodu mogą się wydawać okropną miarą, ale wiele badań, w tym dwa już przeze mnie wspomniane, pokazuje, że w każdym języku czas spędzony na jednej linii kodu jest mniej więcej taki sam. Zatem zmniejszenie liczby linii kodu zwiększa produktywność. Nawet sam codinghorror (programista C#) napisał artykuł o tym, że Python jest bardziej produktywny.
Z pewnością mogę stwierdzić, że Python jest bardziej produktywny niż inne języki programowania. Spowodowane jest to w dużej mierze tym, że Python „ma baterie w zestawie” i wiele bibliotek. Tutaj znajdziesz krótki artykuł o różnicach pomiędzy Pythonem a X. Jeśli nie wiesz, co sprawia, że Python jest kompaktowy i produktywny zapraszam do wypróbowania tego języka. Oto twój pierwszy program:
import __hello__
A co, jeśli prędkość jednak ma znaczenie?
Tona tekstu powyżej prawdopodobnie wskazuje na to, że prędkość nic nie znaczy. Tak naprawdę, w wielu przypadkach ma ona znaczenie. Na przykład, w aplikacji webowej jest konkretny endpoint, który potrzebuje dużo czasu na odpowiedź. Wiesz, jak szybko powinien działać i jak bardzo trzeba go poprawić.
W tym przypadku miały miejsce dwie rzeczy:
- Zauważyłeś jeden endpoint, który wolno działa
- Uważasz, że działa zbyt wolno, bo nie pasuje do tego, co opisujesz jako „wystarczająco szybko”
Nie trzeba robić mikro-optymalizacji wszystkich elementów aplikacji. Wszystko powinno być tylko „wystarczająco szybkie”. Twoi użytkownicy mogą zauważyć, że endpoint potrzebuje więcej czasu na odpowiedź, ale nie zauważą tego, że poprawiłeś czas reakcji z 35 ms do 25 ms. „Wystarczająco dobrze” to efekt, który powinieneś osiągnąć.
Sprostowanie: Powinienem prawdopodobnie zaznaczyć, że niektóre aplikacje, np. służące do licytacji w czasie rzeczywistym potrzebują mikro-optymalizacji i każda milisekunda ma znaczenie. Ale to wyjątek, nie reguła.
Żeby wymyślić, jak zoptymalizować endpoint, po pierwsze musisz sprofilować kod i spróbować znaleźć zator. Bo przecież:
Jakiekolwiek poprawki wprowadzone wszędzie poza wąskim gardłem są iluzją. – Gene Kim
Jeśli Twoje optymalizacje nie dotyczą zatoru, tracisz swój czas i nie naprawiasz prawdziwego problemu. Nie zauważysz żadnej poprawy, dopóki nie pozbędziesz się przeszkody. Jeśli będziesz próbował optymalizować przed znalezieniem zatoru, będziesz tylko grał w ciuciubabkę z częściami swojego kodu. Optymalizacja kodu przed pomiarem i ustaleniem gdzie znajduje się zator, nazywana jest „przedwczesną optymalizacją”. Poniższy cytat przypisuje się często Donaldowi Knuthowi, a on sam twierdzi, że ukradł go komuś innemu:
Przedwczesna optymalizacja to źródło wszelkiego zła.
W dyskusji o utrzymaniu kodu Donald Kunth mówi, że:
Powinniśmy zapomnieć o małych sprawnościach, powiedzmy w 97% przypadków: przedwczesna optymalizacja to źródło wszelkiego zła. Mimo to nie pozwalajmy sobie na zmarnowanie okazji w tych krytycznych 3%.
Innymi słowy, najczęściej powinieneś zapomnieć o optymalizacji swojego kodu. Prawie zawsze będzie on wystarczająco dobry. Jeśli nie jest, to zwykle wystarczy poprawić coś w 3% ścieżki kodu. Nie dostaniesz żadnej nagrody, jeśli przyspieszysz swój endpoint o kilka nanosekund dzięki użyciu “if” zamiast funkcji. Najpierw mierz, potem optymalizuj.
Przedwczesna optymalizacja zawiera preferowanie pewnych struktur danych czy nawet wywołanie nieco szybszej metody, bo ogólnie jest szybsza. Według informatyków, jeśli dana metoda lub algorytm mają taki sam wzrost asymptotyczny (albo Big-O) to są sobie równe, nawet jeśli w praktyce jedno z nich jest 2 razy wolniejsze. Komputery są tak szybkie, że wzrost nakładów obliczeniowych na wykonanie algorytmu przy zwiększeniu danych/użycia staje się ważniejszy niż sama szybkość. Innymi słowy, jeśli masz dwie funkcje O(log n), to nie ma znaczenia, jeśli jedna z nich jest dwa razy wolniejsza. W miarę jak przybywa danych, obie funkcje „zwolnią” w jednakowym tempie. To dlatego przedwczesna optymalizacja jest źródłem wszelkiego zła. Marnuje nasz czas i prawie nigdy nie poprawia naszej ogólnej wydajności.
Jeśli chodzi o Big-O, możemy się spierać, że wszystkie języki są dla twojego programu O(n), gdzie n oznacza linie kodu lub instrukcje. Dla tych samych instrukcji będą rosnąć tak samo. Nie ma znaczenia, jak wolny jest język/czas działania. Jeśli chodzi o wzrost asymptotyczny, wszystkie języki są takie same. Według tej logiki wybór języka ze względu na jego szybkość jest ostateczną formą przedwczesnej optymalizacji. Wybierasz coś pozornie szybkiego bez pomiaru i zrozumienia tego, gdzie pojawi się zator.
Wybór języka ze względu na jego szybkość jest ostateczną formą przedwczesnej optymalizacji.
Optymalizacja Pythona
Najbardziej lubię w Pythonie to, że pozwala on optymalizować kod kawałek po kawałku. Powiedzmy, że znajdujesz w Pythonie metodę, która jest twoim zatorem. Optymalizowałeś ją kilka razy, być może posługując się wskazówkami stąd lub stamtąd i dochodzisz do wniosku, że to Python sam w sobie jest zatorem. Python ma możliwość wywołania kodu C, co oznacza, że możesz tę jedną metodę przepisać w C i pozbyć się problemu. Możesz tak postępować metoda po metodzie. Ten proces pozwoli ci na optymalizację „powolnych” metod w każdym języku, który kompiluje się do kompatybilnego z C assemblera. Dzięki temu możesz programować w Pythonie większość czasu i zmieniać to, tylko kiedy naprawdę musisz.
Istnieje język o nazwie Cython, który jest nadzbiorem Pythona. Stanowi właściwie mix Pythona i C i jest progresywnie typowanym językiem. Każdy kod w Pythonie działa też w Cythonie, który kompiluje się do kodu C. W Cythonie możesz napisać moduł lub metodę i powoli iść w stronę typów z C i wydajności. Możesz mieszać typy C i duck-typing z Pythona. Dzięki Cythonowi, dostaniesz optymalizację wyłącznie tam, gdzie jest to konieczne i piękno Pythona wszędzie indziej.
Jeśli jednak utkniesz w martwym punkcie przez wydajnościowe wady Pythona, nie musisz przenosić całego kodu do innego języka. Wystarczy, że przepiszesz kilka metod w Cythonie. Taką strategią posługuje się Eve Online. Eve to MMOG, która używa tylko Pythona i Cythona. Wydajność na poziomie gry osiągają, optymalizując zatory w C/Cythonie. A jeśli działa to u nich, powinno działać u wszystkich pozostałych. Są też inne sposoby na optymalizację Pythona. Np., PyPy jest implementacją JIT dla Pythona, która pozwoli Ci znacząco poprawić czas wykonania w długo działających aplikacjach (takich jak web serwery), zamieniając CPython (domyślną implementację) na PyPy.
Podsumujmy najważniejsze myśli:
- Optymalizuj swój najbardziej wartościowy zasób. To TY, nie komputer.
- Wybierz język/framework/architekturę, które pomogą ci szybko tworzyć (tak jak Python). Nie wybieraj technologii tylko dlatego, że są szybkie.
- Kiedy natkniesz się na problemy z wydajnością: znajdź zator.
- Twój zator raczej nigdy nie będzie w samym CPU lub Pythonie.
- Jeśli Python jest zatorem (a już zoptymalizowałeś algorytmy itp.), przenieś się do Cythona/C.
- Ciesz się szybkim wykonywaniem zadań.
Mam nadzieję, że spodobał Ci się ten artykuł tak bardzo, jak mi podobało się jego pisanie. Jeśli chciałbyś pogadać ze mną o Pythonie, znajdziesz mnie na twitterze (@nhumrich) i na Python slack channel.
Oryginał znajdziesz tutaj: Yes, Python is Slow, and I Don't Care.