La moltiplicazione e la divisione utilizzando gli operatori di turno in C sono effettivamente più veloci?


288

La moltiplicazione e la divisione possono essere ottenute utilizzando operatori bit, ad esempio

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

e così via.

È effettivamente più veloce usare dire (i<<3)+(i<<1)per moltiplicare per 10 che usare i*10direttamente? C'è qualche tipo di input che non può essere moltiplicato o diviso in questo modo?


8
In realtà, una divisione economica per una costante diversa da una potenza di due è possibile, ma un subjet difficile a cui non stai facendo giustizia con "/ Division ... / diviso" nella tua domanda. Vedi ad esempio hackersdelight.org/divcMore.pdf (o ottieni il libro "La gioia dell'hacker " se puoi).
Pascal Cuoq,

46
Sembra qualcosa che potrebbe essere facilmente testato.
juanchopanza,

25
Come al solito - dipende. Una volta ho provato questo in assemblatore su un Intel 8088 (IBM PC / XT) in cui una moltiplicazione ha richiesto un bazillion clock. Spostamenti e aggiunte eseguiti molto più velocemente, quindi mi è sembrata una buona idea. Tuttavia, durante la moltiplicazione l'unità bus era libera di riempire la coda di istruzioni e le istruzioni successive potevano quindi iniziare immediatamente. Dopo una serie di turni e aggiunte la coda delle istruzioni sarebbe vuota e la CPU avrebbe dovuto attendere il recupero della prossima istruzione dalla memoria (un byte alla volta!). Misura, misura, misura!
Bo Persson,

19
Inoltre, attenzione che lo spostamento a destra è ben definito solo per numeri interi senza segno. Se si dispone di un numero intero con segno, non è definito se 0 o il bit più alto sono riempiti da sinistra. (E non dimenticare il tempo che serve a qualcun altro (anche a te stesso) per leggere il codice un anno dopo!)
Kerrek SB,

29
In realtà, un buon compilatore di ottimizzazione implementerà la moltiplicazione e la divisione con turni quando saranno più veloci.
Peter G.

Risposte:


487

Risposta breve: non probabile.

Risposta lunga: il compilatore ha un ottimizzatore in grado di moltiplicare rapidamente l'architettura del processore di destinazione. La soluzione migliore è dire chiaramente al compilatore le tue intenzioni (cioè i * 2 anziché i << 1) e lasciare che decida qual è la sequenza di codice macchina / assembly più veloce. È anche possibile che il processore stesso abbia implementato l'istruzione moltiplica come una sequenza di turni e aggiunge un microcodice.

In conclusione: non perdere molto tempo a preoccuparti di questo. Se intendi cambiare, cambia. Se intendi moltiplicare, moltiplicare. Fai ciò che è semanticamente più chiaro: i tuoi colleghi ti ringrazieranno più tardi. O, più probabilmente, ti maledire più tardi se lo fai diversamente.


31
Sì, come detto, i possibili guadagni per quasi tutte le applicazioni supereranno totalmente l'oscurità introdotta. Non preoccuparti di questo tipo di ottimizzazione prematuramente. Costruisci ciò che è semantica chiaro, identifica i colli di bottiglia e ottimizza da lì ...
Dave,

4
D'accordo, l'ottimizzazione per la leggibilità e la manutenibilità probabilmente ti farà guadagnare più tempo da dedicare all'ottimizzazione delle cose che secondo il profiler sono percorsi di hot code.
Doug65536,

5
Questi commenti fanno sembrare che ti stai arrendendo a potenziali prestazioni dal dire al compilatore come fare il suo lavoro. Questo non è il caso. In realtà ottieni un codice migliore da gcc -O3su x86 con return i*10che dalla versione shift . Come qualcuno che guarda molto all'output del compilatore (vedi molte delle mie risposte asm / ottimizzazione), non sono sorpreso. Ci sono momenti in cui può aiutare a tenere in mano il compilatore in un modo di fare le cose , ma questo non è uno di questi. gcc è bravo in matematica intera, perché è importante.
Peter Cordes,

Ho appena scaricato uno schizzo di Arduino che ha millis() >> 2; Sarebbe stato troppo chiedere di dividere?
Paul Wieland,

