È buona norma sostituire la divisione con la moltiplicazione quando possibile?


73

Ogni volta che ho bisogno di divisione, ad esempio, il controllo delle condizioni, vorrei riformattare l'espressione della divisione in moltiplicazione, ad esempio:

Versione originale:

if(newValue / oldValue >= SOME_CONSTANT)

Nuova versione:

if(newValue >= oldValue * SOME_CONSTANT)

Perché penso che possa evitare:

  1. Divisione per zero

  2. Overflow quando oldValueè molto piccolo

È giusto? C'è un problema per questa abitudine?


41
Fai attenzione che con numeri negativi, le due versioni controllino cose totalmente diverse. Ne sei sicuro oldValue >= 0?
user2313067,

37
A seconda del linguaggio (ma in particolare con C), qualunque sia l'ottimizzazione a cui riesci a pensare, il compilatore di solito può farlo meglio, o OPPURE , ha abbastanza senso da non farlo affatto.
Mark Benningfield,

63
Non è mai "una buona pratica" sostituire sempre il codice X con il codice Y quando X e Y non sono semanticamente equivalenti. Ma è sempre una buona idea guardare X e Y, accendere il cervello, pensare a quali sono i requisiti e quindi decidere quale delle due alternative sia più corretta. E dopo ciò, dovresti anche pensare a quali test sono necessari per verificare di aver corretto le differenze semantiche.
Doc Brown

12
@MarkBenningfield: non importa, il compilatore non può ottimizzare la divisione in assente per zero. L '"ottimizzazione" che stai pensando è "ottimizzazione della velocità". L'OP sta pensando a un altro tipo di ottimizzazione: l'eliminazione dei bug.
Slebetman,

25
Il punto 2 è falso. La versione originale può overflow per valori piccoli, ma la nuova versione può overflow per valori grandi, quindi nessuno dei due è più sicuro nel caso generale.
Jacques B

Risposte:


74

Due casi comuni da considerare:

Aritmetica intera

Ovviamente se stai usando l'aritmetica dei numeri interi (che tronca) otterrai un risultato diverso. Ecco un piccolo esempio in C #:

