Dlaczego musimy wymieniać abstrakcję na szybkość?


11

Dlaczego języki wysokiego poziomu najwyraźniej nigdy nie osiągają języków niższego poziomu pod względem szybkości? Przykładami języków wysokiego poziomu są Python, Haskell i Java. Języki niskiego poziomu byłyby trudniejsze do zdefiniowania, ale powiedzmy C. Porównania można znaleźć w całym Internecie i wszyscy zgadzają się, że C jest znacznie szybszy, czasami nawet 10-krotnie.1

Co powoduje tak dużą różnicę w wydajności i dlaczego języki wysokiego poziomu nie mogą nadrobić zaległości?

Początkowo uważałem, że to wszystko wina kompilatorów i że sytuacja ulegnie poprawie w przyszłości, ale niektóre z najpopularniejszych języków wyższego poziomu istniały już od dziesięcioleci i wciąż pozostają w tyle, jeśli chodzi o szybkość. Czy nie mogą po prostu skompilować do drzewa składniowego podobnego do C, a następnie wykonać te same procedury, które generują kod maszynowy? A może ma to coś wspólnego z samą składnią?


1 Przykłady:


5
„i wszyscy zgadzają się, że C jest znacznie szybszy” - to z pewnością nie tak.
Raphael

2
W każdym razie myślę, że na pocztę najlepiej odpowiedzieć w mojej odpowiedzi na podobnie źle pomyślane pytanie ; duplikować?
Raphael

2
Zobacz także tutaj i tutaj . Uważaj na śmieciowe odpowiedzi.
Raphael

Odpowiedni: stackoverflow.com/questions/6964392/ ... Mit o powolności wszystkich „języków wysokiego poziomu” jest dość smutny.
Xji

Zgadzam się z innymi, że abstrakcja! = Powolność, ale obawiam się, że istnieje znacznie większy problem, którego nauczyciele informatyki (ja byłem jednym z nich) nie są świadomi. Oznacza to, że w przypadku prawdziwych programów (1000 linii kodu i więcej) istnieje wiele sposobów ich wykonania, a ich działanie może się różnić w zależności od rzędu wielkości. Samo myślenie o big-O całkowicie mija się z celem. Sprawdź tutaj .
Mike Dunlavey,

Odpowiedzi:


19

Obalanie niektórych mitów

  1. Nie ma czegoś takiego jak szybki język. Język może generować szybki kod, ale różne języki wyróżniają się na różnych testach porównawczych. Możemy uszeregować języki według określonego zestawu wadliwych testów, ale nie możemy uszeregować języków w próżni.

  2. Kod C jest zwykle szybszy, ponieważ ludzie, którzy potrzebują każdego centymetra wydajności, używają C. Statystyka, że ​​C jest szybszy „dziesięciokrotnie” może być niedokładna, ponieważ może być tak, że ludzie używający Pythona po prostu nie dbają o to o szybkości i nie napisałem optymalnego kodu Python. Widzimy to w szczególności w językach takich jak Haskell. Jeśli naprawdę się postarasz , możesz napisać Haskell, który działa na równi z C. Ale większość ludzi nie potrzebuje tego wykonania, więc mamy kilka wadliwych porównań.

  3. Czasami brak bezpieczeństwa, a nie abstrakcja sprawia, że ​​C jest szybki. Brak ograniczeń tablic i kontroli zerowych wskaźników pozwala zaoszczędzić czas i przez lata był przyczyną wielu luk w zabezpieczeniach.

  4. Języki nie są szybkie, wdrożenia są szybkie. Wiele języków abstrakcyjnych zaczyna się powoli, ponieważ prędkość nie jest ich celem, ale staje się szybsza w miarę dodawania kolejnych optymalizacji.

Kompresja kontra prędkość jest być może niedokładna. Sugerowałbym lepsze porównanie:

Prostota, szybkość, abstrakcja: wybierz dwa.

Jeśli korzystamy z identycznych algorytmów w różnych językach, pytanie o szybkość sprowadza się do problemu „Ile rzeczy musimy zrobić w czasie wykonywania, aby to zadziałało?”

W bardzo abstrakcyjnym języku, który jest prosty , taki jak Python lub JavaScript, ponieważ nie wiemy o reprezentacji danych do czasu wykonania, w końcu jest dużo usuwania odwołania do wskaźników i sprawdzania dynamicznego w czasie wykonywania, które są powolne.

