Jak duże spowolnienie wynika używania języków wysokiego poziomu?

3

Hipotetyczna sytuacja: Buduję sobie webappkę, dajmy na to w C# albo Javie. Nagle zaczyna mi przybywać użytkowników, apka mi siada, szukam zatem możliwości umieszczenia jej na farmie serwerów i jeszcze co prędzej modyfikuję kod, by dało się go w ten sposób zrównoleglić, bo pisząc apkę nie myślałem o skalowalności i głupio napisałem ją tak, że działa wyłącznie na jednym serwerze.

Ile z tego problemu wynika z faktu, że używam C# a nie C++? Istnieje nastawiony na wydajność framework C++-owy do serwisów webowych. Jak bardzo może być on szybszy od takiego np. ASP .Net? Ile razy więcej użytkowników na raz może obsłużyć na tym samym serwerze? Bo jeśli np. 10, to od razu spadają nam koszta utrzymania apki (mniej musimy płacić za chmurę albo nawet i wcale, bo stronę obsługuje "serwer" w postaci laptopa w szafie w naszym domu).

Aj tam, C++. Pójdźmy dalej, C? Gdzie tam. For the sake of argument zastanówmy się nad webappką napisaną w asemblerze. Są szaleńcy w to się bawiący. Ile mogłoby wynosić przyspieszenie w porównaniu z C++? Kolejne 10 razy? To już jest 100-krotne przyspieszenie w porównaniu w .NET! Laptop w szafie dałby radę nawet dla całkiem dużych serwisów, dla których w przypadku .Neta trzeba by już porządnej farmy serwerów (i architektury appki pod taką farmę pisanej)?

Dlaczego pytam. Naszła mnie bowiem myśl, że chociaż przez ostatnie 15-20 lat komputery przyspieszyły o rzędy wielkości (4 rdzenie po 3GHz każdy to norma nawet dla sprzętów ze średnio-dolnej półki vs jeden rdzeń z taktowaniem liczonym w megahercach), to jednak zarówno dawniej Word jak i teraz otwiera mi się nie natychmiast - trzeba przeczekać splash screen. A przecież podstawowe możliwości nie zmieniły się. Czy nie jest zatem tak, że im bardziej komputery przyspieszają, tym bardziej spowalnia software, bo devom nie chce się go optymalizować oraz korzystają z wolnych języków / frameworków / itp (bo są wygodniejsze), i w konsekwencji przyspieszanie komputerów nic nie daje, bo się to wyrównuje ze spowalniającym oprogramowaniem?

Stąd też wytrzasnąłem to oczekiwane 100-krotne przyspieszenie asemblera w porównaniu w C#; stukrotne przyspieszenie komputerów nie przyspiesza ładowania się Worda.

0

Kolejne 10 razy? To już jest 100-krotne przyspieszenie w porównaniu w .NET!

Z pewnością nie 10 razy, chyba że w jakimś specyficznym, zoptymalizowanym algorytmie. Średnio to będzie raczej 0,1 „raza szybciej”. Albo i wolniej.

zarówno dawniej Word jak i teraz otwiera mi się nie natychmiast

Tu większe znaczenie ma prędkość dysku, i akurat pod tym względem SSD jest sporym krokiem naprzód.

6

@kmph:
Nie musisz gdybać. Jest już zestaw benchmarków: https://www.techempower.com/benchmarks/ Jest C, C++, Java, Go, PHP, Python, C#, JavaScript i wiele innych. Wiarygodność średnia (jak to przy mikrobenchmarkach) ale lepsze to niż nic.

Najbardziej spowalnia baza danych. Jeśli jesteś w stanie trzymać wszystkie dane w RAMie to apka w Javie będzie super szybka. Jeśli musisz trzymać dane w jakimś np MSSQLu to nawet gdybyś resztę okodował w czystym asemblerze to i tak aplikacja jako całość będzie działać wolno. By uzyskać super szybkość musisz w zasadzie pisać cały stos technologiczny od podstaw specjalnie pod własne zastosowania.

2

W C# da się zoptymalizować tak kod żeby latał z prędkością natywnych apek, przy czym w natywnych jezykach bez GC i VM łatwiej sobie w stopę strzelić. Może 15-20 lat temu było inaczej tak obecnie nie widzę powodów by celem optymalizacji pisać duży system w czymś natywnym. Co innego kod niskopoziomowy jak osdev, sterówniki, uC etc. I tak 99,9999% z wydajnością to wina programisty czy architektury a nie języka. Już optymalizowałem kod w C# który realizował to samo zadanie ale schodziłem z czasem wykonania z kilku dni do kilku minut a czasem kilkudziesięciu sekund i już mam pare takich optymalizacji na koncie. Najmniej wydajny kod piszą programiści a nie współczesne języki.

7

@kmph: większość "zwykłych" aplikacji jest I/O bound a nie CPU bound. Nie ogranicza cię moc obliczeniowa tylko odczyty danych (z dysku ale też z pamięci) albo requesty sieciowe. Bo z jednej strony możesz się ładnie wyskalować z mikroserwisami, postawić po 100 nodów każdego, ale potem każda taka hopka do mikroserwisu to stracone milisekundy i nie ma tu znaczenia czy masz te serwisy w kodzie maszynowym naklepane czy w pythonie.

Jak piszesz soft do crackowania hashy to faktycznie lepiej to zrobić w C, bo tam jedyne co robisz to mielisz coś na CPU.

