Il linguaggio assembly inline è più lento del codice C ++ nativo?


183

Ho provato a confrontare le prestazioni del linguaggio assembly inline e del codice C ++, quindi ho scritto una funzione che aggiunge due array di dimensioni 2000 per 100000 volte. Ecco il codice:

#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
    };
}

Ecco 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;
}

Quindi eseguo il programma cinque volte per ottenere i cicli del processore, che potrebbero essere visti come tempi. Ogni volta che chiamo solo una delle funzioni sopra menzionate.

E qui arriva il risultato.

Funzione della versione di assemblaggio:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Funzione della versione C ++:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

Il codice C ++ in modalità di rilascio è quasi 3,7 volte più veloce del codice assembly. Perché?

Immagino che il codice assembly che ho scritto non sia efficace come quelli generati da GCC. È difficile per un programmatore comune come me scrivere codice più velocemente del suo avversario generato da un compilatore. Significa che non dovrei fidarmi delle prestazioni del linguaggio assembly scritto dalle mie mani, concentrarmi sul C ++ e dimenticare il linguaggio assembly?


29
Abbastanza. L'assemblaggio codificato a mano è appropriato in alcune circostanze, ma occorre fare attenzione a garantire che la versione dell'assemblaggio sia effettivamente più veloce di quanto si possa ottenere con un linguaggio di livello superiore.
Magnus Hoff,

161
Potresti trovare istruttivo studiare il codice generato dal compilatore e provare a capire perché è più veloce della versione dell'assembly.
Paul R

34
Sì, sembra che il compilatore sia più bravo a scrivere asm di te. I compilatori moderni sono davvero abbastanza buoni.
David Heffernan,

20
Hai visto l'assemblaggio prodotto da GCC? Il suo possibile GCC utilizzava le istruzioni MMX. La tua funzione è molto parallela: potresti potenzialmente utilizzare N processori per calcolare la somma in 1 / N volta. Prova una funzione in cui non c'è speranza per la parallelizzazione.
Chris

11
Mi sarei aspettato che un buon compilatore lo facesse ~ 100000 volte più veloce ...
PlasmaHH

Risposte:


261

Sì, la maggior parte delle volte.

Innanzitutto si parte dal presupposto errato che un linguaggio di basso livello (assembly in questo caso) produrrà sempre un codice più veloce del linguaggio di alto livello (C ++ e C in questo caso). Non è vero. Il codice C è sempre più veloce del codice Java? No perché esiste un'altra variabile: programmatore. Il modo in cui scrivi il codice e la conoscenza dei dettagli dell'architettura influenzano notevolmente le prestazioni (come hai visto in questo caso).

Puoi sempre produrre un esempio in cui il codice assembly fatto a mano è migliore del codice compilato ma di solito è un esempio fittizio o una singola routine non un vero programma di oltre 500.000 righe di codice C ++). Penso che i compilatori produrranno un codice assembly migliore del 95% volte e a volte, solo alcune volte rare, potrebbe essere necessario scrivere codice assembly per poche, brevi, molto utilizzate , routine critiche per le prestazioni o quando è necessario accedere alle funzionalità del linguaggio di alto livello preferito non espone. Vuoi un tocco di questa complessità? Leggi questa fantastica risposta qui su SO.

Perchè questo?

Prima di tutto perché i compilatori possono fare ottimizzazioni che non possiamo nemmeno immaginare (vedi questo breve elenco ) e le faranno in pochi secondi (quando potremmo aver bisogno di giorni ).

Quando si codifica in assembly, è necessario creare funzioni ben definite con un'interfaccia di chiamata ben definita. Tuttavia possono tenere conto dell'ottimizzazione dell'intero programma e dell'ottimizzazione interproceduale come allocazione dei registri , propagazione costante , eliminazione di sottoespressioni comuni , programmazione delle istruzioni e altre ottimizzazioni complesse, non ovvie ( modello Polytope , ad esempio). Sull'architettura RISC i ragazzi hanno smesso di preoccuparsi di questo molti anni fa (la programmazione delle istruzioni, ad esempio, è molto difficile da mettere a punto a mano ) e anche il moderno CISC CPU hanno condutture molto lunghe .

Per alcuni microcontrollori complessi anche le librerie di sistema sono scritte in C anziché in assembly perché i loro compilatori producono un codice finale migliore (e facile da mantenere).

I compilatori a volte possono utilizzare automaticamente alcune istruzioni MMX / SIMDx da soli, e se non li usi semplicemente non puoi confrontare (altre risposte hanno già rivisto molto bene il tuo codice assembly). Solo per i loop questo è un breve elenco di ottimizzazioni di loop di ciò che viene comunemente verificato da un compilatore (pensi di poterlo fare da solo quando il tuo programma è stato deciso per un programma C #?) Se scrivi qualcosa in assembly, io pensi di dover considerare almeno alcune semplici ottimizzazioni . L'esempio del libro di scuola per gli array è di srotolare il ciclo (la sua dimensione è nota al momento della compilazione). Fallo ed esegui nuovamente il test.

In questi giorni è anche davvero insolito dover usare il linguaggio assembly per un altro motivo: la pletora di diverse CPU . Vuoi supportarli tutti? Ognuno ha una microarchitettura specifica e alcuni set di istruzioni specifici . Hanno un numero diverso di unità funzionali e le istruzioni di montaggio devono essere organizzate per tenerle tutte occupate . Se scrivi in ​​C puoi usare PGO ma nell'assemblaggio avrai bisogno di una grande conoscenza di quella specifica architettura (e ripensare e rifare tutto per un'altra architettura ). Per le piccole attività il compilatore di solito lo fa meglio e per le attività complesse di solito il il lavoro non viene rimborsato (ecompilatore puòfare comunque meglio ).