Podobnie, należy wykonać wiele kontroli, aby upewnić się, że nie zniszczyłeś komputera. Kiedy wywołujesz funkcję w Pythonie, musi ona upewnić się, że wywoływany obiekt faktycznie jest funkcją, ponieważ w przeciwnym razie możesz skończyć na losowym kodzie, który robi straszne rzeczy.

Wreszcie, większość języków abstrakcyjnych ma narzut związany z odśmiecaniem. Większość programistów zgodziła się, że śledzenie żywotności dynamicznie alokowanej pamięci jest uciążliwe i wolą, aby śmieciarz robił to za nich w czasie wykonywania. To wymaga czasu, aby program C nie musiał wydawać na GC.

Streszczenie Nie oznacza powolności

Są jednak języki, które są zarówno abstrakcyjne, jak i szybkie. Najbardziej dominującą w tej erze jest Rust. Wprowadzając moduł sprawdzający pożyczkę i wyrafinowany system typów, pozwala na abstrakcyjny kod i wykorzystuje informacje o czasie kompilacji w celu zmniejszenia ilości pracy, którą musimy wykonać w czasie wykonywania (tj. Odśmiecanie).

Każdy język o typie statycznym oszczędza nam czas, zmniejszając liczbę kontroli środowiska wykonawczego, i wprowadza złożoność, wymagając od nas sprawdzania pisowni podczas kompilacji. Podobnie, jeśli mamy język, który koduje, czy wartość może być pusta w systemie typów, możemy zaoszczędzić czas dzięki sprawdzaniu czasu kompilacji przez wskaźnik zerowy.


2
„Kod C jest zwykle szybszy, ponieważ ludzie, którzy potrzebują każdego centymetra wydajności, używają C” - dokładnie. Chcę zobaczyć wzorce w postaci „średniego czasu działania kodu napisanego przez studentów / specjalistów X-roku z Y-letnim doświadczeniem w Z”. Oczekuję, że odpowiedź na C jest zwykle „kod niepoprawny” dla małych X i Y. Byłoby naprawdę interesujące zobaczyć, o ile więcej doświadczenia / wiedzy potrzebujesz, aby wykorzystać potencjał wydajności C obiecuje.
Raphael

Haskell jest naprawdę wyjątkiem, który potwierdza regułę. ;) Myślę, że C ++ znalazł dość rozsądny środek na temat GC dzięki swoim inteligentnym wskaźnikom, o ile nie zagnieżdżasz wskaźników wspólnych i będzie tak szybki jak C.
Kaveh

0

oto kilka kluczowych pomysłów na ten temat.

  • łatwym porównaniem / studium przypadku, aby uzyskać abstrakcję wrt vs szybkość, jest Java vs c ++. Java została zaprojektowana, aby odciąć niektóre aspekty niższego poziomu c ++, takie jak zarządzanie pamięcią. na początku (około czasu wynalezienia języka w połowie lat 90.) wykrywanie śmieci w Javie nie było bardzo szybkie, ale po kilku dekadach badań, śmieciarze są bardzo dopracowani / szybcy / zoptymalizowani, więc śmieciarki są w dużej mierze eliminowane jako obniżenie wydajności w java. np. patrz nawet ten nagłówek z 1998 r .: Testy wydajności pokazują Javę tak szybko, jak C ++ / javaworld

  • języki programowania i ich długa ewolucja mają nieodłączną „piramidalną / hierarchiczną strukturę” jako rodzaj transcendentalnego wzorca projektowego. na szczycie piramidy znajduje się coś, co kontroluje inne dolne części piramidy. innymi słowy, bloki budulcowe są wykonane z bloków budulcowych. widać to również w strukturze API. w tym sensie większa abstrakcja zawsze prowadzi do jakiegoś nowego komponentu na szczycie piramidy kontrolującego inne komponenty. więc w pewnym sensie nie tyle jest tak, że wszystkie języki są na poziomie, ale że języki wymagają procedur w innych językach. np. wiele języków skryptowych (python / ruby) często wywołuje biblioteki C lub C ++, typowymi przykładami są procedury numeryczne lub macierzowe. więc istnieją języki wyższego i niższego poziomu, a języki wyższego poziomu nazywają je językami niższego poziomu. w tym sensie pomiar prędkości względnej nie jest tak naprawdę porównywalny.

  • można powiedzieć, że cały czas wymyślane są nowe języki w celu optymalizacji kompromisu abstrakcji / prędkości, tj. jego kluczowego celu projektowego. być może nie tyle, że większa abstrakcja zawsze poświęca prędkość, ale że zawsze szuka się lepszej równowagi z nowszymi projektami. np. Google Go został pod wieloma względami specjalnie zoptymalizowany pod kątem kompromisu, aby był jednocześnie zarówno wysoki, jak i wydajny. patrz np. Google Go: dlaczego język programowania Google może rywalizować z Javą w świecie przedsiębiorstw / technologii


