Apakah blok coba / tangkap melukai kinerja ketika pengecualian tidak dilakukan?

274

Selama tinjauan kode dengan karyawan Microsoft, kami menemukan sebagian besar kode di dalam try{}blok. Dia dan perwakilan TI menyarankan ini dapat memiliki efek pada kinerja kode. Bahkan, mereka menyarankan sebagian besar kode harus di luar blok coba / tangkap, dan hanya bagian-bagian penting yang harus diperiksa. Karyawan Microsoft menambahkan dan mengatakan bahwa buku putih yang akan datang memperingatkan terhadap blok try / catch yang salah.

Saya telah melihat sekeliling dan menemukan itu dapat mempengaruhi pengoptimalan , tetapi tampaknya hanya berlaku ketika variabel dibagi di antara cakupan.

Saya tidak bertanya tentang kelestarian kode, atau bahkan menangani pengecualian yang benar (kode yang dipermasalahkan perlu dipertimbangkan ulang, tidak diragukan lagi). Saya juga tidak merujuk menggunakan pengecualian untuk kontrol aliran, ini jelas salah dalam kebanyakan kasus. Itu adalah masalah penting (beberapa lebih penting), tetapi bukan fokus di sini.

Bagaimana pengaruh coba / tangkap blok mempengaruhi kinerja ketika pengecualian tidak dilemparkan?

Kobi
sumber
147
"Dia yang akan mengorbankan kebenaran untuk kinerja tidak layak."
Joel Coehoorn
16
yang mengatakan, kebenaran tidak harus selalu dikorbankan untuk kinerja.
Dan Davies Brackett
19
Bagaimana dengan rasa ingin tahu yang sederhana?
Samantha Branham
63
@ Joel: Mungkin Kobi hanya ingin tahu jawabannya karena penasaran. Mengetahui apakah kinerja akan lebih baik atau lebih buruk tidak berarti dia akan melakukan sesuatu yang gila dengan kodenya. Bukankah mengejar ilmu demi dirinya sendiri adalah hal yang baik?
LukeH
6
Berikut adalah algoritma yang bagus untuk mengetahui apakah akan melakukan perubahan atau tidak. Pertama, tetapkan tujuan kinerja berbasis pelanggan yang bermakna. Kedua, tulis kode untuk menjadi benar dan jelas terlebih dahulu. Ketiga, uji terhadap sasaran Anda. Keempat, jika Anda memenuhi tujuan Anda, berangkat kerja lebih awal dan pergi ke pantai. Kelima, jika Anda tidak memenuhi tujuan Anda, gunakan profiler untuk menemukan kode yang terlalu lambat. Keenam, jika kode itu terjadi terlalu lambat karena penangan pengecualian yang tidak perlu, hanya kemudian menghapus penangan pengecualian. Jika tidak, perbaiki kode yang sebenarnya terlalu lambat. Kemudian kembali ke langkah ketiga.
Eric Lippert

Jawaban:

203

Periksa.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

Keluaran:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

Dalam milidetik:

449
416

Kode baru:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

Hasil baru:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334
Ben M
sumber
24
Bisakah Anda mencobanya dalam urutan terbalik juga untuk memastikan bahwa kompilasi JIT tidak berpengaruh pada yang pertama?
JoshJordan
28
Program-program seperti ini sepertinya bukan kandidat yang baik untuk menguji dampak penanganan pengecualian, terlalu banyak dari apa yang akan terjadi pada percobaan normal {} catch {} block akan dioptimalkan. Saya mungkin keluar untuk makan siang pada itu ...
272 LorenVS
30
Ini adalah build debug. JIT tidak mengoptimalkannya.
Ben M
7
Ini tidak benar sama sekali, Pikirkanlah. Berapa kali Anda menggunakan try catch dalam satu lingkaran? Sebagian besar waktu Anda akan menggunakan loop dalam try.c
Athiwat Chunlakhan
9
Betulkah? "Bagaimana pengaruh coba / tangkap blok mempengaruhi kinerja ketika pengecualian tidak dilempar?"
Ben M
105

Setelah melihat semua statistik dengan coba / tangkap dan tanpa coba / tangkap, rasa ingin tahu memaksa saya untuk melihat ke belakang untuk melihat apa yang dihasilkan untuk kedua kasus. Ini kodenya:

C #:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C #:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

Saya bukan ahli dalam IL tetapi kita dapat melihat bahwa objek pengecualian lokal dibuat pada baris keempat .locals init ([0] class [mscorlib]System.Exception ex)setelah itu semuanya sama seperti untuk metode tanpa mencoba / menangkap sampai baris tujuh belas IL_0021: leave.s IL_002f. Jika pengecualian terjadi, kontrol akan melompat ke baris, IL_0025: ldloc.0jika tidak, kita akan beralih ke label IL_002d: leave.s IL_002fdan fungsi kembali.

