Existe algum benefício de desempenho em verificar a contagem de itens antes de executar um loop foreach?

4

Eu vi isso no código e queria saber se há algum benefício de desempenho para verificar a contagem de itens antes do loop:

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

Eu sempre prefiro fazer uma null check e deixar o loop foreach apenas sair se houver 0 itens.

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

Não é a melhor maneira?

    
por Code Maverick 09.04.2014 / 18:12
fonte

7 respostas

5

Por fim, a melhor resposta é testá-lo. Faça um método que faça um loop sobre uma matriz vazia com e sem verificar o comprimento primeiro, chame cada 100.000 vezes e veja qual possui um tempo de execução mais rápido.

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 escrevo código como esse, geralmente considero isso como um compromisso entre a legibilidade do código e o desempenho percebido ou potencial. Em outras palavras, adicionar uma verificação if (count > 0) afeta negativamente a legibilidade, mesmo que por uma quantidade muito pequena. Então, qual é o ganho? Eu presumo que há pouca ou nenhuma vantagem, mas como você, eu não testei realmente. O que posso dizer é que, em todos os meus anos de criação de perfil, nunca encontrei um loop em um array vazio para ser um gargalo.

Tecnicamente, depende de como o iterador funciona. Para um loop foreach como você, você está criando um objeto iterador nos bastidores. Minha expectativa seria, e novamente, isso não é verificado, é que o objeto iterador FOR ARRAY seria simplesmente implementado assim (isto é Java, mas C # provavelmente é comparável):

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

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

Portanto, iterar esse iterador deve sair muito rapidamente porque hasNext() retornará false. Observe que é a mesma verificação que sua instrução if faria.

Agora, se você estiver interagindo com uma lista vinculada, a implementação deve ser diferente. Se você verificar explicitamente o tamanho, talvez esteja forçando a verificação da lista vinculada. Em Java, LinkedList.size() verifica uma variável explícita size para que não passe na lista, mas não sei como o .NET a implementa.

Outra maneira de ver isso é a probabilidade de a coleção estar vazia? Se esse é um caso raro, então, se a teoria de iterar em um array vazio levar tempo for verdadeira, adicionar uma verificação explícita para empty first seria mais rápido se a coleção estivesse normalmente vazia. Se não, você está, teoricamente, desacelerando seu caso médio apenas para acelerar um pouco seu pior caso.

Mais uma vez, estas são todas as teorias e os tempos estão no nível micro, por isso leve-o com um grão de sal.

TL; DR

Depende do tipo de coleção, da plataforma de tempo de execução e do tamanho típico da coleção, mas, intuitivamente, parece improvável acelerar alguma coisa na maioria das situações. Mas a única maneira de saber com certeza é testá-lo.

    
por 11.04.2014 / 19:55
fonte
9

Não realmente, porque se houver 0 itens, a configuração do loop foreach descobrirá isso de qualquer maneira e simplesmente não executará o corpo do loop.

É teoricamente possível, em um ciclo muito apertado em que é comum que sua coleção esteja vazia, e em que encontrar a contagem é uma operação muito barata, pois há um benefício perceptível no desempenho aqui, mas se você estiver fazendo consultas ao banco de dados, isso certamente não é o caso. Portanto, não se preocupe com isso, a menos que o perfil mostre que isso está realmente levando uma quantidade significativa de tempo de execução.

    
por 09.04.2014 / 18:21
fonte
4

Analisando o código-fonte de System.Collections.Generic.List<T> , vemos que MoveNext() é implementado assim:

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 que o custo total de um cheque é

  • Atribuindo uma coleção existente a outra variável
  • Comparando versões, o que quer que seja
  • Transmitindo dois valores int para uint e comparando-os

Quando analisamos o código-fonte de .Count :

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

Nós vemos que o custo está adquirindo um bloqueio.

Conclusão

Você pode medir e ver qual apresenta melhor desempenho, mas acho seguro concluir que nada disso será importante. Você pode ter algumas coleções incomuns para fazer uma iteração que não fazem uma verificação de tamanho inicial, mas esse é um caso de borda muito específico que será exibido.

Resumindo: não se preocupe e você não deve se incomodar em adicionar uma .Count > 0 ou _collection != null em primeiro lugar.

    
por 11.04.2014 / 19:20
fonte
3

O único cenário em que posso pensar em onde verificar Count primeiro ajudaria o desempenho é com uma coleção com uma implementação IEnumerable.MoveNext() com fome de recursos e ainda uma implementação ICollection.Count altamente eficiente. Embora seja tecnicamente possível, é muito improvável que isso aconteça .

De fato, em alguns cenários, a verificação de Count() primeiro seria realmente menor desempenho. Pense, por exemplo, em estruturas de ORM, como o Entity Framework, que aproveita as IQueryable interface. Nos bastidores, esses objetos chegam a um armazenamento de dados para fornecer coleções de objetos e calcular os resultados da operação, como somas, médias e, claro, contar. Chamar IQueryable.Count() aqui acabaria fazendo uma consulta SELECT COUNT(*) ... no banco de dados. Se você não precisa dessa informação, então seria desnecessário sobrecarga para obtê-lo.

Conclusão: Eu realmente não mudaria nada para o seu estilo de codificação real . É perfeitamente aceitável passar por uma coleção imediatamente, mesmo que esteja vazia. Certificar-se de que não é null primeiro é bom se você não puder ter certeza de sua proveniência.

    
por 09.04.2014 / 21:55
fonte
1

O benefício vem na forma de flexibilidade, mas não tem benefício de desempenho.

Quando um foreach é executado em ICollection com contagem de 0, o enumerador registrará como se já tivesse percorrido todo o conjunto. Funcionalmente, é semelhante ao seguinte for loop:

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

Agora, se houver alguma outra regra de negócios que precise ser acionada antes do foreach , é uma história completamente diferente, e ter a condição if acomoda bem esse recurso futuro. Mas também não é necessário

.

EDIT: Em resposta à sua edição, a verificação nula é uma muito melhor maneira de fazer isso, já que uma coleção pode ser null .

Acabei de escrever um método de extensão IsNullOrEmpty para IEnumerables, porque continuei vendo muitas condições repetidas como:

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

Resposta curta é: NÃO.

Detalhes: SqlParameterCollection implementa a interface IEnumerable que permite usar o loop foreach, o próprio foreach garante que você não obtenha exceção, mas em uma condição que seja System .NullReferenceException levando em consideração que a exceção Null não tem nada a ver com a propriedade (Count), você pode ter 0 parâmetros na coleção e a variável é inicializada (não nula).

Então, o que você precisa fazer é simplesmente verificar null para a sintaxe foreach.

    
por 16.04.2014 / 22:39
fonte
1

Considere o desempenho do sistema em relação a uma única execução. Se a lista não estiver vazia 99% do tempo, você estará pagando o custo de uma verificação extra quase o tempo todo.

Por outro lado, se a lista estiver vazia 99% do tempo, ela poderá funcionar de forma diferente (embora eu me pergunte se a compensação extra é compensada a qualquer momento que você salva - lembre-se de que estamos falando de micro ou nano -segundos neste exemplo).

Assim, você não pode considerar apenas uma única passagem sendo executada em um segmento, você deve considerar o desempenho do sistema todos juntos.

    
por 16.04.2014 / 23:16
fonte