Se ti siedi e dai un'occhiata al tuo codice probabilmente vedrai che otterrai di più per riprogettare l'algoritmo che per tradurre in assembly (leggi questo fantastico post qui su SO ), ci sono ottimizzazioni di alto livello (e suggerimenti per il compilatore) è possibile applicare in modo efficace prima di ricorrere al linguaggio assembly. Probabilmente vale la pena ricordare che spesso usando intrinseci avrai prestazioni migliori che stai cercando e il compilatore sarà ancora in grado di eseguire la maggior parte delle sue ottimizzazioni.

Detto questo, anche quando puoi produrre un codice assembly 5 ~ 10 volte più veloce, dovresti chiedere ai tuoi clienti se preferiscono pagare una settimana del tuo tempo o acquistare una CPU 50 $ più veloce . L'ottimizzazione estrema il più delle volte (e specialmente nelle applicazioni LOB) semplicemente non è richiesta dalla maggior parte di noi.


9
Ovviamente no. Penso che sia meglio del 95% delle persone nel 99% delle volte. A volte perché è semplicemente costoso (a causa della matematica complessa ) o del tempo speso (poi di nuovo costoso). A volte perché ci siamo semplicemente dimenticati delle ottimizzazioni ...
Adriano Repetti,

62
@ ja72 - no, non è meglio scrivere il codice. È meglio ottimizzare il codice.
Mike Baranczak,

14
È controintuitivo fino a quando non lo consideri davvero. Allo stesso modo, le macchine basate su VM stanno iniziando a fare ottimizzazioni di runtime che i compilatori semplicemente non hanno le informazioni da creare.
Bill K

6
@ M28: i compilatori possono utilizzare le stesse istruzioni. Certo, lo pagano in termini di dimensioni binarie (perché devono fornire un percorso di fallback nel caso in cui tali istruzioni non siano supportate). Inoltre, per la maggior parte, le "nuove istruzioni" da aggiungere sono comunque istruzioni SMID, che sia le VM che i compilatori sono piuttosto orribili nell'utilizzare. Le macchine virtuali pagano per questa funzione in quanto devono compilare il codice all'avvio.
Billy ONeal

9
@BillK: PGO fa la stessa cosa per i compilatori.
Billy ONeal

194

Il codice assembly non è ottimale e può essere migliorato:

  • Stai spingendo e aprendo un registro ( EDX ) nel tuo ciclo interno. Questo dovrebbe essere spostato fuori dal ciclo.
  • Ricaricare i puntatori di matrice in ogni iterazione del ciclo. Questo dovrebbe uscire dal circuito.
  • Si utilizza l' loopistruzione, che è nota per essere lenta nella maggior parte delle CPU moderne (probabilmente a causa dell'uso di un antico libro di assemblaggio *)
  • Non approfitti dello srotolamento manuale del loop.
  • Non usi le istruzioni SIMD disponibili .

Quindi, a meno che non miglioriate notevolmente il vostro set di abilità riguardo all'assemblatore, non ha senso scrivere il codice dell'assemblatore per le prestazioni.

* Certo non so se hai davvero ricevuto le loopistruzioni da un antico libro delle assemblee. Ma non lo vedi quasi mai nel codice del mondo reale, poiché ogni compilatore là fuori è abbastanza intelligente da non emetterlo loop, lo vedi solo nei libri cattivi e obsoleti di IMHO.


i compilatori possono comunque emettere loop(e molte istruzioni "deprecate") se ottimizzi per dimensione
phuclv

1
@phuclv, sì, ma la domanda originale era esattamente sulla velocità, non sulle dimensioni.
IGR94

60

Anche prima di approfondire l'assemblaggio, esistono trasformazioni di codice a un livello superiore.

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];
    }
  }
}

può essere trasformato in Loop Rotation :

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];
      }
    }
}

che è molto meglio per quanto riguarda la località di memoria.

Questo potrebbe essere ulteriormente ottimizzato, fare a += bX volte equivale a fare a += X * bcosì otteniamo:

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];
    }
}

tuttavia sembra che il mio ottimizzatore preferito (LLVM) non esegua questa trasformazione.

[modifica] Ho scoperto che la trasformazione viene eseguita se avessimo il restrictqualificatore per xe y. Infatti senza questa restrizione, x[j]e y[j]potrebbe alias nella stessa posizione che rende questa trasformazione errata. [fine modifica]

Ad ogni modo, questa è, credo, la versione C ottimizzata. È già molto più semplice. Sulla base di questo, ecco il mio crack in ASM (lascio che Clang lo generi, sono inutile):

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

Temo di non capire da dove provengano tutte quelle istruzioni, tuttavia puoi sempre divertirti e provare a vedere come si confronta ... ma userei comunque la versione C ottimizzata anziché quella dell'assemblaggio, in codice, molto più portatile.


Grazie per la risposta. Beh, è ​​un po 'confuso che quando ho preso la classe denominata "Principi del compilatore", ho imparato che il compilatore ottimizzerà il nostro codice in molti modi. Ciò significa che dobbiamo ottimizzare il nostro codice manualmente? Possiamo fare un lavoro migliore rispetto al compilatore? Questa è la domanda che mi confonde sempre.
user957121

2
@ user957121: possiamo ottimizzarlo meglio quando abbiamo più informazioni. In particolare qui ciò che ostacola il compilatore è il possibile aliasing tra xe y. Cioè, il compilatore non può essere sicuro che per tutti i,jin [0, length)abbiamo x + i != y + j. In caso di sovrapposizione, l'ottimizzazione è impossibile. Il linguaggio C ha introdotto la restrictparola chiave per indicare al compilatore che due puntatori non possono alias, tuttavia non funziona per gli array perché possono ancora sovrapporsi anche se non esattamente alias.
Matthieu M.

