Czy wbudowany język asemblera jest wolniejszy niż natywny kod C ++?

183

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?

użytkownik957121
źródło
29
Dosyć. Zestaw kodowany ręcznie jest odpowiedni w niektórych okolicznościach, ale należy zadbać o to, aby wersja zestawu była rzeczywiście szybsza niż to, co można osiągnąć przy użyciu języka wyższego poziomu.
Magnus Hoff,
161
Może się okazać, że warto przestudiować kod wygenerowany przez kompilator i spróbować zrozumieć, dlaczego jest on szybszy niż wersja zestawu.
Paul R
34
Tak, wygląda na to, że kompilator lepiej pisze asm niż ty. Nowoczesne kompilatory naprawdę są całkiem dobre.
David Heffernan,
20
Czy obejrzałeś zespół wyprodukowany przez GCC? Możliwe, że GCC użyło instrukcji MMX. Twoja funkcja jest bardzo równoległa - potencjalnie możesz użyć N procesorów do obliczenia sumy w 1 / N czasie. Wypróbuj funkcję, w której nie ma nadziei na równoległość.
Chris
11
Hm, spodziewałbym się, że dobry kompilator to zrobi ~ 100000 razy szybciej ...
PlasmaHH

Odpowiedzi:

261

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.

Adriano Repetti
źródło
9
Oczywiście nie. Myślę, że lepiej w 95% przypadków w 99% przypadków. Czasami dlatego, że jest to po prostu kosztowne (ze względu na złożoną matematykę) lub czasochłonne (a następnie znowu kosztowne). Czasami dlatego, że po prostu zapomnieliśmy o optymalizacjach ...
Adriano Repetti,
62
@ ja72 - nie, nie jest lepiej pisać kod. Lepiej jest zoptymalizować kod.
Mike Baranczak,
14
Jest to sprzeczne z intuicją, dopóki naprawdę tego nie rozważysz. W ten sam sposób maszyny oparte na maszynach wirtualnych zaczynają optymalizować środowisko wykonawcze, których kompilatory po prostu nie mają informacji.
Bill K
6
@ M28: Kompilatory mogą korzystać z tych samych instrukcji. Jasne, płacą za to rozmiar binarny (ponieważ muszą podać ścieżkę rezerwową w przypadku, gdy instrukcje te nie są obsługiwane). Ponadto w większości przypadków „nowe instrukcje”, które zostaną dodane, to instrukcje SMID, z których zarówno maszyny wirtualne, jak i kompilatory są bardzo okropne w użyciu. Maszyny wirtualne płacą za tę funkcję, ponieważ muszą skompilować kod podczas uruchamiania.
Billy ONeal,
9
@BillK: PGO robi to samo dla kompilatorów.
Billy ONeal
194

Twój kod zestawu jest nieoptymalny i może zostać ulepszony:

  • Pchasz i pchasz rejestr ( EDX ) w swojej wewnętrznej pętli. Powinno to zostać usunięte z pętli.
  • Przeładowujesz wskaźniki tablicy w każdej iteracji pętli. To powinno wyjść z pętli.
  • Korzystasz z loopinstrukcji, 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 *)
  • Nie korzystasz z ręcznego rozwijania pętli.
  • Nie korzystasz z dostępnych instrukcji SIMD .

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ś loopinstrukcję 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.

Gunther Piez
źródło
kompilatory mogą nadal emitować loop(i wiele „przestarzałych” instrukcji), jeśli zoptymalizujesz rozmiar
phuclv
1
@ phuclv no tak, ale oryginalne pytanie dotyczyło dokładnie prędkości, a nie rozmiaru.
IGR94
60

Nawet przed zagłębieniem się w asemblerze istnieją transformacje kodu, które istnieją na wyższym poziomie.

static int const 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];
    }
  }
}

można przekształcić w obrót pętli :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

co jest znacznie lepsze, jeśli chodzi o lokalizację pamięci.

Można to dalej optymalizować, wykonywanie a += bX razy jest równoznaczne z robieniem, a += X * bwięc otrzymujemy:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

wydaje się jednak, że mój ulubiony optymalizator (LLVM) nie wykonuje tej transformacji.

[edytuj] Odkryłem, że transformacja jest wykonywana, jeśli mamy restrictkwalifikator do xi y. Rzeczywiście bez tego ograniczenia x[j]i y[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):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

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.