1
Ho provato i / 32vs i >> 5e i / 4vs i >> 2il gcc per Cortex-A9 (che non ha divisione hardware) con l'ottimizzazione -O3 e la conseguente assemblea era esattamente lo stesso. Non mi è piaciuto usare prima le divisioni ma descrive la mia intenzione e l'output è lo stesso.
robsn,

91

Solo un punto di misura concreto: molti anni fa, ho confrontato due versioni del mio algoritmo di hashing:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

e

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

Su ogni macchina su cui l'ho confrontato, il primo è stato veloce almeno quanto il secondo. Un po 'sorprendentemente, a volte era più veloce (ad esempio su un Sun Sparc). Quando l'hardware non supportava la moltiplicazione rapida (e la maggior parte non lo faceva allora), il compilatore convertiva la moltiplicazione nelle combinazioni appropriate di turni e add / sub. E poiché conosceva l'obiettivo finale, a volte poteva farlo in meno istruzioni rispetto a quando scrivevi esplicitamente i turni e l'aggiunta / sottotitoli.

Si noti che questo era qualcosa come 15 anni fa. Spero che i compilatori siano migliorati da allora, quindi puoi fare affidamento sul compilatore che fa la cosa giusta, probabilmente meglio di quanto potresti. (Inoltre, il motivo per cui il codice appare così C'ish è perché era di oltre 15 anni fa. Ovviamente userei std::stringe iteratori oggi.)


5
Potresti essere interessato al seguente post sul blog, in cui l'autore nota che i moderni compilatori di ottimizzazione sembrano retroingegnerizzare schemi comuni che i programmatori potrebbero usare ritenendoli più efficienti nelle loro forme matematiche in modo da generare realmente la sequenza di istruzioni più efficiente per loro . shape-of-code.coding-guidelines.com/2009/06/30/…
Pascal Cuoq

@PascalCuoq Niente di veramente nuovo in questo. Ho scoperto praticamente la stessa cosa per Sun CC quasi 20 anni fa.
James Kanze,

67

Oltre a tutte le altre buone risposte qui, vorrei sottolineare un altro motivo per non usare lo spostamento quando intendi dividere o moltiplicare. Non ho mai visto nessuno introdurre un bug dimenticando la relativa precedenza di moltiplicazione e aggiunta. Ho visto dei bug introdotti quando i programmatori di manutenzione hanno dimenticato che "moltiplicare" tramite un turno è logicamente una moltiplicazione ma non sintatticamente della stessa precedenza della moltiplicazione. x * 2 + ze x << 1 + zsono molto diversi!

Se stai lavorando su numeri, usa operatori aritmetici come+ - * / % . Se stai lavorando su matrici di bit, usa operatori di manipolazione dei bit come & ^ | >>. Non mescolarli; un'espressione che ha sia punte che aritmetiche è un bug che attende di accadere.


5
Evitabile con una semplice parentesi?
Gioele B,

21
@ Gioele: certo. Se ricordi di averne bisogno. Il mio punto è che è facile dimenticare che lo fai. Le persone che prendono l'abitudine mentale di leggere "x << 1" come se fosse "x * 2" prendono l'abitudine mentale di pensare che << sia la stessa precedenza della moltiplicazione, che non lo è.
Eric Lippert,

1
Bene, trovo l'espressione "(ciao << 8) + lo" più rivelatrice di intenti di "ciao * 256 + lo". Probabilmente è una questione di gusti, ma a volte è più chiaro scrivere bit-twiddling. Nella maggior parte dei casi, però, sono totalmente d'accordo con il tuo punto.
Ivan Danilov,

32
@Ivan: E "(ciao << 8) | lo" è ancora più chiaro. L'impostazione dei bit bassi di un array di bit non è un'aggiunta di numeri interi . Sta impostando i bit , quindi scrivi il codice che imposta i bit.
Eric Lippert,

1
Wow. Non ci avevo pensato prima. Grazie.
Ivan Danilov,

50

Questo dipende dal processore e dal compilatore. Alcuni compilatori ottimizzano già il codice in questo modo, altri no. Quindi è necessario controllare ogni volta che il codice deve essere ottimizzato in questo modo.

A meno che tu non abbia disperatamente bisogno di ottimizzare, non vorrei mescolare il mio codice sorgente solo per salvare un'istruzione di assemblaggio o un ciclo del processore.


3
Giusto per aggiungere una stima approssimativa: su un tipico processore a 16 bit (80C166) l'aggiunta di due ints arriva a 1-2 cicli, una moltiplicazione a 10 cicli e una divisione a 20 cicli. Inoltre alcune operazioni di spostamento se ottimizzi i * 10 in più operazioni (ogni movimento per un altro ciclo +1). I compilatori più comuni (Keil / Tasking) non ottimizzano se non per moltiplicazioni / divisioni per una potenza di 2.
Jens

55
E in generale, il compilatore ottimizza il codice meglio di te.
user703016,

Concordo sul fatto che quando si moltiplicano le "quantità", l'operatore di moltiplicazione è generalmente migliore, ma quando divide i valori firmati per potenze di 2, l' >>operatore è più veloce di /e, se i valori firmati possono essere negativi, spesso è anche semanticamente superiore. Se uno ha bisogno del valore che x>>4produrrebbe, è molto più chiaro di x < 0 ? -((-1-x)/16)-1 : x/16;, e non riesco a immaginare come un compilatore possa ottimizzare quest'ultima espressione su qualcosa di carino.
supercat

38

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 intfatto che memorizzerai solo valori x, ye zquindi 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 intvalori. 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 intsta 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.


2
Risposta molto bella. Il confronto tra Core i7 e 486 è illuminante!
Ha disegnato la sala il

Su tutte le architetture ordinarie, intVal>>1avrà la stessa semantica che differisce da quelle di intVal/2in un modo che a volte è utile. Se fosse necessario calcolare in modo portatile il valore che le architetture ordinarie avrebbero prodotto intVal >> 1, l'espressione dovrebbe essere piuttosto più complicata e più difficile da leggere, e probabilmente genererebbe un codice sostanzialmente inferiore a quello per cui è stato prodotto intVal >> 1.
supercat

35

Ho appena provato sulla mia macchina compilando questo:

int a = ...;
int b = a * 10;

Quando si smonta, produce output:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

Questa versione è più veloce del tuo codice ottimizzato a mano con puro spostamento e aggiunta.

In realtà non si sa mai quale sarà il compilatore, quindi è meglio semplicemente scrivere una moltiplicazione normale e lasciargli ottimizzare il modo in cui vuole, tranne in casi molto precisi in cui si sa che il compilatore non può ottimizzare.


1
Avresti ottenuto un grande voto per questo se avessi saltato la parte relativa al vettore. Se il compilatore può correggere il moltiplicare, può anche vedere che il vettore non cambia.
Bo Persson,

Come può un compilatore sapere che la dimensione di un vettore non cambierà senza fare ipotesi davvero pericolose? O non hai mai sentito parlare di concorrenza ...
Charles Goodwin,

1
Ok, quindi cerchi un vettore globale senza blocchi? E cerco un vettore locale il cui indirizzo non è stato preso e chiamo solo le funzioni membro const. Almeno il mio compilatore si rende conto che la dimensione del vettore non cambierà. (e presto qualcuno probabilmente ci segnalerà per chattare :-).
Bo Persson,

1
@BoPersson Alla fine, dopo tutto questo tempo, ho rimosso la mia dichiarazione sul fatto che il compilatore non fosse in grado di ottimizzare vector<T>::size(). Il mio compilatore era piuttosto antico! :)
user703016