GCC e Clang attuali si auto-vettorizzano (dopo aver verificato la non sovrapposizione se si omette __restrict). SSE2 è la linea di base per x86-64 e con lo shuffling SSE2 può fare 2x moltiplicazioni a 32 bit contemporaneamente (producendo prodotti a 64 bit, quindi lo shuffle per rimettere insieme i risultati). godbolt.org/z/r7F_uo . (SSE4.1 è necessario per pmulld: pacchetto 32x32 => moltiplicazione 32-bit). GCC ha un trucco per trasformare moltiplicatori di numeri interi costanti in shift / add (e / o sottrarre), il che è utile per i moltiplicatori con pochi bit impostati. Il codice shuffle pesante di Clang sta per strozzare il throughput shuffle su CPU Intel.
Peter Cordes,

41

Risposta breve: si .

Risposta lunga: sì, a meno che tu non sappia davvero cosa stai facendo e non abbia un motivo per farlo.


3
e quindi solo se hai eseguito uno strumento di profilazione a livello di assembly come vtune per i chip Intel per vedere dove potresti essere in grado di migliorare le cose
Mark Mullin,

1
Questo tecnicamente risponde alla domanda ma è anche completamente inutile. A -1 da parte mia.
Navin,

2
Risposta molto lunga: "Sì, a meno che tu non abbia voglia di cambiare l'intero codice ogni volta che viene utilizzata una nuova (er) CPU. Scegli l'algoritmo migliore, ma lascia che il compilatore
esegua

35

Ho corretto il mio codice 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
};

Risultati per la versione di rilascio:

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

Il codice assembly in modalità di rilascio è quasi 2 volte più veloce di C ++.


18
Ora, se inizi a utilizzare SSE anziché MMX (il nome del registro è xmm0invece di mm0), otterrai un altro aumento di velocità di un fattore due ;-)
Gunther Piez

8
Ho cambiato, ho ottenuto 41 per la versione assembly. È in 4 volte più veloce :)
sasha,

3
può anche ottenere fino al 5% in più se si utilizzano tutti i registri xmm
sasha

7
Ora, se pensi al tempo effettivamente impiegato: assemblaggio, circa 10 ore circa? C ++, qualche minuto immagino? C'è un chiaro vincitore qui, a meno che non sia un codice critico per le prestazioni.
Calimo,

1
Un buon compilatore eseguirà già la vettorializzazione automatica con paddd xmm(dopo aver verificato la sovrapposizione tra xe y, poiché non è stato utilizzato int *__restrict x). Ad esempio gcc lo fa: godbolt.org/z/c2JG0- . O dopo essersi inserito in main, non dovrebbe essere necessario verificare la sovrapposizione perché può vedere l'allocazione e dimostrare che non si sovrappongono. (E arriverebbe ad assumere un allineamento di 16 byte anche su alcune implementazioni x86-64, il che non è il caso della definizione stand-alone.) E se lo compili gcc -O3 -march=native, puoi ottenere 256-bit o 512-bit vettorializzazione.
Peter Cordes,

24

Ciò significa che non dovrei fidarmi dell'esecuzione del linguaggio assembly scritto dalle mie mani

Sì, questo è esattamente ciò che significa, ed è vero per ogni lingua. Se non sai come scrivere codice efficiente in lingua X, non dovresti fidarti della tua capacità di scrivere codice efficiente in X. Quindi, se vuoi un codice efficiente, dovresti usare un'altra lingua.

Il montaggio è particolarmente sensibile a questo, perché, beh, ciò che vedi è ciò che ottieni. Scrivi le istruzioni specifiche che desideri vengano eseguite dalla CPU. Con linguaggi di alto livello, c'è un compilatore in mezzo a, che può trasformare il tuo codice e rimuovere molte inefficienze. Con il montaggio, sei da solo.


2
Penso che sia per scrivere che soprattutto per un moderno processore x86 è eccezionalmente difficile scrivere un codice assembly efficiente a causa della presenza di condutture, unità di esecuzione multiple e altri espedienti all'interno di ogni core. La scrittura di codice che bilancia l'utilizzo di tutte queste risorse al fine di ottenere la massima velocità di esecuzione si tradurrà spesso in un codice con logica poco chiara che "non dovrebbe" essere veloce secondo la saggezza dell'assemblea "convenzionale". Ma per CPU meno complesse è la mia esperienza che la generazione del codice del compilatore C può essere migliorata in modo significativo.
Olof Forshell

4
Il codice dei compilatori C può di solito essere migliorato, anche su una moderna CPU x86. Ma devi capire bene la CPU, che è più difficile da fare con una moderna CPU x86. Questo è il mio punto. Se non capisci l'hardware che stai prendendo di mira, non sarai in grado di ottimizzarlo. E poi il compilatore probabilmente farà un lavoro migliore
jalf

1
E se vuoi davvero far esplodere il compilatore, devi essere creativo e ottimizzare nei modi in cui il compilatore non può farlo. È un compromesso per tempo / ricompensa, ecco perché C è un linguaggio di scripting per alcuni e codice intermedio per un linguaggio di livello superiore per altri. Per me, però, il montaggio è più divertente :). proprio come grc.com/smgassembly.htm
Hawken,

22

L'unico motivo per utilizzare il linguaggio assembly oggi è utilizzare alcune funzionalità non accessibili dal linguaggio.

Questo vale per:

  • Programmazione del kernel che deve accedere a determinate funzionalità hardware come la MMU
  • Programmazione ad alte prestazioni che utilizza istruzioni vettoriali o multimediali molto specifiche non supportate dal compilatore.

