Neuroróżnorodność w miejscu pracy
24.01.202416 min
Oleksandr Kaleniuk

Oleksandr KaleniukSoftware Engineer

Prawdziwi zabójcy C++ (nie ty, Rust)

Gorzkie słowa od inżyniera, który pisze w C++ od 17 lat.

Prawdziwi zabójcy C++ (nie ty, Rust)

Cześć! Nazywam się Oleksandr Kaleniuk i jestem C++holikiem. Piszę w C++ od 17 lat i przez ten cały czas próbowałem odejśc od wyniszczającego nałogu.

Wszystko zaczęło się w 2005 roku od silnika symulatora kosmicznego 3D. Silnik ten miał wszystko, co C++ miał w 2005 roku. Trzygwiazdkowe wskaźniki, osiem warstw zależności i makra w stylu C. Były też elementy asemblera. Iteratory w stylu Stepanova i meta-kod w stylu Alexandrescu. Kod miał wszystko. Oczywiście z wyjątkiem odpowiedzi na najważniejsze pytanie: ale po co?

Po pewnym czasie nawet to pytanie doczekało się odpowiedzi. Nie pytanie „po co”, a „jak to się stało”. Jak się okazało, silnik był pisany przez około 8 lat przez 5 różnych zespołów. Każdy zespół wniósł do projektu swoje własne rozwiązania, owijając stary kod w świeżo stylizowane wrappery, dodając przy tym jedynie około 10-20 mikrokroków funkcjonalności.

Na początku starałem się zrozumieć każdą drobną rzecz. Nie było to jednak satysfakcjonujące doświadczenie i w pewnym momencie się poddałem. Wciąż tylko zamykałem zadania i naprawiałem błędy. Nie byłem wtedy wybitnie produktywny, ale wystarczająco, by nie zostać zwolnionym.

Wtedy mój szef zapytał mnie, czy chcę przepisać część kodu shaderów z Assembly na GLSG. Pomyślałem, że nie wiem, jak to GLSL wygląda, ale nie może być gorsze od C++ i finalnie się zgodziłem. Nie było gorzej.

Wszystko lepsze od C++

Stało się to pewnego rodzaju schematem. Nadal pisałem głównie w C++, ale za każdym razem, gdy ktoś pytał mnie „czy chcesz zrobić coś innego niż C++?” mówiłem „Jasne!” i to robiłem. Cokolwiek to było. Pisałem w C89, MASM32, C#, PHP, Delphi, ActionScript, JavaScript, Erlangu, Pythonie, Haskellu, D, Rust, a nawet w tym skandalicznie złym języku skryptowym InstallShield. Pisałem w VisualBasicu, w bashu i kilku zastrzeżonych językach, o których nie mogę nawet legalnie mówić.

Stworzyłem nawet jeden język przez przypadek. Był to prosty interpreter w stylu Lisp, aby pomóc projektantom gier zautomatyzować ładowanie zasobów. Wtedy też wyjechałem na wakacje. Po moim powrocie okazało się, że pisali całe sceny gry w tym interpreterze, więc musieliśmy go utrzymywać przynajmniej do końca projektu.

Lękajcie się

Tak więc przez ostatnie 17 lat szczerze próbowałem rzucić C++, ale za każdym razem, po wypróbowaniu nowej błyszczącej rzeczy, wracałem. Niemniej jednak uważam, że pisanie w C++ to zły nawyk. Jest to niebezpieczne, nie tak skuteczne, jak się uważa, i marnuje ogromną ilość zdolności umysłowych programisty na rzeczy, które nie mają nic wspólnego z tworzeniem oprogramowania. Czy wiesz, że w MSVC uint16_t(50000) + uin16_t(50000) == -1794967296? A czy wiesz dlaczego? No właśnie.

Uważam, że moralnym obowiązkiem długoletnich programistów C++ jest zniechęcanie młodego pokolenia do uczynienia C++ swoim zawodem, podobnie jak moralnym obowiązkiem alkoholików, którzy nie mogą rzucić nałogu, jest ostrzeganie młodzieży przed niebezpieczeństwem związanym z piciem alkoholu.

