È più veloce il conto alla rovescia che il conto alla rovescia?


131

Il nostro insegnante di informatica una volta ha detto che per qualche motivo è più efficiente il conto alla rovescia che il conto alla rovescia. Ad esempio, se è necessario utilizzare un ciclo FOR e l'indice del ciclo non viene utilizzato da qualche parte (come stampare una linea di N * sullo schermo), intendo quel codice in questo modo:

for (i = N; i >= 0; i--)  
  putchar('*');  

è meglio di:

for (i = 0; i < N; i++)  
  putchar('*');  

È veramente vero? E se è così, qualcuno sa perché?


6
Quale scienziato informatico? In quale pubblicazione?
bmargulies,

26
È concepibile che tu possa risparmiare un nanosecondo per iterazione, o circa quanto un solo capello su una famiglia di mammut lanosi. La putcharsta usando 99,9999% del tempo (più o meno).
Mike Dunlavey,

38
L'ottimizzazione prematura è la radice di tutti i mali. Usa qualunque forma ti sembri giusta, perché (come già sai) sono logicamente equivalenti. La parte più difficile della programmazione è comunicare la teoria del programma ad altri programmatori (e te stesso!). L'uso di un costrutto che fa sì che tu o qualche altro programmatore lo guardi per più di un secondo è una perdita netta. Non recupererai mai il tempo che qualcuno spende pensando "perché questo conta alla rovescia?"
David M,

61
Il primo ciclo è ovviamente più lento, poiché chiama putchar 11 volte, mentre il secondo lo chiama solo 10 volte.
Paul Kuliniewicz,

17
Hai notato che se inon è firmato, il primo ciclo è infinito?
Shahbaz,

Risposte:


371

È veramente vero? e se sì qualcuno sa perché?