Ma i compilatori attuali sono piuttosto intelligenti, possono persino sostituire due istruzioni separate come d = a / b; r = a % b;con una singola istruzione che calcola la divisione e il resto in una volta se è disponibile, anche se C non ha tale operatore.


10
Ci sono altri posti per ASM oltre a questi due. Vale a dire, una libreria di bignum di solito sarà significativamente più veloce in ASM rispetto a C, a causa dell'accesso ai portabandiera e della parte superiore della moltiplicazione e simili. Puoi fare queste cose anche in C portatile, ma sono molto lente.
Mooing Duck

@MooingDuck Questo potrebbe essere considerato come l'accesso a funzionalità hardware hardware che non sono direttamente disponibili nella lingua ... Ma fintanto che stai solo traducendo il tuo codice di alto livello in assembly a mano, il compilatore ti batterà.
fortran,

1
è quello, ma non è la programmazione del kernel, né specifica del fornitore. Anche se con lievi modifiche al workding, potrebbe facilmente rientrare in entrambe le categorie. ID indovinare ASM quando si desidera l'esecuzione delle istruzioni del processore che non hanno mappatura C.
Mooing Duck

1
@fortran In pratica stai solo dicendo che se non ottimizzi il tuo codice non sarà veloce come il codice ottimizzato dal compilatore. L'ottimizzazione è il motivo per cui si dovrebbe scrivere assembly in primo luogo. Se intendi tradurre, allora ottimizza non c'è motivo per cui il compilatore ti batterà a meno che tu non sia bravo a ottimizzare l'assemblaggio. Quindi per battere il compilatore devi ottimizzare in modi che il compilatore non può. È piuttosto autoesplicativo. L'unico motivo per scrivere assembly è se sei migliore di un compilatore / interprete . Questo è sempre stato il motivo pratico per scrivere assembly.
Hawken,

1
Dico solo: Clang ha accesso ai flag di carry, alla moltiplicazione a 128 bit e così via attraverso le funzioni integrate. E può integrare tutto ciò nei suoi normali algoritmi di ottimizzazione.
gnasher729,

19

È vero che un moderno compilatore fa un ottimo lavoro nell'ottimizzazione del codice, ma ti incoraggio comunque a continuare a studiare l'assemblaggio.

Prima di tutto non ne sei chiaramente intimidito , questo è un grande, grande vantaggio, poi - sei sulla strada giusta profilando al fine di convalidare o scartare le tue assunzioni di velocità , stai chiedendo input da persone esperte e tu avere il più grande strumento di ottimizzazione noto all'umanità: un cervello .

Man mano che la tua esperienza aumenta, imparerai quando e dove usarla (di solito i loop più stretti e più intimi nel tuo codice, dopo aver profondamente ottimizzato a livello algoritmico).

Per ispirazione, ti consiglio di cercare gli articoli di Michael Abrash (se non hai avuto sue notizie, è un guru dell'ottimizzazione; ha persino collaborato con John Carmack nell'ottimizzazione del renderer del software Quake!)

"non esiste il codice più veloce" - Michael Abrash


2
Credo che uno dei libri di Michael Abrash sia il libro nero sulla programmazione grafica. Ma non è l'unico ad usare il montaggio, Chris Sawyer ha scritto i primi due giochi di magnate sulle montagne russe da solo.
Hawken

14

Ho cambiato il codice 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
};

Risultati per la versione di rilascio:

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

Il codice assembly in modalità di rilascio è quasi 4 volte più veloce di C ++. IMHo, la velocità del codice assembly dipende dal programmatore


Sì, il mio codice deve davvero essere ottimizzato. Buon lavoro per te e grazie!
user957121

5
È quattro volte più veloce perché esegui solo un quarto del lavoro :-) shr ecx,2È superfluo, perché la lunghezza dell'array è già indicata inte non in byte. Quindi in pratica raggiungi la stessa velocità. Potresti provare la padddrisposta di Harolds, questo sarà davvero più veloce.
Gunther Piez,

13

è un argomento molto interessante!
Ho cambiato MMX di SSE nel codice di Sasha
Ecco i miei risultati:

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

Il codice assembly con SSE è 5 volte più veloce del C ++


12

La maggior parte dei compilatori di lingue di alto livello sono molto ottimizzati e sanno cosa stanno facendo. Puoi provare a scaricare il codice di disassemblaggio e confrontarlo con il tuo assieme nativo. Credo che vedrai alcuni trucchi che il tuo compilatore sta usando.

Solo per esempio, anche se non sono più sicuro che sia giusto :):

fare:

mov eax,0

costa più cicli di

xor eax,eax

che fa la stessa cosa.

Il compilatore conosce tutti questi trucchi e li usa.


4
Ancora vero, vedi stackoverflow.com/questions/1396527/… . Non a causa dei cicli utilizzati, ma a causa dell'ingombro di memoria ridotto.
Gunther Piez

10

Il compilatore ti ha battuto. Ci proverò, ma non fornirò alcuna garanzia. Presumo che la "moltiplicazione" di TIMES abbia lo scopo di renderlo un test delle prestazioni più rilevante, che ye xsono 16 allineati e che lengthè un multiplo diverso da zero di 4. Probabilmente è comunque vero.

  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

Come ho detto, non fornisco garanzie. Ma sarò sorpreso se può essere fatto molto più velocemente - il collo di bottiglia qui è il throughput di memoria anche se tutto è un successo L1.


