Por que é mais rápido verificar se o dicionário contém a chave, em vez de capturar a exceção, caso não contenha?

234

Imagine o código:

public class obj
{
    // elided
}

public static Dictionary<string, obj> dict = new Dictionary<string, obj>();

Método 1

public static obj FromDict1(string name)
{
    if (dict.ContainsKey(name))
    {
        return dict[name];
    }
    return null;
}

Método 2

public static obj FromDict2(string name)
{
    try
    {
        return dict[name];
    }
    catch (KeyNotFoundException)
    {
        return null;
    }
}

Fiquei curioso para saber se há uma diferença no desempenho dessas duas funções, porque a primeira DEVE SER MAIS LENTA que a segunda - uma vez que é necessário verificar duas vezes se o dicionário contém um valor, enquanto a segunda função precisa acessar apenas o dicionário uma vez, mas WOW, na verdade é o oposto:

Loop para 1 000 000 valores (sendo 100 000 existentes e 900 000 inexistentes):

primeira função: 306 milissegundos

segunda função: 20483 milissegundos

Por que é que?

EDIT: Como você pode notar nos comentários abaixo desta pergunta, o desempenho da segunda função é realmente um pouco melhor que o primeiro caso haja 0 chaves inexistentes. Porém, quando houver pelo menos uma ou mais chaves inexistentes, o desempenho da segunda diminuirá rapidamente.

Petr
fonte
39
Por que o primeiro deve ser mais lento? Na verdade, à primeira vista, eu diria que deve ser mais rápido, ContainsKeyé esperado O(1)...
Patryk Ćwiek
8
@Petr Há muito mais instruções envolvidas na emissão de exceções do que O(1)na pesquisa no dicionário ... Especialmente porque realizar duas O(1)operações ainda é assintoticamente O(1).
Patryk Çwiek
9
Como foi observado na boa resposta abaixo, lançar exceções é caro. O nome deles sugere o seguinte: eles devem ser reservados para circunstâncias excepcionais . Se você estiver executando um loop em que consulta um dicionário um milhão de vezes em busca de chaves que não existem, isso meio que deixa de ser uma circunstância excepcional. Se você estiver consultando um dicionário para obter as chaves, e é um caso relativamente comum que elas não estejam presentes, faz sentido verificar primeiro.
Jason R
6
Não se esqueça de que você só comparou o custo da verificação de um milhão de valores ausentes em comparação com o lançamento de um milhão de exceções. Mas os dois métodos também diferem no custo de acessar um valor existente . Se as chaves ausentes forem raras o suficiente, o método de exceção será mais rápido, apesar de seu custo mais alto quando uma chave estiver ausente.
20913 Alexis

Respostas:

404

Por um lado, lançar exceções é inerentemente caro , porque a pilha precisa ser desenrolada etc.
Por outro lado, acessar um valor em um dicionário por sua chave é barato, porque é uma operação O (1) rápida.

BTW: A maneira correta de fazer isso é usar TryGetValue

obj item;
if(!dict.TryGetValue(name, out item))
    return null;
return item;

Isso acessa o dicionário apenas uma vez em vez de duas vezes.
Se você realmente deseja apenas retornar nullse a chave não existir, o código acima pode ser simplificado ainda mais:

obj item;
dict.TryGetValue(name, out item);
return item;

Isso funciona, porque TryGetValuedefine itemcomo nullse nenhuma chave nameexiste.

Daniel Hilgarth
fonte
4
Atualizei meu teste de acordo com a resposta e, por algum motivo, apesar da função sugerida ser mais rápida, na verdade não é muito significativa: 264 ms original, 258ms sugerido um
Petr
52
@ Pet: Sim, não é significativo, porque o acesso ao dicionário é muito rápido, não importa se você faz uma ou duas vezes. A maioria desses 250 ms provavelmente é gasta no próprio loop de teste.
Daniel Hilgarth
4
É bom saber, porque às vezes se tem a impressão de que a exceção é uma maneira melhor ou mais limpa de lidar com uma situação como arquivo inexistente ou ponteiro nulo, independentemente de essas situações serem comuns e sem considerar o custo de desempenho.
precisa saber é
4
@ LarsH também depende do que você está fazendo. Embora simples marcas de microcrédito como essa mostrem penalidades realmente grandes para exceções, uma vez que seus loops começam, incluindo atividades de arquivo ou banco de dados, lançando uma exceção em cada iteração, pouco importa para o desempenho. Compare a 1ª e a 2ª tabela: codeproject.com/Articles/11265/…
Dan Is Fiddling Por Firelight 19/04/2013
8
@LarsH Observe também que, ao tentar acessar um arquivo (ou algum outro recurso externo), ele pode mudar de estado entre a verificação e a tentativa de acesso real. Nesses casos, usar exceções é o caminho correto a seguir. Veja a resposta de Stephen C a esta pergunta para obter informações adicionais.
yoniLavi
6

Os dicionários são projetados especificamente para fazer pesquisas de teclas super rápidas. Eles são implementados como hashtables e quanto mais entradas, mais rápido elas são em relação a outros métodos. O uso do mecanismo de exceção só deve ser feito quando seu método falhar em fazer o que você o projetou, porque é um grande conjunto de objetos que oferece muitas funcionalidades para lidar com erros. Eu construí uma classe de biblioteca inteira uma vez com tudo cercado por blocos try try uma vez e fiquei chocado ao ver a saída de depuração que continha uma linha separada para cada uma das mais de 600 exceções!

Ed Hermanson
fonte
1
Quando os implementadores de linguagem estão decidindo onde gastar esforços em otimização, as tabelas de hash terão prioridade porque são usadas com freqüência, geralmente em loops internos que podem ser gargalos. Espera-se que as exceções sejam usadas apenas com muito menos frequência, em casos incomuns ("excepcionais", por assim dizer), para que geralmente não sejam considerados importantes para o desempenho.
Barmar
"Eles são implementados como hashtables e quanto mais entradas, mais rápido são em relação a outros métodos". certamente isso não é verdade se os baldes se encherem?!?!
AnthonyLambert
1
@AnthonyLambert O que ele está tentando dizer é que a pesquisa em uma hashtable possui complexidade de tempo O (1), enquanto uma pesquisa em árvore de pesquisa binária teria O (log (n)); a árvore diminui à medida que o número de elementos aumenta assintoticamente, enquanto a hashtable não. Portanto, a vantagem da velocidade da hashtable aumenta com o número de elementos, embora o faça lentamente.
Doval
@AnthonyLambert Sob uso normal, existem extremamente poucas colisões na hashtable de um dicionário. Se você estiver usando uma hashtable e seus depósitos forem preenchidos, você terá muitas entradas (ou poucos depósitos). Nesse caso, é hora de usar uma hashtable personalizada.
Andrews