Quando l'assemblaggio è più veloce di C?


475

Uno dei motivi dichiarati per conoscere assemblatore è che, a volte, può essere impiegato per scrivere codice che sarà più performante che scrivere quel codice in un linguaggio di livello superiore, C in particolare. Tuttavia, ho anche sentito molte volte affermare che, sebbene ciò non sia del tutto falso, i casi in cui l'assemblatore può effettivamente essere utilizzato per generare codice più performante sono entrambi estremamente rari e richiedono una conoscenza e un'esperienza esperta dell'assemblaggio.

Questa domanda non comprende nemmeno il fatto che le istruzioni dell'assemblatore saranno specifiche della macchina e non portatili, o uno qualsiasi degli altri aspetti dell'assemblatore. Ci sono molte buone ragioni per conoscere assembly oltre a questo, ovviamente, ma si intende che si tratta di una domanda specifica che richiede esempi e dati, non un discorso esteso sull'assemblatore rispetto ai linguaggi di livello superiore.

Qualcuno può fornire alcuni esempi specifici di casi in cui l'assemblaggio sarà più veloce del codice C ben scritto utilizzando un compilatore moderno e può supportare tale affermazione con prove di profilazione? Sono abbastanza fiducioso che questi casi esistano, ma voglio davvero sapere esattamente quanto siano esoterici questi casi, dal momento che sembra essere un punto di contesa.


17
in realtà è abbastanza banale migliorare il codice compilato. Chiunque abbia una solida conoscenza del linguaggio assembly e C può vederlo esaminando il codice generato. Qualsiasi facile è la prima scogliera di performance di cui cadi quando esaurisci i registri monouso nella versione compilata. In media il compilatore farà molto meglio di un essere umano per un progetto di grandi dimensioni, ma non è difficile in un progetto di dimensioni decenti trovare problemi di prestazioni nel codice compilato.
old_timer

14
In realtà, la risposta breve è: L'assemblatore è sempre più veloce o uguale alla velocità di C. Il motivo è che puoi avere l'assemblaggio senza C, ma non puoi avere C senza assemblaggio (nella forma binaria, che noi nel vecchio giorni chiamati "codice macchina"). Detto questo, la risposta lunga è: i compilatori C sono abbastanza bravi nell'ottimizzare e "pensare" a cose a cui di solito non pensi, quindi dipende davvero dalle tue abilità, ma normalmente puoi sempre battere il compilatore C; è ancora solo un software che non può pensare e ottenere idee. Puoi anche scrivere assemblatore portatile se usi le macro e sei paziente.

11
Non sono assolutamente d'accordo sul fatto che le risposte a questa domanda debbano essere "basate sull'opinione" - possono essere piuttosto oggettive - non è qualcosa come cercare di confrontare le prestazioni delle lingue preferite degli animali domestici, per le quali ognuna avrà punti di forza e svantaggi. Si tratta di capire fino a che punto i compilatori possono portarci e da quale punto è meglio prendere il controllo.
jsbueno,

21
All'inizio della mia carriera, stavo scrivendo molti assemblatori di C e mainframe in una società di software. Uno dei miei coetanei era quello che definirei un "purista assemblatore" (tutto doveva essere assemblatore), quindi scommetto che avrei potuto scrivere una data routine che correva più veloce in C di quella che poteva scrivere in assemblatore. Ho vinto. Ma per finire, dopo che ho vinto, gli ho detto che volevo una seconda scommessa - che avrei potuto scrivere qualcosa di più veloce in assemblatore rispetto al programma C che lo aveva battuto nella puntata precedente. Anche io ho vinto quello, dimostrando che la maggior parte dipende dall'abilità e dall'abilità del programmatore più di ogni altra cosa.
Valerie R,

3
A meno che il tuo cervello non abbia una -O3bandiera, probabilmente stai meglio lasciando l'ottimizzazione al compilatore C :-)
paxdiablo

Risposte:


272

Ecco un esempio del mondo reale: il punto fisso si moltiplica sui vecchi compilatori.

Questi non solo sono utili su dispositivi senza virgola mobile, brillano anche quando si tratta di precisione, poiché offrono 32 bit di precisione con un errore prevedibile (il galleggiante ha solo 23 bit ed è più difficile prevedere la perdita di precisione). cioè precisione assoluta uniforme su tutta la gamma, anziché precisione relativa quasi uniforme ( float).


I compilatori moderni ottimizzano bene questo esempio a virgola fissa, quindi per esempi più moderni che richiedono ancora un codice specifico del compilatore, vedere


C non ha un operatore a moltiplicazione completa (risultato 2N-bit dagli ingressi N-bit). Il solito modo per esprimerlo in C è quello di trasmettere gli input al tipo più ampio e sperare che il compilatore riconosca che i bit superiori degli input non sono interessanti:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

Il problema con questo codice è che facciamo qualcosa che non può essere espresso direttamente nel linguaggio C. Vogliamo moltiplicare due numeri a 32 bit e ottenere un risultato a 64 bit di cui restituiamo il medio a 32 bit. Tuttavia, in C questa moltiplicazione non esiste. Tutto quello che puoi fare è promuovere gli interi a 64 bit e fare una moltiplicazione 64 * 64 = 64.