Nei tempi antichi, quando i computer erano ancora scheggiati a mano dalla silice fusa a mano, quando i microcontrollori a 8 bit vagavano per la Terra e quando il tuo insegnante era giovane (o l'insegnante del tuo insegnante era giovane), c'era un'istruzione macchina comune chiamata decrement and skip se zero (DSZ). I programmatori di assiemi Hotshot hanno utilizzato queste istruzioni per implementare i loop. Le macchine successive ottennero istruzioni più elaborate, ma c'erano ancora parecchi processori su cui era più economico confrontare qualcosa con zero piuttosto che confrontarsi con qualsiasi altra cosa. (È vero anche su alcune moderne macchine RISC, come PPC o SPARC, che riservano un intero registro sempre zero.)

Quindi, se rig i tuoi loop per confrontarli con zero invece di N, cosa potrebbe accadere?

  • È possibile salvare un registro
  • È possibile ottenere un'istruzione di confronto con una codifica binaria più piccola
  • Se si verifica un'istruzione precedente per impostare un flag (probabilmente solo su macchine della famiglia x86), potrebbe non essere necessaria nemmeno un'istruzione di confronto esplicita

È probabile che queste differenze provochino miglioramenti misurabili su programmi reali su un moderno processore fuori servizio? Altamente improbabile. In effetti, sarei impressionato se potessi mostrare un miglioramento misurabile anche su un microbenchmark.

Riepilogo: colpisco il tuo insegnante a testa in giù! Non dovresti imparare pseudo-fatti obsoleti su come organizzare i loop. Dovresti imparare che la cosa più importante dei loop è assicurarsi che terminino , producano risposte corrette e siano facili da leggere . Vorrei che il tuo insegnante si concentrasse sulle cose importanti e non sulla mitologia.


3
++ E inoltre, putcharprende molti ordini di grandezza più a lungo del sovraccarico del loop.
Mike Dunlavey,

41
Non è strettamente mitologia: se sta facendo una sorta di sistema in tempo reale super ottimizzato, sarebbe utile. Ma quel tipo di hacker probabilmente saprebbe già tutto questo e certamente non confonderebbe gli studenti CS di livello base con gli arcani.
Paul Nathan,

4
@Joshua: In che modo questa ottimizzazione sarebbe rilevabile? Come ha detto l'interrogatore, l'indice del ciclo non viene utilizzato nel ciclo stesso, quindi a condizione che il numero di iterazioni sia lo stesso, non si verifica alcun cambiamento nel comportamento. In termini di prova di correttezza, la sostituzione della variabile j=N-imostra che i due loop sono equivalenti.
psmears,

7
+1 per il Riepilogo. Non sudare perché su hardware moderno non fa praticamente alcuna differenza. Non ha fatto praticamente alcuna differenza 20 anni fa. Se pensi di doverti preoccupare, cronometra in entrambi i modi, non vedi alcuna chiara differenza e torna a scrivere il codice in modo chiaro e corretto .
Donal Fellows,

3
Non so se dovrei votare per il corpo o downvote per il riassunto.
Danubian Sailor,

29

Ecco cosa potrebbe accadere su alcuni hardware a seconda di ciò che il compilatore può dedurre sull'intervallo dei numeri che stai usando: con il ciclo incrementale devi testare i<Nogni volta intorno al ciclo. Per la versione decrescente, il flag carry (impostato come effetto collaterale della sottrazione) può indicare automaticamente se i>=0. Ciò consente di risparmiare un test per volta intorno al ciclo.

In realtà, sul moderno hardware del processore pipeline, questa roba è quasi certamente irrilevante in quanto non esiste una semplice mappatura 1-1 dalle istruzioni ai cicli di clock. (Anche se potrei immaginarmelo se steste facendo cose come la generazione di segnali video a tempo preciso da un microcontrollore. Ma poi scriveresti comunque in linguaggio assembly.)


2
non sarebbe quella la bandiera zero e non la bandiera carry?
Bob,

2
@Bob In questo caso potresti voler raggiungere lo zero, stampare un risultato, diminuire ulteriormente, e poi scoprire che sei andato sotto lo zero causando un carry (o prestito). Ma scritto in modo leggermente diverso un ciclo decrescente potrebbe usare invece la bandiera zero.
sigfpe,

1
Solo per essere perfettamente pedanti, non tutto l'hardware moderno è pipeline. I processori integrati avranno molta più rilevanza per questo tipo di microottimizzazione.
Paul Nathan,

@Paul Dato che ho esperienza con gli AVR Atmel, non ho dimenticato di menzionare i microcontrollori ...
sigfpe,

27

Nel set di istruzioni Intel x86, la creazione di un loop per il conto alla rovescia può essere eseguita con un numero inferiore di istruzioni rispetto a un loop che conta fino a una condizione di uscita diversa da zero. In particolare, il registro ECX viene tradizionalmente utilizzato come contatore di loop in x86 asm e il set di istruzioni Intel ha un'istruzione jcxz jump speciale che verifica lo zero del registro ECX e salti in base al risultato del test.

Tuttavia, la differenza di prestazioni sarà trascurabile a meno che il loop non sia già molto sensibile ai conteggi del ciclo di clock. Il conto alla rovescia fino a zero potrebbe eliminare 4 o 5 cicli di clock da ogni iterazione del loop rispetto al conteggio, quindi è davvero più una novità che una tecnica utile.

Inoltre, un buon compilatore di ottimizzazione in questi giorni dovrebbe essere in grado di convertire il codice sorgente del ciclo di conteggio in codice di conto alla rovescia in zero (a seconda di come usi la variabile indice del ciclo), quindi non c'è davvero alcun motivo per scrivere i tuoi loop in modi strani solo per spremere un ciclo o due qua e là.


2
Ho visto il compilatore C ++ di Microsoft da qualche anno a rendere tale ottimizzazione. È in grado di vedere che l'indice del loop non viene utilizzato, quindi lo riorganizza nella forma più veloce.
Mark Ransom,

1
@Mark: anche il compilatore Delphi, a partire dal 1996.
dthorpe,

4
@MarkRansom In realtà, il compilatore potrebbe essere in grado di implementare il loop usando il conto alla rovescia anche se viene utilizzata la variabile indice del loop, a seconda di come viene utilizzata nel loop. Se la variabile dell'indice del loop viene utilizzata solo per indicizzare in array statici (array di dimensioni note al momento della compilazione), l'indicizzazione dell'array può essere eseguita come ptr + dimensione dell'array - indice del loop var, che può comunque essere una singola istruzione in x86. È abbastanza selvaggio essere assemblatore di debug e vedere il conto alla rovescia del loop ma gli indici dell'array salgono!
Dthorpe,

1
In realtà oggi il compilatore probabilmente non utilizzerà le istruzioni loop e jecxz poiché sono più lenti di una coppia dec / jnz.
fuz,

1
@FUZxxl Un motivo in più per non scrivere il tuo loop in modi strani. Scrivi un codice chiaro leggibile e lascia che il compilatore faccia il suo lavoro.
Dthorpe,

23

Sì..!!

Il conteggio da N a 0 è leggermente più veloce del conteggio da 0 a N, nel senso di come l'hardware gestirà il confronto.

Nota il confronto in ogni ciclo

i>=0
i<N

La maggior parte dei processori ha un confronto con zero istruzioni..quindi il primo verrà tradotto in codice macchina come:

  1. Carica i
  2. Confronta e salta se minore di o uguale a zero

Ma il secondo deve caricare ogni volta N form Memory

  1. carica i
  2. caricare N
  3. Sub i e N
  4. Confronta e salta se minore di o uguale a zero

Quindi non è per il conto alla rovescia o verso l'alto .. Ma per come il tuo codice verrà tradotto in codice macchina ..

Quindi contare da 10 a 100 equivale a contare da 100 a 10
Ma contare da i = 100 a 0 è più veloce di da i = 0 a 100 - nella maggior parte dei casi
E contare da i = N a 0 è più veloce di da i = Da 0 a N

  • Nota che al giorno d'oggi i compilatori possono fare questa ottimizzazione per te (se è abbastanza intelligente)
  • Nota anche che la pipeline può causare un effetto simile all'anomalia di Belady (non posso essere sicuro di cosa sarà meglio)
  • Finalmente: tieni presente che i 2 per i loop che hai presentato non sono equivalenti .. il primo ne stampa uno in più * ....

Correlati: Perché n ++ viene eseguito più velocemente di n = n + 1?


6
quindi quello che stai dicendo è che non è più veloce il conto alla rovescia, è solo più veloce da confrontare a zero di qualsiasi altro valore. Significa contare da 10 a 100 e contare da 100 a 10 sarebbe lo stesso?
Bob,

8
Sì .. non si tratta di "conto alla rovescia o in alto" .. ma si tratta di "paragone a cosa" ..
Betamoo,

3
Mentre questo è vero il livello dell'assemblatore. Due cose si combinano con meke falso nella realtà: l'hardware moderno che utilizza pipe lunghe e istruzioni speculative si insinuerà in "Sub i e N" senza incorrere in un ciclo aggiuntivo - e - anche il compilatore più rozzo ottimizzerà "Sub i e N "fuori dall'esistenza.
James Anderson,

2
@nico Non deve essere un sistema antico. Deve solo essere un set di istruzioni in cui è presente un'operazione di confronto con zero che è in qualche modo più veloce / migliore dell'equivalente confronto con il valore di registro. x86 ce l'ha in jcxz. x64 ce l'ha ancora. Non antico Inoltre, le architetture RISC sono spesso zero per casi speciali. Il chip DEC AXP Alpha (nella famiglia MIPS), ad esempio, aveva un "registro zero" - leggi come zero, la scrittura non fa nulla. Il confronto con il registro zero invece che con un registro generale che contiene un valore zero riduce le dipendenze tra istruzioni e aiuta l'esecuzione fuori dall'ordine.
Dthorpe,

5
@Betamoo: mi chiedo spesso perché non le risposte migliori / più corrette (che sono tue) non siano più apprezzate da più voti e giungo alla conclusione che troppo spesso i voti su StackOverflow sono influenzati dalla reputazione (in punti) di una persona che risponde ( che è molto brutto) e non dalla correttezza della risposta
Artur

12

In C per psudo-assemblaggio:

for (i = 0; i < 10; i++) {
    foo(i);
}

diventa

    clear i
top_of_loop:
    call foo
    increment i
    compare 10, i
    jump_less top_of_loop

mentre:

for (i = 10; i >= 0; i--) {
    foo(i);
}

diventa

    load i, 10
top_of_loop:
    call foo
    decrement i
    jump_not_neg top_of_loop

Nota la mancanza del confronto nel secondo psudo-assemblaggio. Su molte architetture ci sono flag che sono impostati da operazioni aritmiche (aggiungi, sottrai, moltiplica, dividi, incrementa, decrementa) che puoi usare per i salti. Questi spesso ti danno ciò che è essenzialmente un confronto del risultato dell'operazione con 0 gratuitamente. In effetti su molte architetture

x = x - 0

è semanticamente uguale a

compare x, 0

Inoltre, il confronto con un 10 nel mio esempio potrebbe comportare un codice peggiore. 10 potrebbe dover vivere in un registro, quindi se sono a corto di scorte che costano e possono comportare un codice aggiuntivo per spostare le cose o ricaricare i 10 ogni volta attraverso il ciclo.

A volte i compilatori possono riorganizzare il codice per trarne vantaggio, ma è spesso difficile perché spesso non sono in grado di essere sicuri che l'inversione della direzione attraverso il loop sia semanticamente equivalente.


È possibile che ci sia una differenza di 2 istruzioni anziché solo 1?
Pacerier,

Inoltre, perché è difficile esserne certi? Finché il var inon viene utilizzato all'interno del loop, ovviamente è possibile capovolgerlo, no?
Pacerier,

6

Conto alla rovescia più veloce in questo caso:

for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}