Saya dapat dengan aman berasumsi bahwa jika tidak ada pengecualian terjadi maka itu adalah overhead untuk membuat variabel lokal untuk menyimpan objek pengecualian saja dan instruksi lompat.

TheVillageIdiot
sumber
33
Nah, IL termasuk blok coba / tangkap dalam notasi yang sama seperti dalam C #, jadi ini tidak benar-benar menunjukkan berapa banyak overhead yang dicoba / tangkap berarti di belakang layar! Hanya saja IL tidak menambahkan lebih banyak, tidak berarti sama karena tidak menambahkan sesuatu dalam kode perakitan yang dikompilasi. IL hanyalah representasi umum dari semua bahasa .NET. BUKAN kode mesin!
awe
64

Tidak. Jika optimasi sepele blok percobaan / akhirnya menghalangi sebenarnya memiliki dampak yang terukur pada program Anda, Anda mungkin tidak boleh menggunakan .NET di tempat pertama.

John Kugelman
sumber
10
Itu poin yang sangat baik - dibandingkan dengan item lain dalam daftar kami, ini harus sangat kecil. Kita harus mempercayai fitur bahasa dasar untuk berperilaku dengan benar, dan mengoptimalkan apa yang dapat kita kontrol (sql, indeks, algoritma).
Kobi
3
Pikirkan pasangan loop ketat. Contoh loop di mana Anda membaca dan menghapus serialisasi objek dari aliran data socket di server game dan Anda mencoba memeras sebanyak yang Anda bisa. Jadi Anda MessagePack untuk serialisasi objek alih-alih binaryformatter, dan gunakan ArrayPool <byte> alih-alih hanya membuat byte array, dll ... Dalam skenario ini apa dampak dari beberapa (mungkin bersarang) coba tangkap blok dalam loop ketat. Beberapa optimasi akan dilewati oleh kompiler juga variabel pengecualian pergi ke Gen0 GC. Yang saya katakan adalah bahwa ada beberapa "skenario" di mana semuanya memiliki dampak.
tcwicks
35

Penjelasan yang cukup komprehensif tentang model pengecualian .NET.

Tidbits Performa Rico Mariani: Biaya Pengecualian: Kapan melempar dan kapan tidak

Jenis biaya pertama adalah biaya statis untuk memiliki penanganan perkecualian dalam kode Anda sama sekali. Pengecualian terkelola sebenarnya cukup baik di sini, yang saya maksudkan biaya statis bisa jauh lebih rendah daripada katakan dalam C ++. Kenapa ini? Nah, biaya statis benar-benar dikeluarkan di dua jenis tempat: Pertama, situs yang sebenarnya dari coba / akhirnya / tangkap / lempar di mana ada kode untuk konstruksi tersebut. Kedua, dalam kode tidak berubah, ada biaya siluman yang terkait dengan melacak semua objek yang harus dihancurkan jika ada pengecualian. Ada sejumlah besar logika pembersihan yang harus ada dan bagian liciknya adalah bahkan kode yang tidak

Dmitriy Zaslavskiy:

Sesuai catatan Chris Brumme: Ada juga biaya yang terkait dengan fakta bahwa beberapa optimasi tidak dilakukan oleh JIT di hadapan tangkapan

arul
sumber
1
Hal tentang C ++ adalah bahwa sebagian besar pustaka standar akan membuang pengecualian. Tidak ada yang opsional tentang mereka. Anda harus mendesain objek Anda dengan semacam kebijakan pengecualian, dan begitu Anda selesai melakukannya, tidak ada lagi biaya tersembunyi.
David Thornley
Klaim Rico Mariani sepenuhnya salah untuk C ++ asli. "Biaya statis bisa jauh lebih rendah daripada mengatakan dalam C ++" - Ini tidak benar. Meskipun, saya tidak yakin apa desain mekanisme pengecualian pada tahun 2003 ketika artikel itu ditulis. C ++ benar-benar tidak memiliki biaya sama sekali ketika pengecualian tidak dilemparkan, tidak peduli berapa banyak blok coba / tangkap yang Anda miliki dan di mana mereka.
BJovke
1
@ BJovke C ++ "penanganan pengecualian biaya nol" hanya berarti tidak ada biaya run-time ketika pengecualian tidak dilemparkan, tetapi masih ada biaya ukuran kode utama karena semua kode pembersihan memanggil destruktor pada pengecualian. Selain itu, meskipun tidak ada kode khusus pengecualian yang dihasilkan pada jalur kode normal, biayanya tetap tidak nol, karena kemungkinan pengecualian masih membatasi pengoptimal (mis. Hal-hal yang diperlukan jika pengecualian perlu tetap ada sekitar suatu tempat -> nilai dapat dibuang kurang agresif -> alokasi register kurang efisien)
Daniel
24