To już nie jest XX wiek

Ale dlaczego nie mogę zrezygnować? O co chodzi? Chodzi o to, że żaden z tych języków, zwłaszcza tak zwani „zabójcy C++”, nie daje żadnej realnej przewagi nad C++ we współczesnym świecie. Wszystkie te nowe propozycje skupiają się głównie na trzymaniu programisty na smyczy dla jego własnego dobra. W porządku, z wyjątkiem tego, że pisanie dobrego kodu z kiepskimi programistami to był problem XX wieku, kiedy to liczba tranzystorów rosła dwukrotnie co 18 miesięcy, a liczba programistów rosła dwukrotnie co 5 lat.

Żyjemy w 2024 roku. Mamy na świecie więcej doświadczonych programistów niż kiedykolwiek wcześniej w historii, a wydajnego oprogramowania potrzebujemy teraz bardziej niż kiedykolwiek.

W XX wieku wszystko było prostsze. Masz pomysł, opakowujesz go w interfejs użytkownika i sprzedajesz jako produkt desktopowy. Jest trochę wolny? Kogo to obchodzi! W ciągu osiemnastu miesięcy komputery stacjonarne i tak staną się 2x szybsze. Liczy się wejście na rynek, sprzedaż nowych ficzerów i w miarę możliwości bez błędów.

W tym klimacie, oczywiście, jeśli kompilator powstrzymuje programistów przed tworzeniem błędów - to dobrze! Ponieważ błędy nie przynoszą pieniędzy, a programistom i tak trzeba płacić niezależnie od tego, czy dodają funkcje, czy błędy.

Teraz sytuacja wygląda nieco inaczej. Masz pomysł, opakowujesz go w kontener Docker i uruchamiasz w chmurze. Obecnie czerpiesz dochody od osób korzystających z Twojego oprogramowania, jeśli sprawia ono, że ich ewentualne problemy znikają. Nawet jeśli robi jedną rzecz, ale dobrze, otrzymasz zapłatę. Nie musisz wypełniać swojego produktu wymyślonymi funkcjami tylko po to, by sprzedać jego nową wersję.

Z drugiej jednak strony, za nieefektywność kodu płacisz ty sam. Każda nieoptymalna procedura jest widoczna na rachunku AWS. Dlatego w nowej rzeczywistości potrzebujesz mniej funkcji, ale lepszej wydajności dla tego, co już masz.

I nagle okazuje się, że wszyscy „zabójcy C++”, nawet ci, których całym sercem kocham i szanuję, jak Rust, Julia i D, nie adresują problemu XXI wieku. Wciąż tkwią w XX. Pomagają one w pisaniu większej liczby funkcji z mniejszą liczbą błędów, ale nie są zbyt pomocne.

Po prostu nie dają przewagi konkurencyjnej nad C++. Albo nawet nad sobą nawzajem. Większość z nich, np. Rust, Julia i Cland, mają nawet ten sam backend. 

Które technologie zapewniają zatem przewagę konkurencyjną nad C++ lub, mówiąc ogólnie, wszystkimi tradycyjnymi kompilatorami czasu przyszłego? Dobre pytanie. 

Zabójca C++ numer 1. - Spiral

Zanim jednak przejdziemy do samego Spirala, sprawdźmy, jak dobrze działa nasza intuicja. Jak myślisz, która funkcja jest szybsza: standardowa funkcja sinus C++ czy 4-elementowy wielomianowy model sinusa?

auto y = std::sin(x);
 
// vs.
 
y = -0.000182690409228785*x*x*x*x*x*x*x
        +0.00830460224186793*x*x*x*x*x
        -0.166651012143690*x*x*x
        +x;

Kolejne pytanie. Co działa szybciej, używanie operacji logicznych ze “zwarciem”, czy oszukiwanie kompilatora, aby tego uniknąć i obliczyć wyrażenie logiczne zbiorczo?

if (xs[i] == 1 && xs[i+1] == 1 && xs[i+2] == 1 && xs[i+3] == 1) // xs are bools stored as ints
 