Matthieu M.
źródło
Dziękuję za odpowiedź. Cóż, to trochę mylące, że kiedy wziąłem udział w klasie o nazwie „Zasady kompilatora”, dowiedziałem się, że kompilator zoptymalizuje nasz kod na wiele sposobów. Czy to oznacza, że ​​musimy ręcznie zoptymalizować nasz kod? Czy możemy wykonać lepszą pracę niż kompilator? To pytanie zawsze mnie myli.
user957121,
2
@ user957121: możemy lepiej go zoptymalizować, gdy będziemy mieli więcej informacji. W szczególności tutaj tym, co utrudnia kompilator, jest możliwe aliasing pomiędzy xi y. Oznacza to, że kompilator nie może być pewny, że dla wszystkich i,jw [0, length)mamy x + i != y + j. Jeśli zachodzi na siebie, optymalizacja jest niemożliwa. Język C wprowadził restrictsł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.
Matthieu M.,
Bieżąca GCC i Clang automatycznie wektoryzują (po sprawdzeniu braku nakładania się, jeśli pominiesz __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 dla pmulld: 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.
Peter Cordes
41

Krótka odpowiedź: tak.

Długa odpowiedź: tak, chyba że naprawdę wiesz, co robisz i masz ku temu powód.

Oliver Charlesworth
źródło
3
i tylko wtedy, gdy uruchomisz narzędzie do profilowania na poziomie zespołu, takie jak vtune for chipy Intel, aby zobaczyć, gdzie możesz poprawić sytuację
Mark Mullin
1
To technicznie odpowiada na pytanie, ale jest również całkowicie bezużyteczne. A -1 ode mnie.
Navin
2
Bardzo długa odpowiedź: „Tak, chyba że masz ochotę zmienić cały kod za każdym razem, gdy używany jest nowy (e) procesor. Wybierz najlepszy algorytm, ale pozwól kompilatorowi wykonać optymalizację”
Tommylee2k
35

Naprawiłem mój kod asm:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Wyniki dla wersji Release:

 Function of assembly version: 81
 Function of C++ version: 161

Kod zestawu w trybie wydania jest prawie 2 razy szybszy niż C ++.

Sasha
źródło
18
Teraz, jeśli zaczniesz używać SSE zamiast MMX ( xmm0zamiast nazwy rejestru mm0), otrzymasz kolejne przyspieszenie dwa razy ;-)
Gunther Piez
8
Zmieniłem, dostałem 41 za wersję montażową. Jest 4 razy szybszy :)
sasha
3
może także uzyskać do 5% więcej, jeśli użyjesz wszystkich rejestrów xmm
sasha
7
A teraz, jeśli pomyślisz o czasie, który ci to zabrał: montaż, około 10 godzin? C ++, chyba kilka minut? Jest tu wyraźny zwycięzca, chyba że jest to kod krytyczny dla wydajności.
Calimo,
1
Dobry kompilator automatycznie się wektoryzuje paddd xmm(po sprawdzeniu nakładania się między xi y, ponieważ nie używałeś int *__restrict x). Na przykład robi to gcc: godbolt.org/z/c2JG0- . Lub po wprowadzeniu do main, 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ę skompilujesz gcc -O3 -march=native, możesz uzyskać 256-bit lub 512-bit wektoryzacja.
Peter Cordes
24

Czy to oznacza, że ​​nie powinienem ufać działaniu języka asemblera napisanego własnymi rękami

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.

jalf
źródło
2
Myślę, że w tym celu, aby napisać, szczególnie w przypadku nowoczesnego procesora x86, wyjątkowo trudno jest napisać wydajny kod asemblera ze względu na obecność potoków, wielu jednostek wykonawczych i innych sztuczek wewnątrz każdego rdzenia. Pisanie kodu, który równoważy wykorzystanie wszystkich tych zasobów w celu uzyskania najwyższej prędkości wykonywania, często skutkuje kodem z nieskomplikowaną logiką, która „nie powinna” być szybka zgodnie z „konwencjonalną” mądrością asemblera. Jednak w przypadku mniej skomplikowanych procesorów z mojego doświadczenia wynika, że ​​generowanie kodu kompilatora C można znacznie poprawić.
Olof Forshell
4
Kod kompilatorów C można zwykle ulepszyć, nawet na nowoczesnym procesorze x86. Ale musisz dobrze zrozumieć procesor, co jest trudniejsze w przypadku nowoczesnego procesora x86. To mój punkt. Jeśli nie rozumiesz sprzętu, na który celujesz, nie będziesz w stanie go zoptymalizować. A potem kompilator prawdopodobnie wykona lepszą robotę
czerwiec
1
A jeśli naprawdę chcesz zdmuchnąć kompilator, musisz być kreatywny i optymalizować w sposób, w jaki kompilator nie może. Jest to kompromis czasu / nagrody, dlatego C jest językiem skryptowym dla niektórych, a kodem pośrednim dla języka wyższego poziomu dla innych. Dla mnie jednak montaż to coś więcej dla zabawy :). podobnie jak grc.com/smgassembly.htm
Hawken
22