Czy nie jest zatem tak, że im bardziej komputery przyspieszają, tym bardziej spowalnia software, bo devom nie chce się go optymalizować oraz korzystają z wolnych języków / frameworków / itp (bo są wygodniejsze), i w konsekwencji przyspieszanie komputerów nic nie daje, bo się to wyrównuje ze spowalniającym oprogramowaniem?

Nie, po prostu soft dzisiaj jest wielokrotnie bardziej złożony niż kiedyś.

2

Nawet jeśli, zakładając czysto hipotetycznie, masz 2 języki programowania, wolniejszy i szybszy, przy czym zakładamy, że wolniejszy obsłuży 1000 req/sec (pomijamy inne czynniki spowalniające, zakładamy same "surowe" cpu bound), a "szybszy" 10x tyle, czyli 10000 req/sec, a Twoja aplikacja dostaje, powiedzmy 100 000 req/sec, to i tak w przypadku nawet użycia tego "szybszego" masz problem wydajnościowy i nie obejdzie bez rozwiązań w architekturze. Za to często, języki nazwijmy to "szybkie" typu C/C++, zwłaszcza jakiś asm, nie są językami w których masz łatwość utrzymywania/rozwijania projektu, do tego aby pisać np. wydajny kod w asm, to chyba musiałbyś być inżynierem od CPU, który zna go na wylot (marny promil takich programistów jest), w każdym razie musiałbyś być bardziej ogarnięty od programistów kompilatorów do C.

Obecnie znaczenie ma szybkość implementacji logiki biznesowej, do tego obecne języki typu Java (i koledzy z JVMa), Go, Python, Ruby, Rust, Erlang czy nawet JS nadają się podobnie, zakładając dostępność odpowiednich bibliotek (czytaj: nie trzeba w danym projekcie biznesowym wynajdować jakiegoś trywialnego koła od nowa). Więc np. możesz użyć Pythona i mieć 1000 req/sec, zamiast C i 100 000 req/sec (hipotetycznie), ale za to w Pythonie np. apkę napiszesz 10x szybciej i łatwiej będzie Ci zdebugować w razie czego, a w razie problemów wydajnościowych założysz na etapie architektury, że możesz dokładać dodatkowe nody a odpowiedni load balancer nie dopuści, by apka dostała zbyt duży ruch.

Tutaj artykuł, który porusza (m.in) też ten problem: http://thume.ca/2019/04/29/comparing-compilers-in-rust-haskell-c-and-python

5
kmph napisał(a):

Dlaczego pytam. Naszła mnie bowiem myśl, że chociaż przez ostatnie 15-20 lat komputery przyspieszyły o rzędy wielkości (4 rdzenie po 3GHz każdy to norma nawet dla sprzętów ze średnio-dolnej półki vs jeden rdzeń z taktowaniem liczonym w megahercach), to jednak zarówno dawniej Word jak i teraz otwiera mi się nie natychmiast - trzeba przeczekać splash screen.

To może twój. Mój się otwiera poniżej sekundy a za kolejnym razem to już w ogóle w pomijalnym czasie. Po prostu używam 20-letniej wersji. Ale nie musisz być hardkorem, zainstaluj sobie kilkunastoletnią :p

Czy nie jest zatem tak, że im bardziej komputery przyspieszają, tym bardziej spowalnia software, bo devom nie chce się go optymalizować oraz korzystają z wolnych języków / frameworków / itp (bo są wygodniejsze), i w konsekwencji przyspieszanie komputerów nic nie daje, bo się to wyrównuje ze spowalniającym oprogramowaniem?

Jest nawet gorzej, Prawo Wirtha powiada, że:

Oprogramowanie zwalnia szybciej, niż sprzęt przyspiesza.

Nieoptymalność współczesnego softu jest po prostu koszmarna. Wszyscy wychodzą z założenia, że uzerzy mają gigachercowe wielordzenie to sobie zmielą co trzeba, a to przecież nie nasze kilowaty :p

3

U mnie ostatnio comiesięczny skan McAfee na firmowym laptopie trwał ok 162 godziny. Co prawda wydaje mi się, że w to wliczał się czas w uśpieniu, ale i tak przez jakiś tydzień antywirus zarzynał mi 2 rdzenie w 2 rdzeniowym laptopie. Dla mnie zostały głównie HyperThready. Ciekawe ile % wydajności traci się na korpo krapie instalowanym w standardzie na korpo lapkach?

1
kmph napisał(a):

Bo jeśli np. 10, to od razu spadają nam koszta utrzymania apki (mniej musimy płacić za chmurę albo nawet i wcale, bo stronę obsługuje "serwer" w postaci laptopa w szafie w naszym domu).

Prawda, tylko że koszty utrzymania to nie tylko infra, to też ludzie którzy muszą ją utrzymywać :) Po prostu w dzisiejszych czasach cena krzemu << cena ludzi. Warto przeliczyć to sobie w głowie, to potem okazuje się, że koszty chmury to jakaś znikoma część :)

0

Jak już tutaj napisano - aplikacje webowe rzadko mają problem z mocą obliczeniową - cięższe procesy są przetwarzane jako batch'e, a aplikacja webowa ma po prostu działać w akceptowalnej dla użytkownika prędkości.
Dodatkowo nawet w samych językach wysokiego poziomu używa się nieefektywnych rozwiązań - choćby i powszechnie stosowane kolekcje obiektów (zamiast np. tablic prymitywów). Ot takie czasy, siła procków rośnie.

6

Języki wysokiego poziomu skracają https://en.wikipedia.org/wiki/Time_to_market a to jest często kluczowe.

3