// vs.
 
inline int sq(int x) {
  return x*x;
}
 
if(sq(xs[i] - 1) + sq(xs[i+1] - 1) + sq(xs[i+2] - 1) + sq(xs[i+3] - 1) == 0)

I jeszcze jedno. Co tu posortuje szybciej: swap-sort czy index-sort?

        if(s[0] > s[1])
            swap(s[0], s[1]);
        if(s[1] > s[2])
            swap(s[1], s[2]);
        if(s[0] > s[1])
            swap(s[0], s[1]);
 
// vs.
 
        const auto a = s[0];
        const auto b = s[1];
        const auto c = s[2];
        s[int(a > b) + int(a > c)] = a;
        s[int(b >= a) + int(b > c)] = b;
        s[int(c >= a) + int(c >= b)] = c;

Jeśli odpowiedziałeś na wszystkie pytania zdecydowanie i bez zastanowienia, to znaczy, że zawiodła Cię intuicja. Nie zauważyłeś pułapki. Żadne z tych pytań nie ma jednoznacznej odpowiedzi bez poznania kontekstu.

Na który procesor CPU lub GPU ukierunkowany jest kod? Który kompilator powinien zbudować kod? Które optymalizacje kompilatora są włączone, a które wyłączone? Przewidywanie można rozpocząć dopiero wtedy, gdy się to wszystko wie, lub jeszcze lepiej, mierząc czas działania dla każdego konkretnego rozwiązania. Co do podanych przykładów:

  1. Model wielomianowy jest 3x szybszy niż standardowy sinus, jeśli został zbudowany przy użyciu clang 11 z -O2 -march=native i uruchomiony na Intel Core i7-9700F. Ale jeśli zostanie zbudowany z nvcc z --use-fast-math i na GPU, a mianowicie GeForce GTX 1050 Ti Mobile, standardowy sinus jest 10x szybszy niż model.
  2. Zamiana podejścia z logiką na arytmetykę wektorową ma sens również w przypadku i7. Sprawia, że snippet działa 2x szybciej. Ale na ARMv7 z tym samym clangiem i -O2, standardowa logika jest 25% szybsza niż mikro-optymalizacja.
  3. W przypadku index-sort i swap-sort, to index-sort jest 3x szybszy na Intelu, a swap-sort jest 3x szybsze na GeForce.

Tak więc drogie mikro optymalizacje, które wszyscy tak bardzo kochamy, mogą zarówno przyspieszyć nasz kod o współczynnik 3, jak i spowolnić go o 90%. Wszystko zależy od kontekstu. Jak cudownie byłoby, gdyby kompilator mógł wybrać dla nas najlepszą alternatywę, więc np. index-sort cudownie zamieniłby się w swap-sort, gdy zmienimy cel kompilacji. Ale nie może.

  1. Nawet jeśli pozwolimy kompilatorowi na ponowne zaimplementowanie sinusa jako modelu wielomianowego, aby wymienić precyzję na szybkość, nadal nie będzie on znał naszej docelowej precyzji. W C++ nie możemy powiedzieć, że „ta funkcja może mieć ten błąd”. Wszystko, co mamy, to flagi kompilatora, takie jak „--use-fast-math” i tylko w zakresie jednostki.
  2. W drugim przykładzie kompilator nie wie, że nasze wartości są ograniczone do 0 lub 1 i nie może zaproponować optymalizacji, którą my możemy. Prawdopodobnie moglibyśmy to zasugerować, używając odpowiedniego typu logicznego, ale byłby to zupełnie inny problem.
  3. W trzecim przykładzie fragmenty kodu znacznie różnią się od siebie, by można je było uznać za synonimy. Zbyt szczegółowo opisaliśmy kod. Gdyby było to po prostu std::sort, kompilator miałby większą swobodę w wyborze algorytmu. Ale nie wybrałby ani index-sort, ani swap-sort, ponieważ oba są nieefektywne w przypadku dużych tablic, a std::sort działa z ogólnym kontenerem iterowalnym.