0

Sposób, w jaki lubię myśleć o wydajności, to „gdzie guma styka się z drogą”. Komputer wykonuje instrukcje, a nie abstrakcje.

Chcę to zobaczyć: czy każda wykonywana instrukcja „zarabia na utrzymaniu” poprzez znaczny wkład w wynik końcowy? Jako zbyt prosty przykład rozważ wyszukiwanie wpisu w tabeli zawierającej 1024 wpisy. Jest to 10-bitowy problem, ponieważ program musi „nauczyć się” 10 bitów, zanim pozna odpowiedź. Jeśli algorytmem jest wyszukiwanie binarne, każda iteracja dostarcza 1 bit informacji, ponieważ zmniejsza niepewność o współczynnik 2, więc zajmuje 10 iteracji, po jednej na każdy bit.

Z drugiej strony wyszukiwanie liniowe jest początkowo bardzo nieefektywne, ponieważ pierwsze iteracje zmniejszają niepewność o bardzo mały czynnik. Więc nie uczą się wiele dla włożonego wysiłku.

OK, więc jeśli kompilator może pozwolić użytkownikowi na zawinięcie dobrych instrukcji w sposób uważany za „abstrakcyjny”, to w porządku.


0

Z natury abstrakcja ogranicza komunikację informacji zarówno dla programisty, jak i niższych warstw systemu (kompilatora, bibliotek i systemu wykonawczego). Na korzyść abstrakcji, ogólnie pozwala to dolnym warstwom założyć, że programista nie jest zainteresowany żadnym nieokreślonym zachowaniem, zapewniając większą elastyczność w dostarczaniu określonego zachowania.

Przykładem potencjalnej korzyści z tego aspektu „nie przejmuj się” jest układ danych. W C (niski poziom abstrakcji) kompilator jest bardziej ograniczony w optymalizacji układu danych. Nawet jeśli kompilator może rozpoznać (np. Poprzez informacje profilowe), że optymalizacje unikania dzielenia się na gorąco lub na zimno lub unikania fałszywego współdzielenia byłyby korzystne, generalnie nie można tego zastosować. (Istnieje pewna swoboda w określaniu „tak jakby”, tj. Traktowaniu specyfikacji bardziej abstrakcyjnie, ale czerpanie wszystkich potencjalnych efektów ubocznych stanowi obciążenie dla kompilatora.)

Bardziej abstrakcyjna specyfikacja jest również bardziej odporna na zmiany kompromisów i zastosowań. Niższe warstwy są mniej ograniczone w ponownej optymalizacji programu pod kątem nowych właściwości systemu lub nowych zastosowań. Bardziej konkretna specyfikacja musi albo zostać przepisana przez programistę, albo dolne warstwy muszą podjąć dodatkowy wysiłek, aby zagwarantować zachowanie „jak gdyby”.

Aspektem utrudniającym wydajność w przypadku abstrakcji informacji jest „nie można wyrazić”, co niższe warstwy zwykle obsługują jako „nie wiem”. Oznacza to, że niższe warstwy muszą rozróżniać informacje przydatne do optymalizacji od innych środków, takich jak typowe ogólne zastosowanie, ukierunkowane użycie lub określone informacje o profilu.

Wpływ ukrywania informacji działa również w innym kierunku. Programista może być bardziej produktywny, nie biorąc pod uwagę i nie określając każdego szczegółu, ale może mieć mniej informacji na temat wpływu wyborów projektowych wyższego poziomu.

Z drugiej strony, gdy kod jest bardziej szczegółowy (mniej abstrakcyjny), niższe warstwy systemu mogą po prostu robić to, co im każą, tak jak im każą. Jeśli kod jest dobrze napisany pod kątem jego docelowego użycia, będzie dobrze pasował do jego docelowego użycia. Mniej abstrakcyjny język (lub paradygmat programowania) pozwala programiście zoptymalizować implementację poprzez szczegółowy projekt i wykorzystanie informacji, które nie są łatwo przekazywane w danym języku do niższych warstw.