Obecnie oprogramowanie jest przesadnie rozrośnięte (bloated). Komputery były szybsze, kiedy były wolniejsze. Rozwój hardware'u częściowo spowodował to, że programiści w większości przestali się przejmować pisaniem wydajnego oprogramowania, bo praktycznie niemalże nie mają ograniczeń sprzętowych, jak kiedyś. Większość współczesnego softu to niestety przeciwieństwo zasad YAGNI, KISS i Unix Philosophy. Wspomniane zostały mikroserwisy. Owszem tam może być jakiś dodatkowy narzut w postaci opóźnień sieciowych itp., ale spowolnienie aplikacji nie wynika tylko z wolniejszych połączeń sieciowych. Na pewno nieraz korzystaliście z powolnej aplikacji desktopowej, która w zasadzie internetu do działania nie potrzebuje, albo przymulił Wam system operacyjny. Dodatkowym problemem jest fakt, że sporo firm typu korpo dorzuca do swojego softu wszelkiej maści spyware, który wysyła na ich serwery informacje o użytkownikach. Tego typu praktyki też spowalniają działanie programów.

11

Czyli kolejne standardowe biadolenie o mitycznej szybkości ASM czy C.

Wychowałem się na ośmiobitowcach (C64 głównie), potem Amiga i w sumie nadal daje rade pisać w ASM (dla sportu - polecam http://www.int80h.org/).

Ale te wszystkie koncepcje, że w ASM lub C będzie szybciej - to bzdury.

Dasz radę dopieścić w ASM jakąś procedurkę typu mnożenie macierzy itp... ale przy większym kodzie zaczniesz się gubić co jest w którym rejestrze i w efekcie wyjdzie kod potencjalnie słabszy nić w C.
A do tego rok później wyjdzie nowe CPU z nowymi rejestrami i okaże się, że kod gościa kóry to napisał w Javie jest szybszy, bo po update JVM sam sobie użyje nowych instrukcji i rejestrów, a ty będziesz musiał na nowo wszystko wyrzeźbić.

Przy nietrywialnym kodzie (projekty kilkumiesięczne) nie masz szans konkurować z językami wyższego poziomu, po prostu nie dasz rady zoptymalizować kodu przed końcem cywilizacji. Jest gorzej: maszyny wirtualne (typu JVM) w teorii mogą osiągnąc dużo wyższą wydajność niż statyczne kompilatory, wiedzą o kodzie dużo więcej, (np. które branche są częściej odpalane itp. ).
Pieknie to jest oczywiście tylko w teorii, bo na razie to raczej w bardzo szczegolnych to dobrze działa. (ale jest coraz ciekawiej - see Graal compiler in java). Chwilowo jakbym miał coś docyklowąć na konkretny sprzęt to pewnie bym brał C++ zamiast javy. Z drugiej strony taką potrzebę (komercyjnie) przez ostatnie 20 lat to miałem: prawie raz.

Trochę programów w C/ASM poznałem - wiele z nich ma koszmarne bugi wydajnościowe, których można by uniknąć, gdyby pisano w czymś wyższego poziomu.
(np. w programach C trudniej się robi bezpieczne cache (m.in przez brak GC), więc bywa, że czytamy i parsujemy ten sam plik po 100 razy na sekundę- taki kod u jednego klienta chodzi... ale wystarcza, a zrobienie tego z cachem okazało się bardzo niebezpieczne/trudne (moduł apache)).

Albo - dla odmiany - mamy ostro mutujący kod w Javie/C++ i nie możemy go zrównoleglić, mimo, że na kompie marnuje się 20 rdzeni.

0

Moim zdaniem w przypadku apek webowych noie ma to sensu. Po prostu w takim przypadku zwykle sprawdza się stwierdzenie że czas programisy kosztuje duzo więcej niz czas maszyny. Jeśli widzisz potrzebę zrobienia rzreczy tego typu tzn ze prawopodobnie coś jest nie tak z samą architekturą aplikacji.

0
jarekr000000 napisał(a):

Dasz radę dopieścić w ASM jakąś procedurkę typu mnożenie macierzy itp... ale przy większym kodzie zaczniesz się gubić co jest w którym rejestrze i w efekcie wyjdzie kod potencjalnie słabszy nić w C.

Bardziej złożony kod w asm wygląda trochę jak C, tylko gorzej – tworzy się procedury (które kompilator mógłby zinline'ować), używa zmiennych w RAMie (bo można je ponazywać, a kompilator spokojnie użyłby rejestru) i w efekcie robi się wolniej, zarówno pod względem czasu pisania jak i wydajności.

No ale w celach edukacyjno-rozrywkowych czasami można tak porzeźbić.

0

@jarekr000000:

Kontrprzykład?

But someone did anyways, a fully graphical, multitasking OS that can run from a floppy: menuetos.net. While it's not exactly the same thing as Windows/Linux/MacOS (more primitive/flickers, etc), it's a cool proof of concept of how much space can be saved when you use only assembler.

= oszczędzamy chociażby na błędach stron

@Shalom

Shalom napisał(a):

Czy nie jest zatem tak, że im bardziej komputery przyspieszają, tym bardziej spowalnia software, bo devom nie chce się go optymalizować oraz korzystają z wolnych języków / frameworków / itp (bo są wygodniejsze), i w konsekwencji przyspieszanie komputerów nic nie daje, bo się to wyrównuje ze spowalniającym oprogramowaniem?

Nie, po prostu soft dzisiaj jest wielokrotnie bardziej złożony niż kiedyś.

bardziej złożony = wolniejszy. A jeśli, przykład: Word, bardziej złożony nie oznacza posiadający istotnie większą funkcjonalność, to jakby point is proven

0
kmph napisał(a):

@jarekr000000:

Kontrprzykład?

But someone did anyways, a fully graphical, multitasking OS that can run from a floppy: menuetos.net. While it's not exactly the same thing as Windows/Linux/MacOS (more primitive/flickers, etc), it's a cool proof of concept of how much space can be saved when you use only assembler.

Ty serio z tym kontrprzykładem?

2
kmph napisał(a):

@jarekr000000:

Kontrprzykład?

But someone did anyways, a fully graphical, multitasking OS that can run from a floppy: menuetos.net. While it's not exactly the same thing as Windows/Linux/MacOS (more primitive/flickers, etc), it's a cool proof of concept of how much space can be saved when you use only assembler.

= oszczędzamy chociażby na błędach stron

No i pytanie czy używasz tego menuetos ? Da się używać ? Ile rodzajów sprzętu obsługuje, grafika, audio, itp. Pójdzie na moim kompie?
Bo taki linux (który wcale nie jest optymalny) to na starcie musi poobsługiwać całkiem dużo sprawdzeń dotyczących różnego rodzaju sprzętu i jeszcze pokompensować wiele błędów hardwarowych. Ten kod gdzieś musi się znaleźc.

Jak masz jedną platformę sprzętwą to sprawa jest dużo prostsza:
Mój pierwszy okienkowy system to Final III na C64 -zarąbisty i mieścił się w 64 KB ROMU

Final 3
https://rr.pokefinder.org/wiki/Final_Cartridge

Zresza IMO linux jest średnio niewydajny, bo jest napisany w C :-) i ma swoje lata. Cholera wie ile z kodu, który jest w kernelu faktycznie potrzebna na nowych sprzętach.
O wiele ciekawiej wygląda teraz Redox w Ruście, kóry ładuje się do GUI w nic sekund i wygląda, że może być uzywalny - kiedyś.
Gorzej, że jak (jeśli) dopiszą obsługą setek driverów sieciowych, chipsetów itp .. to pewnie też spuchnie, przestanie się mieścić w 50MB i szybko startować.
(A nie wiadomo czy kiedykolwiek dopiszą, bo chyba nadal nie stoi za tym żadna firma z dużą ilością kasy).

0
jarekr000000 napisał(a):

No i pytanie czy używasz tego menuetos ? Da się używać ? Ile rodzajów sprzętu obsługuje, grafika, audio, itp. Pójdzie na moim kompie?

Zdaję sobie sprawę, że pisanie w ASM jest p-dobnie DUŻO mniej wygodne niż w C, które z kolei jest DUŻO mniej wygodne niż w językach wysokopoziomowych.

Dlatego, i owszem, najczęściej użyteczne aplikacje z szeroką funkcjonalnością pisane są w językach wysokopoziomowych.

Ja się na przykład nie piszę na przepisywanie tego w ASM.

Jednak: Ja postawiłem hipotezę, że ten (zrozumiały) trend odbywa się kosztem wydajności; i że przyspieszanie sprzętu pozwala w zasadzie na wejście na wyższy poziom abstrakcji a nie na przyspieszenie działania programów, bo wejcie na wyższy poziom abstrakcji znosi korzyści z przyspieszenia sprzętu. Ktoś zapodał prawo Wrighta, które zdawałoby się to potwierdzać.

Ty postawiłeś tezę, że jest wręcz odwrotnie: że dla nietrywialnych aplikacji to przejście na wyższe poziomy abstrakcji umożliwia wysoką wydajność, podczas gdy pozostawanie na niskim poziomie abstrakcji tę wydajność zabija - niemal zawsze na odcinku ASM - C, niekiedy także na odcinku C - Java.

menuetos nie miał być (w mojej intencji) przykładem na to, że w ASM da się (w praktyce, nie tylko w teorii) napisać bardzo rozbudowany, w praktyce użyteczny system. Miał być przykładem na to, że w ASM można napisać wysokowydajny, nietrywialny system, podczas gdy wg Twojej wypowiedzi - o ile dobrze ją rozumiem - wbrew pozorom system w C byłby wydajniejszy niż system w ASM.

3
kmph napisał(a):

Jednak: Ja postawiłem hipotezę, że ten (zrozumiały) trend odbywa się kosztem wydajności; i że przyspieszanie sprzętu pozwala w zasadzie na wejście na wyższy poziom abstrakcji a nie na przyspieszenie działania programów, bo wejcie na wyższy poziom abstrakcji znosi korzyści z przyspieszenia sprzętu. Ktoś zapodał prawo Wrighta, które zdawałoby się to potwierdzać.

Ty postawiłeś tezę, że jest wręcz odwrotnie: że dla nietrywialnych aplikacji to przejście na wyższe poziomy abstrakcji umożliwia wysoką wydajność, podczas gdy pozostawanie na niskim poziomie abstrakcji tę wydajność zabija - niemal zawsze na odcinku ASM - C, niekiedy także na odcinku C - Java.

W sumie to się zgadzam, przyspieszenie sprzętu pozwala robić o wiele więcej dodaktowych rzeczy, których kiedyś nie robiono. Np. tony dodatkowych sprawdzeń związanych z bezpieczeństwem programu. I się z tego korzysta.
A co do wydajności w językach wyższego poziomu:
To ja w sumie formułuję to tak - jeśli mamy problem z wydajnością w czymś typu Java, C#, C++, w jakimś małym fragmencie kodu to na ogól najprosztszym sposobem poprawienia tej wydajności jest przepisanie tego fragmentu lepiej w tym samym jezyku. Schodzenie niżej jest zwykle nieopłacalne, są wyjątki, ale dość specyficzne. Przez 20 lat kilka razy miałem sytuację, że zastanawiałem się, żeby fragment w Javie przerobić na C/Asm. Zawsze wystarczyło poprawić kod w Javie. Sytuację, że wyniesienie na ASM było opłacalne miałem prawie raz (obługa SSE w Javie 1.6 SUN to była kpina).
(Btw. z drugiej strony kilka razy miałem przypadek gdzie sensowne było przejście na GPU, ale to nie były typowe enterprise projekty, co więcej robiłem to GPU w javie :-) (bo można)).