I w ten sposób dochodzimy do Spirala. TL&DR: eksperci od przetwarzania sygnałów znudzili się ręcznym przepisywaniem swoich ulubionych algorytmów dla każdego nowego sprzętu i napisali program, który wykonuje tę pracę za nich. Program pobiera wysokopoziomowy opis algorytmu oraz szczegółowy opis architektury sprzętowej i optymalizuje kod, aż do uzyskania najbardziej wydajnej implementacji algorytmu dla określonego sprzętu.

Ważną różnicą między Fortranem a podobnymi programami jest to, że Spiral naprawdę rozwiązuje problem optymalizacji w sensie matematycznym. Definiuje on czas działania jako funkcję docelową i szuka jej globalnego optimum w przestrzeni wariantów implementacji ograniczonej przez architekturę sprzętową. Jest to coś, czego kompilatory nigdy nie robią.

Kompilator nie szuka prawdziwego optimum. Optymalizuje kod, kierując się heurystyką, której nauczyli go programiści. Zasadniczo kompilator nie działa jak maszyna szukająca optymalnego rozwiązania, ale raczej jak programista asemblera. Natomiast dobry kompilator działa jak dobry programista asemblera. Ot cała filozofia.

Spiral to projekt badawczy. Ma ograniczony zakres i budżet. Jednak wyniki, które przedstawia, robią wrażenie. W przypadku szybkiej transformacji Fouriera, ich rozwiązanie zdecydowanie przewyższa zarówno implementacje MKL, jak i FFTW. Ich kod jest ~2x szybszy. Nawet na Intelu.

Aby podkreślić skalę osiągnięć, MKL to Math Kernel Library opracowana przez Intel, a więc przez ludzi, którzy wiedzą, jak najlepiej wykorzystać swój sprzęt. I jeszcze FFTW. „Fastest Fourier Transform in the West” to wysoce wyspecjalizowana biblioteka od ludzi, którzy najlepiej znają ten algorytm. Są mistrzami w tym, co robią, a sam fakt, że Spiral pokonuje ich dwukrotnie, jest zdumiewający.

Gdy technologia optymalizacji wykorzystywana przez Spiral zostanie sfinalizowana i skomercjalizowana, nie tylko C++, ale także Rust, Julia, a nawet Fortran staną w obliczu konkurencji, z którą nigdy wcześniej nie miały do czynienia. Dlaczego ktokolwiek miałby pisać w C++, jeśli pisanie w wysokopoziomowym języku algorytmicznym sprawia, że kod jest 2x szybszy?

Zabójca C++ numer 2. - Numba

Najlepszym językiem programowania jest ten, który już dobrze znasz. Od kilku dekad najbardziej znanym językiem dla większości programistów jest C. Był on również na przestrzeni lat liderem indeksu TIOBE, a inne języki podobne do C ściśle wypełniały pierwszą 10. Jednak zaledwie dwa lata temu wydarzyło się coś niespotykanego - C oddał swoje pierwsze miejsce innemu językowi - Pythonowi. Temu, którego nikt nie traktował poważnie w latach 90-tych, ponieważ był to kolejny język skryptowy, jakich mnóstwo.

Ktoś może powiedzieć: „No dobra, ale Python jest wolny” i wyjdzie na głupiego, ponieważ jest to terminologiczny nonsens. Podobnie jak akordeon czy patelnia, język nie może być po prostu szybki lub wolny. Tak jak szybkość akordeonu zależy od tego, kto na nim gra, tak „szybkość” języka zależy od tego, jak szybki jest jego kompilator.

„Ale Python nie jest językiem kompilowanym” ktoś może kontynuować i po raz kolejny się mylić. Istnieje wiele kompilatorów Pythona, a najbardziej obiecującym z nich jest z kolei skrypt Pythona. Pozwólcie, że wyjaśnię.