perché someObject.getAllObjects.size()viene eseguito una volta all'inizio.


Certo, un comportamento simile può essere ottenuto chiamando size()fuori dal giro, come ha detto Peter:

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}

5
Non è "decisamente più veloce". In molti casi quella chiamata size () potrebbe essere sollevata fuori dal ciclo durante il conto alla rovescia, quindi verrebbe comunque chiamata solo una volta. Ovviamente questo dipende dal linguaggio e dal compilatore (e dipende dal codice; ad es. In C ++ non verrà sollevato se size () è virtuale), ma è tutt'altro che definito in entrambi i modi.
Peter,

3
@Peter: solo se il compilatore sa per certo che size () è idempotente in tutto il ciclo. Questo è probabilmente quasi sempre non il caso, a meno che il ciclo è molto semplice.
Lawrence Dol,

@LawrenceDol, Il compilatore lo saprà sicuramente a meno che tu non abbia compilatino con codice dinamico exec.
Pacerier,

4

È più veloce il conto alla rovescia che verso l'alto?

Può essere. Ma molto più del 99% delle volte non importerà, quindi dovresti usare il test più "sensato" per terminare il loop, e per senso, intendo dire che ci vuole la minima quantità di pensiero da un lettore per capire cosa fa il loop (incluso cosa lo fa fermare). Rendi il tuo codice corrispondente al modello mentale (o documentato) di ciò che il codice sta facendo.