21

Lo spostamento è generalmente molto più veloce della moltiplicazione a livello di istruzione, ma è probabile che tu stia sprecando il tuo tempo a fare ottimizzazioni premature. Il compilatore può eseguire queste ottimizzazioni durante la compilazione. Farlo da soli influirà sulla leggibilità e probabilmente non avrà alcun effetto sulle prestazioni. Probabilmente vale la pena fare cose come questa solo se hai profilato e hai trovato questo un collo di bottiglia.

In realtà il trucco della divisione, noto come "divisione magica", può effettivamente produrre enormi profitti. Ancora una volta dovresti profilare prima per vedere se è necessario. Ma se lo usi ci sono programmi utili in giro per aiutarti a capire quali istruzioni sono necessarie per la stessa semantica di divisione. Ecco un esempio: http://www.masm32.com/board/index.php?topic=12421.0

Un esempio che ho sollevato dal thread dell'OP su MASM32:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

Genererebbe:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16

7
@Drew per qualche motivo il tuo commento mi ha fatto ridere e rovesciare il mio caffè. Grazie.
asawyer,

30
Non ci sono discussioni casuali nel forum sul gradimento della matematica. Chiunque ami la matematica sa quanto sia difficile generare un vero thread del forum "casuale".
Gioele B,

1
Probabilmente vale la pena fare cose come questa solo se hai profilato e trovato che questo è un collo di bottiglia e hai implementato di nuovo le alternative e il profilo e ottieni un vantaggio prestazionale almeno 10 volte .
Lie Ryan,