Miałem kiedyś projekt. Symulacja druku 3D, która pierwotnie została napisana w Pythonie, a następnie przepisana w C++ „dla zwiększenia wydajności”, a następnie przeniesiona na GPU. Wszystko to przed moim przyjściem. Kolejne miesiące spędziłem na przenoszeniu kompilacji na Linuksa, optymalizowaniu kodu GPU dla Tesli M60, ponieważ w tamtym momencie była to najtańsza opcja w AWS, oraz weryfikowaniu wszystkich zmian w kodzie C++/CU, by współgrały z oryginalnym kodem w Pythonie. Robiłem więc wszystko poza rzeczami, w których zwykle się specjalizuję, czyli opracowywaniem algorytmów geometrycznych.

Gdy wreszcie już wszystko działało, zadzwonił do mnie student z Bremy i zapytał, czy skoro jestem dobry w heterogenicznych rzeczach, to czy mogę mu pomóc uruchomić jeden algorytm na GPU. Oczywiście! Opowiedziałem mu o CUDA, CMake, kompilacji Linuksa, testowaniu i optymalizacji. Rozmawialiśmy z godzinę. Wysłuchał tego wszystkiego bardzo grzecznie, ale na koniec powiedział: „To wszystko jest bardzo interesujące, ale mam bardzo konkretne pytanie. Więc mam funkcję, napisałem @cuda.jit przed jej definicją, a Python mówi coś o tablicach i nie kompiluje jądra. Wiesz, w czym może tkwić problem?”

Nie wiedziałem. Rozgryzł to sam w ciągu jednego dnia. Najwyraźniej Numba nie działa z natywnymi listami Pythona, akceptując tylko dane w tablicach NumPy. Uruchomił swój algorytm na GPU. W Pythonie. Nie miał żadnego z problemów, nad którymi spędziłem miesiące.

Numba zoptymalizuje kod pod kątem platformy, na której jest uruchamiany, ponieważ nie kompiluje się z wyprzedzeniem, ale na żądanie, gdy jest już wdrożony. Chcesz go mieć na Linuksie? Nie ma problemu, wystarczy uruchomić go na Linuksie. Czy ma być spójny z kodem Pythona? Nie ma problemu, bo to kod Pythona. Czy chcesz zoptymalizować kod pod kątem platformy docelowej? Żaden problem. 

Czy to czary? No nie. W każdym razie nie dla mnie. Spędziłem miesiące z C++, rozwiązując problemy, które nigdy nie występują w Numba, a adhockowy pracownik z Bremy zrobił to samo w kilka dni. Mogłoby to trwać kilka godzin, gdyby nie było to jego pierwsze doświadczenie z Numbą. Więc w czym tkwi sekret?

Właśnie nie ma tam nic tajemniczego. Dekoratory Pythona zamieniają każdy fragment kodu w abstrakcyjne drzewo składni, dzięki czemu można z nim zrobić, co tylko się chce. Numba to biblioteka Pythona, która chce kompilować abstrakcyjne drzewa składni z dowolnym backendem i dla dowolnej platformy, którą obsługuje. Jeśli chcesz skompilować swój kod Pythona, aby działał na rdzeniach procesora w sposób masowo równoległy, to po prostu powiedz Numbie, aby go tak skompilowała. Jeśli chcesz uruchomić coś na GPU, ponownie, wystarczy tylko zapytać.

@cuda.jit
def matmul(A, B, C):
        """Perform square matrix multiplication of C = A * B."""
        i, j = cuda.grid(2)
        if i < C.shape[0] and j < C.shape[1]:
        tmp = 0.
        for k in range(A.shape[1]):
                tmp += A[i, k] * B[k, j]
        C[i, j] = tmp

Numba to jeden z kompilatorów Pythona, który sprawia, że C++ staje się przestarzały. Teoretycznie jednak nie jest on "lepszy od C++", ponieważ korzysta z tych samych backendów. Wykorzystuje CUDA do programowania GPU i LLVM dla CPU. W praktyce, ponieważ nie wymaga przebudowy z wyprzedzeniem dla każdej nowej architektury, rozwiązania Numba lepiej dostosowują się do każdego nowego sprzętu i jego dostępnych optymalizacji.

