C'è un vantaggio in termini di prestazioni nel controllare il conteggio degli elementi prima di eseguire un ciclo foreach?

4

L'ho visto nel codice e mi chiedevo se ci sono dei vantaggi in termini di prestazioni nel controllare il conteggio degli elementi prima del ciclo:

if (SqlParams.Count > 0)
    foreach (var prm in SqlParams)
        cmd.Parameters.Add(prm);

Preferisco sempre fare un controllo null invece e lasciare che il ciclo foreach salti fuori se ci sono% elementi di% co_de.

if (SqlParams != null)
    foreach (var prm in SqlParams)
        cmd.Parameters.Add(prm);

Non è questo il modo migliore?

    
posta Code Maverick 09.04.2014 - 18:12
fonte

7 risposte

5

In definitiva, la migliore risposta è testarla effettivamente. Crea un metodo che esegue il loop su un array vuoto con e senza prima controllare la lunghezza, chiama ogni 100.000 volte e controlla quale ha un tempo di esecuzione più veloce.

public void withCheck(Integer[] array) {
    for (int i = 0; i < 100000; i++) {
        if (array.length > 0) {
            for (Integer i : array) {
                // nothing to do.
            }
        }
    }
}

public void withoutCheck(Integer[] array) {
    for (int i = 0; i < 100000; i++) {
        for (Integer i : array) {
            // nothing to do.
        }
    }
}

public void test() {
    long startTime = System.currentTimeMillis();
    withCheck();
    System.out.println("Time with check: " + (System.currentTimeMillis() - startTime) + "ms");
    startTime = System.currentTimeMillis();
    withoutCheck();
    System.out.println("Time without check: " + (System.currentTimeMillis() - startTime) + "ms");
}

Quando scrivo codice come questo, generalmente considero questo come un compromesso tra la leggibilità del codice e le prestazioni percepite o potenziali. In altre parole, l'aggiunta di un controllo if (count > 0) influisce negativamente sulla leggibilità, anche se di una quantità molto piccola. Allora, qual è il guadagno? Presumo che ci sia poco o nessun vantaggio, ma come te, non l'ho ancora testato. Quello che posso dirti è che in tutti i miei anni di profilazione, non ho mai trovato il loop su un array vuoto per essere un collo di bottiglia.

