Чому швидше перевірити, чи містить словник ключ, а не виловлювати виняток у випадку, якщо його немає?


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;
    }
}

Мені було цікаво, чи є різниця у виконанні цих двох функцій, оскільки перша БУДЬ БУДІТЬ меншою, ніж друга - враховуючи, що їй потрібно двічі перевірити, чи словник містить значення, а другій функції потрібно мати доступ лише до словника колись, але WOW, це насправді навпаки:

Цикл на 1 000 000 значень (зі 100 000 існуючих та 900 000 не існує):

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

друга функція: 20483 мілісекунди

Чому так?

EDIT: Як ви можете помітити в коментарях під цим питанням, виконання другої функції насправді трохи краще, ніж першої, якщо є 0 неіснуючих клавіш. Але як тільки є щонайменше 1 чи більше неіснуючих клавіш, продуктивність другого швидко знижується.


39
Чому перший повинен бути повільніше? Насправді, на перший погляд, я б сказав, що це повинно бути швидше, ContainsKeyочікується O(1)...
Patryk Ćwiek


8
@Petr Існує набагато більше інструкцій, що стосуються метання винятків, ніж O(1)пошуку у словнику ... Тим більше, що виконання двох O(1)операцій все ще асимптотично O(1).
Patryk Ćwiek

9
Як було зазначено в гарній відповіді нижче, кидати винятки дорого. Їх назва говорить про це: вони мають бути зарезервовані для винятку - для обставин. Якщо ви працюєте в циклі, де ви мільйони разів запитуєте словник для ключів, які не існують, то він перестає бути винятковою обставиною. Якщо ви запитуєте словник для ключів, і це порівняно поширений випадок, коли вони не будуть присутніми, тоді це має сенс перевірити спочатку.
Джейсон R

6
Не забувайте, що ви порівняли лише вартість перевірки на мільйон відсутніх значень, порівняно з тим, що кинули мільйон винятків. Але два способи також відрізняються вартістю доступу до існуючої вартості. Якщо відсутні ключі досить рідкісні, метод виключення буде швидшим за всіх, незважаючи на більш високу вартість, коли ключ відсутній.
alexis

Відповіді:


404

З одного боку, кидати винятки за своєю суттю дорого , тому що стек повинен бути розкручений тощо.
.

BTW: Правильний спосіб зробити це - використовувати 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/…
Dan Is Fiddling By Firelight

8
@LarsH Також зауважте, що при спробі отримати доступ до файлу (або якогось іншого зовнішнього ресурсу) він може змінити стан між чеком і фактичною спробою доступу. У цих випадках використання виключень - це правильний шлях. Додаткову інформацію див. У відповідь Стівена С на це запитання .
yoniLavi

6

Словники розроблені спеціально для надшвидкого пошуку ключів. Вони реалізовані у вигляді хештелів і чим більше записів, тим швидше вони відносно інших методів. Використання двигуна виключень має бути зроблено лише тоді, коли ваш метод не зміг виконати те, що ви його задумали, оскільки це великий набір об'єктів, який дає вам багато функцій для обробки помилок. Один раз я створив цілий клас бібліотеки з усім, що оточується блоками спробу лову, і здивовано побачив вихід налагодження, який містив окремий рядок для кожного з понад 600 винятків!


1
Коли мовні виконавці вирішують, куди витратити зусилля на оптимізацію, хеш-таблиці отримають пріоритет, оскільки вони використовуються часто, часто у внутрішніх петлях, які можуть бути вузькими місцями. Очікується, що винятки використовуються лише набагато рідше, у незвичних ("виняткових", так би мовити) випадках, тому вони, як правило, не вважаються важливими для продуктивності.
Бармар

"Вони реалізовані як хештелі, і чим більше записів, тим швидше вони відносно інших методів." напевно, це неправда, якщо відра наповнюються?!?!
AnthonyLambert

1
@AnthonyLambert Що він намагається сказати, це те, що пошук за хештелем має часову складність O (1), тоді як двійковий пошук по дереву пошуку матиме O (log (n)); дерево сповільнюється, коли кількість елементів збільшується асимптотично, тоді як хештейн - ні. Тому перевага швидкості хештеля збільшується з кількістю елементів, хоча це робиться повільно.
Доваль

@AnthonyLambert При звичайному використанні в хештейлі словника вкрай мало зіткнень. Якщо ви користуєтеся хештелем і ваші відра заповнюєтесь, у вас є занадто багато записів (або занадто мало відра). У такому випадку настав час скористатися користувацьким хештелем.
AndrewS
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.