Le istruzioni condizionali non banali devono essere spostate nella sezione di inizializzazione dei loop?


21

Ho avuto questa idea da questa domanda su stackoverflow.com

Il seguente modello è comune:

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

Il punto che sto cercando di fare è che l'affermazione condizionale è qualcosa di complicato e non cambia.

È meglio dichiararlo nella sezione di inizializzazione del loop, come tale?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

È più chiaro?

E se l'espressione condizionale è semplice come

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}

47
Perché non spostarlo sulla riga prima del loop, quindi puoi anche dargli un nome ragionevole.
jonrsharpe,

2
@Mehrdad: non sono equivalenti. Se xè grande in grandezza, Math.floor(Math.sqrt(x))+1è uguale a Math.floor(Math.sqrt(x)). :-)
R ..

5
@jonrsharpe Perché ciò amplierebbe l'ambito della variabile. Non sto necessariamente dicendo che sia una buona ragione per non farlo, ma questa è la ragione per cui alcune persone non lo fanno.
Kevin Krumwiede il

3
@KevinKrumwiede Se l'ambito è un problema, limitalo inserendo il codice nel suo blocco, ad esempio, { x=whatever; for (...) {...} }o, meglio ancora, considera se c'è abbastanza da fare che deve essere una funzione separata.
Blrfl,

2
@jonrsharpe Puoi dargli un nome ragionevole anche quando lo dichiari nella sezione init. Non dicendo che lo metterei lì; è ancora più facile da leggere se è separato.
JollyJoker il

Risposte:


62

Quello che farei è qualcosa del genere:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Onestamente, l'unica buona ragione per stipare l'inizializzazione j(ora limit) nell'intestazione del loop è di mantenerlo correttamente. Tutto ciò che serve per fare in modo che un non-problema sia un ambito di applicazione stretto e piacevole.

Posso apprezzare il desiderio di essere veloce ma non sacrificare la leggibilità senza una vera ragione.

Certo, il compilatore può ottimizzare, l'inizializzazione di più var può essere legale, ma i loop sono abbastanza difficili da eseguire il debug così com'è. Per favore sii gentile con gli umani. Se questo davvero ci rallenta, è bello capirlo abbastanza per risolverlo.


Buon punto. Suppongo che non si preoccupi di un'altra variabile se l'espressione è qualcosa di semplice, ad esempio for(int i = 0; i < n*n; i++){...}non assegneresti n*na una variabile, vero?
Celeritas,

1
Vorrei e ho. Ma non per la velocità. Per leggibilità. Il codice leggibile tende ad essere veloce da solo.
candied_orange,

1
Anche il problema di scoping scompare se si contrassegna è come costante ( final). A chi importa se una costante che ha l'applicazione del compilatore che ne impedisce la modifica è accessibile più avanti nella funzione?
jpmc26,

Penso che una cosa importante sia ciò che ti aspetti che accada. So che l'esempio usa sqrt, ma se fosse un'altra funzione? La funzione è pura? Ti aspetti sempre gli stessi valori? Ci sono effetti collaterali? Gli effetti collaterali sono qualcosa che intendi accadere in ogni iterazione?
Pieter B,

38

Un buon compilatore genererà lo stesso codice in entrambi i modi, quindi se stai andando per le prestazioni, apporta una modifica solo se si trova in un ciclo critico e lo hai effettivamente profilato e hai trovato che fa la differenza. Anche se il compilatore non è in grado di ottimizzarlo, come la gente ha sottolineato nei commenti sul caso con chiamate di funzione, nella stragrande maggioranza delle situazioni, la differenza di prestazioni sarà troppo piccola per valere la pena di essere considerata da un programmatore.

Tuttavia...

Non dobbiamo dimenticare che il codice è principalmente un mezzo di comunicazione tra umani ed entrambe le opzioni non comunicano molto bene con gli altri umani. Il primo dà l'impressione che l'espressione debba essere calcolata su ogni iterazione, e il secondo nella sezione di inizializzazione implica che verrà aggiornato da qualche parte all'interno del ciclo, dove è veramente costante.

