Mengapa lebih cepat memeriksa apakah kamus berisi kunci, daripada menangkap pengecualian jika itu tidak terjadi?

234

Bayangkan kodenya:

public class obj
{
    // elided
}

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

Metode 1

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

Metode 2

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

Saya ingin tahu apakah ada perbedaan kinerja 2 fungsi ini, karena yang pertama HARUS lebih lambat dari yang kedua - mengingat bahwa perlu memeriksa dua kali jika kamus berisi nilai, sedangkan fungsi kedua hanya perlu mengakses kamus saja sekali tetapi WOW, sebenarnya berlawanan:

Ulangi untuk 1 000 nilai nilai (dengan 100.000 ada dan 900.000 tidak ada):

fungsi pertama: 306 milidetik

fungsi kedua: 20483 milidetik

Mengapa demikian?

EDIT: Seperti yang Anda lihat dalam komentar di bawah pertanyaan ini, kinerja fungsi kedua sebenarnya sedikit lebih baik daripada yang pertama jika ada 0 kunci yang tidak ada. Tetapi begitu ada setidaknya 1 atau lebih kunci tidak ada, kinerja yang kedua menurun dengan cepat.

Petr
sumber
39
Mengapa yang pertama harus lebih lambat? Sebenarnya, pada pandangan pertama, saya akan mengatakan itu harus lebih cepat, ContainsKeydiharapkan O(1)...
Patryk Ćwiek
8
@Petr Ada lebih banyak instruksi yang terlibat dalam pengecualian melempar daripada O(1)mencari di kamus ... Terutama karena melakukan dua O(1)operasi masih asimptotik O(1).
Patryk Ćwiek
9
Seperti yang telah dicatat dalam jawaban yang baik di bawah ini, melemparkan pengecualian itu mahal. Namanya ini: mereka dimaksudkan untuk disediakan untuk pengecualian keadaan-al. Jika Anda menjalankan perulangan di mana Anda query kamus jutaan kali untuk kunci yang tidak ada, maka itu semacam berhenti menjadi keadaan yang luar biasa. Jika Anda meminta kamus untuk kunci, dan itu adalah kasus yang relatif umum bahwa kunci mereka tidak akan ada, maka masuk akal untuk memeriksa terlebih dahulu.
Jason R
6
Jangan lupa bahwa Anda hanya membandingkan biaya untuk memeriksa satu juta nilai yang tidak ada, vs melemparkan satu juta pengecualian. Tetapi kedua metode ini juga berbeda dalam biaya mengakses nilai yang ada . Jika kunci yang hilang cukup langka, metode pengecualian akan lebih cepat dari semua, meskipun biayanya lebih tinggi ketika kunci tidak ada.
alexis

Jawaban:

404

Di satu sisi, pengecualian melempar pada dasarnya mahal , karena tumpukan harus dibatalkan dll.
Di sisi lain, mengakses nilai dalam kamus dengan kuncinya adalah murah, karena itu adalah operasi yang cepat, O (1).

BTW: Cara yang benar untuk melakukan ini adalah menggunakan TryGetValue

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

Ini mengakses kamus hanya sekali, bukan dua kali.
Jika Anda benar-benar ingin kembali saja nulljika kunci tidak ada, kode di atas dapat disederhanakan lebih lanjut:

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

Ini berfungsi, karena TryGetValuediatur itemke nulljika tidak ada kunci dengan nameada.

Daniel Hilgarth
sumber
4
Saya memperbarui tes saya sesuai dengan jawaban, dan untuk beberapa alasan, meskipun fungsi yang disarankan IS lebih cepat, sebenarnya tidak terlalu signifikan: 264 ms asli, 258ms menyarankan satu
Petr
52
@Petr: Ya, itu tidak penting, karena mengakses kamus sangat cepat, tidak masalah jika Anda melakukannya sekali atau dua kali. Sebagian besar dari 250 ms kemungkinan besar dihabiskan di loop tes itu sendiri.
Daniel Hilgarth
4
Ini bagus untuk diketahui, karena kadang-kadang orang mendapat kesan bahwa melempar pengecualian adalah cara yang lebih baik atau lebih bersih untuk menangani situasi seperti tidak ada file atau null pointer, terlepas dari apakah situasi itu umum, dan tanpa mempertimbangkan biaya kinerja.
LarsH
4
@ LarsH itu juga tergantung apa yang Anda lakukan. Sementara microbenchmark sederhana seperti ini menunjukkan hukuman yang sangat besar untuk pengecualian begitu loop Anda mulai termasuk aktivitas file atau basis data yang memberikan pengecualian pada setiap iterasi, hal yang sangat kecil untuk kinerja. Bandingkan tabel 1 dan 2: codeproject.com/Articles/11265/…
Dan Is Fiddling By Firelight
8
@ LarsH Juga perhatikan bahwa ketika mencoba mengakses file (atau sumber daya eksternal lainnya), ini dapat mengubah status antara pemeriksaan dan upaya akses aktual. Dalam kasus ini, menggunakan pengecualian adalah cara yang benar untuk dilakukan. Lihat jawaban Stephen C untuk pertanyaan ini untuk wawasan tambahan.
yoniLavi
6

Kamus secara khusus dirancang untuk melakukan pencarian kunci super cepat. Mereka diimplementasikan sebagai tagar dan semakin banyak entri semakin cepat relatif terhadap metode lain. Menggunakan mesin pengecualian hanya seharusnya dilakukan ketika metode Anda gagal melakukan apa yang Anda rancang untuk dilakukan karena itu adalah seperangkat objek besar yang memberi Anda banyak fungsi untuk menangani kesalahan. Saya membangun seluruh kelas perpustakaan sekali dengan segala sesuatu yang dikelilingi oleh coba tangkap blok sekali dan terkejut melihat hasil debug yang berisi baris terpisah untuk setiap satu dari lebih dari 600 pengecualian!

Ed Hermanson
sumber
1
Ketika pelaksana bahasa memutuskan di mana harus mengeluarkan upaya optimasi, tabel hash akan mendapatkan prioritas karena mereka sering digunakan, sering dalam loop internal yang mungkin menjadi hambatan. Pengecualian hanya diharapkan untuk digunakan jauh lebih jarang, dalam kasus yang tidak biasa ("luar biasa", sehingga untuk berbicara), sehingga mereka biasanya tidak dianggap sebagai penting untuk kinerja.
Barmar
"Mereka diimplementasikan sebagai tagar dan semakin banyak entri semakin cepat relatif terhadap metode lain." pasti itu tidak benar jika ember terisi?!?!
AnthonyLambert
1
@AnthonyLambert Apa yang dia coba katakan adalah bahwa pencarian hashtable memiliki O (1) kompleksitas waktu, sedangkan pencarian pohon pencarian biner akan memiliki O (log (n)); pohon melambat ketika jumlah elemen bertambah tanpa gejala, sedangkan hashtable tidak. Oleh karena itu, keuntungan kecepatan hashtable meningkat dengan jumlah elemen, meskipun ia melakukannya dengan lambat.
Doval
@AnthonyLambert Dalam penggunaan normal, ada sangat sedikit tabrakan dalam hashtable Kamus. Jika Anda menggunakan hashtable dan bucket Anda terisi, Anda memiliki waaaaay terlalu banyak entri (atau terlalu sedikit bucket). Jika demikian, saatnya menggunakan hashtable khusus.
AndrewS