To, że programy robią tony rzeczy bez sensu i po prostu obecnie te tony uchodzą płazem, bo tylko ocieplają planetę, a użytkownikowi zwisa czy word odpala się jedną czy dwie sekundy to inna sprawa. Gdyby nie wymyślono nic innego niż assembler to pewnie byłoby dokładnie tak samo, tylko programiści więcej by pili. (Jeszcze więcej). Dodatkowo wspólczesny assembler to makra, makra i makra i funkcje systemowe. W zasadzie zastanawiasz się czy to w ogóle jeszcze jest jezyk niskiego poziomu. Czy naprawdę wiesz co faktycznie jest produkowane jako kod maszynowy, i czy masz faktycznie jakikolwiek kontakt ze sprzętem (o ile nie jest to assembler na małe Atari, C64 lub Spectrum to możesz przyjąc, że nie masz).

5

Oszczędzaj RAM gdziekolwiek jesteś. Załóżmy sytuacje teoretyczną - mamy webappke.

Przejście zapytania przez sieć zajmuje, załóżmy, sekundę. Wykonanie i zwrócenie odpowiedzi przez Pythona zajmie około 0,1 sekundy. Potem znowu powrót do użytkownika, czyli kolejna sekunda. Łącznie 2,1 sekundy.

Możemy przepisać ten kod w innym języku, załóżmy, C – kod będzie kilka razy dłuższy, pisanie zajmie go więcej czasu, ale za to wykona się, powiedzmy, 100 razy szybciej. Czyli użytkownik, zamiast poczekać 2,1 sekundy, poczeka 2,001 sekundy, gdyż zazwyczaj to nie sam serwer i kod naszej aplikacji, jest wąskim gardłem, a np. baza danych, połączenie sieciowe czy dysk.

