Co rządzi „prędkością” języka programowania?
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.
Czy ma to coś wspólnego z zarządzaniem pamięcią?
Nie całkiem.
Dlaczego to się dzieje?
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ą.