Cara mengurangi overhead saat menangani pengecualian di C ++



Penanganan kesalahan runtime sangat penting dalam banyak situasi yang kami temui ketika mengembangkan perangkat lunak - dari input pengguna yang salah ke paket jaringan yang rusak. Aplikasi tidak boleh macet jika pengguna tiba-tiba mengunduh PNG dan bukan PDF, atau memutus kabel jaringan saat memperbarui perangkat lunak. Pengguna berharap bahwa program akan bekerja apa pun yang terjadi dan baik menangani situasi darurat di latar belakang atau menawarkannya untuk memilih opsi untuk menyelesaikan masalah melalui pesan yang dikirim melalui antarmuka yang ramah.

Penanganan pengecualian dapat menjadi tugas yang membingungkan dan rumit, dan, yang secara fundamental penting bagi banyak pengembang C ++, dapat sangat memperlambat aplikasi. Tetapi, seperti dalam banyak kasus lain, ada beberapa cara untuk menyelesaikan masalah ini. Selanjutnya, kami akan mempelajari proses penanganan pengecualian di C ++, menangani perangkapnya dan melihat bagaimana hal ini dapat memengaruhi kecepatan aplikasi Anda. Selain itu, kami akan melihat alternatif yang dapat digunakan untuk mengurangi biaya overhead.

Dalam artikel ini, saya tidak akan mendesak Anda untuk berhenti menggunakan pengecualian sepenuhnya. Mereka harus diterapkan, tetapi diterapkan tepat ketika tidak mungkin untuk menghindarinya: misalnya, bagaimana melaporkan kesalahan yang terjadi di dalam konstruktor? Kami terutama akan mempertimbangkan untuk menggunakan pengecualian untuk menangani kesalahan runtime. Menggunakan alternatif yang akan kita bicarakan akan memungkinkan Anda untuk mengembangkan aplikasi yang lebih andal dan mudah dirawat.

Tes kinerja cepat


Berapa banyak pengecualian yang lebih lambat dalam C ++ dibandingkan dengan mekanisme biasa untuk mengendalikan kemajuan suatu program?

Pengecualian jelas lebih lambat daripada operasi break atau return yang sederhana. Tapi mari kita cari tahu seberapa lambat!

Dalam contoh di bawah ini, kami telah menulis fungsi sederhana yang secara acak menghasilkan angka dan, berdasarkan pemeriksaan satu nomor yang dihasilkan, memberi / tidak memberikan pesan kesalahan.

Kami menguji beberapa opsi implementasi untuk penanganan kesalahan:

  1. Lempar pengecualian dengan argumen integer. Meskipun ini tidak diterapkan secara khusus dalam praktiknya, ini adalah cara termudah untuk menggunakan pengecualian dalam C ++. Jadi kami menyingkirkan kerumitan yang berlebihan dalam pelaksanaan pengujian kami.
  2. Buang std :: runtime_error, yang dapat mengirim pesan teks. Opsi ini, tidak seperti yang sebelumnya, jauh lebih sering digunakan dalam proyek nyata. Mari kita lihat apakah opsi kedua akan memberikan kenaikan nyata dalam biaya overhead dibandingkan dengan yang pertama.
  3. Pengembalian kosong.
  4. Kembalikan kode kesalahan int style C

Untuk menjalankan tes, kami menggunakan pustaka patokan Google sederhana . Dia berulang kali menjalankan setiap tes dalam satu siklus. Selanjutnya, saya akan menjelaskan bagaimana semuanya terjadi. Pembaca yang tidak sabar dapat langsung melompat ke hasil .

Kode uji

Generator nomor acak super rumit kami:

const int randomRange = 2;  //     0  2.
const int errorInt = 0; 	//    ,    0.
int getRandom() {
	return random() % randomRange;
}

Fungsi tes:

// 1.
void exitWithBasicException() {
	if (getRandom() == errorInt) {
    	throw -2;
	}
}
// 2.
void exitWithMessageException() {
	if (getRandom() == errorInt) {
    	throw std::runtime_error("Halt! Who goes there?");
	}
}
// 3.
void exitWithReturn() {
	if (getRandom() == errorInt) {
    	return;
	}
}
// 4.
int exitWithErrorCode() {
	if (getRandom() == errorInt) {
    	return -1;
	}
	return 0;
}

Itu dia, sekarang kita bisa menggunakan perpustakaan benchmark Google :