Czy ma to sens w większości przypadków? Przeskok z 2,1 do 2,001s? Sami sobie odpowiedzcie.

Obecnie to raczej nie same języki są jakimiś wąskimi gardłami a baza danych, jakiś zapis odczyt, przesył informacji, albo częściej, niż rzadziej, nieumiejętny/niedbały programista itd. Można by wszystko przepisać z Javy/Pythona itd. na nie wiem, C, ale... Po co?
Jak zejdziemy z 80ms do 72 ms to czy ktoś zauważy różnice, poza nami na benchmarkach? W 99% przypadkach nie, pozostałe przypadki to nie są rzeczy dla mózgów naszego pokroju.

Poza tym nie jest przecież też tak, że ten sam program w asmie wykona się 100 razy szybciej niż w C/Pythonie. Absolutnie nie.

Także, zwłaszcza w webapkach, to nie języki są wąskimi gardłami a bazy danych, I/O, network itd.

Czemu programy nie przyśpieszyły, często wręcz zwolniły mimo komputera kilak razy mocniejszego?

Kiedyś twój program robił wodotrysk.

Dziś klient wymaga by twój program robił wodotrysk, gotował obiad w tle zajmując się dzieckiem i dokonując operacji na otwartym mózgu.

To plus pewne ustępstwa na które się idzie, by szybciej dowieźć produkt - coś tam się zrobi mniej wydajniej, ale załóżmy kilka razy szybciej. Później przyśpieszymy. Co prawda często to później nie następuje, ale wciąż, liczy sie to, że uda ci się wypuścić coś szybciej niż twój konkurent i że to zadziała. Zwłascza w obecnych czasach tfu startupów.

W przykładzie, który rzuciłem wyżej - apka w pythonie co to dwóch studentów ją skleiło w kilka tygodni za jakieś grosze 2.1 s vs super appka w C z 2.001 czasu respona, którą kleić musiało wielu doświadczonych programistów, w kilka miesięcy, budżet miliony. Czas mija wreszcie wypuszczacie to cudo techniki. W międzyczasie ta apka sklecona przez dwóch studenciaków zdobyła jakiś klientów, co prawda miała różne błędy, bywała wolna, ale ogółem jakoś działała, generując jakąś wartość dla biznesu i rozwiązując problemy użyszkodników, powodując, że ludzie się do niej przyzwyczaili. Wtedy wchodzicie wasza piękna nowa błyszcząca appka w C, szybsza, lepsza, ale... Nikt jej nie używa. Proste. Biznes upada, jesteś bankrutem.

Zysk niewspółmierny do kosztu. Proste. Monnies się nie zgadzają, no one gives a shit. W gruncie rzeczy klient (znowu - zaznaczam 99% przypadków) ma to w poważaniu czy ta appka jest w C, Pythonie czy czym tam, czy odpowiada w 2.1 s czy 2.001 s. Ma działać, rozwiązywać jakiś jego problem i koniec.

Co nie zmienia faktu, że teraz za bardzo sobie na pewne rzeczy pozwalamy przez co, jak to ktoś już napisał, mamy bloated software. Szlag mnie przez to często trafia, Tak samo jak landing page ważący po 20 mb psia mać. Albo proces wytwarzania oprogramowani, który często jest biedny i popsuty (jakoś-to-będzie-bylejakizm) czy inne scrum-slavy. Ale co zrobić. Pozostaje swoją robotę robić dobrze i koniec.