Se il ciclo sta funzionando su un array (o un elenco o altro), un contatore incrementale spesso si abbinerà meglio a come il lettore potrebbe pensare a cosa sta facendo il ciclo - codifica il tuo ciclo in questo modo.

Ma se stai lavorando attraverso un contenitore che contiene Noggetti e li rimuovi mentre procedi, potrebbe avere più senso cognitivo lavorare il contatore.

Un po 'più di dettaglio su "forse" nella risposta:

È vero che sulla maggior parte delle architetture, il test per un calcolo che risulta in zero (o passa da zero a negativo) non richiede istruzioni esplicite per il test: il risultato può essere verificato direttamente. Se si desidera verificare se un calcolo determina un altro numero, il flusso di istruzioni dovrà generalmente disporre di un'istruzione esplicita per verificare quel valore. Tuttavia, specialmente con le CPU moderne, questo test di solito aggiunge un tempo aggiuntivo inferiore al livello di rumore a un costrutto in loop. Soprattutto se quel loop sta eseguendo I / O.

D'altro canto, se si esegue il conto alla rovescia da zero e si utilizza il contatore come indice di array, ad esempio, è possibile che il codice funzioni contro l'architettura di memoria del sistema: le letture della memoria spesso causano una cache che "guarda avanti" diverse posizioni di memoria oltre quella corrente in previsione di una lettura sequenziale. Se si lavora all'indietro attraverso la memoria, il sistema di memorizzazione nella cache potrebbe non prevedere le letture di una posizione di memoria con un indirizzo di memoria inferiore. In questo caso, è possibile che il looping "all'indietro" possa danneggiare le prestazioni. Tuttavia, probabilmente continuerei a codificare il ciclo in questo modo (purché le prestazioni non diventino un problema) perché la correttezza è fondamentale e rendere il codice corrispondente a un modello è un ottimo modo per garantire la correttezza. Il codice errato non è ottimizzato come si può ottenere.

Quindi tenderei a dimenticare il consiglio del professore (ovviamente, non durante il suo test - dovresti comunque essere pragmatico per quanto riguarda l'aula), a meno che e fino a quando l'esecuzione del codice non conta davvero.


3

Su alcune CPU meno recenti ci sono / erano istruzioni come DJNZ== "decrementa e salta se non zero". Ciò ha consentito cicli efficienti in cui è stato caricato un valore di conteggio iniziale in un registro e quindi è stato possibile gestire efficacemente un ciclo di decremento con un'istruzione. Stiamo parlando degli ISA degli anni '80 qui - il tuo insegnante è seriamente fuori dal mondo se pensa che questa "regola empirica" ​​si applichi ancora con le moderne CPU.


3

Bob,

Non prima di fare microottimizzazioni, a quel punto avrai a portata di mano il manuale per la tua CPU. Inoltre, se facessi questo genere di cose, probabilmente non avresti bisogno di porre questa domanda comunque. :-) Ma il tuo insegnante evidentemente non si abbona a quell'idea ...

Ci sono 4 cose da considerare nel tuo esempio di loop:

for (i=N; 
 i>=0;             //thing 1
 i--)             //thing 2
{
  putchar('*');   //thing 3
}
  • Confronto

Il confronto è (come altri hanno indicato) rilevante per architetture di processori particolari . Esistono più tipi di processori rispetto a quelli che eseguono Windows. In particolare, potrebbe esserci un'istruzione che semplifica e accelera i confronti con 0.

  • registrazione

In alcuni casi, è più veloce regolare su o giù. In genere un buon compilatore lo capirà e ripeterà il ciclo se può. Tuttavia, non tutti i compilatori sono buoni.

  • Corpo ad anello