// 1.
void BM_exitWithBasicException(benchmark::State& state) {
	for (auto _ : state) {
    	try {
        	exitWithBasicException();
    	} catch (int ex) {
        	//  ,    .
    	}
	}
}
// 2.
void BM_exitWithMessageException(benchmark::State& state) {
	for (auto _ : state) {
    	try {
        	exitWithMessageException();
    	} catch (const std::runtime_error &ex) {
        	//  ,    
    	}
	}
}
// 3.
void BM_exitWithReturn(benchmark::State& state) {
	for (auto _ : state) {
    	exitWithReturn();
	}
}
// 4.
void BM_exitWithErrorCode(benchmark::State& state) {
	for (auto _ : state) {
    	auto err = exitWithErrorCode();
    	if (err < 0) {
        	// `handle_error()` โ€ฆ  - 
    	}
	}
}

//  
BENCHMARK(BM_exitWithBasicException);
BENCHMARK(BM_exitWithMessageException);
BENCHMARK(BM_exitWithReturn);
BENCHMARK(BM_exitWithErrorCode);

//  !
BENCHMARK_MAIN();

Bagi mereka yang ingin menyentuh yang indah, kami telah memposting kode lengkap di sini .

hasil


Di konsol, kami melihat hasil tes yang berbeda, tergantung pada opsi kompilasi:

Debug -O0:

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
BM_exitWithBasicException     1407 ns         1407 ns       491232
BM_exitWithMessageException   1605 ns         1605 ns       431393
BM_exitWithReturn              142 ns          142 ns      5172121
BM_exitWithErrorCode           144 ns          143 ns      5069378

Rilis -O2:

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
BM_exitWithBasicException     1092 ns         1092 ns       630165
BM_exitWithMessageException   1261 ns         1261 ns       547761
BM_exitWithReturn             10.7 ns         10.7 ns     64519697
BM_exitWithErrorCode          11.5 ns         11.5 ns     62180216

(Diluncurkan pada 2015 MacBook Pro 2.5GHz i7)

Hasilnya luar biasa! Lihatlah celah besar antara kecepatan kode dengan dan tanpa pengecualian (return kosong dan errorCode). Dan dengan optimisasi kompiler, celah ini bahkan lebih besar!

Tidak diragukan lagi, ini adalah ujian yang bagus. Kompiler mungkin melakukan sedikit optimasi kode untuk pengujian 3 dan 4, tetapi celahnya tetap besar. Dengan demikian, ini memungkinkan kami untuk memperkirakan biaya overhead saat menggunakan pengecualian.

Berkat model nol biaya, di sebagian besar implementasi blok percobaan di C ++, tidak ada overhead tambahan. Tapi blok-catch bekerja jauh lebih lambat. Dengan menggunakan contoh sederhana kami, kami dapat melihat seberapa lambat melempar dan menangkap pengecualian dapat bekerja. Bahkan pada tumpukan panggilan kecil! Dan dengan meningkatnya kedalaman tumpukan, overhead akan meningkat secara linear. Itulah mengapa sangat penting untuk mencoba menangkap pengecualian sedekat mungkin dengan kode yang melemparkannya.

Kami menemukan bahwa pengecualian berfungsi lambat. Maka mungkin menghentikannya? Tapi tidak semuanya begitu sederhana.

Mengapa semua orang terus menggunakan pengecualian?


Manfaat pengecualian didokumentasikan dengan baik dalam Laporan Teknis Kinerja C ++ (bab 5.4) :

, , errorCode- [ ], . , . , .

Sekali lagi: ide utamanya adalah ketidakmampuan untuk mengabaikan dan melupakan pengecualian. Ini membuat pengecualian sebagai alat C ++ bawaan yang sangat kuat, yang mampu menggantikan pemrosesan yang sulit melalui kode kesalahan, yang kami warisi dari bahasa C.

Selain itu, pengecualian sangat berguna dalam situasi yang tidak terkait langsung dengan program, misalnya, "hard disk penuh "Atau" kabel jaringan rusak. " Dalam situasi seperti itu, pengecualian adalah ideal.

Tapi apa cara terbaik untuk menangani penanganan kesalahan yang terkait langsung dengan program? Dalam kasus apa pun, kita memerlukan mekanisme yang secara jelas menunjukkan kepada pengembang bahwa dia harus memeriksa kesalahan, memberinya informasi yang cukup tentang hal itu (jika muncul), mentransmisikannya dalam bentuk pesan atau dalam format lain. Tampaknya kita kembali ke pengecualian bawaan, tetapi barusan kita akan berbicara tentang solusi alternatif.

Diharapkan


Selain overhead, pengecualian memiliki kelemahan lain: mereka bekerja secara berurutan: pengecualian perlu ditangkap dan diproses segera setelah dilempar, tidak mungkin untuk menunda untuk nanti.