Penso che l'indirizzamento complesso stia rallentando il tuo codice, se cambi il codice in mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eaxe poi usi [esi + ecx] ovunque eviterai 1 ciclo di stallo per istruzione accelerando i lotti di loop. (Se hai l'ultimo Skylake, questo non si applica). L'aggiunta reg, reg rende solo il loop più stretto, il che può o meno aiutare.
Johan,

@Johan che non dovrebbe essere uno stallo, solo una latenza di ciclo extra, ma sicuramente non può far male a non averlo .. Ho scritto questo codice per Core2 che non aveva quel problema. R + r non è anche "complesso" tra l'altro?
Harold,

7

L'implementazione alla cieca dello stesso identico algoritmo, istruzione per istruzione, nell'assembly è garantita per essere più lenta di ciò che il compilatore può fare.

È perché anche la più piccola ottimizzazione del compilatore è migliore del tuo codice rigido senza alcuna ottimizzazione.

Certo, è possibile battere il compilatore, specialmente se si tratta di una piccola parte localizzata del codice, ho anche dovuto farlo da solo per ottenere un ca. 4 volte più veloce, ma in questo caso dobbiamo fare molto affidamento su una buona conoscenza dell'hardware e su numerosi trucchi apparentemente controintuitivi.


3
Penso che questo dipenda dal linguaggio e dal compilatore. Posso immaginare un compilatore C estremamente inefficiente il cui output potrebbe essere facilmente battuto da un semplice assemblaggio di scrittura umana. Il CCG, non tanto.
Casey Rodarmor,

Con i compilatori C / ++ che sono un'impresa del genere, e solo 3 importanti in giro, tendono ad essere piuttosto bravi in ​​quello che fanno. È ancora (molto) possibile in certe circostanze che l'assemblaggio scritto a mano sarà più veloce; molte librerie matematiche scendono in asm per gestire meglio valori multipli / ampi. Quindi, sebbene sia garantito un po 'troppo forte, è probabile.
ssube,

@peachykeen: non intendevo dire che l'assemblaggio sia più lento del C ++ in generale. Intendevo quella "garanzia" nel caso in cui tu abbia un codice C ++ e lo traduca ciecamente riga per riga in assembly. Leggi anche l'ultimo paragrafo della mia risposta :)
vsz

5

Come compilatore vorrei sostituire un loop con una dimensione fissa a molte attività di esecuzione.

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

produrrà

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

e alla fine saprà che "a = a + 0;" è inutile quindi rimuoverà questa linea. Spero che qualcosa nella tua testa sia ora disposto ad allegare alcune opzioni di ottimizzazione come commento. Tutte queste ottimizzazioni molto efficaci renderanno più veloce il linguaggio compilato.


4
E a meno che non asia volatile, ci sono buone probabilità che il compilatore lo faccia int a = 13;fin dall'inizio.
vsz,

4

È esattamente ciò che significa. Lascia le micro-ottimizzazioni al compilatore.


4

Adoro questo esempio perché dimostra un'importante lezione sul codice di basso livello. Sì, puoi scrivere un assembly con la stessa velocità del tuo codice C. Questo è tautologicamente vero, ma non significa necessariamente nulla. Chiaramente qualcuno può, altrimenti l'assemblatore non sarebbe a conoscenza delle ottimizzazioni appropriate.

Allo stesso modo, si applica lo stesso principio man mano che si sale nella gerarchia dell'astrazione del linguaggio. Sì, puoi scrivere un parser in C che è veloce come uno script perl veloce e sporco, e molte persone lo fanno. Ma ciò non significa che, poiché hai usato C, il tuo codice sarà veloce. In molti casi, le lingue di livello superiore fanno ottimizzazioni che potresti non aver mai nemmeno preso in considerazione.


3

In molti casi, il modo ottimale per eseguire alcune attività può dipendere dal contesto in cui l'attività viene eseguita. Se una routine è scritta nel linguaggio assembly, in genere non sarà possibile variare la sequenza delle istruzioni in base al contesto. Come semplice esempio, considera il seguente metodo semplice:

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

Un compilatore per il codice ARM a 32 bit, dato quanto sopra, probabilmente lo renderebbe come qualcosa di simile:

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

o forse

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]

Ciò potrebbe essere leggermente ottimizzato nel codice assemblato a mano, in quanto:

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

o

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

Entrambi gli approcci assemblati a mano richiederebbero 12 byte di spazio di codice anziché 16; quest'ultimo sostituirà un "carico" con un "add", che su un ARM7-TDMI eseguirà due cicli più velocemente. Se il codice fosse eseguito in un contesto in cui r0 non era noto / non importa, le versioni del linguaggio assembly sarebbero quindi leggermente migliori rispetto alla versione compilata. D'altra parte, supponiamo che il compilatore sapesse che alcuni registri [ad es. R5] avrebbero mantenuto un valore compreso tra 2047 byte dell'indirizzo desiderato 0x40001204 [ad es. 0x40001000], e inoltre sapevano che altri registri [ad es. R7] stavano andando per contenere un valore i cui bit bassi erano 0xFF. In tal caso, un compilatore potrebbe ottimizzare la versione C del codice semplicemente:

strb r7,[r5+0x204]

Molto più breve e veloce del codice assembly ottimizzato a mano. Supponiamo inoltre che set_port_high si sia verificato nel contesto:

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