12

Le istruzioni di moltiplicazione di turni e numeri interi hanno prestazioni simili sulla maggior parte delle CPU moderne - le istruzioni di moltiplicazione di numeri interi erano relativamente lente negli anni '80, ma in generale questo non è più vero. Le istruzioni per la moltiplicazione dei numeri interi possono avere una latenza più elevata , quindi potrebbero esserci ancora casi in cui è preferibile un turno. Idem per i casi in cui puoi tenere occupate più unità di esecuzione (anche se questo può tagliare in entrambi i modi).

La divisione in interi è comunque relativamente lenta, quindi usare uno spostamento invece della divisione per una potenza di 2 è ancora una vittoria, e la maggior parte dei compilatori implementerà questo come un'ottimizzazione. Tuttavia, affinché questa ottimizzazione sia valida, il dividendo deve essere non firmato o deve essere noto per essere positivo. Per un dividendo negativo lo spostamento e la divisione non sono equivalenti!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

Produzione:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

Quindi, se vuoi aiutare il compilatore, assicurati che la variabile o l'espressione nel dividendo non sia esplicitamente firmata.


4
I moltiplicatori di numeri interi sono microcodificati, ad esempio, sulla PPU di PlayStation 3 e bloccano l'intera pipeline. Si consiglia di evitare ancora moltiplicazioni di numeri interi su alcune piattaforme :)
Maister,

2
Molte divisioni senza segno sono - supponendo che il compilatore sappia come - implementate usando moltiplicazioni senza segno. Uno o due moltiplicati per alcuni cicli di clock ciascuno possono fare lo stesso lavoro di una divisione di 40 cicli ciascuno e fino.
Olof Forshell il

1
@Olof: vero, ma valido solo per la divisione per una costante di compilazione ovviamente
Paul R

4

Dipende completamente dal dispositivo di destinazione, dalla lingua, dallo scopo, ecc.

Pixel scricchiolio in un driver della scheda video? Molto probabilmente sì!

Applicazione aziendale .NET per il tuo dipartimento? Assolutamente nessun motivo per guardarci dentro.

Per un gioco ad alte prestazioni per un dispositivo mobile potrebbe valere la pena esaminarlo, ma solo dopo aver eseguito ottimizzazioni più semplici.


2

Non farlo a meno che non sia assolutamente necessario e l'intenzione del codice richieda lo spostamento anziché la moltiplicazione / divisione.

In un giorno normale - potresti potenzialmente risparmiare pochi cicli macchina (o perdere, poiché il compilatore sa meglio cosa ottimizzare), ma il costo non ne vale la pena - dedichi tempo a dettagli minori piuttosto che al lavoro effettivo, mantenendo il codice diventa più difficile e i tuoi colleghi ti malediranno.

Potrebbe essere necessario farlo per calcoli a carico elevato, in cui ogni ciclo salvato significa minuti di runtime. Tuttavia, è necessario ottimizzare un posto alla volta ed eseguire test delle prestazioni ogni volta per vedere se è stato davvero più veloce o se si è rotta la logica dei compilatori.


1

Per quanto ne so in alcune macchine la moltiplicazione può richiedere da 16 a 32 cicli di macchina. Quindi , a seconda del tipo di macchina, gli operatori bit-shift sono più veloci della moltiplicazione / divisione.