Tecnicamente, dipende da come funziona l'iteratore. Per un ciclo foreach come quello che hai, stai creando un oggetto iteratore dietro le quinte. La mia aspettativa sarebbe, e ancora, questo non è verificato, è che l'oggetto iteratore FOR AN ARRAY sarebbe semplicemente implementato in questo modo (questo è Java, ma C # è probabilmente comparabile):

public class ArrayIterator<T> implements Iterator<T> {
    private int next = 0;
    public T next() {
        return backingArray[next++];
    }

    public boolean hasNext() {
        return next < backingArray.length;
    }
}

Quindi iterare su un iteratore di questo tipo dovrebbe uscire molto rapidamente perché hasNext() restituirà false. Si noti che è lo stesso controllo che farebbe la dichiarazione if .

Ora se stai iterando su un elenco collegato, l'implementazione dovrebbe essere diversa. Se si controlla esplicitamente la dimensione, è possibile che si stia forzando a eseguire la scansione dell'elenco collegato. In Java, LinkedList.size() controlla una variabile size esplicita in modo che non dia l'elenco, ma non so in che modo .NET lo implementa.

Un altro modo di guardare questo è quanto è probabile che la collezione sia vuota? Se questo è un caso raro, allora se la teoria che iterating su un array vuoto richiede del tempo è vera, quindi aggiungere un controllo esplicito per vuoto per primo sarebbe solo più veloce se la raccolta è di solito vuota. In caso contrario, stai teoricamente rallentando il tuo caso medio per accelerare leggermente il tuo caso peggiore.

Ancora una volta, queste sono tutte teorie e i tempi sono a livello micro, quindi prendilo con un pizzico di sale.

TL; DR

Dipende dal tipo di raccolta, dalla piattaforma di runtime e dalla dimensione tipica della raccolta, ma intuitivamente, sembra improbabile che acceleri nella maggior parte delle situazioni. Ma l'unico modo per saperlo è testarlo.

    
risposta data 11.04.2014 - 19:55
fonte
9

Non proprio, perché se ci sono 0 elementi, l'impostazione del ciclo foreach lo troverà comunque e semplicemente non eseguirà il corpo del ciclo.

È teoricamente possibile, in un ciclo molto stretto in cui è comune che la tua raccolta sia vuota, e in cui trovare il conteggio è un'operazione molto economica, perché qui ci sarà un notevole vantaggio in termini di prestazioni, ma se sei facendo query sul database, questo non è quasi certamente il caso. Quindi non preoccuparti di questo a meno che la profilazione non dimostri che questo sta effettivamente richiedendo una quantità significativa di tempo di esecuzione.

    
risposta data 09.04.2014 - 18:21
fonte
4

Esaminando il codice sorgente di System.Collections.Generic.List<T> vediamo che MoveNext() è implementato in questo modo:

public bool MoveNext() {
    List<T> localList = list;

    if (version == localList._version && ((uint)index < (uint)localList._size)) 
    {                                                     
        current = localList._items[index];                    
        index++;
        return true;
    }
    return MoveNextRare();
}

Significa che il costo totale di un controllo è

  • Assegnazione di una raccolta esistente a un'altra variabile
  • Confronto tra versioni, qualunque sia
  • Lancio di due valori di int in uint e confronto li

Quando guardiamo il codice sorgente di .Count :

public int Count {
    get {
        lock (_root) { 
            return _list.Count; 
        }
    }
}

Vediamo che il costo è l'acquisizione di un lucchetto.

Conclusione

Puoi misurare e vedere quale è la migliore, ma penso che sia sicuro concludere che nulla di tutto ciò potrà mai avere importanza. Potresti avere alcune collezioni insolite per iterare che non eseguono un controllo precoce delle dimensioni, ma si tratta di un caso limite molto specifico che si farà notare da solo.

In conclusione: non preoccuparti e non dovresti preoccuparti di aggiungere un controllo di .Count > 0 o _collection != null in primo luogo.

    
risposta data 11.04.2014 - 19:20
fonte
3

L'unico scenario a cui posso pensare in cui il primo controllo di Count potrebbe aiutare le prestazioni è con una raccolta con un'implementazione di IEnumerable.MoveNext() assetata di risorse e un'efficiente implementazione di ICollection.Count . Sebbene sia tecnicamente possibile, è molto improbabile che possa mai succedere .

In effetti, in alcuni scenari, il primo controllo di Count() sarebbe in realtà meno performante. Pensa ad esempio ai framework ORM come Entity Framework che sfrutta le 's % interfacciaIQueryable. Dietro le quinte, questi oggetti raggiungono un datastore per fornire collezioni di oggetti e calcolare i risultati dell'operazione come somme, medie e naturalmente conteggio. Chiamare IQueryable.Count() qui finirebbe effettivamente con la creazione di una query SELECT COUNT(*) ... sul database. Se non hai bisogno di quelle informazioni, sarebbe semplicemente inutile sovraccaricare per ottenerlo.

In conclusione: Non cambierei assolutamente nulla al tuo stile di codifica attuale . È perfettamente accettabile passare in rassegna una collezione subito, anche se risulta essere vuota. Assicurarsi che non sia prima null va bene se non puoi essere sicuro della sua provenienza.

    
risposta data 09.04.2014 - 21:55
fonte
1

Il vantaggio si presenta sotto forma di flessibilità, ma non ha alcun vantaggio in termini di prestazioni.

Quando un foreach viene eseguito su un ICollection con un conteggio di 0, l'enumeratore si registrerà come se avesse già attraversato l'intero set. Funzionalmente, è simile al seguente for loop:

for(int i = 0; i > -1; i++)
    foo();

Ora, se c'è un'altra regola aziendale che deve essere attivata prima del foreach , si tratta di una storia completamente diversa, e il fatto che la condizione if soddisfi bene quella futura capacità. Ma non è nemmeno necessario strettamente .

EDIT: in risposta alla tua modifica, il controllo null è un modo decisamente migliore per farlo, poiché una raccolta può essere null .

Ho finito per scrivere un metodo di estensione IsNullOrEmpty per IEnumerables, perché continuavo a vedere molte condizioni ripetute come:

if(SomeCollection != null
   && SomeCollection.Count > 0
   && (SomeOtherCondition))
{
    // Stuff that relies on SomeCollection having at least one value.
}
    
risposta data 09.04.2014 - 18:19
fonte
1

Risposta breve è: NO.

Dettagli: SqlParameterCollection implementa l'interfaccia IEnumerable che ti consente di utilizzare il ciclo foreach, il foreach stesso assicura che non si ottiene l'eccezione ma in una condizione che è Sistema .NullReferenceException prendendo in considerazione che l'eccezione Null non ha nulla a che fare con la proprietà (Count), puoi avere 0 parametri nella collezione e la variabile è inizializzata (non nulla).

Quindi, cosa devi fare, controlla semplicemente null prima della sintassi foreach.

    
risposta data 16.04.2014 - 22:39
fonte
1

Considera le prestazioni del sistema rispetto a una singola esecuzione. Se la lista è non-vuota il 99% delle volte, allora paghi il costo di un controllo extra quasi tutto il tempo.

D'altra parte, se la lista è vuota il 99% delle volte, allora potrebbe funzionare in modo diverso (anche se mi chiedo se l'invocazione extra compensa ogni volta che salvi - ricorda che stiamo parlando di micro o nano -secondi in questo esempio).

Quindi non puoi semplicemente considerare un singolo passaggio eseguito in un thread, devi considerare le prestazioni del sistema tutte insieme.

    
risposta data 16.04.2014 - 23:16
fonte

Leggi altre domande sui tag