Non è affatto plausibile quando si codifica un sistema incorporato. Se set_port_highè scritto nel codice assembly, il compilatore dovrebbe spostare r0 (che contiene il valore restituito function1) da qualche altra parte prima di richiamare il codice assembly, quindi riportare quel valore in r0 in seguito (poiché function2si aspetterà il suo primo parametro in r0), quindi il codice assembly "ottimizzato" avrebbe bisogno di cinque istruzioni. Anche se il compilatore non fosse a conoscenza di registri che contengono l'indirizzo o il valore da memorizzare, la sua versione a quattro istruzioni (che potrebbe adattare per usare qualsiasi registro disponibile - non necessariamente r0 e r1) batterebbe l'assemblaggio "ottimizzato" -language version. Se il compilatore avesse l'indirizzo e i dati necessari in r5 e r7 come descritto in precedenza, function1non altererebbe tali registri e quindi potrebbe sostituireset_port_high con un'unica strbistruzione:quattro istruzioni più piccole e veloci rispetto al codice assembly "ottimizzato a mano".

Si noti che il codice assembly ottimizzato a mano può spesso sovraperformare un compilatore nei casi in cui il programmatore conosce il flusso preciso del programma, ma i compilatori brillano nei casi in cui un pezzo di codice viene scritto prima che il suo contesto sia noto o in cui un pezzo di codice sorgente può essere invocato da più contesti [se set_port_highutilizzato in cinquanta posizioni diverse nel codice, il compilatore potrebbe decidere autonomamente per ognuno di quelli il modo migliore per espanderlo].

In generale, suggerirei che il linguaggio assembly è suscettibile di produrre i migliori miglioramenti delle prestazioni nei casi in cui ogni parte di codice può essere affrontata da un numero molto limitato di contesti ed è dannosa per le prestazioni in luoghi in cui una parte di il codice può essere affrontato da molti contesti diversi. In modo interessante (e convenientemente) i casi in cui l'assemblaggio è più vantaggioso per le prestazioni sono spesso quelli in cui il codice è più semplice e facile da leggere. I luoghi in cui il codice della lingua dell'assembly si trasformerebbe in un pasticcio appiccicoso sono spesso quelli in cui la scrittura in assembly offrirebbe il minimo vantaggio in termini di prestazioni.

[Nota minore: ci sono alcuni punti in cui è possibile utilizzare il codice assembly per produrre un pasticcio appiccicoso iper-ottimizzato; ad esempio, un pezzo di codice che ho fatto per l'ARM doveva recuperare una parola dalla RAM ed eseguire una delle dodici routine in base ai sei bit superiori del valore (molti valori mappati sulla stessa routine). Penso di aver ottimizzato quel codice in modo simile a:

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

Il registro r8 conteneva sempre l'indirizzo della tabella di invio principale (all'interno del ciclo in cui il codice trascorre il 98% del suo tempo, nulla lo ha mai usato per altri scopi); tutte e 64 le voci si riferivano agli indirizzi nei 256 byte precedenti. Poiché il loop primario aveva nella maggior parte dei casi un limite di tempo di esecuzione di circa 60 cicli, il recupero e il dispaccio in nove cicli è stato molto strumentale per raggiungere quell'obiettivo. L'uso di una tabella di 256 indirizzi a 32 bit sarebbe stato un ciclo più veloce, ma avrebbe assorbito 1 KB di RAM molto preziosa [il flash avrebbe aggiunto più di uno stato di attesa]. L'uso di 64 indirizzi a 32 bit avrebbe richiesto l'aggiunta di un'istruzione per mascherare alcuni bit dalla parola recuperata, e avrebbe comunque inghiottito 192 byte in più rispetto alla tabella che ho effettivamente usato. Utilizzando la tabella degli offset a 8 bit ha prodotto codice molto compatto e veloce, ma non qualcosa che mi aspetterei che un compilatore potrebbe mai inventare; Inoltre, non mi aspetto che un compilatore dedichi un registro "a tempo pieno" per contenere l'indirizzo della tabella.

Il codice sopra è stato progettato per funzionare come un sistema autonomo; potrebbe periodicamente chiamare il codice C, ma solo in determinati momenti in cui l'hardware con cui stava comunicando poteva essere messo in sicurezza in uno stato "inattivo" per due intervalli di circa un millisecondo ogni 16 ms.


2

In tempi recenti, tutte le ottimizzazioni di velocità che ho fatto stavano sostituendo il codice lento danneggiato al cervello con solo un codice ragionevole. Ma poiché le cose erano veloci era davvero fondamentale e ho fatto uno sforzo serio per rendere qualcosa di veloce, il risultato è stato sempre un processo iterativo, in cui ogni iterazione ha fornito maggiori informazioni sul problema, trovando modi per risolverlo con meno operazioni. La velocità finale dipendeva sempre da quanta comprensione avevo del problema. Se in qualsiasi momento avessi usato il codice assembly o il codice C che era stato ottimizzato eccessivamente, il processo di ricerca di una soluzione migliore avrebbe sofferto e il risultato finale sarebbe più lento.


2

Il C ++ è più veloce a meno che non si stia utilizzando un linguaggio assembly con una conoscenza più approfondita nel modo corretto.

Quando codice in ASM, riorganizzo le istruzioni manualmente in modo che la CPU possa eseguirne più in parallelo quando logicamente possibile. Uso a malapena la RAM quando ad esempio codice in ASM: potrebbero esserci più di 20000 righe di codice in ASM e non ho mai usato push / pop.

Potresti potenzialmente saltare nel mezzo del codice operativo per auto-modificare il codice e il comportamento senza la possibile penalità del codice di auto-modifica. L'accesso ai registri richiede 1 tick (a volte richiede 0,25 tick) della CPU, mentre l'accesso alla RAM potrebbe richiedere centinaia.

Per la mia ultima avventura ASM, non ho mai usato la RAM per memorizzare una variabile (per migliaia di righe di ASM). ASM potrebbe essere potenzialmente inimmaginabilmente più veloce di C ++. Ma dipende da molti fattori variabili come:

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.