Ogółem polecam bardzo w tym temacie ten artykuł: https://tonsky.me/blog/disenchantment/
Wyraża więcej niż 1000 słów.

Tldr; to nie języki są zazwyczaj wąskim gardłem a jak coś się da zrobić prościej w języku wyższego poziomu, to zazwyczaj nie warto sie kłopotać dla mizernego zysku, który jest niewspółmierny do włożonego wysiłku.

Okej, koniec mojego rantu.

0

Zwracam Wam (częściowo) honor. Postanowiłem wreszcie to po prostu sprawdzić. Benchmark C++ vs C#, funkcja Ackermanna:

#include <iostream>
#include <vector>
#include <utility>
#include <stack>
#include <chrono>

using cache_t = std::vector<std::vector<int>>;
using stack_t = std::stack<std::pair<int, int>>;

void ensure_cache_large_enough(int m, int n, cache_t& cache)
{
	if (cache.size() <= m)
	{
		cache.resize(m + 1, std::vector<int>{});
	}

	if (cache[m].size() <= n)
	{
		cache[m].resize(n + 1, 0);
	}
}

int retrieve_or_queue(int m, int n, cache_t& cache, stack_t& stack)
{
	ensure_cache_large_enough(m, n, cache);

	if (cache[m][n] != 0)
	{
		return cache[m][n];
	}
	else
	{
		stack.emplace(m, n);
		return 0;
	}
}

void process_stack(cache_t& cache, stack_t& stack)
{
	while (!stack.empty())
	{
		auto [m, n] = stack.top();

		if (retrieve_or_queue(m, n, cache, stack) != 0)
		{
			stack.pop();
		}
		else if (m == 0)
		{
			cache[m][n] = n + 1;
			stack.pop();
		}
		else if (n == 0)
		{
			auto r = retrieve_or_queue(m - 1, 1, cache, stack);
			if (r != 0)
			{
				cache[m][n] = r;
				stack.pop();
			}
		}
		else
		{
			auto r_in = retrieve_or_queue(m, n - 1, cache, stack);
			if (r_in != 0)
			{
				auto r_out = retrieve_or_queue(m - 1, r_in, cache, stack);
				if (r_out != 0)
				{
					cache[m][n] = r_out;
					stack.pop();
				}
			}
		}
	}
}

int ack(int m, int n)
{
	cache_t cache;
	stack_t stack;
	stack.emplace(m, n);

	process_stack(cache, stack);

	return cache[m][n];
}

int main()
{
	int m = 3, n = 20;

	auto start = std::chrono::system_clock::now();
	int res = ack(m, n);
	auto end = std::chrono::system_clock::now();

	std::cout << "A(" << m << ", " << n << ") = " << res << std::endl;
	std::cout << "Took " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms" << std::endl;
}

Versus:

using System;
using System.Linq;
using System.Collections.Generic;

using Cache = System.Collections.Generic.List<System.Collections.Generic.List<int>>;
using Stack = System.Collections.Generic.Stack<(int, int)>;

namespace csharpackermann
{
    class Program
    {
		static void EnsureCacheLargeEnough(int m, int n, Cache cache)
		{
			if (cache.Count <= m)
			{
				cache.AddRange(Enumerable.Repeat(0, m - cache.Count + 1).Select(_ => new List<int> { }));
			}

			if (cache[m].Count <= n)
			{
				cache[m].AddRange(Enumerable.Repeat(0, n - cache[m].Count + 1));
			}
		}

		static int RetrieveOrQueue(int m, int n, Cache cache, Stack stack)
		{
			EnsureCacheLargeEnough(m, n, cache);

			if (cache[m][n] != 0)
			{
				return cache[m][n];
			}
			else
			{
				stack.Push((m, n));
				return 0;
			}
		}

		static void ProcessStack(Cache cache, Stack stack)
		{
			while (stack.Any())
			{
				var (m, n) = stack.Peek();

				if (RetrieveOrQueue(m, n, cache, stack) != 0)
				{
					stack.Pop();
				}
				else if (m == 0)
				{
					cache[m][n] = n + 1;
					stack.Pop();
				}
				else if (n == 0)
				{
					var r = RetrieveOrQueue(m - 1, 1, cache, stack);
					if (r != 0)
					{
						cache[m][n] = r;
						stack.Pop();
					}
				}
				else
				{
					var r_in = RetrieveOrQueue(m, n - 1, cache, stack);
					if (r_in != 0)
					{
						var r_out = RetrieveOrQueue(m - 1, r_in, cache, stack);
						if (r_out != 0)
						{
							cache[m][n] = r_out;
							stack.Pop();
						}
					}
				}
			}
		}

		static int Ack(int m, int n)
		{
			var cache = new Cache();
			var stack = new Stack();
			stack.Push((m, n));

			ProcessStack(cache, stack);

			return cache[m][n];
		}

		static void Main(string[] args)
		{
			int m = 3, n = 20;

			var start = DateTime.Now;
			int res = Ack(m, n);
			var end = DateTime.Now;

			Console.WriteLine($"A({m}, {n}) = {res}");
			Console.WriteLine($"Took {Math.Round((end - start).TotalMilliseconds)}ms");
		}
	}
}

Wyniki:

C:\Users\m>source\repos\cppackerman\x64\Release\cppackerman.exe
A(3, 20) = 8388605
Took 2009ms

