Jednym z podanych powodów znajomości asemblera jest to, że czasami można go użyć do napisania kodu, który będzie bardziej wydajny niż pisanie tego kodu w języku wyższego poziomu, w szczególności C. Jednak słyszałem też wielokrotnie, że chociaż nie jest to całkowicie fałszywe, przypadki, w których asembler może być rzeczywiście używany do generowania bardziej wydajnego kodu, są niezwykle rzadkie i wymagają specjalistycznej wiedzy i doświadczenia w asemblerze.
To pytanie nawet nie zagłębia się w fakt, że instrukcje asemblera będą specyficzne dla maszyny i nieprzenośne, ani w żadnym innym aspekcie asemblera. Poza tym oczywiście istnieje wiele dobrych powodów, by znać asembler, ale ma to być konkretne pytanie, które gromadzi przykłady i dane, a nie rozszerzony dyskurs na temat asemblera w porównaniu z językami wyższego poziomu.
Czy ktoś może podać konkretne przykłady przypadków, w których montaż będzie szybszy niż dobrze napisany kod C przy użyciu nowoczesnego kompilatora, i czy możesz wesprzeć to twierdzenie profilowaniem dowodów? Jestem przekonany, że te przypadki istnieją, ale naprawdę chcę dokładnie wiedzieć, jak ezoteryczne są te przypadki, ponieważ wydaje się, że jest to kwestia sporna.
źródło
-O3
flagi, prawdopodobnie lepiej zostawić optymalizację kompilatorowi C :-)Odpowiedzi:
Oto przykład z prawdziwego świata: Stałe punkty mnożą się na starych kompilatorach.
Są one przydatne nie tylko na urządzeniach bez zmiennoprzecinkowych, ale świecą, jeśli chodzi o precyzję, ponieważ zapewniają 32 bity precyzji z przewidywalnym błędem (liczba zmiennoprzecinkowa ma tylko 23 bity i trudniej jest przewidzieć utratę precyzji). tj. jednolita absolutna precyzja w całym zakresie, zamiast zbliżonej do jednakowej dokładności względnej (
float
).Nowoczesne kompilatory ładnie optymalizują ten przykład w punkcie stałym, więc dla bardziej nowoczesnych przykładów, które wciąż wymagają kodu specyficznego dla kompilatora, zobacz
uint64_t
32x32 => 64-bitowe nie optymalizuje się na 64-bitowym procesorze, więc potrzebujesz wewnętrznych lub__int128
wydajnego kodu w systemach 64-bitowych.C nie ma operatora pełnego mnożenia (wynik 2N-bitowy z wejść N-bitowych). Zwykłym sposobem wyrażenia tego w C jest rzutowanie danych wejściowych na szerszy typ i nadzieję, że kompilator rozpozna, że górne bity danych wejściowych nie są interesujące:
Problem z tym kodem polega na tym, że robimy coś, czego nie można bezpośrednio wyrazić w języku C. Chcemy pomnożyć dwie liczby 32-bitowe i uzyskać wynik 64-bitowy, z którego zwracamy środkowy 32-bitowy. Jednak w C ten mnożnik nie istnieje. Wszystko, co możesz zrobić, to podwyższyć liczby całkowite do 64-bitowych i zrobić 64 * 64 = 64 pomnożenie.
x86 (i ARM, MIPS i inne) mogą jednak wykonać mnożenie w pojedynczej instrukcji. Niektóre kompilatory ignorowały ten fakt i generowały kod, który wywołuje funkcję biblioteki wykonawczej w celu wykonania mnożenia. Przesunięcie o 16 jest również często wykonywane przez procedurę biblioteczną (również x86 może wykonywać takie przesunięcia).
Pozostaje nam jedno lub dwa wywołania biblioteczne tylko dla pomnożenia. Ma to poważne konsekwencje. Przesunięcie jest nie tylko wolniejsze, ale rejestry muszą być zachowywane w wywołaniach funkcji, a także nie pomaga wstawianie i rozwijanie kodu.
Jeśli przepiszesz ten sam kod w (wbudowanym) asemblerze, możesz uzyskać znaczne przyspieszenie.
Ponadto: korzystanie z ASM nie jest najlepszym sposobem na rozwiązanie problemu. Większość kompilatorów pozwala na użycie niektórych instrukcji asemblera w postaci wewnętrznej, jeśli nie można ich wyrazić w C. Kompilator VS.NET2008 na przykład wyświetla 32 * 32 = 64-bitowy mul jako __emul, a 64-bitowe przesunięcie jako __ll_rshift.
Używając funkcji wewnętrznych, możesz przepisać funkcję w taki sposób, aby kompilator C miał szansę zrozumieć, co się dzieje. Pozwala to na wstawianie kodu, przydzielanie rejestru, wspólną eliminację podwyrażeń i stałą propagację. W ten sposób uzyskasz ogromną poprawę wydajności w stosunku do ręcznie napisanego kodu asemblera.
Dla porównania: Rezultat końcowy dla mulda punktu stałego dla kompilatora VS.NET to:
Różnica wydajności podziału na punkty stałe jest jeszcze większa. Miałem ulepszenia do współczynnika 10 dla ciężkiego kodu stałego punktu dzielącego, pisząc kilka linii asm.
Korzystanie z Visual C ++ 2013 daje ten sam kod asemblera na oba sposoby.
gcc4.1 z 2007 roku ładnie optymalizuje również czystą wersję C. (Eksplorator kompilatora Godbolt nie ma zainstalowanych wcześniejszych wersji gcc, ale prawdopodobnie nawet starsze wersje GCC mogłyby to zrobić bez wewnętrznych elementów).
Zobacz source + asm dla x86 (32-bit) i ARM w eksploratorze kompilatorów Godbolt . (Niestety nie ma żadnych kompilatorów wystarczająco starych, aby wygenerować zły kod z prostej wersji w czystym C.)
Nowoczesne procesory mogą robić rzeczy, C nie ma dla operatorów w ogóle , jak
popcnt
i nieco skanowania do znalezienia pierwszego lub ostatniego zestawu trochę . (POSIX maffs()
funkcję, ale jej semantyka nie pasuje do x86bsf
/bsr
. Zobacz https://en.wikipedia.org/wiki/Find_first_set ).Niektóre kompilatory czasami rozpoznają pętlę, która zlicza liczbę ustawionych bitów w liczbie całkowitej i kompilują ją do
popcnt
instrukcji (jeśli jest włączona w czasie kompilacji), ale o wiele bardziej niezawodne jest używanie jej__builtin_popcnt
w GNU C lub na x86, jeśli jesteś tylko celowanie w sprzęt z SSE4.2:_mm_popcnt_u32
z<immintrin.h>
.Lub w C ++, przypisz do
std::bitset<32>
i użyj.count()
. (Jest to przypadek, w którym język znalazł sposób na przenośne udostępnienie zoptymalizowanej implementacji popcount poprzez standardową bibliotekę, w sposób, który zawsze kompiluje się do czegoś poprawnego i może wykorzystać wszystko, co obsługuje cel.) Zobacz także https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .Podobnie,
ntohl
można skompilować dobswap
(x86 32-bitowa zamiana bajtów dla konwersji endian) na niektórych implementacjach C, które go mają.Innym ważnym obszarem wewnętrznym lub ręcznie pisanym asmem jest ręczna wektoryzacja z instrukcjami SIMD. Kompilatory nie są złe z takimi prostymi pętlami
dst[i] += src[i] * 10.0;
, ale często źle działają lub wcale nie powodują automatycznej wektoryzacji, gdy sprawy stają się bardziej skomplikowane. Na przykład jest mało prawdopodobne, aby uzyskać coś takiego jak Jak wdrożyć atoi za pomocą SIMD? generowane automatycznie przez kompilator z kodu skalarnego.źródło
Wiele lat temu uczyłem kogoś programowania w C. Ćwiczenie polegało na obracaniu grafiki o 90 stopni. Wrócił z rozwiązaniem, które zajęło kilka minut, głównie dlatego, że używał mnożeń i dzieleń itp.
Pokazałem mu, jak przekształcić problem za pomocą przesunięć bitowych, a czas przetwarzania skrócił się do około 30 sekund na nieoptymalizowanym kompilatorze, który posiadał.
Właśnie dostałem kompilator optymalizujący i ten sam kod obrócił grafikę w <5 sekund. Spojrzałem na kod asemblera generowany przez kompilator i na podstawie tego, co zobaczyłem, zdecydowałem, że moje dni pisania asemblera już minęły.
źródło
add di,di / adc al,al / add di,di / adc ah,ah
itp. Dla wszystkich ośmiu rejestrów 8-bitowych, a następnie ponownie wykona wszystkie rejestry 8, a następnie powtórzy całą procedurę trzy więcej razy i na koniec zapisz cztery słowa w ax / bx / cx / dx. Nie ma mowy, żeby asembler zbliżył się do tego.Prawie za każdym razem, gdy kompilator widzi kod zmiennoprzecinkowy, wersja napisana ręcznie będzie szybsza, jeśli używasz starego, złego kompilatora. ( Aktualizacja 2019: Nie jest to ogólnie prawdą w przypadku nowoczesnych kompilatorów. Zwłaszcza podczas kompilacji dla czegokolwiek innego niż x87; kompilatory mają łatwiejszy czas z SSE2 lub AVX dla matematyki skalarnej, lub dla innych niż x86 z płaskim zestawem rejestrów FP, w przeciwieństwie do x87 rejestr stosu).
Głównym powodem jest to, że kompilator nie może wykonać żadnych solidnych optymalizacji. Zobacz ten artykuł z MSDN, aby uzyskać dyskusję na ten temat. Oto przykład, w którym wersja zestawu jest dwa razy szybsza niż wersja C (skompilowana z VS2K5):
I niektóre numery z mojego komputera z domyślną wersją wydania * :
Zainteresowany zamieniłem pętlę na dec / jnz i nie miało to żadnego wpływu na taktowanie - czasem szybciej, a czasem wolniej. Wydaje mi się, że aspekt ograniczonej pamięci przewyższa inne optymalizacje. (Uwaga edytora: bardziej prawdopodobne jest, że wąskie gardło opóźnień w FP wystarcza, aby ukryć dodatkowy koszt
loop
. Wykonanie dwóch podsumowań Kahana równolegle dla elementów nieparzystych / parzystych i dodanie ich na końcu, może może to przyspieszyć 2-krotnie. )Ups, uruchomiłem nieco inną wersję kodu, która wypisała liczby w niewłaściwy sposób (tzn. C był szybszy!). Naprawiono i zaktualizowano wyniki.
źródło
-ffast-math
. Mają poziom optymalizacji,-Ofast
który jest obecnie równoważny-O3 -ffast-math
, ale w przyszłości mogą obejmować więcej optymalizacji, które mogą prowadzić do nieprawidłowego generowania kodu w przypadkach narożnych (takich jak kod oparty na NaEE IEEE).a+b == b+a
), ale nie asocjacyjne (zmiana kolejności operacji, więc zaokrąglanie półproduktów jest inne). re: ten kod: Nie sądzę, by niezakomentowane x87 iloop
instrukcja były bardzo niesamowitą demonstracją szybkiego asm.loop
najwyraźniej nie jest wąskim gardłem z powodu opóźnienia FP. Nie jestem pewien, czy obsługuje on operacje FP, czy nie; x87 jest trudny do odczytania przez ludzi. Dwiefstp results
insynuacje na końcu wyraźnie nie są optymalne. Usunięcie dodatkowego wyniku ze stosu lepiej byłoby zrobić w sklepie innym niż sklep. Jakfstp st(0)
IIRC.Nie podając żadnego konkretnego przykładu ani dowodów profilera, możesz napisać lepszy asembler niż kompilator, jeśli wiesz więcej niż kompilator.
W ogólnym przypadku nowoczesny kompilator C wie znacznie więcej o tym, jak zoptymalizować dany kod: wie, jak działa potok procesora, może próbować zmieniać kolejność instrukcji szybciej niż człowiek, i tak dalej - to w zasadzie to samo, co komputer jest lepszy lub lepszy od najlepszych ludzi do gier planszowych itp. po prostu dlatego, że może szybciej wyszukiwać w obszarze problemów niż większość ludzi. Chociaż teoretycznie możesz działać tak dobrze, jak komputer w konkretnym przypadku, z pewnością nie możesz tego zrobić z tą samą prędkością, co czyni go nieosiągalnym w więcej niż kilku przypadkach (tzn. Kompilator z pewnością przewyższy cię, jeśli spróbujesz napisać więcej niż kilka procedur w asemblerze).
Z drugiej strony zdarzają się przypadki, w których kompilator nie ma tylu informacji - powiedziałbym przede wszystkim podczas pracy z różnymi formami zewnętrznego sprzętu, o których kompilator nie ma wiedzy. Podstawowym przykładem są prawdopodobnie sterowniki urządzeń, w których asembler w połączeniu z dogłębną znajomością danego sprzętu przez człowieka może dawać lepsze wyniki niż kompilator C.
Inni wspominali instrukcje specjalnego przeznaczenia, o czym mówię w powyższym akapicie - instrukcje, o których kompilator mógł mieć ograniczoną wiedzę lub nie mieć jej wcale, umożliwiając człowiekowi pisanie szybszego kodu.
źródło
ocamlopt
pomija planowanie instrukcji na x86 i zamiast tego pozostawia to procesorowi, ponieważ może bardziej efektywnie zmieniać kolejność w czasie wykonywania.W mojej pracy są trzy powody, dla których znam i używam asemblera. W kolejności ważności:
Debugowanie - często otrzymuję kod biblioteki, który zawiera błędy lub niekompletną dokumentację. Rozumiem, co robi, wkraczając na poziomie zespołu. Muszę to robić mniej więcej raz w tygodniu. Używam go również jako narzędzia do debugowania problemów, w których moje oczy nie dostrzegają błędu idiomatycznego w C / C ++ / C #. Spoglądanie na zespół mija to.
Optymalizacja - kompilator radzi sobie dość dobrze w optymalizacji, ale gram na innym boisku niż większość. Piszę kod przetwarzania obrazu, który zwykle zaczyna się od kodu, który wygląda następująco:
„zrób coś” zazwyczaj ma miejsce kilka milionów razy (tj. od 3 do 30). Skrobanie cykli w tej fazie „robienia czegoś” znacznie zwiększa wydajność. Zwykle nie zaczynam od tego - zwykle zaczynam od napisania kodu, aby najpierw działał, a następnie staram się refaktoryzować C, aby był naturalnie lepszy (lepszy algorytm, mniejsze obciążenie w pętli itp.). Zwykle muszę czytać asembler, aby zobaczyć, co się dzieje i rzadko muszę go pisać. Robię to może co dwa lub trzy miesiące.
robienie czegoś, na co język mi nie pozwala. Należą do nich - uzyskanie architektury procesora i określonych funkcji procesora, dostęp do flag nie znajdujących się w CPU (stary, naprawdę chciałbym, żeby C dał ci dostęp do flagi carry), itp. Robię to może raz w roku lub dwóch latach.
źródło
Tylko podczas korzystania z niektórych zestawów instrukcji specjalnego kompilator nie obsługuje.
Aby zmaksymalizować moc obliczeniową nowoczesnego procesora z wieloma potokami i predykcyjnym rozgałęzianiem, musisz ustrukturyzować program asemblowania w sposób, który sprawia, że a) prawie niemożliwe jest napisanie przez człowieka b) jeszcze trudniejsze do utrzymania.
Ponadto lepsze algorytmy, struktury danych i zarządzanie pamięcią zapewnią co najmniej rząd wielkości wyższą wydajność niż mikrooptymalizacje, które można wykonać w asemblerze.
źródło
Chociaż C jest „bliski” manipulacji 8-bitowymi, 16-bitowymi, 32-bitowymi, 64-bitowymi danymi na niskim poziomie, istnieje kilka operacji matematycznych nieobsługiwanych przez C, które często można wykonać elegancko w niektórych instrukcjach montażu zestawy:
Mnożenie w punktach stałych: Iloczyn dwóch liczb 16-bitowych to liczba 32-bitowa. Ale reguły w C mówią, że iloczyn dwóch liczb 16-bitowych jest liczbą 16-bitową, a iloczyn dwóch liczb 32-bitowych jest liczbą 32-bitową - dolna połowa w obu przypadkach. Jeśli chcesz uzyskać górną połowę mnożnika 16 x 16 lub 32 x 32, musisz grać w gry z kompilatorem. Ogólna metoda polega na rzutowaniu na większą niż potrzebną szerokość bitu, pomnożeniu, przesunięciu w dół i ponownym rzutowaniu:
W takim przypadku kompilator może być wystarczająco inteligentny, aby wiedzieć, że tak naprawdę próbujesz uzyskać górną połowę mnożenia 16x16 i zrobić dobrą rzecz z natywnym multiplikatorem 16x16 maszyny. Lub może to być głupie i wymagać wywołania biblioteki, aby wykonać mnożenie 32x32, co jest nadmierną przesadą, ponieważ potrzebujesz tylko 16 bitów produktu - ale standard C nie daje ci żadnej możliwości wyrażenia siebie.
Niektóre operacje przesuwania bitów (rotacja / przenoszenie):
Nie jest to zbyt nieeleganckie w C, ale znowu, chyba że kompilator jest wystarczająco inteligentny, aby zdawać sobie sprawę z tego, co robisz, wykona wiele „niepotrzebnej” pracy. Wiele zestawów instrukcji montażu pozwala obracać lub przesuwać w lewo / prawo z wynikiem w rejestrze przenoszenia, dzięki czemu można wykonać powyższe czynności w 34 instrukcjach: załaduj wskaźnik na początek tablicy, wyczyść przenoszenie i wykonaj 32 8- bit przesuwa się w prawo, używając automatycznego przyrostu wskaźnika.
Dla innego przykładu, istnieją liniowe rejestry przesuwne sprzężenia zwrotnego (LFSR), które są elegancko wykonywane w asemblerze: weź kawałek N bitów (8, 16, 32, 64, 128 itd.), Przesuń całość o 1 (patrz wyżej) algorytm), a jeśli wynikowe przeniesienie wynosi 1, to XOR we wzorcu bitowym reprezentującym wielomian.
Powiedziawszy to, nie użyłbym tych technik, chyba że miałbym poważne ograniczenia wydajności. Jak powiedzieli inni, montaż jest znacznie trudniejszy do udokumentowania / debugowania / przetestowania / obsługi niż kod C: wzrost wydajności wiąże się z pewnymi poważnymi kosztami.
edycja: 3. Wykrywanie przepełnienia jest możliwe w asemblerze (tak naprawdę nie można tego zrobić w C), co znacznie ułatwia niektóre algorytmy.
źródło
Krótka odpowiedź? Czasami.
Technicznie każda abstrakcja ma swój koszt, a język programowania jest abstrakcją dla działania procesora. C jest jednak bardzo blisko. Lata temu pamiętam, jak się śmiałem, gdy zalogowałem się na swoje konto UNIX i otrzymałem następujący komunikat o fortunie (gdy takie rzeczy były popularne):
To zabawne, bo to prawda: C jest jak przenośny język asemblera.
Warto zauważyć, że język asemblera działa tak, jak go piszesz. Istnieje jednak kompilator pomiędzy C a generowanym przez niego językiem asemblera, co jest niezwykle ważne, ponieważ szybkość twojego kodu C ma bardzo dużo wspólnego z tym, jak dobry jest twój kompilator.
Kiedy pojawił się gcc, jedną z rzeczy, które uczyniły go tak popularnym, było to, że często był o wiele lepszy niż kompilatory C dostarczane z wieloma komercyjnymi wersjami UNIX. Nie tylko był to ANSI C (żaden z tych śmieci K&R C), ale był bardziej niezawodny i zazwyczaj produkował lepszy (szybszy) kod. Nie zawsze, ale często.
Mówię ci to wszystko, ponieważ nie ma ogólnej reguły dotyczącej prędkości C i asemblera, ponieważ nie ma obiektywnego standardu dla C.
Podobnie, asembler różni się bardzo w zależności od używanego procesora, specyfikacji systemu, zestawu instrukcji i tak dalej. Historycznie istniały dwie rodziny architektury procesorów: CISC i RISC. Największym graczem w CISC była i nadal jest architektura Intel x86 (i zestaw instrukcji). RISC zdominowało świat UNIX (MIPS6000, Alpha, Sparc i tak dalej). CISC wygrał bitwę o serca i umysły.
W każdym razie popularną mądrością, kiedy byłem młodszym programistą, było to, że odręcznie napisane x86 może często być znacznie szybsze niż C, ponieważ sposób, w jaki działała architektura, miał złożoność, z której korzystał człowiek. Z drugiej strony RISC wydawało się zaprojektowane dla kompilatorów, więc nikt (wiedziałem) nie napisałby, że mówi asembler Sparc. Jestem pewien, że tacy ludzie istnieli, ale bez wątpienia obaj oszaleli i do tej pory zostali zinstytucjonalizowani.
Zestawy instrukcji są ważnym punktem nawet w tej samej rodzinie procesorów. Niektóre procesory Intel mają rozszerzenia takie jak SSE do SSE4. AMD miało własne instrukcje SIMD. Zaletą języka programowania, takiego jak C, było to, że ktoś mógł napisać swoją bibliotekę, aby była zoptymalizowana pod kątem dowolnego procesora, na którym pracujesz. To była ciężka praca w asemblerze.
W asemblerze można jeszcze wprowadzić optymalizacje, których żaden kompilator nie mógłby wykonać, a dobrze napisany algorytm asemblera będzie tak szybki lub szybszy niż jego odpowiednik C. Większe pytanie brzmi: czy warto?
Ostatecznie asembler był produktem swoich czasów i był bardziej popularny w czasach, gdy cykle procesora były drogie. Obecnie procesor, którego produkcja kosztuje 5–10 USD (Intel Atom), może zrobić wszystko, co tylko zechce. Jedynym prawdziwym powodem do napisania asemblera w tych dniach są rzeczy niskiego poziomu, takie jak niektóre części systemu operacyjnego (mimo że ogromna większość jądra Linuksa jest napisana w C), sterowniki urządzeń, ewentualnie urządzenia osadzone (chociaż C ma tam tendencję dominować też) i tak dalej. Lub tylko dla kopnięć (co jest nieco masochistyczne).
źródło
Przypadek użycia, który może już nie mieć zastosowania, ale dla twojej nerdowej przyjemności: na Amiga procesor i układy graficzne / audio walczyłyby o dostęp do określonego obszaru pamięci RAM (konkretnie pierwsze 2 MB pamięci RAM). Gdy więc masz tylko 2 MB pamięci RAM (lub mniej), wyświetlanie złożonej grafiki i odtwarzanie dźwięku może zabić wydajność procesora.
W asemblerze można przeplatać kod w tak sprytny sposób, że procesor spróbuje uzyskać dostęp do pamięci RAM tylko wtedy, gdy układy graficzne / audio są zajęte wewnętrznie (tj. Gdy magistrala jest wolna). Tak więc, zmieniając kolejność instrukcji, sprytnie wykorzystując pamięć podręczną procesora, taktowanie magistrali, można osiągnąć pewne efekty, które po prostu nie były możliwe przy użyciu języka wyższego poziomu, ponieważ trzeba było zsynchronizować każde polecenie, a nawet wstawić tu i tam NOP, aby zachować różne chipy z siebie radar.
To kolejny powód, dla którego instrukcja NOP (brak operacji - nic nie rób) procesora może faktycznie przyspieszyć działanie całej aplikacji.
[EDYCJA] Oczywiście technika zależy od konkretnej konfiguracji sprzętowej. To był główny powód, dla którego wiele gier Amigi nie radziło sobie z szybszymi procesorami: czas wykonywania instrukcji był wyłączony.
źródło
Wskaż jeden, który nie jest odpowiedzią.
Nawet jeśli nigdy się w nim nie programujesz, uważam, że warto znać przynajmniej jeden zestaw instrukcji asemblera. Jest to część niekończących się poszukiwań programistów, aby dowiedzieć się więcej i tym samym być lepszym. Przydaje się również przy wchodzeniu w frameworki, do których nie masz kodu źródłowego i masz co najmniej ogólne pojęcie o tym, co się dzieje. Pomaga także zrozumieć JavaByteCode i .Net IL, ponieważ oba są podobne do asemblera.
Aby odpowiedzieć na pytanie, gdy masz małą ilość kodu lub dużo czasu. Najbardziej przydatny do stosowania we wbudowanych układach scalonych, gdzie niska złożoność układów i niska konkurencja w kompilatorach atakujących te układy mogą przechylić równowagę na korzyść ludzi. Również w przypadku urządzeń z ograniczeniami często wymieniasz rozmiar kodu / rozmiar pamięci / wydajność w sposób, który trudno byłoby poinstruować kompilator. np. wiem, że ta akcja użytkownika nie jest często wywoływana, więc będę mieć mały rozmiar kodu i słabą wydajność, ale ta inna funkcja, która wygląda podobnie, jest używana co sekundę, więc będę miał większy rozmiar kodu i większą wydajność. Jest to rodzaj kompromisu, z którego może skorzystać wykwalifikowany programista.
Chciałbym również dodać, że jest dużo pośredniego miejsca, w którym można kodować w kompilacji C i badać wyprodukowane Zgromadzenie, a następnie albo zmienić kod w C, albo dostosować i zachować jako asembler.
Mój przyjaciel pracuje na mikrokontrolerach, obecnie chipach do sterowania małymi silnikami elektrycznymi. Pracuje w kombinacji niskiego poziomu c i zestawu. Kiedyś powiedział mi o dobrym dniu w pracy, w którym zmniejszył główną pętlę z 48 instrukcji do 43. Stoi też przed wyborem, jak kod urósł, aby wypełnić 256k chip, a firma chce nowej funkcji, prawda?
Chciałbym dodać jako komercyjny programista z dość dużym portfolio lub językami, platformami, rodzajami aplikacji, których nigdy nie czułem potrzeby nurkowania w pisaniu asemblera. Jak zawsze doceniałem wiedzę na ten temat. I czasami debuguje się w tym.
Wiem, że znacznie więcej odpowiedziałem na pytanie „dlaczego mam się uczyć asemblera”, ale uważam, że jest to ważniejsze pytanie, kiedy jest szybsze.
więc spróbujmy jeszcze raz Powinieneś pomyśleć o montażu
Pamiętaj, aby porównać swój zestaw z generowanym kompilatorem, aby zobaczyć, który jest szybszy / mniejszy / lepszy.
David.
źródło
sbi
icbi
), których kompilatory kiedyś (a czasem nadal nie wykorzystują), z powodu ograniczonej wiedzy o sprzęcie.Dziwię się, że nikt tego nie powiedział.
strlen()
Funkcja jest znacznie szybciej, jeśli napisane w montażu! W C najlepsze, co możesz zrobić, topodczas montażu możesz go znacznie przyspieszyć:
długość jest w ecx. To porównuje 4 znaki na raz, więc jest 4 razy szybsze. I pomyśl, używając słowa eax i ebx o wysokim porządku, stanie się 8 razy szybsze niż poprzednia procedura w C!
źródło
(word & 0xFEFEFEFF) & (~word + 0x80808080)
wynosi zero, jeśli wszystkie bajty w słowie są niezerowe.Operacje na macierzach przy użyciu instrukcji SIMD są prawdopodobnie szybsze niż kod generowany przez kompilator.
źródło
Nie mogę podać konkretnych przykładów, ponieważ było to zbyt wiele lat temu, ale było wiele przypadków, w których ręcznie napisany asembler mógł przewyższyć dowolny kompilator. Przyczyny:
Możesz odstąpić od konwencji wywoływania, przekazując argumenty do rejestrów.
Możesz dokładnie rozważyć sposób korzystania z rejestrów i uniknąć przechowywania zmiennych w pamięci.
W przypadku takich tabel skoków można uniknąć konieczności sprawdzania indeksu.
Zasadniczo, kompilatory wykonują całkiem niezłą robotę optymalizacyjną, i to prawie zawsze jest „wystarczająco dobre”, ale w niektórych sytuacjach (takich jak renderowanie grafiki), gdzie płacisz drogo za każdy cykl, możesz skorzystać ze skrótów, ponieważ znasz kod , gdzie kompilator nie mógł, ponieważ musi być po bezpiecznej stronie.
W rzeczywistości słyszałem o graficznym kodzie renderującym, w którym procedura, taka jak procedura rysowania linii lub wypełniania wielokątów, faktycznie generowała mały blok kodu maszynowego na stosie i wykonywała go tam, aby uniknąć ciągłego podejmowania decyzji o stylu linii, szerokości, wzorze itp.
To powiedziawszy, chcę, aby kompilator wygenerował dla mnie dobry kod asemblera, ale nie był zbyt sprytny, a oni w większości to robią. W rzeczywistości jedną z rzeczy, których nienawidzę w Fortranie, jest szyfrowanie kodu w celu „zoptymalizowania” go, zwykle bez większego celu.
Zwykle gdy aplikacje mają problemy z wydajnością, jest to spowodowane marnotrawstwem projektu. W dzisiejszych czasach nigdy nie polecałbym asemblera ze względu na wydajność, chyba że ogólna aplikacja została już dostrojona w calu swojego życia, wciąż nie była wystarczająco szybka i cały czas spędzała w ciasnych wewnętrznych pętlach.
Dodano: Widziałem wiele aplikacji napisanych w języku asemblera, a główną przewagą szybkości nad językiem takim jak C, Pascal, Fortran itp. Było to, że programista był znacznie bardziej ostrożny podczas kodowania w asemblerze. On lub ona będzie pisać około 100 wierszy kodu dziennie, niezależnie od języka, w języku kompilatora, który będzie równy 3 lub 400 instrukcji.
źródło
Kilka przykładów z mojego doświadczenia:
Dostęp do instrukcji, które nie są dostępne z C. Na przykład wiele architektur (takich jak x86-64, IA-64, DEC Alpha i 64-bitowe MIPS lub PowerPC) obsługuje mnożenie 64-bitowe na 64-bitowe, co daje wynik 128-bitowy. GCC niedawno dodało rozszerzenie zapewniające dostęp do takich instrukcji, ale przed tym montażem było wymagane. A dostęp do tej instrukcji może mieć ogromną różnicę w procesorach 64-bitowych podczas implementacji czegoś takiego jak RSA - czasami nawet o 4-krotny wzrost wydajności.
Dostęp do flag specyficznych dla procesora. Tym, który bardzo mnie ugryzł, jest flaga carry; podczas dodawania z wieloma precyzjami, jeśli nie masz dostępu do bitu przenoszenia procesora, musisz zamiast tego porównać wynik, aby zobaczyć, czy nie został on przepełniony, co wymaga 3-5 dodatkowych instrukcji na kończynę; i gorzej, które są dość szeregowe pod względem dostępu do danych, co zabija wydajność współczesnych superskalarnych procesorów. Podczas przetwarzania tysięcy takich liczb całkowitych z rzędu, możliwość korzystania z addc to ogromna wygrana (istnieją problemy superskalarne z rywalizacją o bit przenoszenia, ale współczesne procesory radzą sobie z tym całkiem dobrze).
SIMD. Nawet kompilatory autowektoryzujące potrafią robić tylko stosunkowo proste przypadki, więc jeśli chcesz dobrej wydajności SIMD, niestety często trzeba pisać kod bezpośrednio. Oczywiście możesz używać funkcji wewnętrznych zamiast asemblera, ale kiedy jesteś już na poziomie wewnętrznym, w zasadzie i tak piszesz asembler, używając kompilatora jako przydziału rejestrów i (nominalnie) harmonogramu instrukcji. (Zwykle używam funkcji wewnętrznych dla SIMD po prostu dlatego, że kompilator może generować prologi funkcji i tak dalej, więc mogę używać tego samego kodu w systemie Linux, OS X i Windows bez konieczności zajmowania się zagadnieniami ABI, takimi jak konwencje wywoływania funkcji, ale inne poza tym cechy wewnętrzne SSE naprawdę nie są zbyt ładne - Altivec wydają się lepsze, choć nie mam z nimi dużego doświadczenia).bitslicing AES lub SIMD error error - można sobie wyobrazić kompilator, który może analizować algorytmy i generować taki kod, ale wydaje mi się, że taki inteligentny kompilator jest co najmniej 30 lat od istnienia (w najlepszym razie).
Z drugiej strony, maszyny wielordzeniowe i systemy rozproszone przesunęły wiele z największych zwycięstw w drugą stronę - uzyskaj dodatkowe 20% przyspieszenia pisania wewnętrznych pętli w zespole lub 300% przez uruchomienie ich na wielu rdzeniach, lub 10000% przez uruchamiając je w klastrze maszyn. I oczywiście optymalizacje na wysokim poziomie (takie jak kontrakty futures, zapamiętywanie itp.) Są często znacznie łatwiejsze w języku wyższego poziomu, takim jak ML lub Scala niż C lub asm, i często mogą zapewnić znacznie większą wygraną. Jak zwykle trzeba dokonać kompromisów.
źródło
Ciasne pętle, na przykład podczas zabawy obrazami, ponieważ obraz może zawierać miliony pikseli. Siadanie i zastanawianie się, jak najlepiej wykorzystać ograniczoną liczbę rejestrów procesorów, może mieć znaczenie. Oto próbka z prawdziwego życia:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Wówczas często procesory mają pewne ezoteryczne instrukcje, które są zbyt wyspecjalizowane, aby kompilator mógł nimi zawracać głowę, ale czasami programista asemblera może z nich skorzystać. Weźmy na przykład instrukcję XLAT. Naprawdę świetnie, jeśli potrzebujesz przeglądać tabele w pętli, a tabela jest ograniczona do 256 bajtów!
Zaktualizowano: Och, przyjdź pomyśleć o tym, co jest najważniejsze, gdy mówimy ogólnie o pętlach: kompilator często nie ma pojęcia, ile iteracji będzie to typowy przypadek! Tylko programista wie, że pętla będzie iterowana WIELU razy i dlatego korzystne będzie przygotowanie się do niej z dodatkowym nakładem pracy, lub jeśli będzie ona powtarzana tak mało razy, że konfiguracja faktycznie potrwa dłużej niż iteracje spodziewany.
źródło
Częściej niż myślisz, C musi robić rzeczy, które wydają się zbędne z punktu widzenia kodera asemblera tylko dlatego, że tak mówią standardy C.
Na przykład promocja liczb całkowitych. Jeśli chcesz przesunąć zmienną char w C, zwykle można oczekiwać, że kod wykona właśnie to, przesunięcie o jeden bit.
Jednak standardy wymuszają na kompilatorze wykonanie przed rozszerzeniem znaku rozciągającego się na int i następnie przycinają wynik do char, co może skomplikować kod w zależności od architektury procesora docelowego.
źródło
Nie wiesz, czy dobrze napisany kod C jest naprawdę szybki, jeśli nie spojrzałeś na dezasemblację tego, co wytwarza kompilator. Wiele razy na to patrzysz i widzisz, że „dobrze napisany” był subiektywny.
Więc nie trzeba pisać w asemblerze, aby uzyskać najszybszy kod, ale z pewnością warto znać asembler z tego samego powodu.
źródło
Przeczytałem wszystkie odpowiedzi (ponad 30) i nie znalazłem prostego powodu: asembler jest szybszy niż C, jeśli przeczytałeś i ćwiczyłeś Podręcznik referencyjny optymalizacji architektury Intel® 64 i IA-32 , więc powód, dla którego montaż może być wolniej jest, że ludzie, którzy piszą taki wolniejszy zestaw, nie przeczytali Podręcznika optymalizacji .
W dawnych dobrych czasach Intel 80286 każda instrukcja była wykonywana przy stałej liczbie cykli procesora, ale od czasu wydania Pentium Pro w 1995 r. Procesory Intel stały się superskalarne, wykorzystując złożone przetwarzanie potokowe: wykonywanie poza zamówieniem i zmiana nazwy rejestru. Wcześniej, na Pentium, wyprodukowanym w 1993 r., Istniały rurociągi U i V: podwójne linie rur, które mogłyby wykonywać dwie proste instrukcje w jednym cyklu zegara, jeśli nie były od siebie zależne; ale to nie było nic do porównania z tym, co to jest wykonywanie poza zamówieniem i zmiana nazwy pojawiła się w Pentium Pro i prawie nie zmieniła się w dzisiejszych czasach.
Aby wyjaśnić w kilku słowach, najszybszy kod jest tam, gdzie instrukcje nie zależą od poprzednich wyników, np. Zawsze powinieneś wyczyścić całe rejestry (przez movzx) lub użyć
add rax, 1
zamiast tego lubinc rax
usunąć zależność od poprzedniego stanu flag itp.Możesz przeczytać więcej na temat wykonywania poza zamówieniem i zmiany nazwy rejestru, jeśli czas na to pozwala, w Internecie dostępnych jest wiele informacji.
Istnieją również inne ważne kwestie, takie jak przewidywanie gałęzi, liczba jednostek ładowania i przechowywania, liczba bramek, które wykonują mikrooperacje itp., Ale najważniejszą rzeczą do rozważenia jest mianowicie wykonanie poza kolejnością.
Większość ludzi po prostu nie wie o wykonywaniu poza kolejnością, więc piszą swoje programy asemblerowe, jak w przypadku 80286, oczekując, że wykonanie instrukcji zajmie określony czas niezależnie od kontekstu; podczas gdy kompilatory C są świadome wykonywania poza kolejnością i poprawnie generują kod. Dlatego kod takich nieświadomych ludzi jest wolniejszy, ale jeśli się dowiesz, Twój kod będzie szybszy.
źródło
Myślę, że ogólnym przypadkiem, gdy asembler jest szybszy, jest to, że inteligentny programista asemblera patrzy na dane wyjściowe kompilatora i mówi: „jest to kluczowa ścieżka dla wydajności i mogę to napisać, aby być bardziej wydajnym”, a następnie ta osoba poprawia ten asembler lub przepisuje go od zera.
źródło
Wszystko zależy od obciążenia pracą.
W codziennych operacjach C i C ++ są w porządku, ale są pewne obciążenia (wszelkie transformacje obejmujące wideo (kompresja, dekompresja, efekty graficzne itp.)), Które wymagają złożenia, aby były wydajne.
Zazwyczaj wymagają one również stosowania specyficznych dla procesora rozszerzeń mikroukładów (MME / MMX / SSE / cokolwiek), które są dostosowane do tego rodzaju operacji.
źródło
Mam operację transpozycji bitów, która musi zostać wykonana, na 192 lub 256 bitach co przerwanie, co dzieje się co 50 mikrosekund.
Dzieje się tak za pomocą stałej mapy (ograniczenia sprzętowe). Wykonanie C zajęło około 10 mikrosekund. Kiedy przetłumaczyłem to na asembler, biorąc pod uwagę specyficzne cechy tej mapy, specyficzne buforowanie rejestru i użycie operacji zorientowanych na bity; wykonanie zajęło mniej niż 3,5 mikrosekundy.
źródło
Warto zastanowić się nad Optymalizacją niezmienności i czystości autorstwa Waltera Brighta , nie jest to profil profilowany, ale pokazuje jeden dobry przykład różnicy między ASM pisanym odręcznie a kompilatorem. Walter Bright pisze optymalizujące kompilatory, więc warto przyjrzeć się jego innym postom na blogu.
źródło
How to LInux , zadaje to pytanie i podaje zalety i wady korzystania z asemblera.
źródło
Prosta odpowiedź ... Ten, kto dobrze zna asemblację (znany również jako referencja i korzysta z każdej małej pamięci podręcznej procesora i funkcji potoku itp.), Jest w stanie wygenerować znacznie szybszy kod niż jakikolwiek inny kompilator.
Jednak różnica w tych dniach po prostu nie ma znaczenia w typowym zastosowaniu.
źródło
Jedną z możliwości wersji CP / M-86 programu PolyPascal (od siostrzanego do Turbo Pascal) było zastąpienie funkcji „use-bios-to-output-character-to-the-screen” w języku maszynowym, który w istocie podano x i y oraz ciąg znaków, który należy tam umieścić.
To pozwoliło zaktualizować ekran znacznie, znacznie szybciej niż wcześniej!
W pliku binarnym było miejsce na osadzenie kodu maszynowego (kilkaset bajtów) i były tam też inne rzeczy, więc konieczne było ściśnięcie jak najwięcej.
Okazuje się, że ponieważ ekran miał wymiary 80 x 25, obie współrzędne mogły zmieścić się w jednym bajcie, więc oba mogły zmieścić się w dwubajtowym słowie. Pozwoliło to na wykonanie obliczeń potrzebnych w mniejszej liczbie bajtów, ponieważ pojedynczy dodatek może manipulować obiema wartościami jednocześnie.
Według mojej wiedzy nie ma kompilatorów C, które mogłyby łączyć wiele wartości w rejestrze, wykonywać na nich instrukcje SIMD i rozdzielać je później (i nie sądzę, że instrukcje maszyny i tak będą krótsze).
źródło
Jeden z bardziej znanych fragmentów asemblera pochodzi z pętli mapowania tekstur Michaela Abrasha ( szczegółowo opisanej tutaj ):
Obecnie większość kompilatorów wyraża zaawansowane instrukcje specyficzne dla procesora jako elementy wewnętrzne, tj. Funkcje, które są kompilowane do rzeczywistej instrukcji. MS Visual C ++ obsługuje elementy wewnętrzne dla MMX, SSE, SSE2, SSE3 i SSE4, więc musisz mniej martwić się o zejście do montażu, aby skorzystać z instrukcji specyficznych dla platformy. Visual C ++ może również wykorzystać rzeczywistą architekturę, na którą celujesz, z odpowiednim ustawieniem / ARCH.
źródło
Biorąc pod uwagę odpowiedniego programistę, programy Asemblera mogą zawsze być tworzone szybciej niż ich odpowiedniki C (przynajmniej marginalnie). Trudno byłoby stworzyć program w języku C, w którym nie można było pobrać co najmniej jednej instrukcji asemblera.
źródło
http://cr.yp.to/qhasm.html ma wiele przykładów.
źródło
gcc stał się szeroko stosowanym kompilatorem. Ogólnie jego optymalizacje nie są tak dobre. Znacznie lepiej niż przeciętny programista zajmujący się pisaniem asemblerów, ale dla prawdziwej wydajności, nie tak dobrze. Istnieją kompilatory, które są po prostu niesamowite w kodzie, który produkują. Tak więc ogólną odpowiedzią będzie wiele miejsc, w których można wejść do wyjścia kompilatora i dostosować asembler pod kątem wydajności i / lub po prostu ponownie napisać procedurę od zera.
źródło
Longpoke, jest tylko jedno ograniczenie: czas. Jeśli nie masz zasobów, aby zoptymalizować każdą zmianę kodu i poświęcić czas na przydzielanie rejestrów, zoptymalizować kilka wycieków, a co nie, kompilator wygrywa za każdym razem. Dokonujesz modyfikacji kodu, rekompilujesz i mierzysz. Powtórzyć w razie potrzeby.
Możesz także wiele zrobić na wysokim poziomie. Również sprawdzenie wynikowego zestawu może dać WRAŻENIE, że kod jest badziewny, ale w praktyce będzie działał szybciej niż myślisz, że byłby szybszy. Przykład:
int y = dane [i]; // zrób kilka rzeczy tutaj .. call_function (y, ...);
Kompilator odczyta dane, wypchnie je na stos (rozleje), a następnie odczyta ze stosu i przekaże jako argument. Brzmi gówno? W rzeczywistości może to być bardzo skuteczna kompensacja opóźnień i skutkować szybszym uruchomieniem.
// zoptymalizowana wersja funkcja call_funkcja (dane [i], ...); // mimo wszystko nie tak zoptymalizowany ..
Ideą zoptymalizowanej wersji było zmniejszenie presji rejestru i uniknięcie rozlania. Ale tak naprawdę wersja „gówniana” była szybsza!
Patrząc na kod asemblera, po prostu patrząc na instrukcje i wyciągając wniosek: więcej instrukcji, wolniej, byłoby błędnym osądem.
Należy zwrócić uwagę: wielu ekspertów montażowych uważa , że dużo wie, ale bardzo mało. Reguły również zmieniają się z architektury na następną. Na przykład nie ma kodu x86 w srebrnej kuli, który zawsze jest najszybszy. W te dni lepiej stosować reguły praktyczne:
Zaufanie do kompilatora w magiczny sposób przekształcającego źle przemyślany kod C / C ++ w „teoretycznie optymalny” kod jest również życzeniem. Musisz znać kompilator i łańcuch narzędzi, których używasz, jeśli zależy ci na „wydajności” na tym niskim poziomie.
Kompilatory w C / C ++ na ogół nie są zbyt dobre w ponownym zamawianiu podwyrażeń, ponieważ funkcje mają efekty uboczne, na początek. Języki funkcjonalne nie cierpią z powodu tego zastrzeżenia, ale nie pasują tak dobrze do obecnego ekosystemu. Istnieją opcje kompilatora pozwalające na swobodne reguły precyzji, które pozwalają na zmianę kolejności operacji przez kompilator / linker / generator kodu.
Ten temat jest trochę ślepy zaułek; dla większości nie ma to znaczenia, a reszta i tak wie, co robi.
Wszystko sprowadza się do tego: „aby zrozumieć, co robisz”, to trochę różni się od wiedzy o tym, co robisz.
źródło