Ora sto imparando C # e C ++ perché ho capito che la produttività conta! Potresti provare a fare i programmi più veloci immaginabili usando ASM puro da solo nel tempo libero. Ma per produrre qualcosa, usa un linguaggio di alto livello.

Ad esempio, l'ultimo programma che ho codificato stava usando JS e GLSL e non ho mai notato alcun problema di prestazioni, anche parlando di JS che è lento. Questo perché il semplice concetto di programmazione della GPU per 3D rende quasi irrilevante la velocità del linguaggio che invia i comandi alla GPU.

La velocità del solo assemblatore sul metallo nudo è irrefutabile. Potrebbe essere ancora più lento in C ++? - Potrebbe essere perché stai scrivendo il codice assembly con un compilatore che non utilizza un assemblatore per iniziare.

Il mio consiglio personale è di non scrivere mai il codice dell'assemblea se puoi evitarlo, anche se adoro il montaggio.


1

Tutte le risposte qui sembrano escludere un aspetto: a volte non scriviamo codice per raggiungere uno scopo specifico, ma per puro divertimento . Potrebbe non essere economico investire il tempo per farlo, ma probabilmente non c'è maggiore soddisfazione che battere lo snippet di codice ottimizzato per compilatore più veloce in velocità con un'alternativa asm arrotolata manualmente.


Quando vuoi semplicemente battere il compilatore, di solito è più facile prendere il suo output asm per la tua funzione e trasformarlo in una funzione asm stand-alone che modifichi. L'uso di inline asm è un mucchio di lavoro extra per ottenere l'interfaccia tra C ++ e asm corretta e verificare che si stia compilando in modo ottimale. (Ma almeno quando lo fai solo per divertimento, non devi preoccuparti che sconfigge le ottimizzazioni come la propagazione costante quando la funzione si allinea in qualcos'altro. Gcc.gnu.org/wiki/DontUseInlineAsm ).
Peter Cordes,

Vedi anche la congettura di Collatz C ++ vs. asm domande e risposte scritte a mano per ulteriori informazioni su come battere il compilatore per divertimento :) E anche suggerimenti su come utilizzare ciò che impari a modificare il C ++ per aiutare il compilatore a creare un codice migliore.
Peter Cordes,

@PeterCordes Quindi stai dicendo che sei d'accordo.
madoki,

1
Sì, asm è divertente, tranne che inline asm è di solito la scelta sbagliata anche per giocare. Questa è tecnicamente una domanda inline-asm, quindi sarebbe bene almeno affrontare questo punto nella tua risposta. Inoltre, questo è davvero più un commento che una risposta.
Peter Cordes,

Ok d'accordo. Ero un asm solo, ma erano gli anni '80.
Madoki,

-2