Jedynym powodem używania obecnie języka asemblera jest użycie niektórych funkcji niedostępnych dla tego języka.

Dotyczy to:

  • Programowanie jądra, które musi mieć dostęp do niektórych funkcji sprzętowych, takich jak MMU
  • Programowanie o wysokiej wydajności, które wykorzystuje bardzo specyficzne instrukcje wektorowe lub multimedialne nieobsługiwane przez kompilator.

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.

fortran
źródło
10
Poza tymi dwoma istnieją inne miejsca dla ASM. Mianowicie biblioteka bignum będzie zwykle znacznie szybsza w ASM niż C, ze względu na dostęp do flag przenoszenia i górnej części mnożenia i tym podobnych. Możesz robić te rzeczy również w przenośnym C, ale są one bardzo wolne.
Kaczka Mooing
@MooingDuck Można to uznać za dostęp do funkcji sprzętowych, które nie są bezpośrednio dostępne w języku ... Ale tak długo, jak ręcznie tłumaczysz kod wysokiego poziomu na asembler, kompilator cię pokona.
fortran
1
tak jest, ale nie jest to programowanie jądra ani specyficzne dla producenta. Chociaż z niewielkimi zmianami w pracy, może łatwo wpaść w którąkolwiek z tych kategorii. Odgadnij ASM, gdy chcesz wykonać instrukcje procesora, które nie mają mapowania C.
Kaczka Mooing
1
@fortran Mówiąc wprost, jeśli nie zoptymalizujesz kodu, nie będzie on tak szybki jak kod zoptymalizowany przez kompilator. Optymalizacja jest powodem, dla którego należy pisać asembler w pierwszej kolejności. Jeśli masz na myśli tłumaczenie, a następnie optymalizację, nie ma powodu, aby kompilator cię pobił, chyba że nie jesteś dobry w optymalizacji asemblera. Aby pokonać kompilator, musisz zoptymalizować sposób, w jaki kompilator nie może. To dość oczywiste. Jedynym powodem do napisania zestawu jest to, że jesteś lepszy niż kompilator / interpreter . To zawsze był praktyczny powód do pisania zestawu.
Hawken
1
Wystarczy powiedzieć: Clang ma dostęp do flag przenoszenia, 128-bitowego zwielokrotnienia i tak dalej dzięki wbudowanym funkcjom. I może zintegrować je wszystkie z normalnymi algorytmami optymalizacji.
gnasher729
19

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!)

„nie ma czegoś takiego jak najszybszy kod” - Michael Abrash


źródło
2
Uważam, że jedna z książek Michaela Abrasha to czarna księga poświęcona programowaniu graficznemu. Ale nie tylko on używa assemblera, Chris Sawyer sam napisał pierwsze dwie gry potentata na rollercoasterze.
Hawken,
14

Zmieniłem kod asm:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

Wyniki dla wersji Release:

 Function of assembly version: 41
 Function of C++ version: 161

Kod zestawu w trybie wydania jest prawie 4 razy szybszy niż C ++. IMHo, szybkość kodu asemblera zależy od Programmera

Sasha
źródło
Tak, mój kod naprawdę musi zostać zoptymalizowany. Dobra robota dla ciebie i dzięki!
user957121,
5
Jest czterokrotnie szybszy, ponieważ wykonujesz tylko jedną czwartą pracy :-) shr ecx,2Jest zbyteczny, ponieważ długość tablicy jest już podawana, inta nie bajtowa. Zasadniczo osiągasz tę samą prędkość. Możesz wypróbować odpowiedź padddod Haroldów, będzie to naprawdę szybsze.
Gunther Piez,
13

to bardzo interesujący temat!
Zmieniłem MMX przez SSE w kodzie Sashy
Oto moje wyniki:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

Kod asemblera z SSE jest 5 razy szybszy niż C ++

salaoshi
źródło
12

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ć:

mov eax,0

kosztuje więcej cykli niż

xor eax,eax

który robi to samo.

Kompilator zna wszystkie te sztuczki i używa ich.