Oczywiście lepiej byłoby mieć wyraźną przewagę wydajności, tak jak w przypadku Spiral. Ale Spiral jest bardziej projektem badawczym i może zdetronizować C++, ale tylko ostatecznie i tylko przy odrobinie szczęścia. Numba z Pythonem dusi C++ już teraz, w czasie rzeczywistym. Bo skoro można pisać w Pythonie i mieć wydajność C++, to po co się męczyć?

Zabójca C++ numer 3. - ForwardCom

Zagrajmy w nieco inną grę. Pokażę ci trzy fragmenty kodu, a ty zgadniesz, który lub które z nich są napisane w asemblerze. Oto one:

invoke RegisterClassEx, addr wc         ; register our window class
    invoke CreateWindowEx,NULL,
        ADDR ClassName, ADDR AppName,\
        WS_OVERLAPPEDWINDOW,\
        CW_USEDEFAULT, CW_USEDEFAULT,\
        CW_USEDEFAULT, CW_USEDEFAULT,\
        NULL, NULL, hInst, NULL
        mov   hwnd,eax
        invoke ShowWindow, hwnd,CmdShow         ; display our window on desktop
    invoke UpdateWindow, hwnd           ; refresh the client area
 
        .while TRUE                             ; Enter message loop
                invoke GetMessage, ADDR msg,NULL,0,0
                .break .if (!eax)
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
   .endw

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
        get_local $lhs
        get_local $rhs
        i32.add)
  (export "add" (func $add)))
v0 = my_vector                  // we want the horizontal sum of this
int64 r0 = get_len ( v0 )
int64 r0 = round_u2 ( r0 )
float v0 = set_len ( r0 , v0 )
while ( uint64 r0 > 4) {
        uint64 r0 >>= 1
        float v1 = shift_reduce ( r0 , v0 )
        float v0 = v1 + v0
}

Jeśli uważasz, że wszystkie trzy są napisane w asemblerze, gratulacje! Twoja intuicja nieco się polepszyła.

Pierwszy z nich jest w MASM32. Jest to makroasembler z „if” i „while”, w którym nadal pisze się natywne aplikacje Windowsa. Microsoft gorliwie chroni wsteczną kompatybilność systemu Windows z Win32 API, więc wszystkie programy MASM32, jakie kiedykolwiek napisano, działają dobrze również na nowoczesnych komputerach.

Na ironię zakrawa fakt, że język C został wynaleziony, aby ułatwić tłumaczenie UNIX-a z PDP-7 na PDP-11. Został zaprojektowany jako przenośny asembler zdolny do przetrwania eksplozji kambryjskiej architektur sprzętowych lat 70-tych. Ale w XXI wieku architektura sprzętowa ewoluuje tak wolno, że programy, które napisałem w MASM32 20 lat temu, składają się i działają doskonale nawet dziś. Nie mam natomiast pewności, czy aplikacja C++, którą zbudowałem w zeszłym roku z CMake 3.21, zbuilduje się dziś z CMake 3.25.

Drugim fragmentem kodu jest Web Assembly. Nie jest to nawet makro asembler, nie ma „if” i „while”. To bardziej czytelny koderowi kod maszynowy dla przeglądarek.

Kod Web Assembly jest niezależny od sprzętu. Maszyna na której działa jest abstrakcyjna, wirtualna i uniwersalna, nazywaj to jak chcesz. Jeśli jesteś w stanie przeczytać ten tekst, to znaczy, że zadziała u ciebie.

Najbardziej interesującym fragmentem kodu jest jednak ten trzeci. To ForwardCom - asembler, który proponuje Agner Fog, znany autor podręczników optymalizacji C++ i asemblera. Podobnie jak w przypadku Web Assembly, propozycja obejmuje nie tyle asembler, co uniwersalny zestaw instrukcji, zaprojektowany w celu zapewnienia nie tylko wstecznej, ale i przyszłej kompatybilności. Stąd nazwa. Jej rozwinięcie to "An open forward-compatible instruction set architecture". Innymi słowy, jest to nie tyle propozycja assemblera, co propozycja traktatu pokojowego.