Un compilatore c ++ produrrebbe, dopo l'ottimizzazione a livello organizzativo, un codice che utilizzerebbe le funzioni integrate della CPU di destinazione. HLL non supererà mai o supererà l'assemblatore per diversi motivi; 1.) HLL sarà compilato e prodotto con codice Accessor, controllo dei confini e possibilmente incorporato nella garbage collection (che in precedenza riguardava l'ambito nel manierismo OOP) tutti i cicli che richiedono (flip e flop). Al giorno d'oggi HLL fa un ottimo lavoro (compresi i più recenti C ++ e altri come GO), ma se superano l'assemblatore (vale a dire il codice) è necessario consultare la documentazione della CPU - le corrispondenze con il codice sciatto sono sicuramente inconcludenti e le lingue compilate come assembler si risolvono fino al codice operativo HLL estrae i dettagli e non li elimina altrimenti l'app non verrà eseguita se viene persino riconosciuta dal sistema operativo host.

La maggior parte del codice assembler (principalmente oggetti) viene emesso come "senza testa" per l'inclusione in altri formati eseguibili con molta meno elaborazione, quindi sarà molto più veloce, ma molto più incerto; se un eseguibile viene prodotto dall'assemblatore (NAsm, YAsm; ecc.) funzionerà ancora più velocemente fino a quando non corrisponderà completamente al codice HLL in termini di funzionalità, quindi i risultati potrebbero essere accuratamente ponderati.

La chiamata di un oggetto codice basato su assemblatore da HLL in qualsiasi formato aggiungerà intrinsecamente sovraccarico di elaborazione oltre alle chiamate di spazio di memoria che utilizzano la memoria allocata a livello globale per tipi di dati variabili / costanti (questo vale sia per LLL che per HLL). Ricorda che l'output finale sta usando la CPU come API e ABI relativamente all'hardware (opcode) ed entrambi, assemblatori e "compilatori HLL" sono essenzialmente / fondamentalmente identici con l'unica vera eccezione essendo la leggibilità (grammaticale).

L'applicazione di console Hello World nell'assemblatore che utilizza FAsm è di 1,5 KB (e questo è ancora più piccolo in Windows in FreeBSD e Linux) e supera qualsiasi cosa GCC possa lanciare nel suo giorno migliore; le ragioni sono il riempimento implicito con nops, la convalida dell'accesso e il controllo dei limiti per citarne alcuni. Il vero obiettivo sono lib lib HLL e un compilatore ottimizzabile che indirizza una cpu in modo "hardcore" e la maggior parte lo fa in questi giorni (finalmente). GCC non è migliore di YAsm: sono le pratiche di codifica e la comprensione dello sviluppatore in questione e "l'ottimizzazione" arriva dopo l'esplorazione per principianti e la formazione e l'esperienza temporanee.

I compilatori devono collegare e assemblare per l'output nello stesso codice operativo di un assemblatore perché quei codici sono tutto ciò che una CPU esclude (CISC o RISC [PIC anche]]. YAsm ottimizzato e ripulito molto dai primi NAsm alla fine velocizzando tutto l'output di quell'assemblatore, ma anche allora YAsm, come NAsm, produce eseguibili con dipendenze esterne indirizzati alle librerie del sistema operativo per conto dello sviluppatore, quindi il chilometraggio può variare. Chiudendo il C ++ è incredibile e molto più sicuro dell'assemblatore per l'80% in più, specialmente nel settore commerciale ...


1
C e C ++ non hanno alcun controllo dei limiti a meno che tu non lo richieda e nessuna garbage collection a meno che tu non lo implementi da solo o usi una libreria. La vera domanda è se il compilatore fa loop migliori (e ottimizzazioni globali) di un essere umano. Di solito sì, a meno che l'umano non sappia davvero cosa stanno facendo e ci trascorra molto tempo .
Peter Cordes,

1
È possibile creare eseguibili statici utilizzando NASM o YASM (nessun codice esterno). Entrambi possono essere stampati in formato binario piatto, quindi potresti farli assemblare tu stesso le intestazioni ELF se davvero non vuoi eseguire ld, ma non fa alcuna differenza a meno che tu non stia cercando di ottimizzare davvero la dimensione del file (non solo la dimensione del segmento di testo). Guarda un'esercitazione Whirlwind sulla creazione di eseguibili ELF Really Teensy per Linux .
Peter Cordes,

1
Forse stai pensando a C #, o std::vectorcompilato in modalità debug. Le matrici C ++ non sono così. I compilatori possono controllare le cose in fase di compilazione, ma a meno che non si abilitino ulteriori opzioni di indurimento, non esiste un controllo del tempo di esecuzione. Vedi ad esempio una funzione che incrementa i primi 1024 elementi di un int array[]arg. L'output asm non ha controlli di runtime: godbolt.org/g/w1HF5t . Tutto ciò che ottiene è un puntatore rdi, nessuna informazione sulla dimensione. Spetta al programmatore evitare comportamenti indefiniti non chiamandolo mai con un array inferiore a 1024.
Peter Cordes

1
Qualunque cosa tu stia parlando non è un semplice array C ++ (alloca con new, elimina manualmente con delete, nessun controllo dei limiti). È possibile utilizzare C ++ per produrre asm / code-machine gonfiati di merda (come la maggior parte dei software), ma è colpa del programmatore, non di C ++. È anche possibile utilizzare allocaper allocare lo spazio dello stack come un array.
Peter Cordes,

1
Collega un esempio su gcc.godbolt.org di g++ -O3generazione di codice per il controllo dei limiti per un array semplice o di fare qualsiasi altra cosa tu stia parlando. Il C ++ rende molto più semplice generare binari gonfiati (e in effetti devi stare attento a non puntare alle prestazioni), ma non è letteralmente inevitabile. Se capisci come il C ++ si compila in asm, puoi ottenere codice che è solo un po 'peggio di quanto potresti scrivere a mano, ma con inline e propagazione costante su una scala più ampia di quella che potresti gestire a mano.
Peter Cordes,

-3

L'assemblaggio potrebbe essere più veloce se il compilatore genera molto codice di supporto OO .

Modificare:

Ai downvoter: l'OP ha scritto "dovrei ... concentrarmi sul C ++ e dimenticare il linguaggio assembly?" e resto in attesa della mia risposta. Devi sempre tenere d'occhio il codice generato da OO, in particolare quando usi i metodi. Non dimenticare il linguaggio dell'assembly significa che dovrai rivedere periodicamente l'assemblaggio generato dal tuo codice OO, che credo sia indispensabile per scrivere software ben funzionante.

In realtà, ciò riguarda tutto il codice compilabile, non solo OO.


2
-1: Non vedo alcuna funzione OO in uso. Il tuo argomento è lo stesso di "assembly potrebbe anche essere più veloce se il tuo compilatore aggiunge un milione di NOP".
Sjoerd

Non ero chiaro, questa è in realtà una domanda C. Se scrivi codice C per un compilatore C ++ non stai scrivendo codice C ++ e non otterrai alcun oggetto OO. Una volta che inizi a scrivere in C ++ reale, usando roba OO devi essere molto ben informato per far sì che il compilatore non generi codice di supporto OO.
Olof Forshell l'

quindi la tua risposta non riguarda la domanda? (Inoltre, i chiarimenti vanno nella risposta, non nei commenti. I commenti possono essere eliminati in qualsiasi momento senza preavviso, notifica o cronologia.
Mooing Duck

1
Non sono sicuro di cosa intendi esattamente con "codice di supporto" OO. Ovviamente, se usi molto RTTI e simili, il compilatore dovrà creare molte istruzioni extra per supportare quelle funzionalità - ma qualsiasi problema che sia sufficientemente alto livello per ratificare l'uso di RTTI è troppo complesso per essere fattibile scrivibile in assembly . Quello che puoi fare, ovviamente, è scrivere solo l'interfaccia esterna astratta come OO, inviando un codice procedurale puro ottimizzato per le prestazioni in cui è critico. Ma, a seconda dell'applicazione, C, Fortran, CUDA o semplicemente C ++ senza eredità virtuale potrebbero essere migliori dell'assemblaggio qui.
lasciato circa l'

2
No. Almeno non molto probabilmente. C'è una cosa in C ++ chiamata regola zero overhead, e questo vale per la maggior parte del tempo. Ulteriori informazioni su OO: scoprirai che alla fine migliora la leggibilità del codice, migliora la qualità del codice, aumenta la velocità di codifica, aumenta la robustezza. Anche per embedded - ma usa C ++ perché ti dà più controllo, embedded + OO come ti costerà Java.
Zane,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.