Stai accedendo a una syscall con putchar. È enormemente lento. Inoltre, stai eseguendo il rendering sullo schermo (indirettamente). È ancora più lento. Pensa al rapporto 1000: 1 o più. In questa situazione, il corpo del circuito supera completamente e totalmente il costo della regolazione / confronto del circuito.

  • caches

Un layout di cache e memoria può avere un grande effetto sulle prestazioni. In questa situazione, non importa. Tuttavia, se si accedesse a un array e si necessitasse di prestazioni ottimali, sarebbe opportuno esaminare il modo in cui il compilatore e il processore hanno disposto gli accessi alla memoria e ottimizzare il software per ottenere il massimo. L'esempio di borsa è quello dato in relazione alla moltiplicazione di matrici.


3

Ciò che conta molto di più che aumentare o diminuire il contatore è se si va in memoria o in memoria. La maggior parte delle cache sono ottimizzate per aumentare la memoria, non la memoria. Poiché il tempo di accesso alla memoria è il collo di bottiglia che la maggior parte dei programmi oggi deve affrontare, ciò significa che cambiare il programma in modo da aumentare la memoria può comportare un aumento delle prestazioni anche se ciò richiede il confronto del contatore con un valore diverso da zero. In alcuni dei miei programmi, ho visto un significativo miglioramento delle prestazioni modificando il mio codice per aumentare la memoria anziché scaricarla.

Scettico? Basta scrivere un programma per i cicli di tempo che vanno su / giù nella memoria. Ecco l'output che ho ottenuto:

Average Up Memory   = 4839 mus
Average Down Memory = 5552 mus

Average Up Memory   = 18638 mus
Average Down Memory = 19053 mus

(dove "mus" sta per microsecondi) dall'esecuzione di questo programma:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//Sum all numbers going up memory.
template<class Iterator, class T>
inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

//Sum all numbers going down memory.
template<class Iterator, class T>
inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

//Time how long it takes to make num_repititions identical calls to sum_abs_down().
//We will divide this time by num_repitions to get the average time.
template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &vec_original,
                                  std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &vec_original,
                                std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(vec_size);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up   = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "Average Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Average Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

Entrambi sum_abs_upe sum_abs_downfanno la stessa cosa (sommano il vettore dei numeri) e sono cronometrati allo stesso modo con l'unica differenza che sum_abs_upva in memoria mentre sum_abs_downva in memoria. Passo anche vecper riferimento in modo che entrambe le funzioni accedano alle stesse posizioni di memoria. Tuttavia, sum_abs_upè costantemente più veloce di sum_abs_down. Provalo tu stesso (l'ho compilato con g ++ -O3).

È importante notare quanto sia stretto il circuito che sto programmando. Se il corpo di un ciclo è grande, probabilmente non importerà se il suo iteratore passa alla memoria, poiché il tempo necessario per eseguire il corpo del ciclo probabilmente dominerà completamente. Inoltre, è importante menzionare che con alcuni loop rari, andare in memoria a volte è più veloce che salirla. Ma anche con tali loop non è mai stato possibile che salire di memoria fosse sempre più lento di scendere (a differenza di loop di piccolo corpo che vanno su di memoria, per i quali è spesso vero il contrario; in realtà, per una manciata di loop I ' cinque volte, l'aumento delle prestazioni aumentando la memoria è stato del 40 +%).

Il punto è, come regola empirica, se hai l'opzione, se il corpo del loop è piccolo e se c'è poca differenza tra il fatto che il tuo loop salga su memoria invece che verso il basso, allora dovresti andare su memoria.

FYI vec_originalè lì per la sperimentazione, per rendere più facile il cambiamento sum_abs_upe sum_abs_downin un modo che li fa cambiare vecsenza consentire a questi cambiamenti di influenzare i tempi futuri. Consiglio vivamente di giocare con sum_abs_upe sum_abs_downe tempismo dei risultati.


2

indipendentemente dalla direzione usa sempre il prefisso form (++ i invece di i ++)!

for (i=N; i>=0; --i)  

o

for (i=0; i<N; ++i) 

Spiegazione: http://www.eskimo.com/~scs/cclass/notes/sx7b.html

Inoltre puoi scrivere

for (i=N; i; --i)  

Ma mi aspetto che i compilatori moderni siano in grado di fare esattamente queste ottimizzazioni.


Mai visto persone lamentarsene prima. Ma dopo aver letto il link ha davvero senso :) Grazie.
Tommy Jakobsen,

3
Ehm, perché dovrebbe sempre usare il prefisso? Se non ci sono compiti in corso, sono identici e l'articolo a cui ti colleghi dice anche che il modulo postfix è più comune.
bobDevil,

3
Perché si dovrebbe sempre usare il modulo prefisso? In questo caso, è semanticamente identico.
Ben Zotto,

2
Il modulo postfix può potenzialmente creare una copia non necessaria dell'oggetto, sebbene se il valore non viene mai utilizzato, il compilatore probabilmente lo ottimizzerà comunque nel modulo prefisso.
Nick Lewis,