Struktur ini berbeda dalam contoh dari Ben M . Ini akan diperpanjang overhead di dalam forloop batin yang akan menyebabkannya menjadi perbandingan yang tidak baik antara dua kasus.

Berikut ini lebih akurat untuk perbandingan tempat seluruh kode untuk diperiksa (termasuk deklarasi variabel) di dalam blok Coba / Tangkap:

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Ketika saya menjalankan kode tes asli dari Ben M , saya melihat perbedaan dalam konfigurasi Debug dan Releas.

Versi ini, saya perhatikan ada perbedaan dalam versi debug (sebenarnya lebih dari versi lain), tetapi tidak ada perbedaan dalam versi Rilis.

Kesimpulan :
Berdasarkan tes ini, saya pikir kita dapat mengatakan bahwa Try / Catch memang memiliki dampak kecil pada kinerja.

EDIT:
Saya mencoba untuk meningkatkan nilai loop dari 10.000000 ke 1000000000, dan berlari lagi di Rilis untuk mendapatkan beberapa perbedaan dalam rilis, dan hasilnya adalah ini:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

Anda melihat bahwa hasilnya tidak sesuai. Dalam beberapa kasus, versi yang menggunakan Try / Catch sebenarnya lebih cepat!

perasaan kagum
sumber
1
Saya perhatikan ini juga, kadang lebih cepat dengan try / catch. Saya sudah mengomentari jawaban Ben. Namun, tidak seperti 24 pemilih, saya tidak suka benchmark semacam ini, saya pikir itu bukan indikasi yang baik. Kode ini lebih cepat dalam hal ini, tetapi apakah akan selalu demikian?
Kobi
5
Tidakkah ini membuktikan bahwa mesin Anda melakukan berbagai tugas lain pada saat yang bersamaan? Waktu yang berlalu tidak pernah merupakan ukuran yang baik, Anda harus menggunakan profiler yang mencatat waktu prosesor, bukan waktu yang berlalu.
Colin Desmond
2
@Kobi: Saya setuju bahwa ini bukan cara terbaik untuk membuat tolok ukur jika Anda ingin mempublikasikannya sebagai bukti bahwa program Anda berjalan lebih cepat daripada yang lain atau sesuatu, tetapi dapat memberi Anda sebagai pengembang indikasi satu metode berkinerja lebih baik daripada yang lain . Dalam hal ini, saya pikir kita dapat mengatakan bahwa perbedaan (setidaknya untuk konfigurasi Rilis) diabaikan.
awe
1
Anda tidak punya waktu di try/catchsini. Waktu Anda 12 mencoba / menangkap memasuki-bagian-kritis terhadap 10 juta loop. Kebisingan dari loop akan menghilangkan segala pengaruh try / catch. jika sebaliknya Anda meletakkan try / catch di dalam loop ketat, dan membandingkan dengan / tanpa, Anda akan berakhir dengan biaya try / catch. (tidak diragukan lagi, pengkodean seperti itu umumnya bukan praktik yang baik, tetapi jika Anda ingin menghitung waktu overhead suatu konstruksi, itulah cara Anda melakukannya). Saat ini, BenchmarkDotNet adalah alat bantu untuk waktu eksekusi yang andal.
Abel
15

Saya menguji dampak sebenarnya dari try..catchlingkaran ketat, dan terlalu kecil untuk menjadi perhatian kinerja dalam situasi normal apa pun.

Jika loop tidak banyak bekerja (dalam pengujian saya melakukan x++), Anda dapat mengukur dampak penanganan pengecualian. Pengulangan dengan penanganan pengecualian membutuhkan waktu sekitar sepuluh kali lebih lama untuk dijalankan.

Jika loop melakukan beberapa pekerjaan yang sebenarnya (dalam pengujian saya, saya disebut metode Int32.Parse), penanganan pengecualian memiliki dampak terlalu kecil untuk dapat diukur. Saya mendapat perbedaan yang jauh lebih besar dengan menukar urutan loop ...

Guffa
sumber
11

coba tangkap blok memiliki dampak yang dapat diabaikan pada kinerja tetapi pengecualian Melempar bisa sangat besar, ini mungkin di mana rekan kerja Anda bingung.