Nuno_147
źródło
4
Nadal prawdziwe, patrz stackoverflow.com/questions/1396527/... . Nie z powodu wykorzystanych cykli, ale z powodu zmniejszonego zużycia pamięci.
Gunther Piez
10

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 yi xsą 16-wyrównane, i że lengthjest niezerowe wielokrotnością 4. To chyba wszystko prawda i tak.

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

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.

Harold
źródło
Myślę, że złożone adresowanie spowalnia twój kod, jeśli zmienisz kod na, mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eaxa 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.
Johan
@Johan, to nie powinno być przeciągnięcie, tylko dodatkowe opóźnienie cyklu, ale na pewno nie zaszkodzi go nie mieć. Napisałem ten kod dla Core2, który nie miał tego problemu. Czy r + r nie jest również „złożone”?
Harold
7

Ś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.

vsz
źródło
3
Myślę, że to zależy od języka i kompilatora. Mogę sobie wyobrazić niezwykle nieefektywny kompilator C, którego dane wyjściowe mogłyby być łatwo pobite przez prosty ludzki zestaw. GCC, nie tak bardzo.
Casey Rodarmor
Kompilatory C / ++ są takim przedsięwzięciem, a tylko 3 główne z nich, są raczej dobre w tym, co robią. W pewnych okolicznościach nadal (bardzo) możliwe jest, że odręczny montaż będzie szybszy; wiele bibliotek matematycznych upada na asm, aby lepiej obsługiwać wartości wielokrotne / szerokie. Więc chociaż gwarantowane jest trochę za mocne, jest prawdopodobne.
ssube
@peachykeen: Nie miałem na myśli, że asembler jest ogólnie wolniejszy niż C ++. Miałem na myśli tę „gwarancję” w przypadku, gdy masz kod C ++ i ślepo tłumaczysz go wiersz po wierszu do zestawu. Przeczytaj także ostatni akapit mojej odpowiedzi :)
vsz
5

Jako kompilator zamieniłbym pętlę o stałym rozmiarze na wiele zadań wykonawczych.

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

będzie produkować

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

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.

Miah
źródło
4
I jeśli nie ajest niestabilna, istnieje duża szansa, że ​​kompilator zrobi to int a = 13;od samego początku.
vsz
4

Dokładnie to znaczy. Pozostaw mikrooptymalizacje kompilatorowi.

Luchian Grigore
źródło
4

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ę.

tylerl
źródło
3

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ę:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

Kompilator dla 32-bitowego kodu ARM, biorąc pod uwagę powyższe, prawdopodobnie renderowałby go jako:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

a może

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

Można to nieco zoptymalizować w ręcznie składanym kodzie, ponieważ:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

lub

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

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:

strb r7,[r5+0x204]

Znacznie krótszy i szybszy niż nawet ręcznie zoptymalizowany kod zestawu. Ponadto załóżmy, że set_port_high wystąpił w kontekście:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

W ogóle nie jest to niemożliwe przy kodowaniu systemu wbudowanego. Jeśli set_port_highjest 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ż function2spodziewa 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, function1nie zmieniłby tych rejestrów, a zatem mógłby zastąpićset_port_highz pojedynczą strbinstrukcją - 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_highjest 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:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

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.

supercat
źródło
2

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.

gnasher729
źródło
2

C ++ jest szybszy, chyba że używasz języka asemblera z głębszą znajomością we właściwy sposób.

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:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

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
1

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.

madoki
źródło
Kiedy po prostu chcesz pokonać kompilator, zwykle łatwiej jest wziąć jego wyjście asm dla twojej funkcji i przekształcić ją w samodzielną funkcję asm, którą poprawiasz. Korzystanie z wbudowanego asm to sporo dodatkowej pracy, aby uzyskać poprawność interfejsu między C ++ i asm i sprawdzić, czy kompiluje się do optymalnego kodu. (Ale przynajmniej robiąc to dla zabawy, nie musisz się martwić, że pokonasz optymalizacje, takie jak ciągła propagacja, gdy funkcja wkracza w coś innego. Gcc.gnu.org/wiki/DontUseInlineAsm ).
Peter Cordes,
Zobacz także C ++ i ręcznie napisane Collatz-conjecture Q&A, aby dowiedzieć się więcej na temat pokonania kompilatora dla zabawy :) A także sugestie, jak wykorzystać to, czego się nauczysz, modyfikować C ++, aby pomóc kompilatorowi w ulepszeniu kodu.
Peter Cordes,
@PeterCordes Więc zgadzasz się.
madoki
1
Tak, asm jest fajny, z wyjątkiem tego, że wbudowany asm jest zwykle złym wyborem, nawet do zabawy. Technicznie jest to pytanie asm-line, więc dobrze byłoby przynajmniej odpowiedzieć na ten punkt w swojej odpowiedzi. Jest to także bardziej komentarz niż odpowiedź.
Peter Cordes,
Ok zgoda. Kiedyś byłem facetem tylko asm, ale to były lata 80-te.
madoki
-2

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 ...