Per forza d'abitudine, faccio sempre --i e i ++ perché quando ho imparato i computer C di solito avevano un registro e un pre-incremento di registro, ma non viceversa. Pertanto, * p ++ e * - p erano più veloci di * ++ p e * p-- perché i primi due potevano essere eseguiti in un'istruzione di codice macchina 68000.
JeremyP,

2

È una domanda interessante, ma in pratica non credo sia importante e non rende un loop migliore dell'altro.

Secondo questa pagina di Wikipedia: Leap secondo , "... il giorno solare aumenta di 1,7 ms ogni secolo a causa principalmente dell'attrito delle maree". Ma se conti i giorni fino al tuo compleanno, ti interessa davvero questa piccola differenza nel tempo?

È più importante che il codice sorgente sia di facile lettura e comprensione. Questi due loop sono un buon esempio del motivo per cui la leggibilità è importante: non eseguono lo stesso numero di volte.

Scommetto che la maggior parte dei programmatori legge (i = 0; i <N; i ++) e capisce immediatamente che questo viene ripetuto N volte. Un ciclo di (i = 1; i <= N; i ++), per me comunque, è un po 'meno chiaro, e con (i = N; i> 0; i--) devo pensarci un momento . È meglio se l'intento del codice va direttamente nel cervello senza pensare.


Entrambi i costrutti sono altrettanto facili da capire. Ci sono alcune persone che sostengono che se hai 3 o 4 ripetizioni, è meglio copiare le istruzioni piuttosto che fare un ciclo perché è per loro più facile da capire.
Danubian Sailor,

2

Stranamente, sembra che ci sia una differenza. Almeno, in PHP. Considera il seguente benchmark:

<?php

print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;

$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);

$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);

I risultati sono interessanti:

PHP 5.2.13
$i++ : 8.8842368125916
$i-- : 8.1797409057617
++$i : 8.0271911621094
--$i : 7.1027431488037


PHP 5.3.1
$i++ : 8.9625310897827
$i-- : 8.5790238380432
++$i : 5.9647901058197
--$i : 5.4021768569946

Se qualcuno sa perché, sarebbe bello saperlo :)

EDIT : i risultati sono gli stessi anche se inizi a contare non da 0, ma da altri valori arbitrari. Quindi probabilmente non c'è solo il confronto con lo zero che fa la differenza?


Il motivo per cui è più lento è che l'operatore prefisso non ha bisogno di memorizzare un temporaneo. Considera $ foo = $ i ++; Succedono tre cose: $ i viene archiviato in un temporaneo, $ i viene incrementato e quindi $ foo viene assegnato quel valore temporaneo. Nel caso di $ i ++; un compilatore intelligente potrebbe rendersi conto che il temporaneo non è necessario. PHP semplicemente no. I compilatori C ++ e Java sono abbastanza intelligenti da rendere questa semplice ottimizzazione.
Conspicuous Compiler

e perché $ i-- è più veloce di $ i ++?
ts.

Quante iterazioni del tuo benchmark hai eseguito? Hai tagliato gli outrider e hai preso una media per ogni risultato? Il tuo computer ha fatto qualcos'altro durante i benchmark? Quella differenza di ~ 0,5 potrebbe essere solo il risultato di altre attività della CPU, dell'utilizzo della pipeline o ... o ... beh, hai capito.
Guru a otto bit

Sì, qui sto dando delle medie. Il benchmark è stato eseguito su macchine diverse e la differenza è accidentale.
ts.

@Conspicuous Compiler => sai o pensi?
ts.

2

Si può essere più veloce.

Sul processore NIOS II con cui sto attualmente lavorando, il tradizionale per loop

for(i=0;i<100;i++)

produce l'assemblaggio:

ldw r2,-3340(fp) %load i to r2
addi r2,r2,1     %increase i by 1
stw r2,-3340(fp) %save value of i
ldw r2,-3340(fp) %load value again (???)
cmplti r2,r2,100 %compare if less than equal 100
bne r2,zero,0xa018 %jump

Se contiamo il conto alla rovescia

for(i=100;i--;)

otteniamo un assemblaggio che necessita di 2 istruzioni in meno.

ldw r2,-3340(fp)
addi r3,r2,-1
stw r3,-3340(fp)
bne r2,zero,0xa01c

Se abbiamo loop nidificati, in cui il loop interno viene eseguito molto, possiamo avere una differenza misurabile:

int i,j,a=0;
for(i=100;i--;){
    for(j=10000;j--;){
        a = j+1;
    }
}