C:\Users\m>source\repos\csharpackermann\bin\Release\netcoreapp3.1\csharpackermann.exe
A(3, 20) = 8388605
Took 5718ms

Tak więc różnica między C# a C++ ~2.8 krotna. (Przynajmniej na kompilatorach z rodziny Visual Studio na Windowsie). Istotnie, nie jest to wiele.

Włączyłem optymalizacje zarówno na C# jak i na C++ - z tym, że dla C# były to dwie opcje do odptaszkowania w IDE (optymalizacja + wyrzucenie symboli debugowania), dla C++ było to multum opcji które odznaczałem nieco na czuja, może więc by się dało C++ przyspieszyć jeszcze bardziej (np. nie włączyłem optymalizacji na podstawie profilowania).

Przyznaję, że jestem mile zdziwiony: pamiętam, że ileś lat temu robiłem podobny benchmark, tym razem C++ vs Java na Linuxie, i - o ile pamiętam - różnica wyszła dramatyczna na korzyść C++. Niestety, nie mam już tamtego kodu ani dokładnych wyników. Na podstawie tamtych dawniejszych benchmarków wbiłem sobie do głowy, że fakt wykonywania kodu na maszynie wirtualnej musi spowalniać makabrycznie. Może wprowadzenie JITów ulepszyło sytuację pod tym względem?

Dla pełnego obrazu oczywiście przydałoby się dodać jeszcze asm z mikrooptymalizacjami na jednym ekstremum i Haskella na drugim ekstremum. Niestety, to pierwsze wymaga dość ezoterycznej wiedzy, której nie posiadam, więc nie wstawię (AFAIK przyspieszenia między asemblerem wklepanym na pałę a asemblerem napisanym przez kogoś, kto naprawdę wie od czego zależy wydajność mogą być dramatyczne). Co się zaś tyczy Haskella, to obawiam się, że nieco zaburzyłoby to miarodajność porównania: kod C# i C++ tutaj jest dokładnie analogiczny, tymczasem przełożyć takiego kodu na Haskella się nie da tak łatwo (tzn da się, ale byłby to Haskell bardzo nieidiomatyczny), a poza tym znowu - nie do końca wiem, jak żyłować wydajność w Haskellu (pewnie trzeba by użyć mutowalnych tablic do cache, ale czy wtedy w ogóle jest sens używać Haskella?)

0

Przyznaję, że jestem mile zdziwiony: pamiętam, że ileś lat temu robiłem podobny benchmark, tym razem C++ vs Java na Linuxie, i - o ile pamiętam - różnica wyszła dramatyczna na korzyść C++

A pamiętasz kiedy mniej więcej to bylo? Tzn 3 lata temu czy 10 :P

2

Języki typu C++ czy Java mogą być też szybsze od ASM.

  1. pod względem czasu implementacji
  2. ze względu na szereg optymalizacji które mogą robić w czasie kompilacji, w przypadku C++ to np. eliminacja obliczeń, alokacja rejestrów, zrównoleglanie SIMD
  3. ze względu na czytelniejsze struktury łatwiej alokować specyficznie pod procesor (C++)

C w zasadzie nie powinno być szybsze od C++, C++ ma zasadę "zero cost abstractions".
Można specyficzne procedury (CPU-bound) lepiej zoptymalizować pod ASM czy C ale to są bardzo specyficzne przypadki.

Kiedy Java może być szybsza od ASM:

2
kmph napisał(a):

wirtualnej musi spowalniać makabrycznie. Może wprowadzenie JITów ulepszyło sytuację pod tym względem?

Wprowadzenie JITów rzeczywicie dramatycznie polepszyło. I to było w roku 1999 :/ Od tego czasu oczywiście stale JVM (oracle i inne) się poprawia, ale to był najwiekszy przełom (java 1.3.0).

Generalnie JVM i JITy to świnie przy robieniu benchmarków - łatwo dostać wyniki, które nie odpowiadają temu co będzie na produkcji. Jak czasy benchmarku są poniżej 5sekund to na JVM mierzony jest zwykle czas kompilacji i rozgrzewania maszyny, ludzie często robią benchmarki na milisekundy.... Kody benchmarkowe, jesli np. prowadzą do błedów przepełnien itp. dostają kary od JITa (w sensie pzostają nieskompilowane)

Z drugiej strony może się zdarzyć (kilka procent mikrobenchmarków), że czas na JVM wyjdzie nierealistycznie dobry (dead code elimination).

tymczasem przełożyć takiego kodu na Haskella się nie da tak łatwo (tzn da się, ale byłby to Haskell bardzo nieidiomatyczny), a poza tym znowu - nie do końca wiem, jak żyłować wydajność w Haskellu (pewnie trzeba by użyć mutowalnych tablic do cache, ale czy wtedy w ogóle jest sens używać Haskella?)

Nie chiało mi się przekładać tego kodu na haskell - tu masz dyskusje w temacie:
https://stackoverflow.com/questions/16115815/ackermann-very-inefficient-with-haskell-ghc

Dość "naiwne" podejście:

import Data.Function.Memoize

ack :: Int -> Int -> Int
ack 0 n = succ n
ack m 0 = mack (pred m) 1
ack m n = mack (pred m) (mack m (pred n))

mack::Int->Int->Int
mack = memoize2 ack

main = print $ mack 3 20 

dało mi takie czasy (na brudno, na vmce (jeszcze robiłem inne pierdółki)):

8388605

real	2m7.956s
user	7m43.072s
sys	8m7.765s

//ghc 8.6.5 

