Почему быстрее проверить, содержит ли словарь ключ, а не перехватить исключение, если его нет?

234

Представьте себе код:

public class obj
{
    // elided
}

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

Способ 1

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

Способ 2

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

Мне было любопытно, если есть разница в производительности этих двух функций, потому что первая ДОЛЖНА быть МЕНЬШЕ, чем вторая - учитывая, что необходимо дважды проверить, содержит ли словарь значение, в то время как второй функции нужен только доступ к словарю один раз но ВАУ, на самом деле все наоборот

Цикл для 1 000 000 значений (с 100 000 существующих и 900 000 несуществующих):

первая функция: 306 миллисекунд

вторая функция: 20483 миллисекунды

Это почему?

РЕДАКТИРОВАТЬ: Как вы можете заметить в комментариях ниже этого вопроса, производительность второй функции на самом деле немного лучше, чем первая в случае, если есть 0 несуществующих клавиш. Но при наличии как минимум 1 или более несуществующих ключей производительность второго быстро снижается.

Petr
источник
39
Почему первый должен быть медленнее? На самом деле, на первый взгляд, я бы сказал, что это должно быть быстрее, ContainsKeyкак ожидается O(1)...
Patryk Ćwiek
8
@Petr В создании исключений гораздо больше инструкций, чем O(1)в словаре ... Особенно потому, что выполнение двух O(1)операций все еще асимптотически O(1).
Патрик Свик
9
Как было отмечено в хорошем ответе ниже, бросать исключения стоит дорого. Их имя говорит об этом: они предназначены для исключительных обстоятельств. Если вы выполняете цикл, в котором миллион раз запрашиваете в словаре ключи, которых не существует, то это как бы перестает быть исключительным обстоятельством. Если вы запрашиваете словарь для ключей, и это довольно распространенный случай, когда они не будут присутствовать, тогда имеет смысл проверить сначала.
Джейсон Р
6
Не забывайте, что вы сравнили только стоимость проверки на миллион отсутствующих значений и бросили миллион исключений. Но эти два метода также отличаются стоимостью доступа к существующему значению. Если отсутствующие ключи достаточно редки, метод исключения будет быстрее всего, несмотря на более высокую стоимость, когда ключ отсутствует.
Алексис

Ответы:

404

С одной стороны, выбрасывать исключения по своей природе дорого , потому что стек должен быть размотан и т. Д.
С другой стороны, доступ к значению в словаре по его ключу дешев, потому что это быстрая операция O (1).

Кстати: правильный способ сделать это - использовать TryGetValue

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

Это получает доступ к словарю только один раз вместо двух.
Если вы действительно хотите просто вернуть, nullесли ключ не существует, приведенный выше код может быть упрощен далее:

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

Это работает, потому что TryGetValueнаборы itemдля nullесли ни одна клавиша с nameне существует.

Дэниэл Хилгарт
источник
4
Я обновил свой тест в соответствии с ответом, и по какой-то причине, несмотря на то, что предложенная функция работает быстрее, на самом деле она не очень значительна: исходная длина 264 мсек, рекомендуемая 258 мсек
Петр
52
@Petr: Да, это не важно, потому что доступ к словарю очень быстрый, это не имеет значения, если вы делаете это один или два раза. Большая часть этих 250 мс, скорее всего, тратится на сам цикл тестирования.
Даниэль Хилгарт,
4
Это полезно знать, потому что иногда создается впечатление, что генерирование исключений является лучшим или более чистым способом обработки ситуации, такой как несуществующий файл или нулевой указатель, независимо от того, являются ли эти ситуации общими и без учета затрат на производительность.
LarsH
4
@LarsH это также зависит от того, что вы делаете. В то время как простые микробенчмарки, подобные этому, показывают действительно большие штрафы за исключения, как только ваши циклы запускаются, включая действия с файлами или базой данных, генерирующие исключение на каждой итерации, очень мало влияют на производительность. Сравните 1-ю и 2-ю таблицы: codeproject.com/Articles/11265/…
Дэн возится с Firelight
8
@LarsH Также обратите внимание, что при попытке доступа к файлу (или некоторому другому внешнему ресурсу) он может изменить состояние между проверкой и фактической попыткой доступа. В этих случаях использование исключений - правильный путь. См . Ответ Стивена С. на этот вопрос для дополнительной информации.
yoniLavi
6

Словари специально разработаны для супер-быстрого поиска ключей. Они реализованы в виде хеш-таблиц, и чем больше записей, тем быстрее они по сравнению с другими методами. Предполагается, что использование механизма исключений возможно только в том случае, если ваш метод не смог выполнить то, для чего вы его разработали, поскольку это большой набор объектов, которые предоставляют вам множество функций для обработки ошибок. Однажды я собрал целый класс библиотеки, в котором все было окружено блоками try catch, и я был потрясен, увидев вывод отладки, который содержал отдельную строку для каждого из более чем 600 исключений!

Эд Хермансон
источник
1
Когда разработчики языка решают, куда затратить усилия на оптимизацию, хеш-таблицы получат приоритет, потому что они часто используются, часто во внутренних циклах, которые могут быть узкими местами. Ожидается, что исключения будут использоваться гораздо реже, в необычных (так сказать, «исключительных») случаях, поэтому они обычно не считаются важными для производительности.
Бармар
«Они реализованы в виде хеш-таблиц, и чем больше записей, тем быстрее они по сравнению с другими методами». конечно, это неправда, если ведра заполнятся?!?!
Энтони Ламберт,
1
@AnthonyLambert Что он пытается сказать, так это то, что поиск по хеш-таблице имеет O (1) временную сложность, тогда как поиск по бинарному дереву поиска будет иметь O (log (n)); дерево замедляется, так как число элементов увеличивается асимптотически, а хеш-таблица - нет. Следовательно, преимущество скорости передачи в хэш-таблице увеличивается с увеличением количества элементов, хотя это происходит медленно.
Доваль
@AnthonyLambert При нормальном использовании, в хеш-таблице словаря очень мало коллизий. Если вы используете хеш-таблицу, и ваши блоки заполнены, у вас слишком много записей (или слишком мало блоков). В этом случае пришло время использовать собственную хеш-таблицу.
AndrewS