Se il ciclo interno è scritto come sopra, il tempo di esecuzione è: 0,12199999999999999734 secondi. Se il ciclo interno è scritto nel modo tradizionale, il tempo di esecuzione è: 0,1719999999999999998623 secondi. Quindi il conto alla rovescia del loop è circa il 30% più veloce.

Ma: questo test è stato eseguito con tutte le ottimizzazioni GCC disattivate. Se li accendiamo, il compilatore è in realtà più intelligente di questa ottimizzazione pratica e mantiene persino il valore in un registro durante l'intero ciclo e otterremmo un assembly come

addi r2,r2,-1
bne r2,zero,0xa01c

In questo esempio particolare il compilatore nota anche che la variabile a sarà sempre 1 dopo l'esecuzione del ciclo e salta i loop del tutto.

Tuttavia, ho riscontrato che a volte se il corpo del loop è abbastanza complesso, il compilatore non è in grado di eseguire questa ottimizzazione, quindi il modo più sicuro per ottenere sempre un'esecuzione veloce del ciclo è scrivere:

register int i;
for(i=10000;i--;)
{ ... }

Ovviamente questo funziona solo se non importa che il loop sia eseguito al contrario e come ha detto Betamoo, solo se si sta contando fino a zero.


2

Ciò che il tuo insegnante ha detto è stata una dichiarazione obliqua senza molti chiarimenti. NON è che il decremento sia più veloce dell'incremento, ma è possibile creare un ciclo molto più veloce con il decremento che con l'incremento.

Senza andare avanti a lungo, senza la necessità di utilizzare il contatore di loop ecc., Ciò che conta di seguito è solo la velocità e il conteggio dei loop (diverso da zero).

Ecco come la maggior parte delle persone implementa il loop con 10 iterazioni:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

Per il 99% dei casi è tutto ciò di cui si può avere bisogno, ma insieme a PHP, PYTHON, JavaScript c'è tutto il mondo del software critico nel tempo (di solito incorporato, sistema operativo, giochi ecc.) In cui i tick della CPU contano davvero, quindi guarda brevemente il codice assembly di:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

dopo la compilazione (senza ottimizzazione) la versione compilata può apparire così (VS2015):

-------- C7 45 B0 00 00 00 00  mov         dword ptr [i],0  
-------- EB 09                 jmp         labelB 
labelA   8B 45 B0              mov         eax,dword ptr [i]  
-------- 83 C0 01              add         eax,1  
-------- 89 45 B0              mov         dword ptr [i],eax  
labelB   83 7D B0 0A           cmp         dword ptr [i],0Ah  
-------- 7D 02                 jge         out1 
-------- EB EF                 jmp         labelA  
out1:

L'intero ciclo è composto da 8 istruzioni (26 byte). In esso - ci sono in realtà 6 istruzioni (17 byte) con 2 rami. Sì sì, lo so che può essere fatto meglio (è solo un esempio).

Ora considera questo costrutto frequente che troverai spesso scritto dallo sviluppatore incorporato:

i = 10;
do
{
    //something here
} while (--i);

