In realtà è più veloce usare dire (i << 3) + (i << 1) per moltiplicare per 10 che usare direttamente i * 10?
Potrebbe essere o non essere sulla tua macchina - se ti interessa, misura nel tuo uso nel mondo reale.
Un caso di studio - dal 486 al core i7
Il benchmarking è molto difficile da fare in modo significativo, ma possiamo esaminare alcuni fatti. Da http://www.penguin.cz/~literakl/intel/s.html#SAL e http://www.penguin.cz/~literakl/intel/i.html#IMUL abbiamo un'idea dei cicli di clock x86 necessario per spostamento e moltiplicazione aritmetica. Supponiamo di attenerci a "486" (il più recente elencato), registri e immediati a 32 bit, IMUL richiede 13-42 cicli e IDIV 44. Ogni SAL ne prende 2 e aggiunge 1, quindi anche con alcuni di quelli che si spostano superficialmente insieme come un vincitore.
In questi giorni, con il core i7:
(da http://software.intel.com/en-us/forums/showthread.php?t=61481 )
La latenza è 1 ciclo per un'aggiunta di numeri interi e 3 cicli per una moltiplicazione di numeri interi . È possibile trovare le latenze e il throughput nell'Appendice C del "Manuale di riferimento per l'ottimizzazione delle architetture Intel® 64 e IA-32", che si trova su http://www.intel.com/products/processor/manuals/ .
(da alcuni blurb Intel)
Utilizzando SSE, il Core i7 può emettere istruzioni di aggiunta e moltiplicazione simultanee, ottenendo una frequenza di picco di 8 operazioni in virgola mobile (FLOP) per ciclo di clock
Questo ti dà un'idea di quanto le cose siano andate lontano. Le curiosità sull'ottimizzazione - come il bit shifting contro *
- che sono state prese sul serio anche negli anni '90 sono ormai obsolete. Lo spostamento dei bit è ancora più veloce, ma per non-power-of-two mul / div quando fai tutti i tuoi turni e aggiungi i risultati è ancora più lento. Quindi, più istruzioni significano più errori nella cache, più potenziali problemi nel pipelining, un uso maggiore dei registri temporanei può significare più salvataggio e ripristino del contenuto dei registri dallo stack ... diventa rapidamente troppo complicato per quantificare definitivamente tutti gli impatti ma sono prevalentemente negativo.
funzionalità nel codice sorgente vs implementazione
Più in generale, la tua domanda è contrassegnata con C e C ++. Come linguaggi di terza generazione, sono specificamente progettati per nascondere i dettagli del set di istruzioni della CPU sottostante. Per soddisfare i propri standard linguistici, devono supportare le operazioni di moltiplicazione e spostamento (e molti altri) anche se l'hardware sottostante non lo fa . In tali casi, devono sintetizzare il risultato richiesto usando molte altre istruzioni. Allo stesso modo, devono fornire supporto software per le operazioni in virgola mobile se la CPU manca e non c'è FPU. Tutte le CPU moderne supportano tutte*
e<<
, quindi potrebbe sembrare assurdamente teorico e storico, ma il significato è che la libertà di scegliere l'implementazione va in entrambi i modi: anche se la CPU ha un'istruzione che implementa l'operazione richiesta nel codice sorgente nel caso generale, il compilatore è libero di scegli qualcos'altro che preferisce perché è meglio per il caso specifico che il compilatore deve affrontare.
Esempi (con un ipotetico linguaggio assembly)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Istruzioni come il comando esclusivo o ( xor
) non hanno alcuna relazione con il codice sorgente, ma il fatto di eliminare qualsiasi cosa con se stesso cancella tutti i bit, quindi può essere utilizzato per impostare qualcosa su 0. Il codice sorgente che implica gli indirizzi di memoria non può comportare alcun utilizzo.
Questo tipo di hack è stato utilizzato per tutto il tempo in cui i computer sono stati in circolazione. Nei primi tempi di 3GL, per garantire l'assorbimento degli sviluppatori, l'output del compilatore doveva soddisfare lo sviluppatore del linguaggio assembly che ottimizzava la mano hardcore esistente. comunità che il codice prodotto non era più lento, più dettagliato o altrimenti peggio. I compilatori hanno rapidamente adottato molte grandi ottimizzazioni - ne sono diventate un archivio centralizzato migliore di quanto possa essere un singolo programmatore di linguaggi di assemblaggio, anche se c'è sempre la possibilità che manchino un'ottimizzazione specifica che risulta essere cruciale in un caso specifico - gli umani a volte possono falla fuori e cerca qualcosa di meglio mentre i compilatori fanno solo quello che gli è stato detto fino a quando qualcuno non gli restituisce quell'esperienza.
Quindi, anche se lo spostamento e l'aggiunta sono ancora più veloci su un determinato hardware, è probabile che lo scrittore del compilatore abbia funzionato esattamente quando è sicuro e vantaggioso.
manutenibilità
Se il tuo hardware cambia, puoi ricompilare e guarderà la CPU di destinazione e farà un'altra scelta migliore, mentre è improbabile che tu voglia rivisitare le tue "ottimizzazioni" o elencare quali ambienti di compilazione dovrebbero usare la moltiplicazione e quali dovrebbero spostarsi. Pensa a tutte le "ottimizzazioni" che non cambiano bit di bit, scritte più di 10 anni fa e che ora stanno rallentando il codice in cui si trovano mentre girano su processori moderni ...!
Per fortuna, buoni compilatori come GCC in genere possono sostituire una serie di bit-shift e aritmetica con una moltiplicazione diretta quando è abilitata qualsiasi ottimizzazione (cioè ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
), quindi una ricompilazione può aiutare anche senza correggere il codice, ma ciò non è garantito.
Strano codice di bitshifting che implementa la moltiplicazione o la divisione è molto meno espressivo di ciò che stavi concettualmente cercando di ottenere, quindi altri sviluppatori ne saranno confusi, e un programmatore confuso ha maggiori probabilità di introdurre bug o rimuovere qualcosa di essenziale nel tentativo di ripristinare un'apparente sanità mentale. Se fai cose non ovvie solo quando sono davvero tangibilmente benefiche, e poi le documenti bene (ma non documentare altre cose che sono comunque intuitive), tutti saranno più felici.
Soluzioni generali contro soluzioni parziali
Se hai qualche conoscenza extra, come ad esempio il int
fatto che memorizzerai solo valori x
, y
e z
quindi potresti essere in grado di elaborare alcune istruzioni che funzionano per quei valori e ottenere il tuo risultato più rapidamente rispetto a quando il compilatore non ha tale intuizione e necessita di un'implementazione che funzioni per tutti i int
valori. Ad esempio, considera la tua domanda:
La moltiplicazione e la divisione possono essere ottenute utilizzando gli operatori bit ...
Illustri la moltiplicazione, ma che ne dici della divisione?
int x;
x >> 1; // divide by 2?
Secondo lo standard C ++ 5.8:
-3- Il valore di E1 >> E2 è E1 posizioni bit E2 spostate a destra. Se E1 ha un tipo senza segno o se E1 ha un tipo con segno e un valore non negativo, il valore del risultato è la parte integrale del quoziente di E1 diviso per la quantità 2 elevata alla potenza E2. Se E1 ha un tipo con segno e un valore negativo, il valore risultante è definito dall'implementazione.
Quindi, il tuo bit shift ha un risultato di implementazione definito quando x
è negativo: potrebbe non funzionare allo stesso modo su macchine diverse. Ma, /
funziona molto più prevedibile. (Potrebbe anche non essere perfettamente coerente, poiché macchine diverse potrebbero avere rappresentazioni diverse di numeri negativi, e quindi intervalli diversi anche quando vi è lo stesso numero di bit che compongono la rappresentazione.)
Potresti dire "Non mi interessa ... che int
sta memorizzando l'età dell'impiegato, non può mai essere negativo". Se hai quel tipo di intuizione speciale, allora sì - la tua >>
ottimizzazione sicura potrebbe essere passata dal compilatore a meno che tu non lo faccia esplicitamente nel tuo codice. Ma è rischioso e raramente utile per la maggior parte del tempo non avrai questo tipo di intuizione, e altri programmatori che lavorano sullo stesso codice non sapranno che hai scommesso la casa su alcune insolite aspettative dei dati che " gestirò ... quello che sembra un cambiamento totalmente sicuro per loro potrebbe fallire a causa della tua "ottimizzazione".
C'è qualche tipo di input che non può essere moltiplicato o diviso in questo modo?
Sì ... come menzionato sopra, i numeri negativi hanno un comportamento definito dall'implementazione quando "divisi" per bit-shifting.