Kruk
źródło
1
C i C ++ nie mają żadnego sprawdzania granic, chyba że o to poprosisz, i nie ma śmieci, chyba że sam je zaimplementujesz lub użyjesz biblioteki. Prawdziwe pytanie brzmi, czy kompilator tworzy lepsze pętle (i globalne optymalizacje) niż człowiek. Zwykle tak, chyba że człowiek naprawdę wie, co robi i spędza na tym dużo czasu .
Peter Cordes,
1
Możesz tworzyć statyczne pliki wykonywalne za pomocą NASM lub YASM (bez kodu zewnętrznego). Oba mogą generować dane wyjściowe w płaskim formacie binarnym, więc możesz zmusić je do samodzielnego złożenia nagłówków ELF, jeśli naprawdę chcesz nie uruchamiać 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 .
Peter Cordes,
1
Być może myślisz o C # lub std::vectorskompilowany 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 elementy int array[]arg. Dane wyjściowe asm nie są sprawdzane w czasie wykonywania: godbolt.org/g/w1HF5t . Wszystko, co dostaje, to wskaźnik w rdi, brak informacji o rozmiarze. Programiści muszą unikać niezdefiniowanego zachowania, nigdy nie wywołując go tablicą mniejszą niż 1024.
Peter Cordes
1
Wszystko, o czym mówisz, nie jest zwykłą tablicą C ++ (przydziel new, usuń ręcznie delete, 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ć allocado przydzielenia miejsca na stosie jako tablicy.
Peter Cordes
1
Odwołuje przykład na gcc.godbolt.org od g++ -O3generowania 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ć.
Peter Cordes
-3

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.

Olof Forshell
źródło
2
-1: Nie widzę żadnej używanej funkcji OO. Twój argument jest taki sam, jak: „asemblacja może być również szybsza, jeśli kompilator doda milion NOP”.
Sjoerd
Byłem niejasny, to właściwie pytanie C. Jeśli piszesz kod C dla kompilatora C ++, nie piszesz kodu C ++ i nie dostaniesz żadnych rzeczy OO. Kiedy zaczniesz pisać w prawdziwym C ++, używając OO, musisz być bardzo kompetentny, aby kompilator nie generował kodu obsługi OO.
Olof Forshell
więc twoja odpowiedź nie dotyczy pytania? (Ponadto wyjaśnienia znajdują się w odpowiedzi, a nie w komentarzach. Komentarze można usunąć w dowolnym momencie bez powiadomienia, powiadomienia lub historii.
Kaczka Mooing
1
Nie jestem pewien, co dokładnie rozumiesz przez „kod wsparcia” OO. Oczywiście, jeśli używasz dużo RTTI i tym podobnych, kompilator będzie musiał utworzyć wiele dodatkowych instrukcji w celu obsługi tych funkcji - ale każdy problem, który jest wystarczająco wysoki, aby ratyfikować użycie RTTI, jest zbyt skomplikowany, aby można go było łatwo zapisać w asemblerze . To, co możesz zrobić, to oczywiście napisać tylko abstrakcyjny interfejs zewnętrzny jako OO, wysyłając do zoptymalizowanego pod kątem wydajności czystego kodu proceduralnego tam, gdzie jest to krytyczne. Jednak w zależności od aplikacji C, Fortran, CUDA lub po prostu C ++ bez wirtualnego dziedziczenia może być lepszym rozwiązaniem niż montaż tutaj.
lewo około
2
Nie. Przynajmniej mało prawdopodobne. W C ++ jest coś, co nazywa się zasadą zerowego obciążenia i ma to zastosowanie przez większość czasu. Dowiedz się więcej o OO - przekonasz się, że ostatecznie poprawia to czytelność twojego kodu, poprawia jakość kodu, zwiększa szybkość kodowania, zwiększa niezawodność. Także dla osadzonych - ale używaj C ++, ponieważ daje to większą kontrolę, osadzony + OO w języku Java będzie cię kosztować.
Zane