x86 (e ARM, MIPS e altri) possono comunque fare il moltiplicarsi in una singola istruzione. Alcuni compilatori erano soliti ignorare questo fatto e generare codice che chiama una funzione di libreria di runtime per fare il moltiplicare. Lo spostamento di 16 viene anche spesso eseguito da una routine di libreria (anche l'x86 può fare tali spostamenti).

Quindi rimaniamo con una o due chiamate in libreria solo per un moltiplicarsi. Ciò ha gravi conseguenze. Non solo lo spostamento è più lento, i registri devono essere preservati attraverso le chiamate di funzione e non aiuta nemmeno a allineare e srotolare il codice.

Se si riscrive lo stesso codice nell'assemblatore (inline) è possibile ottenere un aumento di velocità significativo.

Inoltre: l'utilizzo di ASM non è il modo migliore per risolvere il problema. La maggior parte dei compilatori consente di utilizzare alcune istruzioni assembler in forma intrinseca se non è possibile esprimerle in C. Ad esempio il compilatore VS.NET2008 espone il mul 32 * 32 = 64 bit come __emul e lo spostamento a 64 bit come __ll_rshift.

Utilizzando intrinseci è possibile riscrivere la funzione in modo che il compilatore C abbia la possibilità di capire cosa sta succedendo. Ciò consente di incorporare il codice, allocare i registri, eliminare anche la sottoespressione comune e la propagazione costante. In questo modo otterrai un enorme miglioramento delle prestazioni rispetto al codice assembler scritto a mano.

Per riferimento: il risultato finale per il mul punto fisso per il compilatore VS.NET è:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

La differenza di prestazioni delle divisioni in punti fissi è ancora maggiore. Ho avuto miglioramenti fino al fattore 10 per il codice a virgola fissa della divisione scrivendo un paio di righe asm.


L'uso di Visual C ++ 2013 fornisce lo stesso codice assembly in entrambi i modi.

gcc4.1 del 2007 ottimizza anche la versione C pura. (L'esploratore del compilatore Godbolt non ha alcuna versione precedente di gcc installata, ma presumibilmente anche le versioni GCC più vecchie potrebbero farlo senza intrinsechi.)

Vedi source + asm per x86 (32-bit) e ARM sull'esploratore del compilatore Godbolt . (Sfortunatamente non ha compilatori abbastanza vecchi da produrre codice errato dalla semplice versione in puro C.)


Le moderne CPU possono fare cose che C non ha operatori per niente , come popcnto bit-scan per trovare il primo o l'ultimo bit impostato . (POSIX ha una ffs()funzione, ma la sua semantica non corrisponde a x86 bsf/ bsr. Vedi https://en.wikipedia.org/wiki/Find_first_set ).

Alcuni compilatori a volte riconoscono un ciclo che conta il numero di bit impostati in un numero intero e lo compila in popcntun'istruzione (se abilitato in fase di compilazione), ma è molto più affidabile da usare __builtin_popcntin GNU C o su x86 se sei solo targeting hardware con SSE4.2: _mm_popcnt_u32da<immintrin.h> .

O in C ++, assegnare a std::bitset<32>e utilizzare .count(). (Questo è un caso in cui il linguaggio ha trovato un modo per esporre in modo portabile un'implementazione ottimizzata di popcount attraverso la libreria standard, in un modo che si compili sempre in qualcosa di corretto e può trarre vantaggio da qualunque cosa il target supporti.) Vedi anche https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .

Allo stesso modo, ntohlpuò essere compilato in bswap(scambio di byte x86 a 32 bit per la conversione endian) su alcune implementazioni C che lo hanno.


Un'altra area importante per intrinseche o scritte a mano è la vettorializzazione manuale con istruzioni SIMD. I compilatori non sono male con semplici loop come dst[i] += src[i] * 10.0;, ma spesso fanno male o non si auto-vettorizzano affatto quando le cose si complicano. Ad esempio, è improbabile che tu ottenga qualcosa di simile Come implementare atoi usando SIMD? generato automaticamente dal compilatore dal codice scalare.


6
Che ne dici di cose come {x = c% d; y = c / d;}, i compilatori sono abbastanza intelligenti da renderlo un singolo div o idiv?
Jens Björnhager,

4
In realtà, un buon compilatore produrrebbe il codice ottimale dalla prima funzione. Oscurare il codice sorgente con intrinseci o inline assembly senza alcun vantaggio non è la cosa migliore da fare.
fannullone

65
Ciao Slacker, penso che non hai mai lavorato prima su un codice critico in termini di tempo ... l'assemblaggio in linea può fare una * grande differenza. Anche per il compilatore un intrinseco è lo stesso dell'aritmetica normale in C. Questo è il punto intrinseco. Ti consentono di utilizzare una funzionalità di architettura senza dover affrontare gli svantaggi.
Nils Pipenbrinck,

6
@slacker In realtà, il codice qui è abbastanza leggibile: il codice inline esegue un'operazione univoca, che è immediatamente instabile leggendo la firma del metodo. Il codice ha perso solo lentamente in leggibilità quando viene utilizzata un'istruzione oscura. Ciò che conta qui è che abbiamo un metodo che fa solo un'operazione chiaramente identificabile, ed è davvero il modo migliore per produrre codice leggibile queste funzioni atomiche. A proposito, questo non è così oscuro un piccolo commento come / * (a * b) >> 16 * / non può spiegarlo immediatamente.
Dereckson,

5
Ad essere onesti, questo esempio è scarso, almeno oggi. I compilatori C sono stati a lungo in grado di fare una moltiplicazione 32x32 -> 64 anche se la lingua non lo offre direttamente: riconoscono che quando si lanciano argomenti a 32 bit a 64 bit e quindi li si moltiplica, non è necessario eseguire una moltiplicazione completa a 64 bit, ma che un 32x32 -> 64 andrà bene. Ho controllato e tutti i clang, gcc e MSVC nella loro versione attuale hanno ragione . Non è una novità: ricordo di aver guardato l'output del compilatore e di aver notato questo un decennio fa.
BeeOnRope

143

Molti anni fa insegnavo a qualcuno a programmare in C. L'esercizio consisteva nel ruotare un grafico di 90 gradi. È tornato con una soluzione che ha richiesto diversi minuti per completare, principalmente perché stava usando moltiplicazioni e divisioni ecc.

Gli ho mostrato come rifondere il problema usando i bit shift e il tempo di elaborazione è sceso a circa 30 secondi sul compilatore non ottimizzato che aveva.

Avevo appena ottenuto un compilatore ottimizzante e lo stesso codice ruotava l'immagine in <5 secondi. Ho guardato il codice assembly che il compilatore stava generando, e da quello che ho visto lì deciso e poi che i miei giorni di scrittura assembler erano finiti.


3
Sì, era un sistema monocromatico a un bit, in particolare erano i blocchi di immagini monocromatici su un Atari ST.
lilburne,

16
Il compilatore di ottimizzazione ha compilato il programma originale o la tua versione?
Thorbjørn Ravn Andersen,

Su quale processore? Su 8086, mi aspetto che il codice ottimale per una rotazione 8x8 caricherà DI con 16 bit di dati usando SI, ripeti add di,di / adc al,al / add di,di / adc ah,ahecc. Per tutti gli otto registri a 8 bit, quindi ripeti tutti gli 8 registri, quindi ripeti l'intera procedura tre più volte e infine salva quattro parole in ax / bx / cx / dx. In nessun modo un assemblatore si avvicinerà a questo.
supercat

1
Non riesco davvero a pensare a nessuna piattaforma in cui un compilatore potrebbe rientrare in un fattore o due del codice ottimale per una rotazione 8x8.
supercat

65

Praticamente ogni volta che il compilatore vede un codice in virgola mobile, una versione scritta a mano sarà più veloce se stai usando un vecchio compilatore non valido. ( Aggiornamento 2019: questo non è vero in generale per i compilatori moderni. Soprattutto quando si compila per qualcosa di diverso da x87; i compilatori hanno un tempo più facile con SSE2 o AVX per la matematica scalare o qualsiasi non-x86 con un set di registri FP piatto, a differenza di x87 pila di registri.)

Il motivo principale è che il compilatore non può eseguire alcuna ottimizzazione efficace. Vedi questo articolo da MSDN per una discussione sull'argomento. Ecco un esempio in cui la versione dell'assembly ha una velocità doppia rispetto alla versione C (compilata con VS2K5):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

E alcuni numeri dal mio PC che eseguono una build di versione predefinita * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

Per interesse, ho scambiato il loop con un dec / jnz e non ha fatto alcuna differenza nei tempi - a volte più veloce, a volte più lento. Immagino che l'aspetto limitato della memoria nani altre ottimizzazioni. (Nota del redattore: è più probabile che il collo di bottiglia della latenza FP sia sufficiente a nascondere il costo aggiuntivo di loop. Fare due somme Kahan in parallelo per gli elementi pari / dispari e aggiungere quelli alla fine, potrebbe forse accelerare di un fattore 2. )

Spiacenti, stavo eseguendo una versione leggermente diversa del codice e ha emesso i numeri nel modo sbagliato (ovvero C era più veloce!). Risolti e aggiornati i risultati.


20
O in GCC, puoi sciogliere le mani del compilatore sull'ottimizzazione in virgola mobile (purché tu prometta di non fare nulla con infiniti o NaN) usando il flag -ffast-math. Hanno un livello di ottimizzazione, -Ofastche è attualmente equivalente -O3 -ffast-math, ma in futuro potrebbe includere più ottimizzazioni che possono portare a una generazione errata di codice in casi angolari (come il codice che si basa su NaN IEEE).
David Stone,

2
Sì, i float non sono commutativi, il compilatore deve fare ESATTAMENTE quello che hai scritto, in sostanza quello che ha detto @DavidStone.
Alec Teal,

2
Hai provato la matematica SSE? Le prestazioni sono state una delle ragioni per cui MS ha abbandonato completamente x87 in x86_64 e il doppio di 80 bit in x86
phuclv

4
@Praxeolitic: add FP è commutativo ( a+b == b+a), ma non associativo (riordino delle operazioni, quindi l'arrotondamento degli intermedi è diverso). ri: questo codice: non credo che l'X87 non commentato e loopun'istruzione siano una dimostrazione fantastica di asm veloce. loopapparentemente non è in realtà un collo di bottiglia a causa della latenza del PQ. Non sono sicuro che stia organizzando o meno le operazioni FP; x87 è difficile da leggere per gli umani. Due fstp resultsinsn alla fine non sono chiaramente ottimali. Interrompere il risultato extra dallo stack sarebbe meglio fare con un non store. Come fstp st(0)IIRC.
Peter Cordes,

2
@PeterCordes: Una conseguenza interessante del rendere commutativa l'addizione è che mentre 0 + x e x + 0 sono equivalenti tra loro, nessuno dei due è sempre equivalente a x.
supercat

58

Senza fornire alcun esempio specifico o prova del profiler, puoi scrivere un assemblatore migliore del compilatore quando ne conosci più del compilatore.

In generale, un moderno compilatore C sa molto di più su come ottimizzare il codice in questione: sa come funziona la pipeline del processore, può provare a riordinare le istruzioni più velocemente di una lattina umana, e così via - è sostanzialmente lo stesso di un computer buono o migliore del miglior giocatore umano per i giochi da tavolo, ecc. semplicemente perché può effettuare ricerche nello spazio problematico più velocemente della maggior parte degli umani. Sebbene teoricamente sia possibile eseguire sia il computer in un caso specifico, di certo non è possibile farlo alla stessa velocità, rendendolo impossibile per più di alcuni casi (vale a dire che il compilatore sicuramente supererà se si tenta di scrivere più di alcune routine in assemblatore).

D'altra parte, ci sono casi in cui il compilatore non ha tante informazioni - direi principalmente quando si lavora con diverse forme di hardware esterno, di cui il compilatore non ha conoscenza. L'esempio principale è probabilmente rappresentato dai driver di dispositivo, in cui l'assemblatore combinato con l'intima conoscenza dell'hardware in questione può produrre risultati migliori di quelli che un compilatore C potrebbe fare.

Altri hanno menzionato istruzioni per scopi speciali, che è ciò di cui sto parlando nel paragrafo sopra - istruzioni di cui il compilatore potrebbe avere conoscenze limitate o nessuna conoscenza, rendendo possibile per un essere umano scrivere codice più veloce.


In generale, questa affermazione è vera. Il compilatore fa meglio con DWIW, ma in alcuni casi limite l'assemblatore di codifica manuale esegue il lavoro quando le prestazioni in tempo reale sono indispensabili.
spoulson,

1
@Liedman: "può provare a riordinare le istruzioni più velocemente di una lattina umana". OCaml è noto per essere veloce e, sorprendentemente, il suo compilatore di codice nativo ocamloptsalta la programmazione delle istruzioni su x86 e, invece, lo lascia alla CPU perché può riordinare in modo più efficace in fase di esecuzione.
Jon Harrop,

1
I compilatori moderni fanno molto, e ci vorrebbe troppo tempo per farlo a mano, ma non sono affatto perfetti. Cerca i bug tracker di gcc o llvm per bug "mancati di ottimizzazione". Ci sono molti. Inoltre, quando si scrive in asm, è possibile trarre più facilmente vantaggio da condizioni come "questo input non può essere negativo" che sarebbe difficile per un compilatore dimostrare.
Peter Cordes,

48

Nel mio lavoro, ci sono tre ragioni per conoscere e usare il montaggio. In ordine di importanza:

  1. Debug - Ricevo spesso codice di libreria che presenta bug o documentazione incompleta. Capisco cosa sta facendo intervenendo a livello di assemblaggio. Devo farlo circa una volta alla settimana. Lo uso anche come strumento per il debug di problemi in cui i miei occhi non individuano l'errore idiomatico in C / C ++ / C #. Guardare l'assemblea lo supera.

  2. Ottimizzazione: il compilatore funziona abbastanza bene nell'ottimizzazione, ma io gioco in un campo da baseball diverso rispetto alla maggior parte. Scrivo codice di elaborazione delle immagini che di solito inizia con un codice simile al seguente:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    il "fai qualcosa" avviene in genere nell'ordine di diversi milioni di volte (cioè tra 3 e 30). Raschiando i cicli in quella fase "fai qualcosa", i guadagni delle prestazioni sono enormemente amplificati. Di solito non inizio da lì - di solito inizio scrivendo prima il codice per funzionare, quindi faccio del mio meglio per riformattare la C in modo che sia naturalmente migliore (algoritmo migliore, meno carico nel ciclo, ecc.). Di solito ho bisogno di leggere assembly per vedere cosa sta succedendo e raramente ho bisogno di scriverlo. Lo faccio forse ogni due o tre mesi.

  3. facendo qualcosa che la lingua non mi permetterà. Questi includono: ottenere l'architettura del processore e funzionalità specifiche del processore, accedere a flag non nella CPU (amico, vorrei davvero che C ti avesse dato accesso al flag carry), ecc. Lo faccio forse una volta all'anno o due anni.


Non affianchi i tuoi anelli? :-)
Jon Harrop,

1
@plinth: come intendi "cicli di raschiatura"?
lang2

@ lang2: significa sbarazzarsi di tutto il tempo superfluo trascorso nel ciclo interno - tutto ciò che il compilatore non è riuscito a estrarre, che può includere l'uso dell'algebra per sollevare un moltiplicarsi da un ciclo per renderlo un add all'interno, ecc.
zoccolo

1
La piastrellatura ad anello sembra non essere necessaria se si effettua un solo passaggio sui dati.
James M. Lay

@ JamesM.Lay: se tocchi ogni elemento solo una volta, un ordine di attraversamento migliore può darti una località spaziale. (ad esempio, utilizza tutti i byte di una riga della cache che hai toccato, anziché eseguire il loop down delle colonne di una matrice utilizzando un elemento per riga della cache.)
Peter Cordes,

42

Solo quando si utilizzano alcune istruzioni per scopi speciali il compilatore non supporta.

Per massimizzare la potenza di calcolo di una CPU moderna con più pipeline e diramazioni predittive è necessario strutturare il programma di assemblaggio in modo da renderlo a) quasi impossibile per un umano scrivere b) ancora più impossibile da mantenere.

Inoltre, algoritmi, strutture di dati e gestione della memoria migliori offriranno almeno un ordine di grandezza in più di prestazioni rispetto alle microottimizzazioni che è possibile eseguire nell'assemblaggio.


4
+1, anche se l'ultima frase in realtà non appartiene a questa discussione - si potrebbe presumere che l'assemblatore entri in gioco solo dopo aver realizzato tutti i possibili miglioramenti dell'algoritmo ecc.
mghie,

18
@Matt: ASM scritto a mano è spesso molto meglio su alcune delle piccole CPU con cui EE lavora con un supporto scadente del compilatore del fornitore.
Zan Lynx,

5
"Solo quando si utilizzano alcuni set di istruzioni per scopi speciali" ?? Probabilmente non hai mai scritto un pezzo di codice asm ottimizzato a mano prima. Una conoscenza moderatamente intima dell'architettura su cui stai lavorando ti dà buone possibilità di generare un codice (dimensioni e velocità) migliore rispetto al tuo compilatore. Ovviamente, come ha commentato @mghie, inizi sempre a scrivere i migliori algoritmi che puoi trovare per il tuo problema. Anche per compilatori molto bravi, devi davvero scrivere il tuo codice C in un modo che porti il ​​compilatore al miglior codice compilato. In caso contrario, il codice generato sarà non ottimale.

2
@ysap: su computer reali (non piccoli chip integrati con potenza insufficiente) nell'uso del mondo reale, il codice "ottimale" non sarà più veloce perché per qualsiasi set di dati di grandi dimensioni le prestazioni saranno limitate dall'accesso alla memoria e dagli errori di pagina ( e se non disponi di un set di dati di grandi dimensioni, questo sarà veloce in entrambi i casi e non ha senso ottimizzarlo) - quei giorni lavoro principalmente in C # (nemmeno c) e le prestazioni aumentano dal gestore della memoria compattante in uscita - ponderare il sovraccarico della garbage collection, compattazione e compilazione JIT.
Nir

4
+1 per affermare che i compilatori (in particolare JIT) possono fare un lavoro migliore rispetto agli umani, se sono ottimizzati per l'hardware su cui vengono eseguiti.
Sebastian,

38

Sebbene C sia "vicino" alla manipolazione di basso livello di dati a 8 bit, 16 bit, 32 bit, 64 bit, ci sono alcune operazioni matematiche non supportate da C che spesso possono essere eseguite elegantemente in alcune istruzioni di assemblaggio imposta:

  1. Moltiplicazione a virgola fissa: il prodotto di due numeri a 16 bit è un numero a 32 bit. Ma le regole in C dicono che il prodotto di due numeri a 16 bit è un numero a 16 bit, e il prodotto di due numeri a 32 bit è un numero a 32 bit - la metà inferiore in entrambi i casi. Se vuoi la metà superiore di una moltiplicazione 16x16 o una moltiplicazione 32x32, devi giocare con il compilatore. Il metodo generale è eseguire il cast su una larghezza di bit maggiore del necessario, moltiplicare, spostare verso il basso e tornare indietro:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    In questo caso il compilatore potrebbe essere abbastanza intelligente da sapere che stai davvero cercando di ottenere la metà superiore di un moltiplicatore 16x16 e fare la cosa giusta con la moltiplicazione 16x16 nativa della macchina. Oppure può essere stupido e richiedere una chiamata in libreria per eseguire la moltiplicazione 32x32 che è eccessivo perché hai solo bisogno di 16 bit del prodotto, ma lo standard C non ti dà modo di esprimerti.

  2. Alcune operazioni di bitshifting (rotazione / carry):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    Questo non è troppo inelegante in C, ma di nuovo, a meno che il compilatore non sia abbastanza intelligente da capire cosa stai facendo, farà molto lavoro "non necessario". Molti set di istruzioni di assemblaggio consentono di ruotare o spostare a sinistra / a destra con il risultato nel registro carry, in modo da poter eseguire quanto sopra in 34 istruzioni: caricare un puntatore all'inizio dell'array, cancellare il carry ed eseguire 32 8- bit sposta a destra, usando l'incremento automatico sul puntatore.

    Per un altro esempio, ci sono registri a spostamento lineare di feedback (LFSR) che sono elegantemente eseguiti nell'assemblaggio: prendi un pezzo di N bit (8, 16, 32, 64, 128, ecc.), Sposta l'intera cosa a destra di 1 (vedi sopra algoritmo), quindi se il carry risultante è 1, allora XOR in un pattern di bit che rappresenta il polinomio.

Detto questo, non ricorrerei a queste tecniche se non avessi seri limiti di prestazione. Come altri hanno già detto, l'assemblaggio è molto più difficile da documentare / eseguire il debug / test / mantenere rispetto al codice C: il miglioramento delle prestazioni comporta alcuni costi importanti.

modifica: 3. Il rilevamento di overflow è possibile nell'assembly (non è possibile farlo in C), questo rende alcuni algoritmi molto più facili.


23

Risposta breve? Qualche volta.

Tecnicamente ogni astrazione ha un costo e un linguaggio di programmazione è un'astrazione per il funzionamento della CPU. C comunque è molto vicino. Anni fa ricordo di aver riso a crepapelle quando ho effettuato l'accesso al mio account UNIX e ho ricevuto il seguente messaggio di fortuna (quando tali cose erano popolari):

Il linguaggio di programmazione C - Un linguaggio che combina la flessibilità del linguaggio assembly con la potenza del linguaggio assembly.

È divertente perché è vero: C è come un linguaggio di assemblaggio portatile.

Vale la pena notare che il linguaggio assembly funziona solo nel modo in cui lo scrivi. C'è comunque un compilatore tra C e il linguaggio assembly che genera e questo è estremamente importante perché la velocità con cui il tuo codice C ha a che fare ha molto a che fare con quanto è buono il tuo compilatore.

Quando gcc è apparso sulla scena, una delle cose che lo ha reso così popolare è che spesso era molto meglio dei compilatori C forniti con molte versioni UNIX commerciali. Non solo era ANSI C (nessuna di queste immondizie di K&R C), ma era anche più robusto e in genere produceva un codice migliore (più veloce). Non sempre ma spesso.

Vi dico tutto questo perché non esiste una regola generale sulla velocità di C e assemblatore perché non esiste uno standard oggettivo per C.

Allo stesso modo, l'assemblatore varia molto a seconda del processore in uso, delle specifiche del sistema, del set di istruzioni in uso e così via. Storicamente ci sono state due famiglie di architettura CPU: CISC e RISC. Il più grande player in CISC era ed è ancora l'architettura Intel x86 (e il set di istruzioni). RISC ha dominato il mondo UNIX (MIPS6000, Alpha, Sparc e così via). CISC ha vinto la battaglia per i cuori e le menti.

Comunque, la saggezza popolare quando ero uno sviluppatore più giovane era che x86 scritto a mano poteva spesso essere molto più veloce di C perché il modo in cui l'architettura funzionava aveva una complessità che beneficiava di un essere umano. RISC d'altra parte sembrava progettato per i compilatori, quindi nessuno (lo sapevo) ha scritto dire assemblatore Sparc. Sono sicuro che esistessero persone del genere, ma senza dubbio sono diventate entrambe pazze e ormai sono state istituzionalizzate.

I set di istruzioni sono un punto importante anche nella stessa famiglia di processori. Alcuni processori Intel hanno estensioni come SSE tramite SSE4. AMD aveva le proprie istruzioni SIMD. Il vantaggio di un linguaggio di programmazione come C era che qualcuno poteva scrivere la propria libreria, quindi era ottimizzato per qualsiasi processore su cui stavate lavorando. È stato un duro lavoro in assemblatore.

Ci sono ancora ottimizzazioni che puoi fare nell'assemblatore che nessun compilatore potrebbe fare e un algoirthm assemblatore ben scritto sarà più veloce o più veloce dell'equivalente C. La domanda più grande è: ne vale la pena?

Alla fine però l'assemblatore era un prodotto del suo tempo ed era più popolare in un momento in cui i cicli della CPU erano costosi. Oggi una CPU che costa $ 5-10 per la produzione (Intel Atom) può fare praticamente tutto ciò che chiunque potrebbe desiderare. L'unico vero motivo per scrivere assembler in questi giorni è per cose di basso livello come alcune parti di un sistema operativo (anche se la stragrande maggioranza del kernel Linux è scritto in C), driver di dispositivo, possibilmente dispositivi embedded (anche se C tende a dominare lì anche) e così via. O solo per i calci (che è alquanto masochista).


C'erano molte persone che usavano l'assemblatore ARM come lingua preferita nelle macchine Acorn (primi anni '90). IIRC hanno detto che il piccolo set di istruzioni di risc ha reso più facile e divertente. Ma sospetto che sia perché il compilatore C è stato un arrivo in ritardo per Acorn e il compilatore C ++ non è mai stato completato.
Andrew M,

3
"... perché non esiste uno standard soggettivo per C." Intendi obiettivo .
Thomas,

@AndrewM: Sì, ho scritto applicazioni in lingua mista in assemblatore BASIC e ARM per circa 10 anni. Ho imparato C in quel periodo, ma non è stato molto utile perché è ingombrante come assemblatore e più lento. Norcroft ha fatto delle fantastiche ottimizzazioni, ma penso che il set di istruzioni condizionate sia stato un problema per i compilatori del giorno.
Jon Harrop,

1
@AndrewM: beh, in realtà ARM è una specie di RISC fatto al contrario. Altri ISA RISC sono stati progettati a partire da ciò che un compilatore userebbe. ARM ISA sembra essere stato progettato a partire da ciò che offre la CPU (cambio barilotto, flag delle condizioni → esponiamoli in ogni istruzione).
ninjalj,

16

Un caso d'uso che potrebbe non valere più ma per il tuo piacere da secchione: sull'Amiga, la CPU e i chip grafici / audio avrebbero lottato per accedere a una determinata area di RAM (i primi 2 MB di RAM per essere specifici). Quindi, quando avevi solo 2 MB di RAM (o meno), la visualizzazione di grafica complessa e la riproduzione del suono avrebbero ucciso le prestazioni della CPU.

In assemblatore, è possibile interlacciare il codice in modo così intelligente che la CPU tenterebbe di accedere alla RAM solo quando i chip grafici / audio erano occupati internamente (ovvero quando il bus era libero). Quindi, riordinando le tue istruzioni, l'uso intelligente della cache della CPU, i tempi del bus, potresti ottenere alcuni effetti che non erano semplicemente possibili usando un linguaggio di livello superiore perché dovevi temporizzare ogni comando, persino inserire NOP qua e là per mantenere i vari trucioli l'uno dall'altro radar.

Questo è un altro motivo per cui l'istruzione NOP (Nessuna operazione - non fare nulla) della CPU può effettivamente rendere l'intera applicazione più veloce.

[EDIT] Naturalmente, la tecnica dipende da una specifica configurazione hardware. Qual è stato il motivo principale per cui molti giochi Amiga non sono stati in grado di far fronte a CPU più veloci: il tempismo delle istruzioni era spento.


L'Amiga non aveva 16 MB di RAM di chip, più come da 512 kB a 2 MB a seconda del chipset. Inoltre, molti giochi Amiga non funzionavano con CPU più veloci a causa di tecniche come la descrivi.
bk1e,

1
@ bk1e - Amiga ha prodotto una vasta gamma di diversi modelli di computer, l'Amiga 500 è stato spedito con ram da 512K estesa a 1Meg nel mio caso. amigahistory.co.uk/amiedevsys.html è un amiga con 128Meg Ram
David Waters,

@ bk1e: rimango corretto. La mia memoria potrebbe fallire, ma la RAM del chip non era limitata al primo spazio di indirizzi a 24 bit (ovvero 16 MB)? E Fast è stato mappato sopra quello?
Aaron Digulla,

@Aaron Digulla: Wikipedia ha maggiori informazioni sulle distinzioni tra RAM chip / veloce / lenta: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e: il mio errore. La CPU 68k aveva solo 24 corsie di indirizzi, ecco perché avevo in testa i 16 MB.
Aaron Digulla,

15

Punto uno che non è la risposta.
Anche se non ci si programma mai, trovo utile conoscere almeno un set di istruzioni assembler. Questo fa parte della ricerca senza fine dei programmatori di saperne di più e quindi di essere migliore. Utile anche quando si entra in framework in cui non si dispone del codice sorgente e si ha almeno un'idea approssimativa di ciò che sta succedendo. Ti aiuta anche a capire JavaByteCode e .Net IL in quanto sono entrambi simili all'assemblatore.

Per rispondere alla domanda quando hai una piccola quantità di codice o una grande quantità di tempo. Utile soprattutto per l'uso in chip integrati, in cui la bassa complessità dei chip e la scarsa concorrenza nei compilatori destinati a questi chip possono favorire l'equilibrio a favore degli umani. Anche per i dispositivi con restrizioni stai spesso scambiando le dimensioni del codice / dimensioni della memoria / prestazioni in un modo che sarebbe difficile istruire un compilatore a fare. ad esempio, so che questa azione dell'utente non viene chiamata spesso, quindi avrò dimensioni di codice ridotte e prestazioni scadenti, ma quest'altra funzione che sembra simile viene utilizzata ogni secondo, quindi avrò una dimensione di codice maggiore e prestazioni più veloci. Questo è il tipo di trade off che un programmatore di assemblee esperto può usare.

Vorrei anche aggiungere che c'è molta via di mezzo in cui è possibile codificare in C compilare ed esaminare l'Assemblea prodotta, quindi modificare il codice C o modificare e mantenere come assembly.

Il mio amico lavora su micro controller, attualmente chip per il controllo di piccoli motori elettrici. Lavora in una combinazione di basso livello ce Assembly. Una volta mi ha raccontato di una buona giornata di lavoro in cui ha ridotto il ciclo principale da 48 istruzioni a 43. Si trova anche di fronte a scelte come il codice è cresciuto per riempire il chip da 256k e l'azienda desidera una nuova funzionalità, vero?

  1. Rimuovi una funzione esistente
  2. Ridurre le dimensioni di alcune o tutte le funzionalità esistenti, forse a scapito delle prestazioni.
  3. Sostenere il passaggio a un chip più grande con un costo più elevato, un consumo di energia più elevato e un fattore di forma più grande.

Vorrei aggiungere come sviluppatore commerciale un bel portfolio o linguaggi, piattaforme, tipi di applicazioni che non ho mai sentito il bisogno di immergermi nella scrittura di un assembly. Ho sempre apprezzato le conoscenze che ho acquisito al riguardo. E a volte debug in esso.

So di aver risposto molto di più alla domanda "perché dovrei imparare l'assemblatore" ma ritengo che sia una domanda più importante di quando sarà più veloce.

quindi proviamo ancora una volta Dovresti pensare all'assemblaggio

  • lavorando sulla funzione del sistema operativo di basso livello
  • Lavorando su un compilatore.
  • Lavorando su un chip estremamente limitato, un sistema integrato ecc

Ricorda di confrontare il tuo assieme con il compilatore generato per vedere quale è più veloce / più piccolo / migliore.

David.


4
+1 per considerare le applicazioni incorporate su piccoli chip. Troppi ingegneri del software qui non considerano incorporato o pensano che significhi uno smartphone (32 bit, MB RAM, MB flash).
Martin,

1
Le applicazioni integrate nel tempo sono un ottimo esempio! Ci sono spesso istruzioni strane (anche molto semplici come avr's sbie cbi) che i compilatori non usavano (e talvolta lo fanno ancora) per non trarre pieno vantaggio, a causa della loro limitata conoscenza dell'hardware.
Felixphew,

15

Sono sorpreso che nessuno l'abbia detto. La strlen()funzione è molto più veloce se scritta in assembly! In C, la cosa migliore che puoi fare è

int c;
for(c = 0; str[c] != '\0'; c++) {}

mentre in assembly è possibile accelerare notevolmente:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

la lunghezza è in ecx. Questo confronta 4 caratteri alla volta, quindi è 4 volte più veloce. E pensate che usando la parola di ordine superiore di eax ed ebx, diventerà 8 volte più veloce della precedente routine C!


3
Come si confronta con quelli in strchr.nfshost.com/optimized_strlen_function ?
ninjalj,

@ninjalj: sono la stessa cosa :) Non pensavo che potesse essere fatto in questo modo in C. Può essere leggermente migliorato penso
BlackBerry

C'è ancora un'operazione AND bit per bit prima di ogni confronto nel codice C. È possibile che il compilatore sia abbastanza intelligente da ridurlo a confronti di byte alti e bassi, ma non scommetterei soldi su di esso. In realtà esiste un algoritmo di ciclo più veloce basato sulla proprietà (word & 0xFEFEFEFF) & (~word + 0x80808080)zero se tutti i byte in parola sono diversi da zero.
user2310967

@MichaWiedenmann vero, dovrei caricare bx dopo aver confrontato i due caratteri in ax. Grazie
BlackBear il

14

Le operazioni con matrici che utilizzano le istruzioni SIMD sono probabilmente più veloci del codice generato dal compilatore.


Alcuni compilatori (il VectorC, se ricordo bene) generano codice SIMD, quindi anche questo probabilmente non è più un argomento per usare il codice assembly.
OregonGhost,

I compilatori creano codice
compatibile con

5
Per molte di queste situazioni è possibile utilizzare SSE intrisics anziché assembly. Questo renderà il tuo codice più portatile (gcc visual c ++, 64bit, 32bit ecc) e non dovrai effettuare l'allocazione dei registri.
Laserallan,

1
Certo che lo faresti, ma la domanda non mi ha posto dove dovrei usare assembly invece di C. Diceva quando il compilatore C non generava un codice migliore. Ho assunto una sorgente C che non utilizza chiamate SSE dirette o assembly in linea.
Mehrdad Afshari,

9
Mehrdad ha ragione, però. Ottenere SSE nel modo giusto è piuttosto difficile per il compilatore e anche in situazioni ovvie (per gli umani, cioè) la maggior parte dei compilatori non lo utilizzano.
Konrad Rudolph,

13

Non posso dare esempi specifici perché era troppi anni fa, ma c'erano molti casi in cui un assemblatore scritto a mano poteva superare qualsiasi compilatore. Le ragioni per cui:

  • Potresti deviare dal chiamare convenzioni, passando argomenti nei registri.

  • È possibile valutare attentamente come utilizzare i registri ed evitare di memorizzare le variabili in memoria.

  • Per cose come le tabelle di salto, potresti evitare di dover controllare i limiti dell'indice.

Fondamentalmente, i compilatori fanno un ottimo lavoro di ottimizzazione, e questo è quasi sempre "abbastanza buono", ma in alcune situazioni (come il rendering grafico) in cui stai pagando caro per ogni singolo ciclo, puoi prendere scorciatoie perché conosci il codice , dove un compilatore non potrebbe perché deve essere al sicuro.

In effetti, ho sentito parlare di alcuni codici di rendering grafico in cui una routine, come una linea di disegno o di riempimento poligonale, ha effettivamente generato un piccolo blocco di codice macchina nello stack ed eseguito lì, in modo da evitare il continuo processo decisionale su stile della linea, larghezza, motivo, ecc.

Detto questo, ciò che voglio che faccia un compilatore è generare un buon codice assembly per me, ma non essere troppo intelligente, e lo fanno principalmente. In effetti, una delle cose che odio di Fortran è la sua confusione nel codice nel tentativo di "ottimizzarlo", di solito senza uno scopo significativo.

Di solito, quando le app hanno problemi di prestazioni, ciò è dovuto a uno spreco di progettazione. In questi giorni, non consiglierei mai l'assemblatore per le prestazioni a meno che l'app complessiva non fosse già stata sintonizzata entro un centimetro dalla sua vita, non fosse ancora abbastanza veloce e passasse tutto il suo tempo in stretti circuiti interni.

Aggiunto: ho visto molte app scritte in linguaggio assembly e il principale vantaggio di velocità rispetto a un linguaggio come C, Pascal, Fortran, ecc. Era perché il programmatore era molto più attento durante la codifica in assembler. Scriverà all'incirca 100 righe di codice al giorno, indipendentemente dalla lingua, e in un linguaggio di compilazione che equivarrà a 3 o 400 istruzioni.


8
+1: "Potresti discostarti dalle convenzioni di chiamata". I compilatori C / C ++ tendono a risucchiare per restituire più valori. Spesso usano il modulo sret in cui lo stack del chiamante alloca un blocco contiguo per uno struct e gli ha passato un riferimento per il compilatore. La restituzione di più valori nei registri è molte volte più veloce.
Jon Harrop,

1
@Jon: i compilatori C / C ++ lo fanno bene quando la funzione viene incorporata (le funzioni non incorporate devono essere conformi all'ABI, questo non è un limite di C e C ++ ma il modello di collegamento)
Ben Voigt

@BenVoigt: ecco un contro esempio flyingfrogblog.blogspot.co.uk/2012/04/…
Jon Harrop

2
Non vedo nessuna chiamata di funzione essere in linea lì.
Ben Voigt,

13

Alcuni esempi della mia esperienza:

  • Accesso a istruzioni non accessibili da C. Ad esempio, molte architetture (come x86-64, IA-64, DEC Alpha e MIPS a 64 bit o PowerPC) supportano una moltiplicazione a 64 bit per 64 bit producendo un risultato a 128 bit. GCC ha recentemente aggiunto un'estensione che fornisce l'accesso a tali istruzioni, ma prima che fosse necessario tale assemblaggio. E l'accesso a queste istruzioni può fare un'enorme differenza sulle CPU a 64 bit quando si implementa qualcosa come RSA, a volte fino a un fattore di miglioramento delle prestazioni 4.

  • Accesso a flag specifici della CPU. Quello che mi ha morso molto è la bandiera carry; quando si esegue un'aggiunta di precisione multipla, se non si ha accesso al bit di trasporto della CPU, è necessario confrontare il risultato per vedere se è traboccato, il che richiede 3-5 istruzioni in più per arto; e peggio ancora, che sono abbastanza seriali in termini di accessi ai dati, il che uccide le prestazioni sui moderni processori superscalari. Quando si elaborano migliaia di tali numeri interi di seguito, essere in grado di utilizzare l'addc è una grande vittoria (ci sono anche problemi superscalari con contesa sul bit di riporto, ma le CPU moderne se la cavano abbastanza bene).

  • SIMD. Anche i compilatori con autovectorizing possono fare solo casi relativamente semplici, quindi se vuoi buone prestazioni SIMD è purtroppo spesso necessario scrivere direttamente il codice. Ovviamente puoi usare intrinseci invece di assembly ma una volta che sei al livello intrinseco stai praticamente scrivendo comunque assembly, usando semplicemente il compilatore come allocatore di registro e (nominalmente) programmatore di istruzioni. (Tendo a usare i intrinseci per SIMD semplicemente perché il compilatore può generare i prologhi di funzione e quant'altro per me, così posso usare lo stesso codice su Linux, OS X e Windows senza dover affrontare problemi ABI come convenzioni di chiamata di funzioni, ma altro di quello gli intrinseci SSE in realtà non sono molto belli - quelli Altivec sembrano migliori anche se non ho molta esperienza con loro).correzione di errori AES o SIMD - si potrebbe immaginare un compilatore in grado di analizzare algoritmi e generare tale codice, ma mi sembra che un compilatore così intelligente sia a almeno 30 anni dall'esistente (nella migliore delle ipotesi).

D'altra parte, le macchine multicore e i sistemi distribuiti hanno spostato molte delle maggiori prestazioni ottenute nella direzione opposta: ottieni un ulteriore 20% di velocità scrivendo i tuoi loop interni nell'assemblaggio, o il 300% eseguendoli su più core, o 10000% di eseguendoli attraverso un gruppo di macchine. E, naturalmente, le ottimizzazioni di alto livello (cose come futures, memoization, ecc.) Sono spesso molto più facili da fare in un linguaggio di livello superiore come ML o Scala rispetto a C o asm, e spesso possono fornire una performance performance molto più grande. Quindi, come sempre, ci sono compromessi da fare.


2
@Dennis ed è per questo che ho scritto "Certo che puoi usare intrinseci invece di assembly, ma una volta che sei a livello intrinseco stai praticamente scrivendo assembly, usando semplicemente il compilatore come allocatore di registro e (nominalmente) programmatore di istruzioni".
Jack Lloyd,

Inoltre, il codice SIMD a base intrinseca tende ad essere meno leggibile rispetto allo stesso codice scritto in assembler: molto codice SIMD si basa su reinterpretazioni implicite dei dati nei vettori, che è una PITA che ha a che fare con i tipi di dati intrinseci del compilatore.
cmaster - ripristina monica il

10

Circuiti stretti, come quando si gioca con le immagini, poiché un'immagine può costare milioni di pixel. Sedersi e capire come utilizzare al meglio il numero limitato di registri del processore può fare la differenza. Ecco un esempio di vita reale:

http://danbystrom.se/2008/12/22/optimizing-away-ii/

Quindi spesso i processori hanno alcune istruzioni esoteriche che sono troppo specializzate per essere disturbate da un compilatore, ma a volte un programmatore di assemblatori può farne buon uso. Prendi ad esempio le istruzioni XLAT. Davvero fantastico se devi cercare delle tabelle in un ciclo e la tabella è limitata a 256 byte!

Aggiornato: oh, vieni a pensare a ciò che è più cruciale quando parliamo di loop in generale: il compilatore spesso non ha idea di quante iterazioni che saranno il caso comune! Solo il programmatore sa che un ciclo verrà ripetuto MOLTE volte e che pertanto sarà utile prepararsi al ciclo con un po 'di lavoro in più, o se verrà ripetuto così poche volte che l'installazione effettivamente impiegherà più tempo delle iterazioni previsto.


3
L'ottimizzazione diretta del profilo fornisce al compilatore informazioni sulla frequenza con cui viene utilizzato un loop.
Zan Lynx,

10

Più spesso di quanto si pensi, C deve fare cose che sembrano inutili dal punto di vista di un programmatore dell'Assemblea solo perché lo dicono gli standard C.

Promozione intera, ad esempio. Se si desidera spostare una variabile char in C, di solito ci si aspetterebbe che il codice farebbe proprio questo, un singolo spostamento di bit.

Gli standard, tuttavia, impongono al compilatore di fare un segno esteso a int prima del turno e di troncare il risultato in caratteri successivi, il che potrebbe complicare il codice a seconda dell'architettura del processore di destinazione.


Compilatori di qualità per piccoli microgrammi sono stati per anni in grado di evitare di elaborare le parti superiori dei valori nei casi in cui ciò non potrebbe mai influenzare in modo significativo i risultati. Le regole di promozione causano problemi, ma il più delle volte nei casi in cui un compilatore non ha modo di sapere quali casi corner sono e non sono rilevanti.
supercat

9

In realtà non sai se il tuo codice C ben scritto è davvero veloce se non hai guardato allo smontaggio di ciò che il compilatore produce. Molte volte lo guardi e vedi che "ben scritto" era soggettivo.

Quindi non è necessario scrivere in assemblatore per ottenere il codice più veloce di sempre, ma sicuramente vale la pena conoscere assemblatore per lo stesso motivo.


2
"Quindi non è necessario scrivere in assembler per ottenere il codice più veloce di sempre" Beh, non ho visto un compilatore fare la cosa ottimale in ogni caso che non era banale. Un essere umano esperto può fare di meglio del compilatore praticamente in tutti i casi. Quindi, è assolutamente necessario scrivere in assembler per ottenere "il codice più veloce di sempre".
cmaster - ripristina monica il

@cmaster Nella mia esperienza, l'output del compilatore è casuale. A volte è davvero buono e ottimale, a volte è "come potrebbe essere stata emessa questa spazzatura".
sharptooth,

9

Ho letto tutte le risposte (più di 30) e non ho trovato un semplice motivo: l'assemblatore è più veloce di C se hai letto e praticato il Manuale di riferimento per l'ottimizzazione delle architetture Intel® 64 e IA-32 , quindi il motivo per cui assembly potrebbe essere più lento è che le persone che scrivono un tale assemblaggio più lento non hanno letto il Manuale di ottimizzazione .

Ai vecchi tempi di Intel 80286, ogni istruzione veniva eseguita con un numero fisso di cicli della CPU, ma da quando Pentium Pro, rilasciato nel 1995, i processori Intel sono diventati superscalari, utilizzando Pipeline complesse: esecuzione fuori ordine e ridenominazione del registro. Prima di ciò, su Pentium, prodotto nel 1993, c'erano tubazioni U e V: doppie tubazioni che potevano eseguire due semplici istruzioni in un ciclo di clock se non dipendessero l'una dall'altra; ma questo non era nulla da confrontare con ciò che è Esecuzione fuori ordine e Rinomina registro è apparso in Pentium Pro e oggi è rimasto quasi invariato.

Per spiegare in poche parole, il codice più veloce è dove le istruzioni non dipendono dai risultati precedenti, ad esempio dovresti sempre cancellare interi registri (da movzx) o usare add rax, 1invece o inc raxper rimuovere la dipendenza dallo stato precedente delle bandiere, ecc.

Puoi leggere di più su Esecuzione fuori ordine e rinominare il registro se il tempo lo consente, ci sono molte informazioni disponibili su Internet.

Ci sono anche altri problemi importanti come la previsione delle filiali, il numero di unità di carico e di magazzino, il numero di gate che eseguono micro-operazioni, ecc., Ma la cosa più importante da considerare è in particolare l'Esecuzione fuori servizio.

La maggior parte delle persone semplicemente non è a conoscenza dell'esecuzione fuori ordine, quindi scrivono i loro programmi di assemblaggio come per 80286, aspettandosi che le loro istruzioni richiedano un tempo fisso per l'esecuzione indipendentemente dal contesto; mentre i compilatori C sono a conoscenza dell'esecuzione fuori ordine e generano il codice correttamente. Ecco perché il codice di queste persone inconsapevoli è più lento, ma se diventerai consapevole, il tuo codice sarà più veloce.


8

Penso che il caso generale quando l'assemblatore è più veloce è quando un programmatore di assemblaggi intelligenti guarda all'output del compilatore e dice "questo è un percorso critico per le prestazioni e posso scriverlo per essere più efficiente" e poi quella persona modifica quell'assemblatore o lo riscrive da zero.


7

Tutto dipende dal carico di lavoro.

Per le operazioni quotidiane, C e C ++ vanno bene, ma ci sono alcuni carichi di lavoro (eventuali trasformazioni che coinvolgono video (compressione, decompressione, effetti di immagine, ecc.) Che richiedono praticamente l'assemblaggio per essere performanti.

Di solito comportano anche l'utilizzo di estensioni di chipset specifiche della CPU (MME / MMX / SSE / qualunque) ottimizzate per quel tipo di operazione.


6

Ho un'operazione di trasposizione di bit che deve essere eseguita, su 192 o 256 bit ogni interruzione, che avviene ogni 50 microsecondi.

Succede da una mappa fissa (vincoli hardware). Usando C, ci sono voluti circa 10 microsecondi per fare. Quando l'ho tradotto in Assembler, prendendo in considerazione le caratteristiche specifiche di questa mappa, la memorizzazione nella cache del registro specifico e l'utilizzo di operazioni orientate ai bit; ci sono voluti meno di 3,5 microsecondi per eseguire.




5

La semplice risposta ... Uno che conosce il montaggio bene il (aka ha il riferimento accanto a lui, e sta sfruttando ogni piccola cache del processore, funzionalità della pipeline ecc.) È garantito per essere in grado di produrre codice molto più veloce di qualsiasi compilatore.

Tuttavia, la differenza in questi giorni non ha importanza nell'applicazione tipica.


1
Hai dimenticato di dire "dato molto tempo e fatica" e "creare un incubo per la manutenzione". Un mio collega stava lavorando all'ottimizzazione di una sezione critica del codice del sistema operativo e ha lavorato in C molto più che nell'assemblaggio, poiché gli ha permesso di studiare l'impatto delle prestazioni di modifiche di alto livello in tempi ragionevoli.
Artelius,

Sono d'accordo. A volte si utilizzano macro e script per generare il codice assembly per risparmiare tempo e svilupparsi rapidamente. La maggior parte degli assemblatori in questi giorni ha macro; in caso contrario, è possibile creare un pre-processore macro (semplice) utilizzando uno script Perl (abbastanza semplice RegEx).

Questo. Precisamente. Il compilatore per battere gli esperti del dominio non è stato ancora inventato.
cmaster - ripristina monica il

4

Una delle possibilità per la versione CP / M-86 di PolyPascal (fratello di Turbo Pascal) era quella di sostituire la struttura "usa bios per produrre caratteri sullo schermo" con una routine di linguaggio macchina che in sostanza è stata data la xe ye la stringa da mettere lì.

Ciò ha permesso di aggiornare lo schermo molto, molto più velocemente di prima!

Nel binario c'era spazio per incorporare il codice macchina (alcune centinaia di byte) e c'erano anche altre cose lì, quindi era essenziale spremere il più possibile.

Si scopre che poiché lo schermo era 80x25 entrambe le coordinate potevano adattarsi in un byte ciascuna, quindi entrambe potevano adattarsi in una parola a due byte. Ciò ha permesso di eseguire i calcoli necessari in meno byte poiché una singola aggiunta potrebbe manipolare entrambi i valori contemporaneamente.

Per quanto ne sappia, non esistono compilatori C in grado di unire più valori in un registro, eseguire istruzioni SIMD su di essi e suddividerli nuovamente in seguito (e non credo che le istruzioni della macchina saranno comunque più brevi).


4

Uno dei frammenti più famosi dell'assemblaggio proviene dal ciclo di mappatura delle trame di Michael Abrash ( qui descritto in dettaglio ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

Oggi la maggior parte dei compilatori esprime istruzioni specifiche specifiche della CPU come funzioni intrinseche, ovvero funzioni che vengono compilate fino all'istruzione effettiva. MS Visual C ++ supporta le funzionalità intrinseche per MMX, SSE, SSE2, SSE3 e SSE4, quindi devi preoccuparti meno di passare all'assembly per trarre vantaggio dalle istruzioni specifiche della piattaforma. Visual C ++ può anche trarre vantaggio dall'architettura effettiva che si sta prendendo di mira con l'impostazione appropriata / ARCH.


Ancora meglio, questi intrinseci SSE sono specificati da Intel, quindi in realtà sono abbastanza portatili.
James,

4

Dato il programmatore giusto, i programmi Assembler possono sempre essere realizzati più velocemente delle loro controparti C (almeno marginalmente). Sarebbe difficile creare un programma C in cui non si potesse estrarre almeno un'istruzione dell'Assemblatore.


Questo sarebbe un po 'più corretto: "Sarebbe difficile creare un programma C non banale in cui ..." In alternativa, potresti dire: "Sarebbe difficile trovare un programma C nel mondo reale in cui ..." Il punto è , ci sono loop banali per i quali i compilatori producono un output ottimale. Tuttavia, buona risposta.
cmaster - ripristina monica il


4

gcc è diventato un compilatore ampiamente utilizzato. Le sue ottimizzazioni in generale non sono così buone. Molto meglio del programmatore medio che scrive assemblatore, ma per prestazioni reali, non così buono. Ci sono compilatori che sono semplicemente incredibili nel codice che producono. Quindi, come risposta generale, ci saranno molti posti in cui è possibile andare nell'output del compilatore e modificare l'assemblatore per le prestazioni e / o semplicemente riscrivere la routine da zero.


8
GCC esegue ottimizzazioni "indipendenti dalla piattaforma" estremamente intelligenti. Tuttavia, non è così bravo a utilizzare al massimo i set di istruzioni particolari. Per un compilatore così portatile fa un ottimo lavoro.
Artelius,

2
concordato. La sua portabilità, le lingue in arrivo e gli obiettivi in ​​uscita sono sorprendenti. Essere quel portatile può e può davvero essere bravo in una lingua o target. Quindi le opportunità per un essere umano di fare meglio ci sono per una particolare ottimizzazione su un obiettivo specifico.
old_timer

+1: GCC certamente non è competitivo nel generare codice veloce ma non sono sicuro che sia perché è portatile. LLVM è portatile e l'ho visto generare un codice 4 volte più veloce dei GCC.
Jon Harrop,

Preferisco GCC, dato che è stato solido per molti anni, inoltre è disponibile per quasi tutte le piattaforme in grado di eseguire un moderno compilatore portatile. Sfortunatamente non sono stato in grado di creare LLVM (Mac OS X / PPC), quindi probabilmente non sarò in grado di passare ad esso. Una delle cose positive di GCC è che se scrivi il codice che si crea in GCC, molto probabilmente ti stai avvicinando agli standard e sarai sicuro che possa essere costruito per quasi tutte le piattaforme.

4

Longpoke, c'è solo una limitazione: il tempo. Quando non hai le risorse per ottimizzare ogni singola modifica al codice e dedicare il tuo tempo all'allocazione dei registri, all'ottimizzazione di pochi sversamenti e cosa no, il compilatore vincerà ogni volta. Apportare la modifica al codice, ricompilare e misurare. Ripetere se necessario.

Inoltre, puoi fare molto nel lato di alto livello. Inoltre, ispezionare l'assemblaggio risultante può dare all'IMPRESSIONE che il codice è una schifezza, ma in pratica verrà eseguito più velocemente di quanto si pensi sia più veloce. Esempio:

int y = data [i]; // fai alcune cose qui .. call_function (y, ...);

Il compilatore leggerà i dati, li spingerà per impilare (versare) e successivamente leggerà dallo stack e passerà come argomento. Sembra merda? In realtà potrebbe essere una compensazione della latenza molto efficace e determinare un tempo di esecuzione più rapido.

// versione ottimizzata call_function (data [i], ...); // non così ottimizzato dopo tutto ..

L'idea con la versione ottimizzata era che abbiamo ridotto la pressione del registro ed evitato di versare. Ma in verità, la versione "di merda" era più veloce!

Guardando il codice dell'assemblaggio, solo guardando le istruzioni e concludendo: più istruzioni, più lente, sarebbero un errore di valutazione.

La cosa qui da prestare attenzione è: molti esperti di assemblaggio pensano di sapere molto, ma sanno molto poco. Anche le regole cambiano dall'architettura alla successiva. Non esiste un codice x86 Silver-bullet, ad esempio, che è sempre il più veloce. In questi giorni è meglio seguire le regole empiriche:

  • la memoria è lenta
  • la cache è veloce
  • prova a usare meglio la cache
  • quanto spesso ti perderai? hai una strategia di compensazione della latenza?
  • è possibile eseguire 10-100 istruzioni ALU / FPU / SSE per un singolo errore nella cache
  • l'architettura dell'applicazione è importante ..
  • .. ma non aiuta quando il problema non è nell'architettura

Inoltre, fidarsi troppo del compilatore che trasforma magicamente il codice C / C ++ mal concepito in un codice "teoricamente ottimale" è un pio desiderio. Devi conoscere il compilatore e la catena di strumenti che usi se ti preoccupi delle "prestazioni" a questo livello basso.

I compilatori in C / C ++ generalmente non sono molto bravi a riordinare le sottoespressioni perché le funzioni hanno effetti collaterali, per cominciare. I linguaggi funzionali non soffrono di questo avvertimento, ma non si adattano così bene all'ecosistema attuale. Esistono opzioni del compilatore per consentire regole di precisione rilassate che consentono di modificare l'ordine delle operazioni da parte del compilatore / linker / generatore di codice.

Questo argomento è un po 'un vicolo cieco; per la maggior parte non è rilevante, e per il resto sanno già cosa stanno già facendo.

Tutto si riduce a questo: "per capire cosa stai facendo", è un po 'diverso dal sapere cosa stai facendo.

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.