RHicke
sumber
8

Coba / tangkap HAS berdampak pada kinerja.

Tapi itu bukan dampak yang besar. coba / tangkap kompleksitas umumnya O (1), sama seperti tugas sederhana, kecuali ketika mereka ditempatkan dalam satu lingkaran. Jadi, Anda harus menggunakannya dengan bijak.

Berikut ini adalah referensi tentang kinerja try / catch (tidak menjelaskan kompleksitasnya, tetapi tersirat). Lihatlah Throw sedikit Pengecualian bagian

Ishak
sumber
3
Kompleksitas adalah O (1) tidak terlalu berarti. Sebagai contoh jika Anda melengkapi bagian kode yang disebut sangat sering dengan try-catch (atau Anda menyebutkan loop), O (1) dapat menambahkan hingga angka yang dapat diukur pada akhirnya.
Csaba Toth
6

Secara teori, blok coba / tangkap tidak akan berpengaruh pada perilaku kode kecuali jika benar-benar terjadi pengecualian. Namun, ada beberapa keadaan yang jarang, di mana keberadaan blok coba / tangkap mungkin memiliki efek besar, dan beberapa yang tidak umum tetapi hampir tidak jelas di mana efeknya dapat terlihat. Alasannya adalah karena kode yang diberikan seperti:

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

kompiler mungkin dapat mengoptimalkan statement1 berdasarkan pada fakta bahwa statement2 dijamin untuk dieksekusi sebelum statement3. Jika kompilator dapat mengenali bahwa thing1 tidak memiliki efek samping dan thing2 tidak benar-benar menggunakan x, ia dapat dengan aman menghilangkan thing1 sama sekali. Jika [seperti dalam kasus ini] hal1 mahal, itu bisa menjadi optimasi besar, meskipun kasus di mana hal1 mahal juga mereka yang paling tidak mungkin dikompilasi oleh kompiler. Misalkan kode diubah:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

Sekarang ada urutan peristiwa di mana statement3 bisa dieksekusi tanpa statement2 dieksekusi. Bahkan jika tidak ada dalam kode untuk thing2bisa melempar pengecualian, akan ada kemungkinan bahwa utas lain dapat menggunakan Interlocked.CompareExchangeuntuk melihat yang qtelah dihapus dan mengaturnya Thread.ResetAbort, dan kemudian melakukan Thread.Abort()sebelum statement2 menulis nilainya x. Maka catchakan mengeksekusi Thread.ResetAbort()[via delegateq ], yang memungkinkan eksekusi untuk melanjutkan dengan statement3. Urutan peristiwa seperti itu tentu saja sangat tidak mungkin, tetapi kompiler diperlukan untuk menghasilkan kode yang bekerja sesuai dengan spesifikasi bahkan ketika peristiwa yang mustahil terjadi.

Secara umum, kompiler jauh lebih mungkin untuk melihat peluang untuk meninggalkan bit kode sederhana daripada yang kompleks, dan dengan demikian akan jarang untuk mencoba / menangkap dapat mempengaruhi kinerja banyak jika pengecualian tidak pernah dibuang. Namun, ada beberapa situasi di mana keberadaan blok coba / tangkap dapat mencegah optimasi yang - tetapi untuk coba / tangkap - akan memungkinkan kode untuk berjalan lebih cepat.

supercat
sumber
5

Meskipun " Pencegahan lebih baik daripada penanganan ", dalam perspektif kinerja dan efisiensi kita bisa memilih try-catch daripada pre-varication. Pertimbangkan kode di bawah ini:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Inilah hasilnya:

With Checking: 20367
With Exception: 13998
Ted Oddman
sumber
4

Lihat diskusi tentang implementasi try / catch untuk diskusi tentang cara kerja try / catch blocks, dan bagaimana beberapa implementasi memiliki overhead yang tinggi, dan beberapa memiliki overhead nol, ketika tidak ada pengecualian. Secara khusus, saya pikir implementasi Windows 32 bit memiliki overhead yang tinggi, dan implementasi 64 bit tidak.

Ira Baxter
sumber
Apa yang saya jelaskan adalah dua pendekatan berbeda untuk menerapkan pengecualian. Pendekatan berlaku untuk C ++ dan C #, serta kode yang dikelola / tidak dikelola. MS mana yang dipilih untuk C # saya tidak tahu persis, tetapi arsitektur penanganan-pengecualian dari aplikasi tingkat mesin yang disediakan oleh MS menggunakan skema yang lebih cepat. Saya akan sedikit terkejut jika implementasi C # untuk 64 bit tidak menggunakannya.
Ira Baxter