public static void TestIntegerArithmetic()
{
    int newValue = 101;
    int oldValue = 10;
    int SOME_CONSTANT = 10;

    if(newValue / oldValue > SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue > oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Produzione:

First comparison says it's not bigger.
Second comparison says it's bigger.

Aritmetica a virgola mobile

A parte il fatto che la divisione può produrre un risultato diverso quando si divide per zero (genera un'eccezione, mentre la moltiplicazione no), può anche causare errori di arrotondamento leggermente diversi e un risultato diverso. Semplice esempio in C #:

public static void TestFloatingPoint()
{
    double newValue = 1;
    double oldValue = 3;
    double SOME_CONSTANT = 0.33333333333333335;

    if(newValue / oldValue >= SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue >= oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Produzione:

First comparison says it's not bigger.
Second comparison says it's bigger.

Nel caso in cui non mi credi, ecco un violino che puoi eseguire e vedere di persona.

Altre lingue possono essere diverse; tenere presente, tuttavia, che C #, come molti linguaggi, implementa una libreria a virgola mobile standard IEEE (IEEE 754) , quindi si dovrebbero ottenere gli stessi risultati in altri tempi di esecuzione standardizzati.

Conclusione

Se stai lavorando in campo verde , probabilmente stai bene.

Se stai lavorando su un codice legacy e l'applicazione è un'applicazione finanziaria o di altro tipo che esegue operazioni aritmetiche ed è necessaria per fornire risultati coerenti, fai molta attenzione quando cambi le operazioni. Se è necessario, assicurarsi di disporre di unit test che rileveranno eventuali lievi modifiche nell'aritmetica.

Se stai semplicemente facendo cose come contare gli elementi in un array o altre funzioni di calcolo generali, probabilmente starai bene. Tuttavia, non sono sicuro che il metodo di moltiplicazione renda il tuo codice più chiaro.

Se stai implementando un algoritmo su una specifica, non cambierei nulla, non solo a causa del problema degli errori di arrotondamento, ma in modo che gli sviluppatori possano rivedere il codice e mappare ciascuna espressione sulla specifica per garantire che non ci siano implementazioni difetti.


41
Secondo il bit finanziario. Questo tipo di interruttore chiede ai contabili di inseguirti con forconi. Ricordo 5.000 righe in cui ho dovuto fare più sforzi per tenere a bada i forconi piuttosto che per trovare la risposta "giusta", che in realtà era leggermente sbagliata. Essere fuori dallo 0,01% non aveva importanza, le risposte assolutamente coerenti erano obbligatorie. Quindi sono stato costretto a fare il calcolo in modo da causare un sistematico errore di arrotondamento.
Loren Pechtel,

8
Pensa all'acquisto di caramelle da 5 centesimi (non ce ne sono più.) Acquista 20 pezzi, la risposta "corretta" non era una tassa perché non c'erano tasse su 20 acquisti di un pezzo.
Loren Pechtel,

24
@LorenPechtel, perché la maggior parte dei sistemi fiscali include una regola (per ovvi motivi) che l'imposta viene riscossa per transazione e l'imposta è dovuta in incrementi non inferiori alla moneta più piccola del regno e gli importi frazionari sono arrotondati per favore. Tali regole sono "corrette" perché sono lecite e coerenti. I contabili con forcone probabilmente sanno quali sono le regole in un modo che i programmatori di computer non faranno (a meno che non siano anche esperti contabili). Un errore dello 0,01% probabilmente causerà un errore di bilanciamento ed è illegale avere un errore di bilanciamento.
Steve

9
Poiché non avevo mai sentito il termine greenfield prima, l'ho cercato. Wikipedia afferma che "è un progetto privo di vincoli imposti dal lavoro precedente".
Henrik Ripa,

9
@Steve: Il mio capo ha recentemente contrastato "greenfield" con "brownfield". Ho osservato che alcuni progetti sono più simili a "campo nero" ...
MrGreen

25

Mi piace la tua domanda in quanto potenzialmente copre molte idee. Nel complesso, sospetto che la risposta dipenda , probabilmente dai tipi coinvolti e dal possibile intervallo di valori nel tuo caso specifico.

Il mio istinto iniziale è di riflettere sullo stile , cioè. la tua nuova versione è meno chiara al lettore del tuo codice. Immagino che dovrei pensare per un secondo o due (o forse più a lungo) per determinare l'intenzione della tua nuova versione, mentre la tua vecchia versione è immediatamente chiara. La leggibilità è un attributo importante del codice, quindi c'è un costo nella tua nuova versione.

Hai ragione che la nuova versione evita una divisione per zero. Certamente non è necessario aggiungere una guardia (sulla falsariga di if (oldValue != 0)). Ma ha senso? La tua vecchia versione riflette un rapporto tra due numeri. Se il divisore è zero, il rapporto non è definito. Questo può essere più significativo nella tua situazione, ad es. non dovresti produrre un risultato in questo caso.

La protezione da overflow è discutibile. Se sai che newValueè sempre più grande di oldValue, forse potresti argomentare. Tuttavia, potrebbero esserci casi in cui (oldValue * SOME_CONSTANT)traboccerà anche. Quindi non vedo molto guadagno qui.

È possibile che si ottenga una prestazione migliore perché la moltiplicazione può essere più veloce della divisione (su alcuni processori). Tuttavia, ci dovrebbero essere molti calcoli come questi per ottenere un guadagno significativo, ad es. attenzione all'ottimizzazione prematura.

Riflettendo su tutto quanto sopra, in generale non penso che ci sia molto da guadagnare con la tua nuova versione rispetto alla versione precedente, in particolare data la riduzione della chiarezza. Tuttavia, potrebbero esserci casi specifici in cui vi è qualche vantaggio.


16
Ehm, la moltiplicazione arbitraria essendo più efficiente della divisione arbitraria non dipende dal processore, per le macchine del mondo reale.
Deduplicatore

1
Esiste anche il problema dell'aritmetica tra numeri interi e virgola mobile. Se il rapporto è frazionario, la divisione deve essere eseguita in virgola mobile, richiedendo un cast. La mancanza del cast provocherà un errore involontario. Se la frazione risulta essere un rapporto tra due piccoli numeri interi, quindi la loro disposizione consente di effettuare il confronto in aritmetica intera. (A quel punto verranno applicati i tuoi argomenti.)
Nuovo

@rwong Non sempre. Diverse lingue là fuori fanno divisione intera facendo cadere la parte decimale, quindi non è necessario il cast.
T. Sar - Ripristina Monica

@ T.Sar La tecnica che descrivi e la semantica descritta nella risposta sono diverse. La semantica è se il programmatore intende che la risposta sia un valore in virgola mobile o frazionario; la tecnica che descrivi è la divisione per moltiplicazione reciproca, che a volte è una perfetta approssimazione (sostituzione) per una divisione intera. Quest'ultima tecnica viene in genere applicata quando il divisore è noto in anticipo, poiché la derivazione dell'intero reciproco (spostato di 2 ** 32) può essere eseguita in fase di compilazione. Farlo in fase di esecuzione non sarebbe utile perché è più costoso della CPU.
rwong

22

No.

Probabilmente chiamerei quell'ottimizzazione prematura , in senso lato, indipendentemente dal fatto che tu stia ottimizzando le prestazioni , come generalmente fa riferimento alla frase, o qualsiasi altra cosa che possa essere ottimizzata, come il conteggio dei bordi , le righe di codice o ancora più in generale, cose come "design".

L'implementazione di quel tipo di ottimizzazione come procedura operativa standard mette a rischio la semantica del codice e potenzialmente nasconde i bordi. I casi limite che ritieni opportuni da eliminare silenziosamente potrebbero dover essere comunque esplicitamente affrontati . Ed è infinitamente più facile eseguire il debug dei problemi attorno ai bordi rumorosi (quelli che generano eccezioni) su quelli che falliscono silenziosamente.

E, in alcuni casi, è persino vantaggioso "disottimizzare" per motivi di leggibilità, chiarezza o esplicitazione. Nella maggior parte dei casi, i tuoi utenti non noteranno che hai salvato alcune righe di codice o cicli della CPU per evitare la gestione dei casi limite o la gestione delle eccezioni. D'altra parte, il codice scomodo o in silenzio non influirà sulle persone - almeno i tuoi colleghi. (E anche, quindi, il costo per costruire e mantenere il software.)

Predefinito a tutto ciò che è più "naturale" e leggibile rispetto al dominio dell'applicazione e al problema specifico. Mantenerlo semplice, esplicito e idiomatico. Ottimizza come è necessario per guadagni significativi o per raggiungere una soglia di usabilità legittima.

Nota anche: i compilatori spesso ottimizzano la divisione per te, quando è sicuro farlo.


11
-1 Questa risposta non si adatta alla domanda, che riguarda le potenziali insidie ​​della divisione - niente a che fare con l'ottimizzazione
Ben Cottrell,

13
@BenCottrell Si adatta perfettamente. La trappola sta nel mettere valore in inutili ottimizzazioni delle prestazioni a scapito della manutenibilità. Dalla domanda "c'è qualche problema per questa abitudine?" - sì. Porterà rapidamente alla scrittura di parole incomprensibili.
Michael

9
@Michael, la domanda non si pone nemmeno su nessuna di queste cose: si tratta in particolare della correttezza di due espressioni diverse, ognuna con una semantica e un comportamento diversi, ma entrambe intese a soddisfare lo stesso requisito.
Ben Cottrell,

5
@BenCottrell Forse potresti indicarmi dove nella domanda si fa menzione della correttezza?
Michael

5
@BenCottrell Avresti dovuto semplicemente dire 'Non posso' :)
Michael

13

Utilizzare quello che è meno buggy e ha più senso logico.

Di solito , la divisione per una variabile è comunque una cattiva idea, poiché di solito il divisore può essere zero.
La divisione per una costante di solito dipende solo dal significato logico.

Ecco alcuni esempi per dimostrarlo dipende dalla situazione:

Divisione buona:

if ((ptr2 - ptr1) >= n / 3)  // good: check if length of subarray is at least n/3
    ...

Moltiplicazione non valida:

if ((ptr2 - ptr1) * 3 >= n)  // bad: confusing!! what is the intention of this code?
    ...

Moltiplicazione buona:

if (j - i >= 2 * min_length)  // good: obviously checking for a minimum length
    ...

Divisione negativa:

if ((j - i) / 2 >= min_length)  // bad: confusing!! what is the intention of this code?
    ...

Moltiplicazione buona:

if (new_length >= old_length * 1.5)  // good: is the new size at least 50% bigger?
    ...

Divisione negativa:

if (new_length / old_length >= 2)  // bad: BUGGY!! will fail if old_length = 0!
    ...

2
Sono d'accordo che dipende dal contesto, ma le tue prime due coppie di esempi sono estremamente povere. Non preferirei l'uno all'altro in entrambi i casi.
Michael

6
@Michael: Uhm ... trovi (ptr2 - ptr1) * 3 >= nfacile capire quanto l'espressione ptr2 - ptr1 >= n / 3? Non ti fa inciampare nel cervello e tornare indietro cercando di decifrare il significato di triplicare la differenza tra due puntatori? Se è davvero ovvio per te e la tua squadra, quindi più potere per te immagino; Devo solo essere nella lenta minoranza.
Mehrdad,

2
Una variabile chiamata ne un numero arbitrario 3 creano confusione in entrambi i casi ma, sostituiti con nomi ragionevoli, no, non trovo l'uno più confuso dell'altro.
Michael

1
Questi esempi non sono davvero scadenti ... sicuramente non "estremamente scadenti" - anche se si inseriscono "nomi ragionevoli" hanno ancora meno senso quando li si scambia per i casi negativi. Se fossi nuovo a un progetto, preferirei di gran lunga vedere i casi "buoni" elencati in questa risposta quando sono andato a sistemare un po 'di codice di produzione.
John-M,

3

Fare qualsiasi cosa "quando possibile" è molto raramente una buona idea.

La priorità numero uno dovrebbe essere la correttezza, seguita da leggibilità e manutenibilità. Sostituire ciecamente la divisione con la moltiplicazione, quando possibile, spesso fallirà nel reparto di correttezza, a volte solo in casi rari e quindi difficili da trovare.

Fai ciò che è corretto e più leggibile. Se disponi di solide prove del fatto che la scrittura del codice nel modo più leggibile causa un problema di prestazioni, puoi prendere in considerazione la possibilità di modificarlo. Cura, matematica e recensioni di codici sono i tuoi amici.


1

Per quanto riguarda la leggibilità del codice, penso che la moltiplicazione sia effettivamente più leggibile in alcuni casi. Ad esempio, se c'è qualcosa che devi verificare se newValueè aumentato del 5 percento o più sopra oldValue, allora 1.05 * oldValuec'è una soglia rispetto alla quale testare newValueed è naturale scrivere

    if (newValue >= 1.05 * oldValue)

Ma fai attenzione ai numeri negativi quando rifletti le cose in questo modo (sostituendo la divisione con la moltiplicazione o sostituendo la moltiplicazione con la divisione). Le due condizioni considerate equivalenti oldValuesono garantite per non essere negative; ma supponiamo che newValuesia -13.5 e oldValue-10.1. Poi

newValue/oldValue >= 1.05

valuta vero , ma

newValue >= 1.05 * oldValue

valuta falso .


1

Nota la famosa divisione cartacea di Invariant Integers utilizzando la moltiplicazione .

Il compilatore sta effettivamente facendo moltiplicazioni, se l'intero è invariante! Non una divisione. Questo accade anche per non potenza di 2 valori. La potenza di 2 divisioni usa ovviamente i bit shift e quindi è ancora più veloce.

Tuttavia, per numeri interi non invarianti, è tua responsabilità ottimizzare il codice. Assicurati prima di ottimizzare che stai davvero ottimizzando un vero collo di bottiglia e che la correttezza non è sacrificata. Attenzione al trabocco di numeri interi.

Mi interessa la micro-ottimizzazione, quindi probabilmente darei un'occhiata alle possibilità di ottimizzazione.

Pensa anche alle architetture su cui viene eseguito il tuo codice. Soprattutto ARM ha una divisione estremamente lenta; devi chiamare una funzione per dividere, non ci sono istruzioni di divisione in ARM.

Inoltre, sulle architetture a 32 bit, la divisione a 64 bit non è ottimizzata, come ho scoperto .


1

Raccogliendo il punto 2, impedirà davvero l'overflow per un valore molto piccolo oldValue. Tuttavia, se SOME_CONSTANTè anche molto piccolo, il tuo metodo alternativo finirà con underflow, dove il valore non può essere rappresentato con precisione.

E viceversa, cosa succede se oldValueè molto grande? Hai gli stessi problemi, esattamente il contrario.

Se si desidera evitare (o minimizzare) il rischio di overflow / underflow, il modo migliore è verificare se la newValuegrandezza è la più vicina oldValueo meno SOME_CONSTANT. È quindi possibile scegliere anche l'operazione di divisione appropriata

    if(newValue / oldValue >= SOME_CONSTANT)

o

    if(newValue / SOME_CONSTANT >= oldValue)

e il risultato sarà più preciso.

Per dividere per zero, nella mia esperienza questo non è quasi mai appropriato essere "risolto" in matematica. Se hai una divisione per zero nei tuoi controlli continui, allora quasi sicuramente hai una situazione che richiede alcune analisi e tutti i calcoli basati su questi dati sono privi di significato. Un controllo esplicito di divisione per zero è quasi sempre la mossa appropriata. (Nota che dico "quasi" qui, perché non pretendo di essere infallibile. Noterò solo che non ricordo di aver visto una buona ragione per questo in 20 anni di scrittura di software incorporato, e andare avanti .)

Tuttavia, se hai un reale rischio di overflow / underflow nella tua applicazione, probabilmente questa non è la soluzione giusta. Più probabilmente, dovresti generalmente controllare la stabilità numerica del tuo algoritmo, o forse semplicemente passare a una rappresentazione di maggiore precisione.

E se non hai un comprovato rischio di overflow / underflow, non ti preoccupi di nulla. Ciò significa che devi letteralmente dimostrare di averne bisogno, con i numeri, nei commenti accanto al codice che spiegano a un manutentore perché è necessario. Come ingegnere principale che revisionava il codice di altre persone, se avessi incontrato qualcuno che si sforzava di farlo, personalmente non avrei accettato nulla di meno. Questo è un po 'l'opposto dell'ottimizzazione prematura, ma in genere avrebbe la stessa causa principale: l'ossessione per i dettagli che non fa alcuna differenza funzionale.


0

Incapsula l'aritmetica condizionale in metodi e proprietà significativi. Non solo buona denominazione dirvi che cosa "A / B" significa , parametro controllo e la gestione degli errori possono ordinatamente nascondersi anche lì.

È importante sottolineare che, poiché questi metodi sono composti in una logica più complessa, la complessità estrinseca rimane molto gestibile.

Direi che la sostituzione della moltiplicazione sembra una soluzione ragionevole perché il problema è mal definito.


0

Penso che non potrebbe essere una buona idea sostituire le moltiplicazioni con le divisioni perché l'ALU (Arithmetic-Logic Unit) della CPU esegue algoritmi, sebbene siano implementati nell'hardware. Tecniche più sofisticate sono disponibili nei processori più recenti. Generalmente, i processori si sforzano di parallelizzare le operazioni di coppie di bit per ridurre al minimo i cicli di clock richiesti. Gli algoritmi di moltiplicazione possono essere parallelizzati abbastanza efficacemente (sebbene siano necessari più transistor). Gli algoritmi di divisione non possono essere parallelizzati in modo efficiente. Gli algoritmi di divisione più efficienti sono piuttosto complessi. In genere, richiedono più cicli di clock per bit.

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.