Zauważyłem, że niektóre aplikacje lub algorytmy zbudowane na języku programowania, na przykład C ++ / Rust, działają szybciej lub szybciej niż te zbudowane na powiedzmy Java / Node.js, działające na tym samym komputerze. Mam kilka pytań na ten temat:
- Dlaczego to się dzieje?
- Co rządzi „prędkością” języka programowania?
- Czy ma to coś wspólnego z zarządzaniem pamięcią?
Byłbym bardzo wdzięczny, gdyby ktoś to dla mnie zepsuł.
programming-languages
compilers
evil_potato
źródło
źródło
Odpowiedzi:
Przy projektowaniu i implementacji języka programowania istnieje wiele opcji, które mogą wpłynąć na wydajność. Wspomnę tylko o kilku.
Każdy język musi być ostatecznie uruchamiany przez wykonanie kodu maszynowego. „Skompilowany” język, taki jak C ++, jest analizowany, dekodowany i tłumaczony na kod maszynowy tylko raz, w czasie kompilacji. Język „zinterpretowany”, jeśli jest implementowany bezpośrednio, jest dekodowany w czasie wykonywania, na każdym kroku i za każdym razem. Oznacza to, że za każdym razem, gdy uruchamiamy instrukcję, interpreter musi sprawdzić, czy jest to „jeśli-to-inaczej”, czy przydział itp. I działać odpowiednio. Oznacza to, że jeśli zapętlimy 100 razy, dekodujemy ten sam kod 100 razy, marnując czas. Na szczęście tłumacze często to optymalizują poprzez np. System kompilacji „just-in-time”. (Bardziej poprawnie, nie ma czegoś takiego jak „skompilowany” lub „zinterpretowany” język - jest to właściwość implementacji, a nie języka. Mimo to,
Różni kompilatorzy / tłumacze dokonują różnych optymalizacji.
Jeśli język ma automatyczne zarządzanie pamięcią, jego implementacja musi wykonać odśmiecanie. Ma to koszt działania, ale uwalnia programistę od zadania podatnego na błędy.
Język może być bliższy maszynie, pozwalając ekspertowi na mikrooptymalizację wszystkiego i wyciśnięcie większej wydajności z procesora. Jednak jest dyskusyjne, czy jest to faktycznie korzystne w praktyce, ponieważ większość programistów tak naprawdę nie optymalizuje mikro, a kompilator może zoptymalizować dobry język wyższego poziomu niż to, co zrobiłby przeciętny programista. (Czasami jednak bycie dalej od maszyny może mieć również swoje zalety! Na przykład Haskell ma bardzo wysoki poziom, ale dzięki swoim możliwościom wyboru projektu może zawierać bardzo lekkie zielone nici.)
Statyczne sprawdzanie typu może również pomóc w optymalizacji. W dynamicznie wpisywanym, interpretowanym języku, za każdym razem
x - y
, gdy wykonuje się obliczenia , tłumacz często musi sprawdzać, czy obiex,y
są liczbami i (np.) Zgłaszać wyjątek. To sprawdzenie można pominąć, jeśli typy zostały już sprawdzone podczas kompilacji.Niektóre języki zawsze zgłaszają błędy czasu wykonywania w rozsądny sposób. Jeśli piszesz
a[100]
w Javie, w któreja
jest tylko 20 elementów, otrzymujesz wyjątek czasu wykonywania. W C spowodowałoby to niezdefiniowane zachowanie, co oznacza, że program może ulec awarii, nadpisać niektóre losowe dane w pamięci, a nawet wykonać absolutnie cokolwiek innego (norma ISO C nie ma żadnych ograniczeń). Wymaga to sprawdzenia środowiska wykonawczego, ale zapewnia programistom znacznie ładniejszą semantykę.Należy jednak pamiętać, że podczas oceny języka wydajność to nie wszystko. Nie miej na tym punkcie obsesji. Powszechną pułapką jest próba mikrooptymalizacji wszystkiego, ale nie dostrzega się, że stosowana jest nieefektywna struktura algorytmu / danych. Knuth powiedział kiedyś: „przedwczesna optymalizacja jest źródłem wszelkiego zła”.
Nie lekceważ, jak trudno jest poprawnie napisać program . Często lepiej jest wybrać „wolniejszy” język, który ma bardziej przyjazną dla człowieka semantykę. Ponadto, jeśli istnieją pewne części krytyczne pod względem wydajności, zawsze można je zaimplementować w innym języku. Tytułem odniesienia, w konkursie programowym ICFP 2016 , były to języki używane przez zwycięzców:
Żaden z nich nie używał jednego języka.
źródło
Nie ma czegoś takiego jak „prędkość” języka programowania. Jest tylko prędkość określonego programu napisanego przez określonego programistę wykonanego przez określoną wersję konkretnej implementacji konkretnego silnika wykonawczego działającego w określonym środowisku.
Mogą występować ogromne różnice w wydajności podczas uruchamiania tego samego kodu napisanego w tym samym języku na tym samym komputerze przy użyciu różnych implementacji. Lub nawet używając różnych wersji tej samej implementacji. Na przykład uruchomienie dokładnie tego samego testu porównawczego ECMAScript na dokładnie tej samej maszynie przy użyciu wersji SpiderMonkey sprzed 10 lat w porównaniu z wersją z tego roku prawdopodobnie przyniesie wzrost wydajności pomiędzy 2 × –5 ×, a może nawet 10 ×. Czy to oznacza, że ECMAScript jest 2 razy szybszy niż ECMAScript, ponieważ uruchamianie tego samego programu na tym samym komputerze jest 2 razy szybsze w nowszej implementacji? To nie ma sensu.
Nie całkiem.
Zasoby. Pieniądze. Microsoft prawdopodobnie zatrudnia więcej osób przygotowujących kawę dla programistów kompilatorów niż cała społeczność PHP, Ruby i Python łącznie ma ludzi pracujących na swoich maszynach wirtualnych.
Dla mniej więcej jakiejkolwiek funkcji języka programowania, która w jakiś sposób wpływa na wydajność, istnieje również rozwiązanie. Na przykład C (używam C jako stand-in dla klasy podobnych języków, z których niektóre istniały nawet przed C) nie jest bezpieczny dla pamięci, więc wiele programów C działających w tym samym czasie może deptać wzajemna pamięć. Tak więc wynajdujemy pamięć wirtualną i sprawiamy, że wszystkie programy C przechodzą przez warstwę pośredniczącą, aby mogły udawać, że są jedynymi uruchomionymi na komputerze. Jest to jednak powolne, dlatego wynajdujemy MMU i implementujemy pamięć wirtualną w sprzęcie, aby ją przyspieszyć.
Ale! Języki bezpieczne dla pamięci nie potrzebują tego wszystkiego! Posiadanie wirtualnej pamięci nie pomaga im ani trochę. W rzeczywistości jest gorzej: pamięć wirtualna nie tylko nie pomaga w bezpiecznych dla pamięci językach, pamięć wirtualna, nawet jeśli jest implementowana sprzętowo, nadal wpływa na wydajność. Może to być szczególnie szkodliwe dla wydajności śmieciarek (czego używa znaczna liczba implementacji języków bezpiecznych dla pamięci).
Kolejny przykład: nowoczesne uniwersalne procesory ogólnego przeznaczenia wykorzystują wyrafinowane sztuczki, aby zmniejszyć częstotliwość pomyłek w pamięci podręcznej. Wiele z tych sztuczek polega na próbie przewidzenia, jaki kod zostanie wykonany i jaka pamięć będzie potrzebna w przyszłości. Jednak w przypadku języków o wysokim stopniu polimorfizmu w środowisku wykonawczym (np. Języki OO) bardzo trudno jest przewidzieć te wzorce dostępu.
Istnieje jednak inny sposób: całkowity koszt pominięcia pamięci podręcznej to liczba pomyłek pamięci podręcznej pomnożona przez koszt pojedynczego braku pamięci podręcznej. Procesory głównego nurtu starają się zmniejszyć liczbę nieudanych prób, ale co by było, gdybyś mógł obniżyć koszty pojedynczego niepowodzenia?
Procesor Azul Vega-3 został specjalnie zaprojektowany do uruchamiania zwirtualizowanych maszyn JVM i miał bardzo potężną MMU z niektórymi specjalistycznymi instrukcjami pomagającymi w zbieraniu śmieci i wykrywaniu ucieczki (dynamiczny odpowiednik statycznej analizy ucieczki) oraz potężnymi kontrolerami pamięci, a także całym systemem nadal może robić postępy w ponad 20000 zaległych brakach pamięci podręcznej w locie. Niestety, podobnie jak większość procesorów specyficznych dla języka, jego konstrukcja została po prostu wydana i brutalnie wymuszona przez „gigantów” Intel, AMD, IBM i tym podobne.
Architektura procesora to tylko jeden przykład, który ma wpływ na to, jak łatwo lub jak trudno jest mieć wysokowydajną implementację języka. Język taki jak C, C ++, D, Rust, który dobrze pasuje do współczesnego głównego modelu programowania procesorów, będzie łatwiejszy do zrobienia niż język, który musi „walczyć” i ominąć procesor, taki jak Java, ECMAScript, Python, Ruby , PHP.
Naprawdę, to wszystko kwestia pieniędzy. Jeśli wydasz tyle samo pieniędzy na opracowanie wysokowydajnego algorytmu w ECMAScript, wysokowydajnej implementacji ECMAScript, wydajnego systemu operacyjnego zaprojektowanego dla ECMAScript, wysokowydajnego procesora zaprojektowanego dla ECMAScript, tak jak zostało to wydane w ciągu ostatniego przez dziesięciolecia, aby języki podobne do C działały szybko, wtedy prawdopodobnie zobaczysz taką samą wydajność. Tyle, że w tej chwili wydano znacznie więcej pieniędzy na szybkie tworzenie języków podobnych do C niż na szybkie tworzenie języków podobnych do ECMAS, a założenia dotyczące języków podobnych do C są wypierane na cały stos, od MMU i procesorów po systemy operacyjne i systemy pamięci wirtualnej do bibliotek i frameworków.
Osobiście jestem najbardziej zaznajomiony z Ruby (który jest ogólnie uważany za „wolny język”), dlatego podam dwa przykłady:
Hash
klasę (jedną z centralnych struktur danych w Ruby, słowniku klucz-wartość) w Rubiniusie Implementacja Ruby jest napisana w 100% czystym języku Ruby i ma mniej więcej taką samą wydajność jakHash
klasa w YARV (najczęściej używana implementacja), która jest napisana w C. I jest biblioteka do obróbki obrazów napisana jako rozszerzenie C dla YARV, która również ma (powolną) czystą Rubinową „wersję rezerwową” dla implementacji, które nie obsługuje C, który wykorzystuje tonę bardzo dynamicznych i refleksyjnych sztuczek Ruby; eksperymentalna gałąź JRuby, wykorzystująca szkielet interpretera AST Truffle i ramę kompilacji Graal JIT firmy Oracle Labs, może wykonać tę czystą „awaryjną wersję” Rubiego tak szybko, jak YARV może wykonać oryginalną wysoce zoptymalizowaną wersję C. Jest to po prostu (no cokolwiek, ale) osiągnięte przez naprawdę sprytnych ludzi, którzy robią naprawdę sprytne rzeczy z dynamicznymi optymalizacjami środowiska uruchomieniowego, kompilacją JIT i częściową oceną.źródło
int
ze względu na wydajność, pomimo faktu, że nieograniczone liczby całkowite, takie jak te używane przez Python, są znacznie bardziej matematycznie naturalne. Wdrażanie nielimitowanych liczb całkowitych w sprzęcie nie byłoby tak szybkie, jak liczby całkowite o stałym rozmiarze. Języki, które próbują ukryć szczegóły implementacji, wymagają złożonych optymalizacji, aby zbliżyć się do naiwnych implementacji C.Teoretycznie, jeśli napiszesz kod, który robi dokładnie to samo w dwóch językach i skompilujesz oba używając „idealnego” kompilatora, wydajność obu powinna być taka sama.
W praktyce istnieje kilka powodów, dla których wydajność będzie inna:
Niektóre języki są trudniejsze do optymalizacji.
Dotyczy to zwłaszcza funkcji, które zwiększają dynamikę kodu (dynamiczne pisanie, metody wirtualne, wskaźniki funkcji), ale także na przykład wyrzucanie elementów bezużytecznych.
Istnieją różne sposoby szybkiego uczynienia kodu przy użyciu takich funkcji, ale zwykle kończy się to przynajmniej nieco wolniej niż bez ich używania.
Niektóre implementacje językowe wymagają kompilacji w czasie wykonywania.
Dotyczy to zwłaszcza języków z maszynami wirtualnymi (jak Java) i języków, które wykonują kod źródłowy, bez pośredniego kroku dla pliku binarnego (jak JavaScript).
Takie implementacje językowe muszą wykonywać więcej pracy w środowisku wykonawczym, co wpływa na wydajność, szczególnie na czas uruchamiania / wydajność wkrótce po uruchomieniu.
Implementacje językowe celowo poświęcają mniej czasu na optymalizacje niż mogliby.
Wszystkie kompilatory dbają o wydajność samego kompilatora, a nie tylko generowanego kodu. Jest to szczególnie ważne w przypadku kompilatorów wykonawczych (kompilatorów JIT), w których kompilacja zbyt długo spowalnia wykonywanie aplikacji. Ale dotyczy to kompilatorów z wyprzedzeniem, takich jak te dla C ++. Na przykład przydział rejestrów jest problemem kompletnym dla NP, więc nie jest realistyczne rozwiązanie go idealnie, a zamiast tego stosuje się heurystykę, która jest wykonywana w rozsądnym czasie.
Różnice w idiomach w różnych językach.
Kod napisany we wspólnym stylu dla określonego języka (kod idiomatyczny) przy użyciu wspólnych bibliotek może powodować różnice w wydajności, ponieważ taki kod idiomatyczny zachowuje się subtelnie inaczej w każdym języku.
Jako przykład rozważmy
vector[i]
w C ++,list[i]
w C # ilist.get(i)
w Javie. Kod C ++ prawdopodobnie nie sprawdza zasięgu i nie wykonuje żadnych wirtualnych wywołań, kod C # wykonuje sprawdzanie zasięgu, ale nie ma wirtualnych wywołań, a kod Java wykonuje sprawdzanie zasięgu i jest to wywołanie wirtualne. Wszystkie trzy języki obsługują metody wirtualne i niewirtualne, a zarówno C ++, jak i C # mogą obejmować sprawdzanie zasięgu lub unikanie go podczas uzyskiwania dostępu do podstawowej tablicy. Ale standardowe biblioteki tych języków postanowiły napisać te równoważne funkcje w różny sposób, w wyniku czego ich wydajność będzie inna.W niektórych kompilatorach może brakować optymalizacji.
Autorzy kompilatorów mają ograniczone zasoby, więc nie mogą wdrożyć każdej możliwej optymalizacji, nawet ignorując inne problemy. Zasoby, które dysponują, mogą być bardziej skoncentrowane na jednym obszarze optymalizacji niż na innych. W rezultacie kod napisany w różnych językach może mieć różną wydajność z powodu różnic w ich kompilatorach, nawet jeśli nie ma podstawowego powodu, dla którego jeden język powinien być szybszy lub nawet łatwiejszy do optymalizacji niż drugi.
źródło
To bardzo ogólne pytanie, ale w twoim przypadku odpowiedź może być prosta. C ++ kompiluje się do kodu maszynowego, gdzie Java kompiluje się do kodu bajtowego Java, który jest następnie uruchamiany na maszynie wirtualnej Java (chociaż istnieje również kompilacja just-in-time, która dodatkowo kompiluje kod bajtowy Java do natywnego kodu maszynowego). Kolejną różnicą może być wyrzucanie elementów bezużytecznych, które jest usługą, którą zapewnia tylko Java.
źródło
Twoje pytanie jest zbyt ogólne, więc obawiam się, że nie mogę udzielić dokładnej odpowiedzi, której potrzebujesz.
Dla mojego najlepszego wyjaśnienia, spójrzmy na platformę C ++ i .Net.
C ++, bardzo zbliżony do kodu maszynowego i jeden z ulubionych programów do programowania w przeszłości (jak ponad 10 lat temu?) Ze względu na wydajność. Nie ma zbyt wielu zasobów potrzebnych do opracowania i uruchomienia programu C ++, nawet z IDE, są one uważane za bardzo lekkie i bardzo szybkie. Możesz także napisać kod C ++ w konsoli i stamtąd rozwinąć grę. Jeśli chodzi o pamięć i zasoby, z grubsza zapomniałem, ile pojemności zajmuje komputer, ale jest to pojemność, której nie można porównać z obecną generacją języka programowania.
Teraz spójrzmy na .Net. Warunkiem koniecznym do rozwoju .Net jest jeden gigantyczny IDE, który składa się nie tylko z jednego rodzaju języków programowania. Nawet jeśli chcesz po prostu programować, powiedzmy C #, samo IDE domyślnie zawiera wiele platform programistycznych, takich jak J #, VB, mobilne itp. Chyba że wykonasz instalację niestandardową i zainstalujesz tylko dokładnie to, czego chcesz, pod warunkiem, że masz wystarczające doświadczenie, aby grać z instalacją IDE.
Poza instalowaniem samego oprogramowania IDE, .Net ma także ogromną pojemność bibliotek i platform w celu ułatwienia dostępu do dowolnej platformy, której potrzebują programiści ORAZ nie.
Programowanie w .Net może być świetną zabawą, ponieważ domyślnie dostępnych jest wiele popularnych funkcji i komponentów. Możesz przeciągać i upuszczać i używać wielu metod sprawdzania poprawności, odczytu plików, dostępu do bazy danych i wiele więcej w IDE.
W rezultacie jest to kompromis między zasobami komputerowymi a szybkością rozwoju. Te biblioteki i środowisko zajmuje pamięć i zasoby. Gdy uruchomisz program w .Net IDE, może to zająć mnóstwo czasu, próbując skompilować biblioteki, komponenty i wszystkie twoje pliki. Podczas debugowania komputer wymaga wielu zasobów do zarządzania procesem debugowania w środowisku IDE.
Zwykle, aby wykonać program .Net, potrzebujesz dobrego komputera z niektórymi pamięciami RAM i procesorem. W przeciwnym razie równie dobrze możesz nie programować. Pod tym względem platforma C ++ jest znacznie lepsza niż .Net. Chociaż nadal potrzebujesz dobrego komputera, ale pojemność nie będzie zbytnio martwić w porównaniu do .Net.
Mam nadzieję, że moja odpowiedź może pomóc w twoim pytaniu. Daj mi znać, jeśli chcesz dowiedzieć się więcej.
EDYTOWAĆ:
Po prostu wyjaśnienie mojej głównej kwestii, odpowiadam głównie na pytanie „Co rządzi prędkością programowania”.
Z punktu widzenia IDE użycie języka C ++ lub .Net we względnym IDE nie wpływa na szybkość pisania kodu, ale wpłynie na szybkość rozwoju, ponieważ kompilator Visual Studio zajmuje więcej czasu, ale program IDE C ++ jest znacznie lżejszy i zużywają mniej zasobów komputerowych. Na dłuższą metę możesz szybciej programować z IDE typu C ++ w porównaniu z IDE .Net, które w dużym stopniu zależy od bibliotek i frameworka. Jeśli poświęcisz czas oczekiwania IDE na uruchomienie, kompilację, uruchomienie programu itp., Wpłynie to na szybkość programowania. Chyba że „szybkość programowania” skupia się tylko na „szybkości pisania kodu”.
Ilość bibliotek i frameworka w Visual Studio również zużywa pojemność komputera. Nie jestem pewien, czy to odpowiada na pytanie „zarządzanie pamięcią”, ale chcę tylko podkreślić, że Visual Studio IDE może zająć wiele zasobów podczas jego uruchamiania, a tym samym spowolnić ogólną „szybkość programowania”. Oczywiście nie ma to nic wspólnego z „szybkością pisania kodu”.
Jak można się domyślać, nie znam zbyt dużo C ++, ponieważ używam go tylko jako przykładu, moja główna uwaga dotyczy typu ciężkiego IDE typu Visual Studio, które pochłania zasoby komputerowe.
Jeśli wpadłem na pomysł i w ogóle nie odpowiedziałem na pytanie dotyczące wątku, przepraszam za długi post. Ale radziłbym wątkowi, aby wyjaśnił pytanie i zapytał dokładnie, co powinien wiedzieć o „szybszym” i „wolniejszym”
źródło