Jestem długoletnim użytkownikiem Pythona. Kilka lat temu zacząłem uczyć się C ++, aby zobaczyć, co może zaoferować pod względem szybkości. W tym czasie nadal używałbym Pythona jako narzędzia do prototypowania. Wydawało się, że był to dobry system: zwinne programowanie w Pythonie, szybkie wykonanie w C ++.
Ostatnio coraz częściej używam Pythona i uczę się, jak unikać wszystkich pułapek i anty-wzorów, z których szybko korzystałem w tym języku. Rozumiem, że korzystanie z niektórych funkcji (listy, wyliczenia itp.) Może zwiększyć wydajność.
Ale czy istnieją ograniczenia techniczne lub funkcje językowe, które uniemożliwiają działanie mojego skryptu Python tak szybko, jak równoważnego programu w C ++?
c++
python
performance
language-features
KidElephant
źródło
źródło
Odpowiedzi:
W pewnym sensie sam uderzyłem w tę ścianę, kiedy kilka lat temu podjąłem pracę w pełnym zakresie programowania w języku Python. Uwielbiam Python, naprawdę, ale kiedy zacząłem dostrajać wydajność, doznałem niegrzecznych wstrząsów.
Surowi Pythoniści mogą mnie poprawić, ale oto rzeczy, które znalazłem, pomalowane bardzo szerokimi pociągnięciami.
Ma to wpływ na wydajność, ponieważ oznacza to, że istnieją dodatkowe poziomy pośredniczenia w czasie wykonywania, oprócz bicia dużej ilości pamięci w porównaniu do innych języków.
Inni mogą rozmawiać z modelem wykonania, ale Python jest kompilowany w czasie wykonywania, a następnie interpretowany, co oznacza, że nie przechodzi on do kodu maszynowego. Ma to również wpływ na wydajność. Możesz łatwo łączyć w moduły C lub C ++ lub je znaleźć, ale jeśli uruchomisz bezpośrednio Pythona, będzie to miało wpływ na wydajność.
Teraz, w testach porównawczych usług WWW, Python wypada korzystnie w porównaniu do innych języków kompilacji w czasie wykonywania, takich jak Ruby lub PHP. Ale jest daleko w tyle za większością skompilowanych języków. Nawet języki kompilujące się na język pośredni i działające na maszynie wirtualnej (takie jak Java lub C #) radzą sobie znacznie, znacznie lepiej.
Oto naprawdę interesujący zestaw testów porównawczych, do których czasami się odnoszę:
http://www.techempower.com/benchmarks/
(To powiedziawszy, nadal bardzo kocham Pythona, a jeśli mam szansę wybrać język, w którym pracuję, jest to mój pierwszy wybór. W większości przypadków nie jestem ograniczony przez szalone wymagania dotyczące przepustowości.)
źródło
__slots__
. PyPy powinien pod tym względem radzić sobie znacznie lepiej, ale nie wiem wystarczająco dużo, aby oceniać.Implementacją referencji Python jest interpreter „CPython”. Stara się być dość szybki, ale obecnie nie stosuje zaawansowanych optymalizacji. I w wielu scenariuszach użycia jest to dobra rzecz: kompilacja do jakiegoś kodu pośredniczącego odbywa się bezpośrednio przed środowiskiem uruchomieniowym i za każdym razem, gdy program jest wykonywany, kod jest kompilowany od nowa. Czas potrzebny na optymalizację należy zatem porównać z czasem uzyskanym dzięki optymalizacji - jeśli nie ma zysku netto, optymalizacja jest bezwartościowa. W przypadku bardzo długotrwałego programu lub programu z bardzo ciasnymi pętlami przydatne byłoby zastosowanie zaawansowanych optymalizacji. Jednak CPython jest używany do niektórych zadań, które wykluczają agresywną optymalizację:
Krótko działające skrypty, używane np. Do zadań sysadmin. Wiele systemów operacyjnych, takich jak Ubuntu, buduje dużą część swojej infrastruktury na Pythonie: CPython jest wystarczająco szybki do zadania, ale praktycznie nie ma czasu uruchamiania. O ile jest szybszy niż bash, jest dobry.
CPython musi mieć wyraźną semantykę, ponieważ jest implementacją referencyjną. Pozwala to na proste optymalizacje, takie jak „optymalizacja implementacji operatora foo” lub „kompilacja wyliczeń list do szybszego kodu bajtowego”, ale ogólnie wyklucza optymalizacje, które niszczą informacje, takie jak funkcje wstawiania.
Oczywiście jest więcej implementacji Pythona niż tylko CPython:
Jython jest zbudowany na JVM. JVM może interpretować lub kompilować JIT podany kod bajtowy i ma optymalizacje oparte na profilach. Cierpi z powodu wysokiego czasu rozruchu i zajmuje trochę czasu, zanim rozpocznie się JIT.
PyPy to najnowocześniejsza maszyna JITting Python VM. PyPy jest napisany w RPython, ograniczonym podzbiorze Pythona. Ten podzbiór usuwa pewną ekspresję z Pythona, ale umożliwia statyczne wnioskowanie o typie dowolnej zmiennej. Maszynę wirtualną zapisaną w RPython można następnie przetransponować do C, co daje wydajność podobną do RPython C. Jednak RPython jest nadal bardziej ekspresyjny niż C, co pozwala na szybsze opracowywanie nowych optymalizacji. PyPy jest przykładem ładowania kompilatora. PyPy (nie RPython!) Jest w większości kompatybilny z implementacją referencyjną CPython.
Cython jest (podobnie jak RPython) niekompatybilnym dialektem Pythona ze statycznym pisaniem. Transponuje się również do kodu C i jest w stanie łatwo generować rozszerzenia C dla interpretera CPython.
Jeśli chcesz przetłumaczyć swój kod Python na Cython lub RPython, uzyskasz wydajność podobną do C. Nie należy ich jednak rozumieć jako „podzbiór Pythona”, a raczej jako „C ze składnią Pythona”. Jeśli przełączysz się na PyPy, twój waniliowy kod Python otrzyma znaczny wzrost prędkości, ale nie będzie mógł również współpracować z rozszerzeniami napisanymi w C lub C ++.
Ale jakie właściwości lub funkcje uniemożliwiają waniliowemu Pythonowi osiągnięcie poziomów wydajności podobnych do C, oprócz długich czasów uruchamiania?
Uczestnicy i finansowanie. W przeciwieństwie do Javy lub C #, za tym językiem nie ma jednej firmy prowadzącej samochód, która byłaby zainteresowana uczynieniem tego języka najlepszym w swojej klasie. Ogranicza to rozwój głównie do wolontariuszy i okazjonalnych dotacji.
Późne wiązanie i brak jakiegokolwiek pisania statycznego. Python pozwala nam pisać takie bzdury:
W Pythonie dowolną zmienną można przypisać w dowolnym momencie. Zapobiega to buforowaniu lub wstawianiu; każdy dostęp musi przejść przez zmienną. Ta pośrednia waga obniża wydajność. Oczywiście: jeśli twój kod nie robi tak szalonych rzeczy, aby każdej zmiennej można było nadać ostateczny typ przed kompilacją, a każdej zmiennej przypisano tylko jeden raz, to teoretycznie można wybrać bardziej wydajny model wykonywania. Język, który ma to na uwadze, umożliwiłby oznaczenie identyfikatorów jako stałe i przynajmniej pozwoliłby na opcjonalne adnotacje typu („stopniowe pisanie”).
Wątpliwy model obiektowy. O ile nie są używane gniazda, trudno jest zorientować się, jakie pola ma obiekt (obiekt Pythona jest w zasadzie tablicą skrótów pól). Nawet gdy już tam jesteśmy, nadal nie mamy pojęcia, jakie typy mają te pola. Zapobiega to reprezentowaniu obiektów jako ciasno upakowanych struktur, jak ma to miejsce w C ++. (Oczywiście, reprezentacja obiektów w C ++ również nie jest idealna: z uwagi na naturę struktury nawet prywatne pola należą do publicznego interfejsu obiektu.)
Zbieranie śmieci. W wielu przypadkach GC można całkowicie uniknąć. C ++ pozwala nam statycznie przydzielić obiektów, które są zniszczone automatycznie gdy bieżący zakres pozostało:
Type instance(args);
. Do tego czasu obiekt żyje i może być użyczany innym funkcjom. Zwykle odbywa się to poprzez „przekazanie przez odniesienie”. Języki takie jak Rust pozwalają kompilatorowi sprawdzać statycznie, czy żaden wskaźnik do takiego obiektu nie przekracza jego żywotności. Ten schemat zarządzania pamięcią jest całkowicie przewidywalny, wysoce wydajny i pasuje do większości przypadków bez skomplikowanych grafów obiektowych. Niestety, Python nie został zaprojektowany z myślą o zarządzaniu pamięcią. Teoretycznie można zastosować analizę ucieczki, aby znaleźć przypadki, w których można uniknąć GC. W praktyce proste łańcuchy metod, takie jakfoo().bar().baz()
będzie musiał przydzielić dużą liczbę krótkotrwałych obiektów na stercie (generacja GC jest jednym ze sposobów na ograniczenie tego problemu).W innych przypadkach programista może już znać ostateczny rozmiar jakiegoś obiektu, takiego jak lista. Niestety, Python nie oferuje sposobu na przekazanie tego podczas tworzenia nowej listy. Zamiast tego nowe przedmioty zostaną przesunięte na koniec, co może wymagać wielokrotnych realokacji. Kilka uwag:
Można utworzyć listy o określonym rozmiarze
fixed_size = [None] * size
. Pamięć obiektów znajdujących się na tej liście będzie jednak musiała zostać przydzielona osobno. Kontrastuj w C ++, gdzie możemy to zrobićstd::array<Type, size> fixed_size
.Pakiety tablic określonego typu macierzystego można tworzyć w Pythonie za pomocą
array
wbudowanego modułu. Ponadtonumpy
oferuje wydajne reprezentacje buforów danych o określonych kształtach dla rodzimych typów numerycznych.Podsumowanie
Python został zaprojektowany z myślą o łatwości użytkowania, a nie wydajności. Jego konstrukcja sprawia, że tworzenie wysoce wydajnego wdrożenia jest raczej trudne. Jeśli programista powstrzyma się od problematycznych funkcji, kompilator rozumiejący pozostałe idiomy będzie w stanie emitować wydajny kod, który może konkurować z wydajnością C.
źródło
Tak. Podstawowym problemem jest to, że język jest zdefiniowany jako dynamiczny - to znaczy, że nigdy nie wiesz, co robisz, dopóki tego nie zrobisz. To sprawia, że bardzo trudno jest produkować wydajnego kodu maszynowego, bo nie wiem, co produkować kodu maszynowego dla . Kompilatory JIT mogą wykonywać pewne prace w tym obszarze, ale nigdy nie jest to porównywalne z C ++, ponieważ kompilator JIT po prostu nie może poświęcić czasu i pamięci na działanie, ponieważ jest to czas i pamięć, które nie spędzasz na uruchomieniu programu, i istnieją twarde ograniczenia dotyczące tego, co mogą to osiągnąć bez przerywania dynamicznej semantyki języka.
Nie zamierzam twierdzić, że jest to niedopuszczalny kompromis. Jednak dla natury Pythona zasadnicze znaczenie ma to, aby rzeczywiste implementacje nigdy nie były tak szybkie jak implementacje C ++.
źródło
Istnieją trzy główne czynniki, które wpływają na wydajność wszystkich języków dynamicznych, niektóre bardziej niż inne.
W przypadku C / C ++ koszty względne tych 3 czynników wynoszą prawie zero. Instrukcje są wykonywane bezpośrednio przez procesor, wysyłanie zajmuje najwyżej pośrednio lub dwa, pamięć sterty nigdy nie jest przydzielana, chyba że tak mówisz. Dobrze napisany kod może zbliżyć się do języka asemblera.
W przypadku C # / Java z kompilacją JIT pierwsze dwa są niskie, ale pamięć wyrzucana przez śmieci ma swój koszt. Dobrze napisany kod może zbliżyć się do 2x C / C ++.
W przypadku Python / Ruby / Perl koszt wszystkich trzech z tych czynników jest stosunkowo wysoki. Pomyśl 5x w porównaniu do C / C ++ lub gorzej. (*)
Pamiętaj, że kod biblioteki wykonawczej może być napisany w tym samym języku co twoje programy i mieć te same ograniczenia wydajności.
(*) Ponieważ kompilacja Just-In_Time (JIT) jest rozszerzona na te języki, one również będą zbliżać się (zwykle 2x) do prędkości dobrze napisanego kodu C / C ++.
Należy również zauważyć, że gdy różnica jest wąska (między konkurującymi językami), różnice są zdominowane przez algorytmy i szczegóły implementacji. Kod JIT może pobić C / C ++, a C / C ++ może pobić język asemblera, ponieważ łatwiej jest napisać dobry kod.
źródło
Hash
klasa Rubinius (jedna z podstawowych struktur danych w Ruby) jest napisana w Ruby i działa porównywalnie, czasem nawet szybciej, niżHash
klasa YARV, która jest napisana w C. I jednym z powodów jest to, że duże części środowiska uruchomieniowego Rubiniusa są napisane w języku Ruby, aby mogły…Nie. To tylko kwestia pieniędzy i zasobów włożonych w szybkie uruchomienie C ++ w porównaniu z pieniędzmi i zasobami włożonymi w szybkie uruchomienie Pythona.
Na przykład, kiedy pojawiła się Self VM, był to nie tylko najszybszy dynamiczny język OO, to był najszybszy okres języka OO. Pomimo tego, że jest niezwykle dynamicznym językiem (znacznie bardziej niż na przykład Python, Ruby, PHP czy JavaScript), był szybszy niż większość dostępnych implementacji C ++.
Ale potem Sun anulował projekt Self (dojrzały język OO ogólnego przeznaczenia do tworzenia dużych systemów), aby skupić się na małym języku skryptowym dla animowanych menu w dekoderach telewizyjnych (być może słyszałeś o tym, nazywa się Java), nie było więcej funduszy. Jednocześnie Intel, IBM, Microsoft, Sun, Metrowerks, HP i in. wydał ogromne ilości pieniędzy i zasobów, dzięki czemu C ++ jest szybki. Producenci procesorów dodali funkcje do swoich układów, aby C ++ był szybki. Systemy operacyjne zostały napisane lub zmodyfikowane, aby uczynić C ++ szybkim. C ++ jest więc szybki.
Nie znam się zbytnio na Pythonie, jestem raczej Ruby, więc dam przykład z Ruby:
Hash
klasa (równoważna pod względem funkcji i ważnościdict
w Pythonie) w implementacji Rubinius Ruby jest napisana w 100% czystym Ruby; jednak konkuruje korzystnie, a czasem nawet lepiej niżHash
klasa w YARV, która jest napisana ręcznie zoptymalizowanym C. W porównaniu z niektórymi komercyjnymi systemami Lisp lub Smalltalk (lub wyżej wspomnianą Self VM), kompilator Rubiniusa nie jest nawet tak sprytny .W Pythonie nie ma nic nieodłącznego, co spowalnia działanie. W dzisiejszych procesorach i systemach operacyjnych są funkcje, które szkodzą Pythonowi (np. Wiadomo, że pamięć wirtualna ma ogromny wpływ na wydajność czyszczenia pamięci). Istnieją funkcje, które pomagają C ++, ale nie pomagają Pythonowi (współczesne procesory próbują unikać błędów pamięci podręcznej, ponieważ są one tak drogie. Niestety unikanie błędów pamięci podręcznej jest trudne, gdy masz OO i polimorfizm. Zamiast tego należy zmniejszyć koszt pamięci podręcznej tęskni. Procesor Azul Vega, który został zaprojektowany dla Javy, robi to.)
Jeśli wydajesz tyle pieniędzy, badań i zasobów na szybkie tworzenie Pythona, jak to zrobiono w C ++, i wydajesz tyle pieniędzy, badań i zasobów na tworzenie systemów operacyjnych, które sprawiają, że programy Python działają szybko, jak w przypadku C ++ i wydajesz dużo pieniędzy, badań i zasobów na tworzenie procesorów, które sprawiają, że programy Pythona działają szybko, jak to zrobiono dla C ++, więc nie mam wątpliwości, że Python mógłby osiągnąć wydajność porównywalną do C ++.
Z ECMAScript widzieliśmy, co może się stać, jeśli tylko jeden gracz poważnie podchodzi do wydajności. W ciągu roku mieliśmy zasadniczo 10-krotny wzrost wydajności dla wszystkich głównych dostawców.
źródło