Preferirei davvero che fosse estratto sopra il ciclo e fatto finalper renderlo immediatamente e abbondantemente chiaro a chiunque leggesse il codice. Questo non è l'ideale neanche perché aumenta l'ambito della variabile, ma la tua funzione che racchiude non dovrebbe contenere molto più di quel ciclo comunque.


5
Una volta che inizi a includere le chiamate di funzione, diventa molto più difficile per il compilatore ottimizzare. Il compilatore dovrebbe avere una conoscenza speciale che Math.sqrt non ha effetti collaterali.
Peter Green,

@PeterGreen Se questo è Java, la JVM può capirlo, ma potrebbe richiedere del tempo.
Chrylis

La domanda è taggata sia in C ++ che in Java. Non so quanto sia avanzata la JVM di Java e quando può e non può capirlo, ma so che i compilatori C ++ in generale non riescono a capirlo ma una funzione è pura a meno che non abbia un'annotazione non standard che dice al compilatore così, o il corpo della funzione è visibile e tutte le funzioni chiamate indirettamente possono essere rilevate come pure con gli stessi criteri. Nota: gli effetti collaterali non sono sufficienti per spostarlo fuori dalla condizione di loop. Le funzioni che dipendono dallo stato globale non possono essere spostate fuori dal loop se il corpo del loop può modificare lo stato.
hvd il

Diventa interessante quando Math.sqrt (x) viene sostituito da Mymodule.SomeNonPureMethodWithSideEffects (x).
Pieter B,

9

Come ha affermato @Karl Bielefeldt nella sua risposta, questo di solito non è un problema.

Tuttavia, un tempo era un problema comune in C e C ++, e un trucco si avvicinò al problema senza ridurre la leggibilità del codice - iterare all'indietro, fino a0 .

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Ora il condizionale in ogni iterazione è proprio quello >= 0che ogni compilatore compilerà in 1 o 2 istruzioni di assemblaggio. Ogni CPU prodotta negli ultimi decenni dovrebbe avere controlli di base come questi; facendo un rapido controllo sulla mia macchina x64 vedo che questo si trasforma in prevedibilmente in cmpl $0x0, -0x14(%rbp)(valore di confronto lungo int 0 rispetto al registro rbp compensato -14) e jl 0x100000f59(passa all'istruzione seguendo il ciclo se il confronto precedente era vero per “2nd-arg <1st-arg ”) .

Si noti che ho rimosso il + 1da Math.floor(Math.sqrt(x)) + 1; affinché la matematica si risolva, il valore iniziale dovrebbe essere int i = «iterationCount» - 1. Vale anche la pena notare che il tuo iteratore deve essere firmato; unsigned intnon funzionerà e probabilmente compilerà un avviso.

Dopo aver programmato in linguaggi basati su C per circa 20 anni, ora scrivo solo cicli di iterazione con indice inverso a meno che non ci sia un motivo specifico per iterare con indice in avanti. Oltre ai controlli più semplici nei condizionali, l'iterazione inversa spesso fa anche da contraltare a quelle che altrimenti sarebbero fastidiose mutazioni dell'array durante l'iterazione.


1
Tutto ciò che scrivi è tecnicamente corretto. Il commento sulle istruzioni può essere fuorviante, poiché tutte le CPU moderne sono state progettate per funzionare bene con i cicli di iterazione in avanti, indipendentemente dal fatto che abbiano un'istruzione speciale per loro. In ogni caso, la maggior parte del tempo viene normalmente trascorsa all'interno del ciclo, senza eseguire iterazioni.
Jørgen Fogh,

4
Va notato che le moderne ottimizzazioni del compilatore sono progettate tenendo presente il codice "normale". Nella maggior parte dei casi, il compilatore genererà un codice altrettanto veloce, indipendentemente dal fatto che si utilizzino o meno "trucchi" di ottimizzazione. Ma alcuni trucchi possono effettivamente ostacolare l' ottimizzazione a seconda di quanto siano complicati. Se l'utilizzo di alcuni trucchi rende il tuo codice più leggibile o aiuta a rilevare bug comuni, è fantastico, ma non indurti a pensare "se scrivo codice in questo modo sarà più veloce". Scrivi il codice come faresti normalmente, quindi profila per trovare i luoghi in cui devi ottimizzare.
0x5453

Si noti anche che unsigned contatori funzioneranno qui se si modifica il controllo (il modo più semplice è aggiungere lo stesso valore su entrambi i lati); per esempio, per qualsiasi decremento Dec, il controllo (i + Dec) >= Decdeve sempre avere lo stesso risultato di signedcontrollo i >= 0, sia con signede unsignedcontatori, a condizione che il linguaggio ha regole avvolgenti ben definiti per unsignedle variabili (in particolare, -n + n == 0deve essere vero per entrambi signede unsigned). Si noti, tuttavia, che questo potrebbe essere meno efficiente di un >=0controllo firmato , se il compilatore non ha un'ottimizzazione per esso.
Justin Time - Ripristina Monica il

1
@JustinTime Sì, il requisito firmato è la parte più difficile; l'aggiunta di una Deccostante sia al valore iniziale che a quello finale funziona, ma lo rende molto meno intuitivo e se si utilizza icome indice di matrice, è necessario eseguire anche una unsigned int arrayI = i - Dec;sequenza nel corpo del ciclo. Uso solo l'iterazione in avanti quando bloccato con un iteratore non firmato; spesso con una i <= count - 1condizione per mantenere la logica parallela ai loop di iterazione inversa.
Slipp D. Thompson,

1
@ SlippD.Thompson Non intendevo aggiungere Decspecificamente i valori di inizio e fine, ma spostando il controllo delle condizioni da Decentrambi i lati. for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ Ciò consente di utilizzare inormalmente all'interno del loop, garantendo al contempo che il valore più basso possibile sul lato sinistro della condizione è 0(per impedire l'interferenza del wraparound). È sicuramente molto più semplice usare l'iterazione diretta quando si lavora con contatori non firmati.
Justin Time - Ripristina Monica il

