Dlaczego szybciej jest sprawdzić, czy słownik zawiera klucz, niż uchwycić wyjątek, jeśli nie ma?

234

Wyobraź sobie kod:

public class obj
{
    // elided
}

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

Metoda 1

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

Metoda 2

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

Byłem ciekawy, czy jest różnica w wydajności tych 2 funkcji, ponieważ pierwsza POWINNA BYĆ wolniejsza niż druga - biorąc pod uwagę, że musi ona dwukrotnie sprawdzić, czy słownik zawiera wartość, podczas gdy druga funkcja musi mieć dostęp tylko do słownika raz, ale WOW, w rzeczywistości jest odwrotnie:

Pętla dla 1 000 000 wartości (przy 100 000 istniejących i 900 000 nieistniejących):

pierwsza funkcja: 306 milisekund

druga funkcja: 20483 milisekund

Dlaczego?

EDYCJA: Jak można zauważyć w komentarzach pod tym pytaniem, wydajność drugiej funkcji jest w rzeczywistości nieco lepsza niż pierwsza w przypadku, gdy nie ma 0 kluczy. Ale gdy jest co najmniej 1 lub więcej nieistniejących kluczy, wydajność drugiego z nich gwałtownie spada.

Petr
źródło
39
Dlaczego pierwszy powinien być wolniejszy? Właściwie na pierwszy rzut oka powiedziałbym, że powinno być szybciej, ContainsKeyoczekuje się O(1)...
Patryk Ćwiek
8
@Petr W zgłaszaniu wyjątku jest o wiele więcej instrukcji niż O(1)wyszukiwanie w słowniku ... Zwłaszcza, że ​​wykonywanie dwóch O(1)operacji jest wciąż asymptotycznie O(1).
Patryk Ćwiek
9
Jak zauważono w dobrej odpowiedzi poniżej, zgłaszanie wyjątków jest drogie. Ich nazwa sugeruje to: mają być zarezerwowane na wyjątkowe okoliczności. Jeśli uruchomisz pętlę, w której milion razy przeszukujesz słownik w poszukiwaniu kluczy, które nie istnieją, to przestaje to być wyjątkowa okoliczność. Jeśli pytasz słownika o klucze i stosunkowo często zdarza się, że klucz nie będzie obecny, warto najpierw sprawdzić.
Jason R
6
Nie zapominaj, że porównałeś tylko koszt sprawdzenia miliona nieobecnych wartości, a rzuciłeś milion wyjątków. Ale te dwie metody różnią się również kosztem dostępu do istniejącej wartości. Jeśli brakujące klucze są dość rzadkie, metoda wyjątków będzie ogólnie szybsza, pomimo wyższych kosztów, gdy klucz jest nieobecny.
Alexis

Odpowiedzi:

404

Z jednej strony zgłaszanie wyjątków jest z natury drogie , ponieważ stos musi zostać rozwinięty itp.
Z drugiej strony dostęp do wartości w słowniku według jego klucza jest tani, ponieważ jest to szybka operacja O (1).

BTW: Prawidłowym sposobem na to jest użycie TryGetValue

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

Umożliwia to dostęp do słownika tylko raz zamiast dwa razy.
Jeśli naprawdę chcesz po prostu wrócić, nulljeśli klucz nie istnieje, powyższy kod można dodatkowo uprościć:

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

To działa, ponieważ TryGetValueustawia itemsię, nulljeśli nie nameistnieje żaden klucz .

Daniel Hilgarth
źródło
4
Zaktualizowałem mój test zgodnie z odpowiedzią iz jakiegoś powodu, pomimo sugerowanej funkcji, jest ona szybsza, w rzeczywistości nie jest to bardzo znacząca: oryginał 264 ms, sugerowany jeden 258 ms
Petr
52
@Petr: Tak, to nie ma znaczenia, ponieważ dostęp do słownika jest bardzo szybki, nie ma znaczenia, czy zrobisz to raz czy dwa. Większość z tych 250 ms najprawdopodobniej jest wydawana w samej pętli testowej.
Daniel Hilgarth
4
Warto o tym wiedzieć, ponieważ czasami można odnieść wrażenie, że zgłaszanie wyjątków jest lepszym lub czystszym sposobem radzenia sobie z sytuacją taką jak nieistniejący plik lub wskaźnik zerowy, bez względu na to, czy sytuacje te są powszechne i bez uwzględnienia kosztu wydajności.
LarsH 19.04.13
4
@ LarS zależy również od tego, co robisz. Podczas gdy takie proste znaki mikrobenchowe jak ten pokazują naprawdę duże kary za wyjątki, gdy pętle zaczną obejmować działania związane z plikami lub bazami danych, rzucając wyjątek na każdą iterację, ma bardzo małe znaczenie dla wydajności. Porównaj 1. i 2. tabelę: codeproject.com/Articles/11265/…
Dan Is Fiddling By Firelight
8
@LarsH Należy również pamiętać, że podczas próby uzyskania dostępu do pliku (lub innego zewnętrznego zasobu) może on zmienić stan między próbą sprawdzenia a faktyczną próbą dostępu. W takich przypadkach stosowanie wyjątków jest właściwą drogą. Zobacz odpowiedź Stephena C na to pytanie, aby uzyskać dodatkowe informacje.
yoniLavi
6

Słowniki są specjalnie zaprojektowane do przeprowadzania superszybkich wyszukiwań kluczy. Są one implementowane jako tabele skrótów i im więcej wpisów, tym szybciej są względem innych metod. Używanie mechanizmu wyjątków powinno się odbywać tylko wtedy, gdy metoda nie wykonała tego, do czego została zaprojektowana, ponieważ jest to duży zestaw obiektów, który zapewnia wiele funkcji obsługi błędów. Raz zbudowałem całą klasę biblioteki ze wszystkim otoczonym przez try catch catch i byłem przerażony, widząc wyniki debugowania, które zawierały osobną linię dla każdego z ponad 600 wyjątków!

Ed Hermanson
źródło
1
Kiedy implementatorzy języka decydują, gdzie wydać wysiłek na optymalizację, tabele skrótów będą traktowane priorytetowo, ponieważ są często używane, często w wewnętrznych pętlach, które mogą być wąskimi gardłami. Oczekuje się, że wyjątki będą stosowane znacznie rzadziej, w nietypowych („że tak powiem” wyjątkowych) przypadkach, więc zwykle nie są uważane za ważne dla wydajności.
Barmar 24.04.13
„Są one implementowane jako tabele skrótów i im więcej wpisów, tym szybciej są względem innych metod”. z pewnością nie jest to prawdą, jeśli wiadra się napełnią?!?!
AnthonyLambert
1
@AnthonyLambert Próbuje powiedzieć, że wyszukiwanie tablicy hasht ma złożoność czasową O (1), podczas gdy wyszukiwanie drzewa wyszukiwania binarnego ma O (log (n)); drzewo zwalnia, gdy liczba elementów rośnie asymptotycznie, podczas gdy tablica mieszająca nie. Dlatego przewaga prędkości tablicy mieszającej rośnie wraz z liczbą elementów, chociaż robi to powoli.
Doval
@AnthonyLambert Podczas normalnego użytkowania w tablicy hashtaktycznej słownika jest bardzo mało kolizji. Jeśli używasz tablicy mieszającej, a wiadra wypełniają się, masz zbyt wiele wpisów (lub zbyt mało wiader). W takim przypadku nadszedł czas, aby użyć niestandardowego tabeli mieszającej.
AndrewS,