Wiemy, że wszystkie najpopularniejsze rodziny architektur: x64, ARM i RISC-V mają różne zestawy instrukcji, ale nikt nie zna dobrego powodu, dlaczego tak jest. Wszystkie nowoczesne procesory, z wyjątkiem być może najprostszych, uruchamiają nie kod, którym je karmisz, ale mikrokod, na który przekładają się dane wejściowe. Tak więc nie tylko M1 ma warstwę kompatybilności wstecznej dla Intel. Każdy procesor zasadniczo ma warstwę kompatybilności wstecznej dla wszystkich swoich wcześniejszych wersji.

Co więc powstrzymuje projektantów architektury przed uzgodnieniem podobnej warstwy, ale w celu zapewnienia kompatybilności? Poza sprzecznymi ambicjami firm bezpośrednio ze sobą konkurującymi, nic. Ale jeśli producenci procesorów w pewnym momencie zdecydują się na wspólny zestaw instrukcji zamiast implementować nową warstwę kompatybilności dla każdego innego konkurenta, ForwardCom przywróci programowanie asemblera do głównego nurtu.

Ta warstwa kompatybilności w przód uleczyłaby najgorszy lęk każdego programisty asemblera, brzmiący mniej więcej: „A co, jeśli napiszę jedyny w swoim rodzaju kod dla tej konkretnej architektury, a ona stanie się przestarzała w ciągu roku?” Dzięki warstwie kompatybilności w przód nigdy się nie zdezaktualizuje. O to właśnie chodzi.

Programowanie w asemblerze jest również ograniczane przez mit, że pisanie w nim jest trudne, a zatem niepraktyczne. Propozycja Foga odnosi się również do tego problemu. Jeśli ktoś uważa, że pisanie w asemblerze jest trudne, ale w C nie, to niech asembler wygląda jak C. Żaden problem. Nie ma dobrego powodu, aby nowoczesny język asemblera wyglądał dokładnie tak samo, jak jego dziadek w latach 50-tych.

Podsumowując, zobaczyłeś trzy próbki assemblera. Żadna z nich nie wygląda jak „tradycyjny” assembly i żadna nie powinna taka być. Tak więc ForwardCom jest asemblerem, w którym można pisać optymalny kod, który nigdy się nie zdezaktualizuje, niewymagający przy tym nauki „tradycyjnego” asemblera. Z praktycznego punktu widzenia jest to C przyszłości. Nie C++.

Kiedy w końcu C++ umrze?

Żyjemy w postmodernistycznym świecie. Tak nigdy tak naprawdę nie umarła, łacina, COBOL, Algol 68 i Ada, tak i C++ jest skazany na wieczne pół-istnienie między żywotem a śmiercią języka. Uważam, że nigdy tak naprawdę nie umrze, a zostanie jedynie wyparty z głównego nurtu przez nowsze, bardziej wydajne technologie.

Tutaj poprawka - on już jest wypierany. Przyszedłem do mojej obecnej pracy jako programista C++, a dziś mój dzień pracy zaczyna się od Pythona. Piszę równania, SymPy rozwiązuje je za mnie, a następnie proponuje rozwiązanie na C++. Następnie wklejam ten kod do biblioteki C++, nawet nie zawracając sobie głowy formatowaniem, ponieważ clang-tidy i tak zrobi to za mnie. Analizator statyczny sprawdzi, czy nie pomyliłem przestrzeni nazw, a analizator dynamiczny sprawdzi wycieki pamięci. CI/CD zajmie się kompilacją międzyplatformową. Profiler pomoże mi zrozumieć, jak faktycznie działa mój kod, a dezasembler powie dlaczego.

Jeśli zamienię C++ na „nie C++”, 80% mojej pracy pozostanie bez zmian. Jest on po prostu nieistotny dla większości tego, co robię. Czy to może oznaczać, że dla mnie C++ jest już w 80% martwy?


Przeczytaj także:



Artykuł w języku angielskim przeczytasz tutaj.

<p>Loading...</p>