3

Diventa interessante quando Math.sqrt (x) viene sostituito da Mymodule.SomeNonPureMethodWithSideEffects (x).

Fondamentalmente il mio modus operandi è: se si prevede che qualcosa dia sempre lo stesso valore, valutarlo solo una volta. Ad esempio List.Count, se si prevede che l'elenco non cambi durante il funzionamento del ciclo, ottenere il conteggio esterno al ciclo in un'altra variabile.

Alcuni di questi "conteggi" possono essere sorprendentemente costosi soprattutto quando si ha a che fare con database. Anche se stai lavorando su un set di dati che non dovrebbe cambiare durante l'iterazione dell'elenco.


Quando il conteggio è costoso, non dovresti usarlo affatto. Invece dovresti fare l'equivalente difor( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt il

@BenVoigt L'utilizzo di un iteratore è sicuramente il modo migliore per queste operazioni. L'ho semplicemente menzionato per illustrare il mio punto sull'uso di metodi non puri con effetti collaterali.
Pieter B,

0

A mio avviso, questo è altamente specifico della lingua. Ad esempio, se utilizzassi C ++ 11, sospetterei che se il controllo delle condizioni fosse una constexprfunzione, il compilatore sarebbe molto probabilmente in grado di ottimizzare le esecuzioni multiple poiché sa che produrrà lo stesso valore ogni volta.

Tuttavia, se la chiamata di funzione è una funzione di libreria che non è constexpril compilatore, la eseguirà quasi sicuramente su ogni iterazione in quanto non può dedurla (a meno che non sia in linea e quindi possa essere dedotta come pura).

So meno di Java, ma dato che è compilato JIT immagino che il compilatore abbia abbastanza informazioni in fase di esecuzione per probabilmente inline e ottimizzare la condizione. Ma questo farebbe affidamento su una buona progettazione del compilatore e il compilatore che ha deciso che questo ciclo rappresentava una priorità di ottimizzazione che possiamo solo immaginare.

Personalmente ritengo che sia leggermente più elegante mettere la condizione all'interno del ciclo for se puoi, ma se è complessa la scriverò in una constexpro inlinefunzione, o i tuoi linguaggi equivalgono a suggerire che la funzione è pura e ottimizzabile. Ciò rende evidente l'intento e mantiene lo stile idiomatico del loop senza creare una linea enorme illeggibile. Dà anche un nome alla verifica della condizione se è la sua stessa funzione in modo che i lettori possano immediatamente vedere logicamente a cosa serve la verifica senza leggerla se complessa.

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.