Iterate anche 10 volte (sì, lo so, il valore è diverso rispetto a mostrato per il ciclo, ma ci interessa il conteggio dell'iterazione qui). Questo può essere compilato in questo:

00074EBC C7 45 B0 01 00 00 00 mov         dword ptr [i],1  
00074EC3 8B 45 B0             mov         eax,dword ptr [i]  
00074EC6 83 E8 01             sub         eax,1  
00074EC9 89 45 B0             mov         dword ptr [i],eax  
00074ECC 75 F5                jne         main+0C3h (074EC3h)  

5 istruzioni (18 byte) e un solo ramo. In realtà ci sono 4 istruzioni nel loop (11 byte).

La cosa migliore è che alcune CPU (compatibile x86 / x64 incluso) hanno istruzioni che possono decrementare un registro, confrontare successivamente il risultato con zero ed eseguire il ramo se il risultato è diverso da zero. Praticamente TUTTI i PC cpus implementano queste istruzioni. Usarlo il loop è in realtà solo un'istruzione (sì una) da 2 byte:

00144ECE B9 0A 00 00 00       mov         ecx,0Ah  
label:
                          // something here
00144ED3 E2 FE                loop        label (0144ED3h)  // decrement ecx and jump to label if not zero

Devo spiegare quale è più veloce?

Ora, anche se una particolare CPU non implementa le istruzioni precedenti, tutto ciò che serve per emularlo è un decremento seguito da un salto condizionale se il risultato dell'istruzione precedente risulta essere zero.

Quindi, indipendentemente da alcuni casi che potresti indicare come un commento, perché mi sbaglio, ecc. Ecc., SOTTOLINEO - SÌ È VANTAGGIOSO SVOLGERE VERSO IL BASSO se sai come, perché e quando.

PS. Sì, lo so che il compilatore saggio (con il livello di ottimizzazione appropriato) riscriverà per loop (con contatore di loop crescente) in do..quanto equivalente per iterazioni di loop costanti ... (o srotolarlo) ...


1

No, non è proprio vero. Una situazione in cui potrebbe essere più veloce è quando altrimenti si chiamerebbe una funzione per verificare i limiti durante ogni iterazione di un ciclo.

for(int i=myCollection.size(); i >= 0; i--)
{
   ...
}

Ma se è meno chiaro farlo in questo modo, non vale la pena. Nelle lingue moderne, dovresti usare un ciclo foreach quando possibile, comunque. Indichi specificamente il caso in cui dovresti usare un ciclo foreach - quando non hai bisogno dell'indice.


1
Per essere chiari ed efficienti dovresti avere l'abitudine di almeno for(int i=0, siz=myCollection.size(); i<siz; i++).
Lawrence Dol,

1

Il punto è che durante il conto alla rovescia non è necessario controllare i >= 0separatamente per il decremento i. Osservare:

for (i = 5; i--;) {
  alert(i);  // alert boxes showing 4, 3, 2, 1, 0
}

Sia il confronto che il decremento ipossono essere eseguiti in un'unica espressione.

Vedi altre risposte sul perché ciò si riduce a un minor numero di istruzioni x86.

Per quanto riguarda se fa una differenza significativa nella tua applicazione, suppongo che dipenda da quanti loop hai e da quanto profondamente sono annidati. Ma per me è altrettanto leggibile farlo in questo modo, quindi lo faccio comunque.


Penso che questo sia uno stile scadente, perché dipende dal fatto che il lettore sappia che il valore di ritorno di i-- è il vecchio valore di i, per il possibile valore di salvare un ciclo. Sarebbe significativo solo se ci fossero molte iterazioni di loop e il ciclo rappresentasse una frazione significativa della lunghezza dell'iterazione e si presentasse effettivamente in fase di esecuzione. Successivamente, qualcuno proverà per (i = 5; --i;) perché ha sentito che in C ++ potresti voler evitare di creare un po 'temporaneo quando sono un tipo non banale, e ora sei nella terra dei bug con gettato via con insistenza la tua opportunità di far sembrare sbagliato il codice sbagliato.
mabraham,

0

Ora, penso che tu abbia avuto abbastanza lezioni di assemblaggio :) Vorrei presentarti un altro motivo per l'approccio top-> down.

Il motivo per andare dall'alto è molto semplice. Nel corpo del loop, è possibile modificare accidentalmente il confine, che potrebbe finire in un comportamento errato o persino in un loop non terminante.

Guarda questa piccola porzione di codice Java (la lingua non ha importanza, immagino per questo motivo):

    System.out.println("top->down");
    int n = 999;
    for (int i = n; i >= 0; i--) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }
    System.out.println("bottom->up");
    n = 1;
    for (int i = 0; i < n; i++) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }

Quindi il mio punto è che dovresti considerare di preferire andare dall'alto verso il basso o avere una costante come limite.


Eh? !! Il tuo esempio fallito è davvero contro-intuitivo, vale a dire un argomento da pagliaccio - nessuno lo scriverebbe mai. Uno avrebbe scritto for (int i=0; i < 999; i++) {.
Lawrence Dol,

@Software Monkey immagina di essere il risultato di alcuni calcoli ... ad esempio potresti voler scorrere su una raccolta e la sua dimensione è il limite, ma come effetto collaterale, aggiungi nuovi elementi alla raccolta nel corpo del loop.
Gabriel Ščerbák,

Se è quello che intendevi comunicare, allora è quello che dovrebbe illustrare il tuo esempio:for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Lawrence Dol

@Software Monkey Volevo essere più generale del parlare in particolare delle collezioni, perché ciò su cui sto ragionando non ha nulla a che fare con le collezioni
Gabriel Ščerbák,

2
Sì, ma se hai intenzione di ragionare con l'esempio, i tuoi esempi devono essere credibili e illustrativi del punto.
Lawrence Dol,

-1

A livello di assemblatore, un loop che conta fino a zero è generalmente leggermente più veloce di uno che conta fino a un dato valore. Se il risultato di un calcolo è uguale a zero, la maggior parte dei processori imposta un flag zero. Se sottraendo uno si fa un calcolo intorno allo zero questo normalmente cambierà il flag carry (su alcuni processori lo imposterà su altri lo cancellerà), quindi il confronto con zero viene essenzialmente gratuito.

Ciò è ancora più vero quando il numero di iterazioni non è una costante ma una variabile.

In casi banali, il compilatore potrebbe essere in grado di ottimizzare automaticamente la direzione di conteggio di un loop, ma in casi più complessi il programmatore può sapere che la direzione del loop è irrilevante per il comportamento generale, ma il compilatore non può dimostrarlo.

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.