Jak zauważono, mniej abstrakcyjne języki (lub techniki programowania) są atrakcyjne, gdy dodatkowe umiejętności i wysiłek programisty mogą przynieść wartościowe wyniki. Przy zastosowaniu większego wysiłku i umiejętności programisty wyniki zazwyczaj będą lepsze. Ponadto system językowy, który jest rzadziej wykorzystywany w aplikacjach krytycznych pod względem wydajności (zamiast kładzenia nacisku na wysiłek programistyczny lub niezawodność - kontrole granic i odśmiecanie nie dotyczą wyłącznie wydajności programisty, ale także poprawności, abstrakcja zmniejszająca obciążenie umysłowe programisty może poprawić niezawodność) będzie miał mniejszy nacisk na poprawę wydajności.

Swoistość działa również wbrew zasadzie „nie powtarzaj się”, ponieważ optymalizacja jest zazwyczaj możliwa poprzez dostosowanie kodu do określonego zastosowania. Ma to oczywiste konsekwencje dla niezawodności i wysiłku programistycznego.

Abstrakcje dostarczane przez język mogą również obejmować niepożądane lub niepotrzebne prace bez możliwości wyboru mniejszej wagi abstrakcji. Podczas gdy dolne warstwy mogą czasem wykryć i usunąć niepotrzebną pracę (np. Kontrole granic mogą być wydobyte z bryły pętli i całkowicie usunięte w niektórych przypadkach), ustalenie, że taka poprawność jest poprawna, wymaga więcej „umiejętności i wysiłku” przez kompilator.

Wiek języka i popularność są również ważnymi czynnikami, zarówno pod względem dostępności wykwalifikowanych programistów, jak i jakości niższych warstw systemu (w tym dojrzałych bibliotek i przykładów kodu).

Innym mylącym czynnikiem w takich porównaniach jest nieco ortogonalna różnica między kompilacją z wyprzedzeniem a kompilacją just-in-time. Podczas gdy kompilacja just-in-time może łatwiej wykorzystywać informacje o profilu (nie polegając na programiście w celu zapewnienia uruchamiania profilów) i optymalizację specyficzną dla systemu (kompilacja z wyprzedzeniem może być ukierunkowana na szerszą kompatybilność), narzut agresywnej optymalizacji jest rozliczany jako część wydajności środowiska wykonawczego. Wyniki JIT mogą być buforowane, co zmniejsza obciążenie powszechnie używanego kodu. (Alternatywa binarnej ponownej optymalizacji może zapewnić pewne zalety kompilacji JIT, ale tradycyjne binarne formaty dystrybucji upuszczają większość informacji o kodzie źródłowym, potencjalnie zmuszając system do próby rozróżnienia zamiarów konkretnej implementacji.)

(Niższe języki abstrakcji, ze względu na ich nacisk na sterowanie programistą, sprzyjają stosowaniu kompilacji z wyprzedzeniem. Kompilacja w czasie instalacji może być tolerowana, chociaż wybór implementacji w czasie łącza zapewni większą kontrolę programisty. Kompilacja JIT poświęca znaczącą kontrolę. )

Istnieje również kwestia metodologii analizy porównawczej. Taki sam wysiłek / umiejętności są praktycznie niemożliwe do ustalenia, ale nawet jeśli można je osiągnąć, cele językowe mogą wpłynąć na wyniki. Gdyby wymagany był niski maksymalny czas programowania, program dla mniej abstrakcyjnego języka może nawet nie zostać całkowicie napisany w porównaniu z prostym wyrażeniem idiomatycznym w bardziej abstrakcyjnym języku. Gdyby dozwolony był wysoki maksymalny czas / wysiłek programowania, języki o niższej abstrakcji miałyby przewagę. Testy porównawcze przedstawiające wyniki najlepszych starań byłyby oczywiście stronnicze na korzyść mniej abstrakcyjnych języków.

Czasami jest możliwe programowanie w mniej idiomatyczny sposób w języku, aby zyskać zalety innych paradygmatów programowania, ale nawet gdy dostępna jest moc ekspresji, kompromisy w tym zakresie mogą nie być korzystne.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.