Apa boleh buat?

Ternyata ada jalan. Profesor Andrei Alexandrescu datang dengan kelas khusus untuk kami yang disebut Diharapkan. Ini memungkinkan Anda untuk membuat objek kelas T (jika semuanya berjalan baik) atau objek pengecualian kelas (jika terjadi kesalahan). Yaitu, ini atau itu. Atau atau.
Intinya, ini adalah pembungkus atas struktur data, yang dalam C ++ disebut union.

Karena itu, kami menggambarkan idenya sebagai berikut:

template <class T>
class Expected {
private:
	//  union:  ,   .     
	union {
    	T value;
    	Exception exception;
	};

public:
	//    `Expected`   T,   .
	Expected(const T& value) ...

	//    `Expected`   Exception,  -   
	Expected(const Exception& ex) ...

	//    :    
	bool hasError() ...

	//   T
	T value() ...

	//        (  Exception)
	Exception error() ...
};

Implementasi penuh tentu akan lebih sulit. Tetapi ide utama Diharapkan adalah bahwa dalam objek yang sama (Diharapkan & e) kita dapat menerima data untuk operasi reguler program (yang akan memiliki jenis dan format yang diketahui oleh kita: T & nilai) dan data kesalahan ( Pengecualian & ex). Oleh karena itu, sangat mudah untuk memeriksa apa yang datang kepada kami sekali lagi (misalnya, menggunakan metode hasError).

Namun, sekarang tidak ada yang akan memaksa kita untuk menangani pengecualian saat ini juga. Kami tidak akan memiliki lemparan () panggilan atau blok tangkapan. Sebaliknya, kita dapat merujuk ke objek Pengecualian kami dengan nyaman.

Tes Kinerja untuk yang Diharapkan


Kami akan menulis tes kinerja serupa untuk kelas baru kami:

// 5. Expected! Testcase 5 

Expected<int> exitWithExpected() {
	if (getRandom() == errorInt) {
    	return std::runtime_error("Halt! If you want...");  //  : return,   throw!
	}
	return 0;
}

// Benchmark.


void BM_exitWithExpected(benchmark::State& state) {
	for (auto _ : state) {
    	auto expected = exitWithExpected();

    	if (expected.hasError()){
        	// Handle in our own time.
    	}
    	// Or we can use the value...
    	// else {
    	// 	doSomethingInteresting(expected.value());
    	// }
	}
}

//  
BENCHMARK(BM_exitWithExpected);

// 
BENCHMARK_MAIN();

Drumroll !!!

Debug -O0:

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
BM_exitWithExpected            147 ns          147 ns      4685942

Rilis -O2:

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
BM_exitWithExpected           57.5 ns         57.5 ns     11873261

Tidak buruk! Untuk std :: runtime_error kami tanpa optimisasi, kami dapat mengurangi waktu pengoperasian dari 1605 menjadi 147 nanodetik. Dengan optimasi, semuanya terlihat lebih baik: penurunan dari 1261 menjadi 57,5 โ€‹โ€‹nanodetik. Ini lebih dari 10 kali lebih cepat daripada dengan -O0 dan lebih dari 20 kali lebih cepat daripada dengan -O2.

Jadi dibandingkan dengan pengecualian bawaan, Diharapkan bekerja jauh lebih cepat, dan juga memberi kita mekanisme penanganan kesalahan yang lebih fleksibel. Selain itu, memiliki kemurnian semantik dan menghilangkan kebutuhan untuk mengorbankan pesan kesalahan dan menghilangkan umpan balik kualitas pengguna.

Kesimpulan


Pengecualian bukanlah kejahatan absolut. Dan kadang-kadang itu bagus sama sekali, karena mereka bekerja sangat efektif untuk tujuan yang dimaksudkan: dalam keadaan luar biasa. Kami hanya mulai menghadapi masalah ketika kami menggunakannya di mana solusi yang lebih efektif tersedia.

Pengujian kami, meskipun faktanya cukup sederhana, menunjukkan: Anda dapat sangat mengurangi overhead jika Anda tidak menangkap pengecualian (blok tangkapan lambat) jika cukup mengirim data (menggunakan pengembalian).

Dalam artikel ini, kami juga secara singkat memperkenalkan kelas yang diharapkan dan bagaimana kami dapat menggunakannya untuk mempercepat proses penanganan kesalahan. Diharapkan memudahkan untuk melacak kemajuan program, dan juga memungkinkan kita untuk lebih fleksibel dan mengirim pesan kepada pengguna dan pengembang.


All Articles