Próbowałem porównać wydajność wbudowanego języka asemblerowego i kodu C ++, więc napisałem funkcję, która dodaje dwie tablice o wielkości 2000 na 100000 razy. Oto kod:
#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
for(int i = 0; i < TIMES; i++)
{
for(int j = 0; j < length; j++)
x[j] += y[j];
}
}
void calcuAsm(int *x,int *y,int lengthOfArray)
{
__asm
{
mov edi,TIMES
start:
mov esi,0
mov ecx,lengthOfArray
label:
mov edx,x
push edx
mov eax,DWORD PTR [edx + esi*4]
mov edx,y
mov ebx,DWORD PTR [edx + esi*4]
add eax,ebx
pop edx
mov [edx + esi*4],eax
inc esi
loop label
dec edi
cmp edi,0
jnz start
};
}
Oto main()
:
int main() {
bool errorOccured = false;
setbuf(stdout,NULL);
int *xC,*xAsm,*yC,*yAsm;
xC = new int[2000];
xAsm = new int[2000];
yC = new int[2000];
yAsm = new int[2000];
for(int i = 0; i < 2000; i++)
{
xC[i] = 0;
xAsm[i] = 0;
yC[i] = i;
yAsm[i] = i;
}
time_t start = clock();
calcuC(xC,yC,2000);
// calcuAsm(xAsm,yAsm,2000);
// for(int i = 0; i < 2000; i++)
// {
// if(xC[i] != xAsm[i])
// {
// cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
// errorOccured = true;
// break;
// }
// }
// if(errorOccured)
// cout<<"Error occurs!"<<endl;
// else
// cout<<"Works fine!"<<endl;
time_t end = clock();
// cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";
cout<<"time = "<<end - start<<endl;
return 0;
}
Następnie uruchamiam program pięć razy, aby uzyskać cykle procesora, które można uznać za czas. Za każdym razem wywołuję tylko jedną z wyżej wymienionych funkcji.
I oto rezultat.
Funkcja wersji montażu:
Debug Release
---------------
732 668
733 680
659 672
667 675
684 694
Average: 677
Funkcja wersji C ++:
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
Kod C ++ w trybie wydania jest prawie 3,7 razy szybszy niż kod asemblera. Czemu?
Wydaje mi się, że kod asemblera, który napisałem, nie jest tak skuteczny, jak kod wygenerowany przez GCC. Dla zwykłego programisty takiego jak ja trudno jest napisać kod szybciej niż jego przeciwnik generowany przez kompilator. Czy to oznacza, że nie powinienem ufać wydajności języka asemblera napisanego własnymi rękami, skupić się na C ++ i zapomnieć o języku asemblera?
źródło
Odpowiedzi:
Tak, większość razy.
Przede wszystkim zaczynasz od błędnego założenia, że język niskiego poziomu (w tym przypadku asembler) zawsze będzie generował szybszy kod niż język wysokiego poziomu (w tym przypadku C ++ i C). To nie prawda. Czy kod C jest zawsze szybszy niż kod Java? Nie, ponieważ istnieje inna zmienna: programista. Sposób pisania kodu i znajomość szczegółów architektury mają duży wpływ na wydajność (tak jak w tym przypadku).
Można zawsze produkować przykład, w którym ręcznie kod montaż jest lepszy niż kod skompilowany, ale zazwyczaj jest to fikcyjny przykład lub pojedynczy rutyna nie jest prawdziwy program 500.000 linii kodu C ++). Myślę, że kompilatory wygenerują lepszy kod asemblera 95% razy, a czasami, tylko w niektórych rzadkich przypadkach, może być konieczne napisanie kodu asemblera dla kilku, krótkich, bardzo używanych , krytycznych pod względem wydajności procedur lub gdy będziesz musiał uzyskać dostęp do funkcji swojego ulubionego języka wysokiego poziomu nie ujawnia. Czy chcesz dotknąć tej złożoności? Przeczytaj tę niesamowitą odpowiedź tutaj na SO.
Dlaczego to
Przede wszystkim dlatego, że kompilatory mogą przeprowadzać optymalizacje, których nawet nie jesteśmy w stanie sobie wyobrazić (zobacz tę krótką listę ), i zrobią to w ciągu kilku sekund (kiedy możemy potrzebować dni ).
Kiedy kodujesz w asemblerze, musisz wykonywać dobrze zdefiniowane funkcje z dobrze zdefiniowanym interfejsem wywołania. Mogą jednak brać pod uwagę optymalizację całego programu i optymalizację między procedurami, takie jak przydział rejestrów , stała propagacja , eliminacja wspólnego podwyrażenia , planowanie instrukcji i inne złożone, nieoczywiste optymalizacje ( na przykład model Polytope ). W architekturze RISC faceci przestali się tym martwić wiele lat temu (na przykład planowanie instrukcji jest bardzo trudne do dostrojenia ręcznie ), a nowoczesne procesory CISC mają bardzo długie potoki też.
W przypadku niektórych złożonych mikrokontrolerów nawet biblioteki systemowe są zapisywane w C zamiast w asemblerze, ponieważ ich kompilatory wytwarzają lepszy (i łatwy w utrzymaniu) kod końcowy.
Kompilatory czasami mogą automatycznie korzystać z niektórych instrukcji MMX / SIMDx , a jeśli ich nie użyjesz, po prostu nie możesz ich porównać (inne odpowiedzi bardzo dobrze sprawdzały kod asemblera). Tylko dla pętli jest to krótka lista optymalizacji pętli tego, co jest zwykle sprawdzane przez kompilator (czy myślisz, że możesz to zrobić sam, kiedy zostanie ustalony harmonogram dla programu w języku C #?) Jeśli napiszesz coś w asemblerze, ja myślę, że musisz rozważyć przynajmniej kilka prostych optymalizacji . Przykładem szkolnych tablic jest rozwinięcie cyklu (jego rozmiar jest znany w czasie kompilacji). Zrób to i ponownie uruchom test.
W dzisiejszych czasach naprawdę rzadko trzeba używać języka asemblera z innego powodu: mnogości różnych procesorów . Czy chcesz je wszystkie wspierać? Każda z nich ma określoną mikroarchitekturę i niektóre określone zestawy instrukcji . Mają różną liczbę jednostek funkcjonalnych i instrukcje dotyczące montażu powinny być ustawione tak, aby były zajęte . Jeśli piszesz w C, możesz użyć PGO, ale podczas montażu będziesz potrzebować dużej wiedzy na temat tej konkretnej architektury ( i ponownie przemyśleć i powtórzyć wszystko dla innej architektury ). W przypadku małych zadań kompilator zwykle robi to lepiej, a w przypadku złożonych zadań zwykle praca nie jest zwracana (ikompilator i tak może działać lepiej ).
Jeśli usiądziesz i spojrzysz na kod, prawdopodobnie zobaczysz, że zyskasz więcej na przeprojektowaniu algorytmu niż na tłumaczeniu na asemblerze (przeczytaj ten świetny post tutaj na SO ), istnieją optymalizacje na wysokim poziomie (i wskazówki do kompilatora), możesz skutecznie zastosować, zanim będziesz musiał skorzystać z języka asemblera. Prawdopodobnie warto wspomnieć, że często stosując wewnętrzne funkcje, uzyskasz wzrost wydajności, którego szukasz, a kompilator nadal będzie w stanie przeprowadzić większość swoich optymalizacji.
Wszystko to powiedziawszy, nawet jeśli możesz stworzyć kod montażu 5 ~ 10 razy szybszy, powinieneś zapytać swoich klientów, czy wolą zapłacić tydzień czasu lub kupić procesor szybszy o 50 $ . Ekstremalna optymalizacja częściej (a zwłaszcza w aplikacjach LOB) po prostu nie jest wymagana od większości z nas.
źródło
Twój kod zestawu jest nieoptymalny i może zostać ulepszony:
loop
instrukcji, która jest znana jako powolna na większości współczesnych procesorów (być może w wyniku użycia starożytnej książki montażowej *)Tak więc, chyba że znacznie poprawisz swoje umiejętności dotyczące asemblera, nie ma sensu pisać kodu asemblera dla wydajności.
* Oczywiście, że nie wiem, czy naprawdę otrzymałeś
loop
instrukcję ze starożytnej księgi zgromadzeń. Ale prawie nigdy nie widzisz go w kodzie świata rzeczywistego, ponieważ każdy dostępny kompilator jest wystarczająco inteligentny, aby go nie emitowaćloop
, widzisz go tylko w złych i nieaktualnych książkach IMHO.źródło
loop
(i wiele „przestarzałych” instrukcji), jeśli zoptymalizujesz rozmiarNawet przed zagłębieniem się w asemblerze istnieją transformacje kodu, które istnieją na wyższym poziomie.
można przekształcić w obrót pętli :
co jest znacznie lepsze, jeśli chodzi o lokalizację pamięci.
Można to dalej optymalizować, wykonywanie
a += b
X razy jest równoznaczne z robieniem,a += X * b
więc otrzymujemy:wydaje się jednak, że mój ulubiony optymalizator (LLVM) nie wykonuje tej transformacji.
[edytuj] Odkryłem, że transformacja jest wykonywana, jeśli mamy
restrict
kwalifikator dox
iy
. Rzeczywiście bez tego ograniczeniax[j]
iy[j]
może być alias do tej samej lokalizacji, co powoduje, że ta transformacja jest błędna. [koniec edycji]W każdym razie, to jest, jak sądzę, zoptymalizowaną wersję C. Już jest o wiele prostsze. W oparciu o to, oto mój crack w ASM (pozwalam Clangowi go wygenerować, jestem w tym bezużyteczny):
Obawiam się, że nie rozumiem, skąd się biorą te instrukcje, jednak zawsze możesz się dobrze bawić i spróbować zobaczyć, jak się to porównuje ... ale nadal używałbym zoptymalizowanej wersji C, a nie montażu, w kodzie, dużo bardziej przenośny.
źródło
x
iy
. Oznacza to, że kompilator nie może być pewny, że dla wszystkichi,j
w[0, length)
mamyx + i != y + j
. Jeśli zachodzi na siebie, optymalizacja jest niemożliwa. Język C wprowadziłrestrict
słowo kluczowe, aby poinformować kompilator, że dwa wskaźniki nie mogą aliasu, jednak nie działa dla tablic, ponieważ mogą się one nakładać, nawet jeśli nie są dokładnie aliasem.__restrict
). SSE2 jest linią bazową dla x86-64, a przy tasowaniu SSE2 może wykonywać 2x 32-bitowe zwielokrotnienia na raz (wytwarzając produkty 64-bitowe, stąd tasowanie, aby ponownie zebrać wyniki). godbolt.org/z/r7F_uo . (SSE4.1 jest potrzebne dlapmulld
: spakowanych 32x32 => 32-bitowe pomnożenie). GCC ma fajną sztuczkę polegającą na zamianie stałych mnożników całkowitych na shift / add (i / lub odejmowanie), co jest dobre dla mnożników z kilkoma ustawionymi bitami. Kod Clanga, który jest tasujący, będzie miał wąskie gardło w zakresie tasowania przepustowości procesorów Intel.Krótka odpowiedź: tak.
Długa odpowiedź: tak, chyba że naprawdę wiesz, co robisz i masz ku temu powód.
źródło
Naprawiłem mój kod asm:
Wyniki dla wersji Release:
Kod zestawu w trybie wydania jest prawie 2 razy szybszy niż C ++.
źródło
xmm0
zamiast nazwy rejestrumm0
), otrzymasz kolejne przyspieszenie dwa razy ;-)paddd xmm
(po sprawdzeniu nakładania się międzyx
iy
, ponieważ nie używałeśint *__restrict x
). Na przykład robi to gcc: godbolt.org/z/c2JG0- . Lub po wprowadzeniu domain
, nie powinno być konieczne sprawdzanie nakładania się, ponieważ może zobaczyć alokację i udowodnić, że się nie pokrywają. (I zakładałoby to 16-bajtowe wyrównanie również w niektórych implementacjach x86-64, co nie ma miejsca w przypadku definicji autonomicznej). A jeśli się skompilujeszgcc -O3 -march=native
, możesz uzyskać 256-bit lub 512-bit wektoryzacja.Tak, dokładnie to oznacza i dotyczy to każdego języka. Jeśli nie wiesz, jak napisać efektywny kod w języku X, nie powinieneś ufać swojej umiejętności pisania wydajnego kodu w X. A zatem, jeśli chcesz wydajnego kodu, powinieneś użyć innego języka.
Zgromadzenie jest na to szczególnie wrażliwe, ponieważ cóż, to, co widzisz, dostajesz. Pisz szczegółowe instrukcje, które procesor ma wykonać. W przypadku języków wysokiego poziomu w betweeen znajduje się kompilator, który może przekształcić kod i usunąć wiele nieefektywności. Dzięki montażowi jesteś sam.
źródło
Jedynym powodem używania obecnie języka asemblera jest użycie niektórych funkcji niedostępnych dla tego języka.
Dotyczy to:
Ale obecne kompilatory są dość sprytne, mogą nawet zastąpić dwie oddzielne instrukcje, takie jak
d = a / b; r = a % b;
pojedyncza instrukcja, która oblicza podział i resztę za jednym razem, jeśli jest dostępna, nawet jeśli C nie ma takiego operatora.źródło
To prawda, że nowoczesny kompilator wykonuje niesamowitą pracę w zakresie optymalizacji kodu, ale nadal zachęcam do dalszego uczenia się asemblera.
Po pierwsze, wyraźnie Cię to nie przeraża , to świetny, świetny plus, dalej - jesteś na dobrej drodze, profilując się, aby zweryfikować lub odrzucić założenia dotyczące prędkości , prosisz o wkład doświadczonych ludzi , a ty mieć największe narzędzie optymalizujące znane ludzkości: mózg .
Wraz ze wzrostem doświadczenia dowiesz się, kiedy i gdzie go używać (zwykle najściślejsze, najbardziej wewnętrzne pętle w kodzie, po głębokiej optymalizacji na poziomie algorytmu).
Aby uzyskać inspirację, polecam przejrzenie artykułów Michaela Abrasha (jeśli nie otrzymałeś od niego wiadomości, jest guru optymalizacji; nawet współpracował z Johnem Carmackiem przy optymalizacji renderera oprogramowania Quake!)
źródło
Zmieniłem kod asm:
Wyniki dla wersji Release:
Kod zestawu w trybie wydania jest prawie 4 razy szybszy niż C ++. IMHo, szybkość kodu asemblera zależy od Programmera
źródło
shr ecx,2
Jest zbyteczny, ponieważ długość tablicy jest już podawana,int
a nie bajtowa. Zasadniczo osiągasz tę samą prędkość. Możesz wypróbować odpowiedźpaddd
od Haroldów, będzie to naprawdę szybsze.to bardzo interesujący temat!
Zmieniłem MMX przez SSE w kodzie Sashy
Oto moje wyniki:
Kod asemblera z SSE jest 5 razy szybszy niż C ++
źródło
Większość kompilatorów języków wysokiego poziomu jest bardzo zoptymalizowana i wie, co robi. Możesz spróbować zrzucić kod dezasemblujący i porównać go z rodzimym zestawem. Wierzę, że zobaczysz kilka fajnych sztuczek, których używa twój kompilator.
Na przykład, nawet jeśli nie jestem pewien, czy to prawda :)
Robić:
kosztuje więcej cykli niż
który robi to samo.
Kompilator zna wszystkie te sztuczki i używa ich.
źródło
Kompilator cię pokonał. Spróbuję, ale nie dam żadnych gwarancji. Będę zakładać, że „mnożenie” za czasów jest to, by to bardziej odpowiednie testy wydajności, które
y
ix
są 16-wyrównane, i żelength
jest niezerowe wielokrotnością 4. To chyba wszystko prawda i tak.Jak powiedziałem, nie udzielam żadnych gwarancji. Ale będę zaskoczony, jeśli można to zrobić znacznie szybciej - wąskim gardłem jest tutaj przepustowość pamięci, nawet jeśli wszystko jest hitem L1.
źródło
mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax
a następnie po prostu użyjesz [esi + ecx] wszędzie, gdzie unikniesz 1 przeciągnięcia cyklu na instrukcję, co przyspieszy partie pętli. (Jeśli masz najnowszą wersję Skylake, nie dotyczy to). Add reg, reg powoduje, że pętla jest mocniejsza, co może, ale nie musi pomóc.Ślepo realizacji dokładny samego algorytmu, dyspozycję instrukcji, w montaż jest gwarantowany być wolniejsze niż to, co kompilator może zrobić.
Dzieje się tak, ponieważ nawet najmniejsza optymalizacja, jaką wykonuje kompilator, jest lepsza niż sztywny kod bez żadnej optymalizacji.
Oczywiście możliwe jest pokonanie kompilatora, zwłaszcza jeśli jest to niewielka, zlokalizowana część kodu, musiałem to zrobić sam, aby uzyskać ok. 4x przyspieszenia, ale w tym przypadku musimy mocno polegać na dobrej znajomości sprzętu i licznych pozornie intuicyjnych sztuczkach.
źródło
Jako kompilator zamieniłbym pętlę o stałym rozmiarze na wiele zadań wykonawczych.
będzie produkować
i ostatecznie będzie wiedział, że „a = a + 0;” jest bezużyteczny, więc usunie tę linię. Mam nadzieję, że coś w twojej głowie chce teraz dołączyć pewne opcje optymalizacji jako komentarz. Wszystkie te bardzo skuteczne optymalizacje przyspieszą skompilowany język.
źródło
a
jest niestabilna, istnieje duża szansa, że kompilator zrobi toint a = 13;
od samego początku.Dokładnie to znaczy. Pozostaw mikrooptymalizacje kompilatorowi.
źródło
Podoba mi się ten przykład, ponieważ pokazuje ważną lekcję na temat kodu niskiego poziomu. Tak, można napisać, że montaż jest tak szybki jak kod C. Jest to tautologicznie prawdziwe, ale niekoniecznie nic nie znaczy . Najwyraźniej ktoś może, w przeciwnym razie asembler nie poznałby odpowiednich optymalizacji.
Podobnie obowiązuje ta sama zasada, gdy wchodzisz w górę hierarchii abstrakcji języka. Tak, można napisać parser w C, który jest tak szybko, jak szybki i brzydka skrypt Perl, a wiele osób. Ale to nie znaczy, że ponieważ użyłeś C, twój kod będzie szybki. W wielu przypadkach języki wyższego poziomu wykonują optymalizacje, których być może nawet nie wziąłeś pod uwagę.
źródło
W wielu przypadkach optymalny sposób wykonania jakiegoś zadania może zależeć od kontekstu, w którym zadanie jest wykonywane. Jeśli procedura jest napisana w języku asemblera, generalnie nie będzie możliwe zmienianie sekwencji instrukcji w zależności od kontekstu. Jako prosty przykład rozważ następującą prostą metodę:
Kompilator dla 32-bitowego kodu ARM, biorąc pod uwagę powyższe, prawdopodobnie renderowałby go jako:
a może
Można to nieco zoptymalizować w ręcznie składanym kodzie, ponieważ:
lub
Oba ręcznie zmontowane podejścia wymagałyby 12 bajtów przestrzeni kodu zamiast 16; ten ostatni zastąpiłby „obciążenie” „dodaniem”, co w przypadku ARM7-TDMI wykona dwa cykle szybciej. Gdyby kod miał być wykonywany w kontekście, w którym r0 nie wiedział / nie przejmował się, wersje językowe asemblera byłyby nieco lepsze niż wersja skompilowana. Z drugiej strony załóżmy, że kompilator wiedział, że jakiś rejestr [np. R5] będzie przechowywał wartość mieszczącą się w granicach 2047 bajtów od pożądanego adresu 0x40001204 [np. 0x40001000], a ponadto wiedział, że idzie inny rejestr [np. R7] do przechowywania wartości, której niskie bity to 0xFF. W takim przypadku kompilator może zoptymalizować wersję C kodu, aby po prostu:
Znacznie krótszy i szybszy niż nawet ręcznie zoptymalizowany kod zestawu. Ponadto załóżmy, że set_port_high wystąpił w kontekście:
W ogóle nie jest to niemożliwe przy kodowaniu systemu wbudowanego. Jeśli
set_port_high
jest zapisany w kodzie asemblera, kompilator musiałby przenieść r0 (który przechowuje wartość zwracanąfunction1
) gdzie indziej przed wywołaniem kodu asemblera, a następnie przenieść tę wartość z powrotem do r0 (ponieważfunction2
spodziewa się swojego pierwszego parametru w r0), więc „zoptymalizowany” kod zestawu wymagałby pięciu instrukcji. Nawet jeśli kompilator nie wiedział o żadnym rejestrze zawierającym adres lub wartość do przechowywania, jego czteroinstrukcyjna wersja (którą mógłby przystosować do korzystania z dowolnych dostępnych rejestrów - niekoniecznie r0 i r1) pobiłaby „zoptymalizowany” zestaw wersja językowa. Gdyby kompilator miał niezbędny adres i dane w r5 i r7, jak opisano wcześniej,function1
nie zmieniłby tych rejestrów, a zatem mógłby zastąpićset_port_high
z pojedyncząstrb
instrukcją - cztery instrukcje mniejsze i szybsze niż kod asemblera „zoptymalizowany ręcznie”.Zauważ, że ręcznie zoptymalizowany kod asemblera często przewyższa kompilator w przypadkach, gdy programiści znają dokładny przebieg programu, ale kompilatory świecą w przypadkach, gdy kawałek kodu jest napisany przed poznaniem jego kontekstu lub gdy jeden fragment kodu źródłowego może być wywoływany z wielu kontekstów [jeśli
set_port_high
jest używany w pięćdziesięciu różnych miejscach kodu, kompilator może niezależnie dla każdego z nich zdecydować, jak najlepiej go rozwinąć].Zasadniczo sugerowałbym, że język asemblera jest w stanie zapewnić największą poprawę wydajności w tych przypadkach, w których do każdego fragmentu kodu można podejść z bardzo ograniczonej liczby kontekstów, i może być szkodliwy dla wydajności w miejscach, w których fragment do kodu można podchodzić z wielu różnych kontekstów. Co ciekawe (i dogodnie) przypadki, w których montaż jest najbardziej korzystny dla wydajności, to często przypadki, w których kod jest najbardziej prosty i łatwy do odczytania. Miejsca, w których kod języka asemblerowego zamieniłby się w lepki bałagan, to często te, w których pisanie w asemblerze zapewniałoby najmniejszą korzyść w zakresie wydajności.
[Drobna uwaga: jest kilka miejsc, w których można użyć kodu asemblera, aby wywołać hiperoptymalizowany lepki bałagan; na przykład jeden kawałek kodu, który zrobiłem dla ARM, potrzebował pobrać słowo z pamięci RAM i wykonać jedną z około dwunastu procedur na podstawie sześciu górnych bitów wartości (wiele wartości odwzorowanych na tę samą procedurę). Myślę, że zoptymalizowałem ten kod do czegoś takiego:
Rejestr r8 zawsze zawierał adres głównej tablicy wysyłkowej (w pętli, w której kod spędza 98% swojego czasu, nic nigdy nie wykorzystywało go do żadnych innych celów); wszystkie 64 wpisy odnosiły się do adresów w 256 bajtach poprzedzających. Ponieważ pętla pierwotna miała w większości przypadków sztywny limit czasu wykonania wynoszący około 60 cykli, pobieranie i wysyłanie w dziewięciu cyklach było bardzo istotne dla osiągnięcia tego celu. Użycie tabeli 256 32-bitowych adresów byłoby o jeden cykl szybsze, ale pochłonęłoby 1 KB bardzo cennej pamięci RAM [flash dodałby więcej niż jeden stan oczekiwania]. Użycie 64 32-bitowych adresów wymagałoby dodania instrukcji maskowania niektórych bitów z pobranego słowa i nadal pochłonąłoby 192 bajty więcej niż tabela, której faktycznie użyłem. Korzystanie z tabeli 8-bitowych przesunięć dało bardzo kompaktowy i szybki kod, ale nie jest to coś, czego oczekiwałbym od kompilatora; Nie spodziewałbym się również, że kompilator poświęci rejestrowi „pełny czas” na przechowywanie adresu tabeli.
Powyższy kod został zaprojektowany do działania jako samodzielny system; może okresowo wywoływać kod C, ale tylko w pewnych momentach, gdy sprzęt, z którym się komunikuje, może być bezpiecznie wprowadzony w stan „bezczynności” na dwa mniej więcej co milisekundowe interwały co 16 ms.
źródło
W ostatnim czasie wszystkie optymalizacje prędkości, które przeprowadziłem, zastępowały wolny kod uszkodzonego mózgu tylko rozsądnym kodem. Ale ponieważ szybkość była naprawdę krytyczna i włożyłem duży wysiłek w szybkie zrobienie czegoś, w rezultacie zawsze był to proces iteracyjny, w którym każda iteracja dawała więcej wglądu w problem, znajdując sposoby rozwiązania problemu za pomocą mniejszej liczby operacji. Ostateczna prędkość zawsze zależała od tego, ile wglądu w problem. Jeśli na jakimkolwiek etapie użyłem kodu asemblera lub kodu C, który został nadmiernie zoptymalizowany, ucierpiałby proces znalezienia lepszego rozwiązania, a wynik końcowy byłby wolniejszy.
źródło
Kiedy koduję w ASM, reorganizuję instrukcje ręcznie, aby procesor mógł wykonywać więcej z nich równolegle, o ile jest to logicznie możliwe. Ledwo używam pamięci RAM, gdy koduję w ASM, na przykład: w ASM może być ponad 20000 linii kodu i nigdy nie użyłem push / pop.
Możesz potencjalnie przeskoczyć w środku kodu operacji, aby samodzielnie zmodyfikować kod i zachowanie bez możliwej kary za samododyfikację kodu. Dostęp do rejestrów zajmuje 1 tik (czasem zajmuje .25 tików) procesora. Dostęp do pamięci RAM może zająć setki.
W mojej ostatniej przygodzie ASM nigdy nie użyłem pamięci RAM do przechowywania zmiennej (dla tysięcy linii ASM). ASM może być potencjalnie niewyobrażalnie szybszy niż C ++. Ale zależy to od wielu zmiennych czynników, takich jak:
Uczę się teraz C # i C ++, ponieważ zdałem sobie sprawę, że produktywność ma znaczenie !! W wolnym czasie możesz spróbować robić najszybsze możliwe programy, używając czystego ASM. Ale aby coś stworzyć, użyj jakiegoś wysokiego poziomu języka.
Na przykład ostatni program, który kodowałem, używał JS i GLSL i nigdy nie zauważyłem żadnego problemu z wydajnością, nawet mówiąc o JS, który jest powolny. Wynika to z faktu, że sama koncepcja programowania GPU dla 3D sprawia, że szybkość języka, który wysyła polecenia do GPU, jest prawie nieistotna.
Szybkość samego asemblera na gołym metalu jest niezaprzeczalna. Czy może być jeszcze wolniej w C ++? - Być może dlatego, że piszesz kod asemblera za pomocą kompilatora, który nie używa asemblera na początek.
Moją osobistą radą jest, aby nigdy nie pisać kodu asemblera, jeśli możesz go uniknąć, mimo że uwielbiam asembler.
źródło
Wszystkie odpowiedzi tutaj wydają się wykluczać jeden aspekt: czasami nie piszemy kodu, aby osiągnąć konkretny cel, ale dla samej zabawy . Zainwestowanie czasu w to może być nieopłacalne, ale prawdopodobnie nie ma większej satysfakcji niż pokonanie najszybszego fragmentu kodu zoptymalizowanego pod kątem kompilatora za pomocą ręcznie walcowanej alternatywy asm.
źródło
Kompilator c ++, po optymalizacji na poziomie organizacyjnym, wytworzyłby kod, który wykorzystywałby wbudowane funkcje docelowej jednostki centralnej. HLL nigdy nie prześcignie ani nie prześcignie asemblera z kilku powodów; 1.) HLL zostanie skompilowany i wyprowadzony z kodem Accessora, sprawdzeniem granic i ewentualnie wbudowanym wyrzucaniem elementów bezużytecznych (wcześniej adresując zakres w manieryzmie OOP), wszystkie wymagające cykli (przerzutów i klap). HLL wykonuje obecnie doskonałą robotę (w tym nowsze C ++ i inne, takie jak GO), ale jeśli przewyższają one asembler (a mianowicie twój kod), musisz skonsultować dokumentację procesora - porównania z niechlujnym kodem są z pewnością niejednoznaczne, a skompilowane języki, takie jak asembler, wszystkie rozwiązują aż do kodu operacyjnego HLL streszcza szczegóły i nie eliminuje ich, w przeciwnym razie aplikacja nie uruchomi się, nawet jeśli zostanie rozpoznana przez system operacyjny hosta.
Większość kodu asemblera (przede wszystkim obiektów) jest wyprowadzana jako „bezgłowa” w celu włączenia do innych formatów wykonywalnych, wymagając znacznie mniej przetwarzania, dlatego będzie znacznie szybszy, ale o wiele bardziej niezabezpieczony; jeśli asembler generuje plik wykonywalny (NAsm, YAsm; itp.), to będzie on nadal działał szybciej, aż do pełnego dopasowania kodu HLL pod względem funkcjonalności, wówczas wyniki mogą być dokładnie zważone.
Wywołanie obiektu kodu opartego na asemblerze z HLL w dowolnym formacie z natury spowoduje dodatkowy narzut przetwarzania, a także wywołania przestrzeni pamięci przy użyciu globalnie przydzielonej pamięci dla zmiennych / stałych typów danych (dotyczy to zarówno LLL, jak i HLL). Pamiętaj, że ostatecznym wyjściem jest użycie procesora jako interfejsu API i abi względem sprzętu (opcode), a oba asemblery i „kompilatory HLL” są zasadniczo / zasadniczo identyczne, a jedynym prawdziwym wyjątkiem jest czytelność (gramatyka).
Witaj, światowa aplikacja konsolowa w asemblerze używającym FAsm ma 1,5 KB (aw Windowsie jest jeszcze mniejsza w FreeBSD i Linux) i przewyższa wszystko, co GCC może wyrzucić w najlepszym dniu; Powodem jest niejawne wypełnienie zerami, sprawdzanie poprawności dostępu i sprawdzanie granic, aby wymienić tylko kilka. Prawdziwym celem są czyste biblioteki HLL i optymalizowany kompilator, który celuje w procesor w „hardkorowy” sposób i większość robi to obecnie (w końcu). GCC nie jest lepsze niż YAsm - chodzi o praktyki kodowania i zrozumienie dewelopera, o których mowa, a „optymalizacja” następuje po eksploracji nowicjuszy oraz przejściowym szkoleniu i doświadczeniu.
Kompilatory muszą łączyć i składać dane wyjściowe w tym samym kodzie operacyjnym co asembler, ponieważ te kody to wszystko, co CPU będzie wyjątkiem (CISC lub RISC [PIC też]). YAsm zoptymalizował i wyczyścił wiele na wczesnym NAsm, ostatecznie przyspieszając wszystkie dane wyjściowe z tego asemblera, ale nawet wtedy YAsm, podobnie jak NAsm, tworzy pliki wykonywalne z zewnętrznymi zależnościami atakującymi biblioteki systemu operacyjnego w imieniu programisty, więc przebieg może się różnić. Na zakończenie C ++ jest w punkcie, który jest niesamowity i znacznie bezpieczniejszy niż asembler dla ponad 80 procent, szczególnie w sektorze komercyjnym ...
źródło
ld
, ale nie robi to różnicy, chyba że próbujesz naprawdę zoptymalizować rozmiar pliku (nie tylko rozmiar segment tekstowy). Zobacz poradnik Whirlwind na temat tworzenia plików wykonywalnych ELF dla systemu Linux .std::vector
skompilowany w trybie debugowania. Macierze C ++ nie są takie. Kompilatory mogą sprawdzać rzeczy w czasie kompilacji, ale jeśli nie włączysz dodatkowych opcji hartowania, nie będzie sprawdzania czasu wykonywania. Zobacz na przykład funkcję, która inkrementuje pierwsze 1024 elementyint array[]
arg. Dane wyjściowe asm nie są sprawdzane w czasie wykonywania: godbolt.org/g/w1HF5t . Wszystko, co dostaje, to wskaźnik wrdi
, brak informacji o rozmiarze. Programiści muszą unikać niezdefiniowanego zachowania, nigdy nie wywołując go tablicą mniejszą niż 1024.new
, usuń ręczniedelete
, bez sprawdzania granic). Państwo może używać C ++ produkować gówniany nadęty asm / kodu maszynowego (jak większość oprogramowania), ale to wina programisty, a nie C ++ 's. Możesz nawet użyćalloca
do przydzielenia miejsca na stosie jako tablicy.g++ -O3
generowania granice sprawdzania kodu dla zwykłej tablicy, lub robi cokolwiek innego, co mówisz. C ++ sprawia, że znacznie łatwiej jest wygenerować nadęty plików binarnych (i faktycznie trzeba uważać, nie do Jeśli dążysz do wykonania), ale to nie jest dosłownie nieuniknione. Jeśli rozumiesz, w jaki sposób C ++ kompiluje się w asm, możesz uzyskać kod, który jest tylko nieco gorszy, niż możesz napisać ręcznie, ale z wbudowanym i ciągłym propagowaniem na większą skalę, niż możesz ręcznie zarządzać.Montaż może być szybszy, jeśli kompilator generuje dużo kodu obsługi OO .
Edytować:
Do downvoters: OP napisał: „czy powinienem ... skupić się na C ++ i zapomnieć o asemblerze?” i podtrzymuję moją odpowiedź. Zawsze musisz mieć oko na kod generowany przez OO, szczególnie podczas korzystania z metod. Nie zapominanie o języku asemblera oznacza, że będziesz okresowo sprawdzać zestaw, który generuje Twój kod OO, co moim zdaniem jest niezbędne do pisania dobrze działającego oprogramowania.
W rzeczywistości dotyczy to całego kompilowalnego kodu, nie tylko OO.
źródło