Tuttavia, alcune macchine hanno il loro processore matematico, che contiene istruzioni speciali per la moltiplicazione / divisione.


7
Le persone che scrivono compilatori per quelle macchine hanno probabilmente letto Hackers Delight e ottimizzato di conseguenza.
Bo Persson,

1

Concordo con la risposta contrassegnata da Drew Hall. La risposta potrebbe usare alcune note aggiuntive però.

Per la stragrande maggioranza degli sviluppatori di software, il processore e il compilatore non sono più pertinenti alla domanda. Molti di noi vanno ben oltre l'8088 e MS-DOS. È forse rilevante solo per coloro che stanno ancora sviluppando per i processori integrati ...

Nella mia società di software, la matematica (add / sub / mul / div) dovrebbe essere usata per tutta la matematica. Mentre Shift dovrebbe essere usato durante la conversione tra tipi di dati ad es. ushort a byte come n >> 8 e non n / 256.


Sono d'accordo anche con te. Seguo inconsciamente la stessa linea guida, anche se non ho mai avuto un requisito formale per farlo.
Ha disegnato la sala il

0

Nel caso di numeri interi con segno e spostamento a destra vs divisione, può fare la differenza. Per i numeri negativi, lo spostamento viene arrotondato verso l'infinito negativo mentre la divisione viene arrotondata verso zero. Naturalmente il compilatore cambierà la divisione in qualcosa di più economico, ma di solito la cambierà in qualcosa che ha lo stesso comportamento di arrotondamento della divisione, perché non è in grado di provare che la variabile non sarà negativa o semplicemente non lo fa cura. Quindi, se puoi dimostrare che un numero non sarà negativo o se non ti interessa in che modo arrotonderà, puoi fare quell'ottimizzazione in un modo che è più probabile che faccia la differenza.


oppure lancia il numero aunsigned
Lie Ryan,

4
Sei sicuro che il comportamento di spostamento sia standardizzato? Ho avuto l'impressione che lo spostamento a destra su ints negativi sia definito dall'implementazione.
Kerrek SB,

1
Anche se dovresti forse menzionare che il codice che si basa su un comportamento particolare per i numeri negativi con spostamento a destra dovrebbe documentare tale requisito, il vantaggio dello spostamento a destra è enorme nei casi in cui produce naturalmente il giusto valore e l'operatore di divisione genererebbe il codice da sprecare il calcolo del tempo di un valore indesiderato che il codice utente dovrebbe quindi perdere altro tempo ad adeguarsi per produrre ciò che lo spostamento avrebbe dato in primo luogo. In realtà, se avessi i miei druther, i compilatori avrebbero la possibilità di scricchiolare nei tentativi di eseguire la divisione firmata, dal momento che ...
supercat

1
... il codice che sa che gli operandi sono positivi potrebbe migliorare l'ottimizzazione se viene trasmesso a unsigned prima della divisione (possibilmente il cast di nuovo a firmato in seguito), e il codice che sa che gli operandi potrebbero essere negativi dovrebbero generalmente affrontare esplicitamente quel caso (nel qual caso si può anche supporre che siano positivi).
supercat

0

Test di Python eseguendo la stessa moltiplicazione 100 milioni di volte contro gli stessi numeri casuali.

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

Quindi nel fare un turno piuttosto che una moltiplicazione / divisione per una potenza di due in pitone, c'è un leggero miglioramento (~ 10% per la divisione; ~ 1% per la moltiplicazione). Se è una non-potenza di due, c'è probabilmente un notevole rallentamento.

Ancora una volta questi # cambieranno a seconda del tuo processore, del tuo compilatore (o interprete - fatto in Python per semplicità).

Come con tutti gli altri, non ottimizzare prematuramente. Scrivi codice molto leggibile, profilo se non è abbastanza veloce, quindi prova a ottimizzare le parti lente. Ricorda, il tuo compilatore è molto meglio nell'ottimizzazione di te.


0

Ci sono ottimizzazioni che il compilatore non può fare perché funzionano solo per un set ridotto di input.

Di seguito c'è un codice di esempio c ++ che può fare una divisione più veloce facendo una "moltiplicazione per il reciproco" a 64 bit. Sia il numeratore che il denominatore devono essere al di sotto di una certa soglia. Si noti che deve essere compilato per utilizzare le istruzioni a 64 bit per essere effettivamente più veloce della normale divisione.

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}

