Penso che sarebbe più utile per l'interrogante avere una risposta più differenziata, perché vedo diverse ipotesi non esaminate nelle domande e in alcune delle risposte o dei commenti.
Il runtime relativo risultante di spostamento e moltiplicazione non ha nulla a che fare con C. Quando dico C, non intendo l'istanza di un'implementazione specifica, come quella o quella versione di GCC, ma il linguaggio. Non intendo prendere questo annuncio assurdo, ma usare un esempio estremo per l'illustrazione: potresti implementare un compilatore C completamente conforme agli standard e far sì che la moltiplicazione richieda un'ora, mentre lo spostamento richiede millisecondi - o viceversa. Non sono a conoscenza di tali limiti di prestazione in C o C ++.
Potrebbe non interessarti di questo tecnicismo nell'argomentazione. La tua intenzione era probabilmente quella di testare le prestazioni relative dei turni rispetto alle moltiplicazioni e hai scelto C, perché è generalmente percepito come un linguaggio di programmazione di basso livello, quindi ci si può aspettare che il suo codice sorgente si traduca in istruzioni corrispondenti più direttamente. Tali domande sono molto comuni e penso che una buona risposta dovrebbe sottolineare che anche in C il codice sorgente non si traduce in istruzioni così direttamente come si potrebbe pensare in una determinata istanza. Di seguito ti ho dato alcuni possibili risultati di compilazione.
È qui che entrano in gioco i commenti che mettono in dubbio l'utilità di sostituire questa equivalenza nel software del mondo reale. Puoi vedere alcuni dei commenti alla tua domanda, come quello di Eric Lippert. È in linea con la reazione che generalmente otterrete da ingegneri più esperti in risposta a tali ottimizzazioni. Se usi i turni binari nel codice di produzione come mezzo generale di moltiplicazione e divisione, molto probabilmente le persone si piegheranno al tuo codice e avranno un certo grado di reazione emotiva ("Ho sentito questa affermazione senza senso fatta su JavaScript per l'amor del cielo") per ciò potrebbe non avere senso per i programmatori alle prime armi, a meno che non comprendano meglio le ragioni di quelle reazioni.
Tali ragioni sono principalmente una combinazione della ridotta leggibilità e futilità di tale ottimizzazione, come forse avrete già scoperto confrontando le loro prestazioni relative. Tuttavia, non credo che le persone avrebbero una reazione così forte se la sostituzione del cambiamento con la moltiplicazione fosse l'unico esempio di tali ottimizzazioni. Domande come la tua spesso sorgono in varie forme e in vari contesti. Penso che ciò a cui più ingegneri senior effettivamente reagiscono così fortemente, almeno a volte ho, è che esiste un potenziale per una gamma molto più ampia di danno quando le persone impiegano tali micro-ottimizzazioni liberamente attraverso la base di codice. Se lavori in un'azienda come Microsoft su una base di codice di grandi dimensioni, passerai molto tempo a leggere il codice sorgente di altri ingegneri o tenterai di individuare un determinato codice al suo interno. Potrebbe anche essere il tuo codice che proverai a dare un senso tra qualche anno, in particolare in alcuni dei momenti più inopportuni, come quando devi risolvere un'interruzione della produzione a seguito di una chiamata che hai ricevuto mentre cercavi dovere un venerdì sera, in procinto di uscire per una serata di divertimento con gli amici ... Se passi così tanto tempo a leggere il codice, apprezzerai che è il più leggibile possibile. Immagina di leggere il tuo romanzo preferito, ma l'editore ha deciso di pubblicare una nuova edizione in cui usano abbrv. tutto ovr th pl bc tuo thnk it svs spc. Questo è simile alle reazioni che altri ingegneri potrebbero avere sul tuo codice, se li cospargi di tali ottimizzazioni. Come hanno indicato altre risposte, è meglio indicare chiaramente cosa intendi,
Anche in quegli ambienti, tuttavia, potresti trovarti a risolvere una domanda di intervista in cui dovresti conoscere questa o qualche altra equivalenza. Conoscerli non è male e un buon ingegnere sarebbe consapevole dell'effetto aritmetico dello spostamento binario. Nota che non ho detto che questo rende un buon ingegnere, ma che un buon ingegnere lo saprebbe, secondo me. In particolare, potresti ancora trovare un manager, di solito verso la fine del tuo ciclo di interviste, che ti sorriderà ampiamente in attesa della delizia di rivelarti questo "trucco" ingegnoso intelligente in una domanda di codifica e dimostrare che lui / lei anch'esso era o è uno degli ingegneri esperti e non "solo" un manager. In quelle situazioni, cerca di sembrare impressionato e ringrazilo per l'intervista illuminante.
Perché non hai visto una differenza di velocità in C? La risposta più probabile è che entrambi hanno prodotto lo stesso codice assembly:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Entrambi possono compilare in
shift(int):
lea eax, [0+rdi*4]
ret
Su GCC senza ottimizzazioni, ovvero utilizzando il flag "-O0", è possibile ottenere questo:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Come puoi vedere, passare "-O0" a GCC non significa che non sarà in qualche modo intelligente riguardo al tipo di codice che produce. In particolare, si noti che anche in questo caso il compilatore ha evitato l'uso di un'istruzione moltiplicata. Puoi ripetere lo stesso esperimento con turni di altri numeri e persino moltiplicazioni per numeri che non sono potenze di due. È probabile che sulla tua piattaforma vedrai una combinazione di turni e aggiunte, ma nessuna moltiplicazione. Sembra un po 'una coincidenza per il compilatore apparentemente evitare di usare le moltiplicazioni in tutti quei casi se le moltiplicazioni e i turni avessero davvero lo stesso costo, non è vero? Ma non intendo fornire supposizione come prova, quindi andiamo avanti.
Puoi rieseguire il test con il codice sopra e vedere se noti una differenza di velocità ora. Anche allora non stai testando shift contro moltiplicare, come puoi vedere dall'assenza di una moltiplicazione, ma il codice che è stato generato con un certo set di flag da GCC per le operazioni C di shift e si moltiplica in una particolare istanza . Pertanto, in un altro test è possibile modificare manualmente il codice assembly e utilizzare invece un'istruzione "imul" nel codice per il metodo "moltiplicare".
Se volessi sconfiggere alcuni di quei furbi del compilatore, potresti definire un metodo di spostamento e moltiplicazione più generale e finirai con qualcosa del genere:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
Che può produrre il seguente codice assembly:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Qui finalmente abbiamo, anche al massimo livello di ottimizzazione di GCC 4.9, l'espressione nelle istruzioni di assemblaggio che potresti aspettarti quando hai iniziato il tuo test. Penso che di per sé possa essere una lezione importante nell'ottimizzazione delle prestazioni. Possiamo vedere la differenza che ha fatto per sostituire le variabili con costanti concrete nel nostro codice, in termini di intelligenza che il compilatore è in grado di applicare. Le micro-ottimizzazioni come la sostituzione di moltiplicare i turni sono alcune ottimizzazioni di livello molto basso che un compilatore può fare facilmente da solo. Altre ottimizzazioni che hanno un impatto molto maggiore sulle prestazioni richiedono una comprensione dell'intenzione del codiceche spesso non è accessibile dal compilatore o può essere solo ipotizzato da un euristico. È qui che entri come ingegnere del software e di solito non comporta la sostituzione di moltiplicazioni con turni. Implica fattori come evitare una chiamata ridondante a un servizio che produce I / O e può bloccare un processo. Se vai sul tuo disco rigido o, per favore, in un database remoto per alcuni dati extra che potresti aver derivato da ciò che hai già in memoria, il tempo che passi in attesa supera l'esecuzione di un milione di istruzioni. Ora, penso che ci siamo allontanati un po 'dalla tua domanda originale, ma penso di indicarlo a un interrogante, specialmente se supponiamo che qualcuno abbia appena iniziato a capire la traduzione e l'esecuzione del codice,
Quindi, quale sarà più veloce? Penso che sia stato scelto un buon approccio per testare effettivamente la differenza di prestazioni. In generale, è facile essere sorpresi dalle prestazioni di runtime di alcune modifiche al codice. Esistono molte tecniche impiegate dai moderni processori e anche l'interazione tra software può essere complessa. Anche se dovessi ottenere risultati di prestazione vantaggiosi per un determinato cambiamento in una situazione, penso che sia pericoloso concludere che questo tipo di cambiamento produrrà sempre vantaggi in termini di prestazioni. Penso che sia pericoloso eseguire tali test una volta, dire "Okay, ora so quale è più veloce!" e quindi applicare indiscriminatamente la stessa ottimizzazione al codice di produzione senza ripetere le misurazioni.
E se lo spostamento fosse più veloce della moltiplicazione? Vi sono certamente indicazioni sul perché ciò sia vero. GCC, come puoi vedere sopra, sembra pensare (anche senza ottimizzazione) che evitare una moltiplicazione diretta a favore di altre istruzioni sia una buona idea. Il Manuale di riferimento per l'ottimizzazione delle architetture Intel 64 e IA-32 ti darà un'idea del costo relativo delle istruzioni della CPU. Un'altra risorsa, più focalizzata sulla latenza e il throughput delle istruzioni, è http://www.agner.org/optimize/instruction_tables.pdf. Si noti che non sono un buon predicatore del runtime assoluto, ma dell'esecuzione delle istruzioni l'una rispetto all'altra. In un circuito ristretto, mentre il test è in simulazione, la metrica del "throughput" dovrebbe essere più pertinente. È il numero di cicli per i quali un'unità di esecuzione verrà generalmente legata quando si esegue una determinata istruzione.
Quindi cosa succede se lo spostamento NON è più veloce della moltiplicazione? Come ho detto sopra, le architetture moderne possono essere piuttosto complesse e cose come la previsione del ramo, la memorizzazione nella cache, il pipelining e le unità di esecuzione parallela possono rendere difficile prevedere le prestazioni relative di due pezzi di codice logicamente equivalenti a volte. Voglio davvero enfatizzare questo aspetto, perché è qui che non sono contento della maggior parte delle risposte a domande come queste e con l'accampamento di persone che afferma apertamente che semplicemente non è più vero (più) che lo spostamento è più veloce della moltiplicazione.
No, per quanto ne so, negli anni '70 o in qualsiasi occasione non abbiamo inventato un po 'di salsa ingegneristica segreta per annullare improvvisamente la differenza di costo di un'unità di moltiplicazione e un po' di cambio. Una moltiplicazione generale, in termini di porte logiche, e certamente in termini di operazioni logiche, è ancora più complessa di uno spostamento con un cambio a barilotto in molti scenari, su molte architetture. Il modo in cui questo si traduce in un runtime complessivo su un computer desktop può essere un po 'opaco. Non so con certezza come siano implementati in processori specifici, ma ecco una spiegazione di una moltiplicazione: la moltiplicazione intera è davvero la stessa velocità dell'aggiunta sulla CPU moderna
Mentre qui è una spiegazione di un cambio a botte . I documenti a cui ho fatto riferimento nel paragrafo precedente forniscono un'altra visione del costo relativo delle operazioni, mediante delega delle istruzioni della CPU. Gli ingegneri di Intel sembrano spesso avere domande simili: i cicli di clock dei forum della zona degli sviluppatori Intel per la moltiplicazione dei numeri interi e l'aggiunta nel processore core 2 duo
Sì, nella maggior parte degli scenari della vita reale, e quasi sicuramente in JavaScript, tentare di sfruttare questa equivalenza per motivi di prestazioni è probabilmente un'impresa futile. Tuttavia, anche se abbiamo forzato l'uso delle istruzioni di moltiplicazione e non abbiamo riscontrato alcuna differenza nel tempo di esecuzione, ciò è più dovuto alla natura della metrica di costo che abbiamo usato, per essere precisi e non perché non vi è alcuna differenza di costo. Il runtime end-to-end è una metrica e se è l'unica a cui teniamo, va tutto bene. Ma ciò non significa che tutte le differenze di costo tra moltiplicazione e spostamento siano semplicemente scomparse. E penso che non sia certamente una buona idea trasmettere quell'idea a un interrogante, implicitamente o meno, che ovviamente sta appena iniziando a farsi un'idea dei fattori coinvolti nel tempo di esecuzione e nel costo del codice moderno. L'ingegneria riguarda sempre i compromessi. Indagine e spiegazione su quali compromessi hanno fatto i moderni processori per mostrare i tempi di esecuzione che noi, come utenti, finiscono per vedere, possono dare una risposta più differenziata. E penso che una risposta più differenziata di "questo semplicemente non è più vero" è giustificata se vogliamo vedere meno ingegneri controllare il codice micro-ottimizzato che annulla la leggibilità, perché ci vuole una comprensione più generale della natura di tali "ottimizzazioni" per individuare le sue varie e diverse incarnazioni rispetto al semplice riferimento ad alcuni casi specifici come obsoleti.