Czyli - troche to trwało, ale to jednak jest raczej naiwna wersja (nie wiem nawet czy ta memoizacja jest zrobiona dobrze).
(czasy pokazują, że użyte było kilka procków w trakcie obliczeń).

1
kmph napisał(a):

Tak więc różnica między C# a C++ ~2.8 krotna. (Przynajmniej na kompilatorach z rodziny Visual Studio na Windowsie). Istotnie, nie jest to wiele.

3x szybciej to niewiele? Że różnica 50km/h a 150km/h to niewiele? ;)
Jasne, że jak coś się odpala raz na ruski rok to tego nie zauważysz, ale jak coś hula praktycznie non-stop to jednak czuć różnicę.
Problemem jest raczej wszechobecny źle rozumiany OOP oraz bezmyślne wciskanie wszystkiego(często na siłę) we wszelkiego rodzaju frameworki.
Tak z głowy to mogę polecić dwie prezentacje. Dotyczą stricte C++, ale pokazują ile można zyskać nie tyle na zmianie języka co samym podejściu i zrozumieniu konkretnego problemu.
CppCon 2019: Matt Godbolt “Path Tracing Three Ways: A Study of C++ Style”
Pacific++ 2018: Kirit Sælensminde "Designing APIs for performance"

10

To ja się podzielę ostatnim doświadczeniem.
Mamy w naszym projekcie pewien task, który czyta pliki z danymi, obrabia je i zapisuje przetworzone dane na dysku jako nowe pliki. Nie jest istotne co dokładnie robi, ale musi wczytać całe pliki od deski do deski, zdekodować pewien dość mocno pokrecony format danych, wykonać dość prosta logikę na tych danych a następnie zserializować do innego formatu.

Kod napisano w Javie i osiąga zawrotną predkość 10 MB/s... na dysku SSD o wydajności 2+ GB/s i 3GHz CPU. Problemem jest CPU.

Firma zatrudniła eksperta od tuningu Javy. Ekspert w ciągu dwóch miesięcy przepisał kluczowy fragment kodu tak, że nie używa alokacji na stercie, nie używa praktycznie dziedziczenia i interfejsow a obiekty są jedynie miejscami na trzymanie danych w pamięci. Kod przyspieszył jakieś 5-8x. Niestety kod jest bardzo niebezpieczny i nieidiomatyczny - wszystkie obiekty są mutowalne i dostępne w publicznym API. Zalecana ostrożność jak przy kodzie w C. Ba, ten kod wygląda jak C ze składnią Javy.

Później ja dla jaj wziąłem ten kod i w kilka dni przeportowałem go na Rust, jednak bez uciekania się do unsafe i starając się zachować oryginalne, wygodne i bezpieczne API. Pokazałem go ludziom nie znającym Rusta i stwierdzili, że rozumieją jak ten kod działa, więc chyba był wystarczająco czytelny.

Efekt - kod jest kolejne 5x szybszy od tego zoptymalizowanego kodu w Javie. Łącznie jakieś 20-30x szybszy od oryginału. Btw nie jestem ekspertem od Rust - właściwie uczyłem się tego języka jak pisałem kod.

I jeszcze wisienka na torcie - zużycie pamięci. Oryginalny kod w Javie potrzebował minimum 0.5 GB, inaczej wywalał się z OOM.
Kod w Rust potezebował 20 MB, z czego większość to pliki wejściowe zmapowane do pamięci oraz sam segment kodu. Wykorzystanie sterty: 0. Oczywiście to trochę nie fair porównanie, bo oryginalny kod miał więcej ficzerów i prawdopodobnie ładował niepotrzebne rzeczy do pamięci (nieużywane w tym teście).

Przejście zapytania przez sieć zajmuje, załóżmy, sekundę. Wykonanie i zwrócenie odpowiedzi przez Pythona zajmie około 0,1 sekundy. Potem znowu powrót do użytkownika, czyli kolejna sekunda. Łącznie 2,1 sekundy.

Wiem o co Ci chodzi, ale w praktyce realne wartości mają znaczenie i mocno zmieniają wnioski z takiej analizy.

Po pierwsze - sieć 2s? Na edge na wsi czy modemie wdzwanianym na 0202122? Bo ja nawet na lichym ADSL mam 20-40 ms.
Natomiast na kablówce można zejść do 2-10 ms roundtrip. Sieć nie jest na ogół problemem.

Zwrócenie odpowiedzi z Pythona 0,1s - jesli to prawda to jest to masakrycznie wolno. Heca polega na tym, że na serwerze nie jesteś sam. Wystarczy że w tej samej sekundzie przyjdzie akurat 20 zapytań i już poczekasz sobie znacznie dłużej. Poza tym obecne apki webowe nie wykonują jednego zapytania. Często leci sekwencja kilkudziesięciu lub kilkuset zapytań. Większość równolegle, ale teraz wystarczy że jedno z nich trafi na większego laga i użytkownik zobaczy to jako mulenie strony. Dlatego nie powinno się patrzeć wyłącznie na średni czas odpowiedzi, a na percentyle - najlepiej co najmniej 99.9%. I pod tym względem takie wynalazki jak Java/Python itp wypadają bardzo blado. Jeżeli co setne zapytanie potrzebuje 2 sekund, a spa wysyła 100 zapytań na akcje użytkownika, to niemal każdy użytkownik będzie widział taki właśnie beznadziejny czas odpowiedzi.

Teoretyzuję? No nie. JIRA cloud właśnie tak działa w godzinach szczytu. Masakra.

0

Erlang VM. Możesz sobie pisać w Elixirze. Wydajna technologia.

1 użytkowników online, w tym zalogowanych: 0, gości: 1