0

Penso che nell'unico caso in cui si desideri moltiplicare o dividere per una potenza di due, non si può sbagliare con l'uso di operatori di bitshift, anche se il compilatore li converte in un MUL / DIV, perché alcuni processori microcodice (davvero, un macro) comunque, quindi per quei casi otterrete un miglioramento, specialmente se lo spostamento è maggiore di 1. O più esplicitamente, se la CPU non ha operatori bit-shift, sarà comunque un MUL / DIV, ma se la CPU ha operatori bit-shift, si evita un ramo di microcodice e questo è un paio di istruzioni in meno.

Sto scrivendo un po 'di codice in questo momento che richiede molte operazioni di raddoppio / dimezzamento perché sta lavorando su un denso albero binario e c'è un'altra operazione che sospetto possa essere più ottimale di un'aggiunta: una sinistra (potenza di due moltiplicare ) spostare con un'aggiunta. Questo può essere sostituito con uno shift sinistro e uno xor se lo shift è più ampio del numero di bit che si desidera aggiungere, un semplice esempio è (i << 1) ^ 1, che aggiunge uno a un valore raddoppiato. Questo ovviamente non si applica a uno spostamento a destra (potenza di due divisioni) perché solo uno spostamento a sinistra (little endian) riempie il vuoto di zeri.

Nel mio codice, questi si moltiplicano / dividono per due e i poteri di due operazioni sono usati in modo molto intenso e poiché le formule sono già piuttosto brevi, ogni istruzione che può essere eliminata può essere un guadagno sostanziale. Se il processore non supporta questi operatori bit-shift, non si verificherà alcun guadagno ma non si verificherà alcuna perdita.

Inoltre, negli algoritmi che sto scrivendo, rappresentano visivamente i movimenti che si verificano, in tal senso in realtà sono più chiari. La parte sinistra di un albero binario è più grande e la destra è più piccola. Inoltre, nel mio codice, i numeri pari e dispari hanno un significato speciale, e tutti i bambini della mano sinistra nella struttura sono dispari e tutti i bambini della mano destra, e la radice, sono pari. In alcuni casi, che non ho ancora riscontrato, ma forse, oh, in realtà non ci avevo nemmeno pensato, x & 1 potrebbe essere un'operazione più ottimale rispetto a x% 2. x & 1 su un numero pari produrrà zero, ma produrrà 1 per un numero dispari.

Andando un po 'oltre la semplice identificazione dispari / pari, se ottengo zero per x & 3 so che 4 è un fattore del nostro numero e lo stesso per x% 7 per 8 e così via. So che questi casi hanno probabilmente un'utilità limitata, ma è bello sapere che è possibile evitare un'operazione di modulo e utilizzare invece un'operazione di logica bit a bit, poiché le operazioni a bit sono quasi sempre le più veloci e probabilmente meno ambigue per il compilatore.

Sto praticamente inventando il campo di alberi binari densi, quindi mi aspetto che le persone non possano cogliere il valore di questo commento, poiché molto raramente le persone vogliono eseguire solo fattorizzazioni su soli poteri di due o solo moltiplicare / dividere i poteri di due.



0

Se si confronta l'output per x + x, x * 2 e x << 1 sintassi su un compilatore gcc, si otterrebbe lo stesso risultato nell'assembly x86: https://godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

Quindi puoi considerare gcc abbastanza intelligente da determinare la sua migliore soluzione indipendentemente da ciò che hai digitato.


0

Anch'io volevo vedere se potevo battere la casa. questo è un bit a bit più generale per qualsiasi numero per qualsiasi moltiplicazione di numeri. le macro che ho realizzato sono circa il 25% in più rispetto al doppio rispetto alla normale * moltiplicazione. come detto da altri, se è vicino a un multiplo di 2 o composto da pochi multipli di 2, potresti vincere. come X * 23 composto da (X << 4) + (X << 2) + (X << 1) + X sarà più lento di X * 65 composto da (X << 6) + X.

#include <stdio.h>
#include <time.h>

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
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.