كيفية تقليل النفقات العامة عند معالجة الاستثناءات في C ++



يعد التعامل مع أخطاء وقت التشغيل أمرًا مهمًا للغاية في العديد من المواقف التي نواجهها عند تطوير البرامج - من إدخال المستخدم غير الصحيح إلى حزم الشبكة التالفة. يجب ألا يتعطل التطبيق إذا قام المستخدم بتنزيل PNG بشكل مفاجئ بدلاً من PDF ، أو قطع كبل الشبكة عند تحديث البرنامج. يتوقع المستخدم أن يعمل البرنامج بغض النظر عما يحدث ، وإما أن يعالج حالات الطوارئ في الخلفية أو يعرض عليه اختيار خيار لحل المشكلة عن طريق رسالة مرسلة عبر واجهة سهلة الاستخدام.

يمكن أن تكون معالجة الاستثناءات مهمة معقدة ومربكة ، وهي مهمة للعديد من مطوري C ++ ، ويمكن أن تبطئ التطبيق بشكل كبير. ولكن ، كما هو الحال في العديد من الحالات الأخرى ، هناك عدة طرق لحل هذه المشكلة. بعد ذلك ، سوف نتعمق في عملية معالجة الاستثناء في C ++ ، ونتعامل مع عيوبها ونرى كيف يمكن أن يؤثر ذلك على سرعة تطبيقك. بالإضافة إلى ذلك ، سننظر في البدائل التي يمكن استخدامها لتقليل النفقات العامة.

في هذه المقالة ، لن أحثك ​​على التوقف عن استخدام الاستثناءات بالكامل. يجب تطبيقها ، ولكن يتم تطبيقها بدقة عندما يكون من المستحيل تجنبها: على سبيل المثال ، كيفية الإبلاغ عن خطأ حدث داخل المنشئ؟ سنأخذ في الاعتبار بشكل أساسي استخدام الاستثناءات للتعامل مع أخطاء وقت التشغيل. سيسمح لك استخدام البدائل التي سنتحدث عنها بتطوير تطبيقات أكثر موثوقية وسهولة الصيانة.

اختبار الأداء السريع


ما مدى بطء الاستثناءات في C ++ مقارنة بالآليات المعتادة للتحكم في تقدم البرنامج؟

من الواضح أن الاستثناءات أبطأ من عمليات القطع أو الإرجاع البسيطة. ولكن دعنا نكتشف كم أبطأ!

في المثال أدناه ، قمنا بكتابة وظيفة بسيطة تقوم بإنشاء أرقام بشكل عشوائي ، وعلى أساس التحقق من رقم واحد تم إنشاؤه ، يعطي / لا يعطي رسالة خطأ.

اختبرنا العديد من خيارات التنفيذ لمعالجة الأخطاء:

  1. قم بطرح استثناء بحجة صحيحة. على الرغم من أن هذا لا يتم تطبيقه بشكل خاص في الممارسة العملية ، إلا أنه أسهل طريقة لاستخدام الاستثناءات في C ++. لذلك نتخلص من التعقيد المفرط في تنفيذ اختبارنا.
  2. تخلص من std :: runtime_error ، والذي يمكنه إرسال رسالة نصية. يستخدم هذا الخيار ، على عكس الخيار السابق ، في كثير من الأحيان في المشاريع الحقيقية. دعونا نرى ما إذا كان الخيار الثاني سيعطي زيادة ملموسة في التكاليف العامة مقارنة بالخيار الأول.
  3. عودة فارغة.
  4. إرجاع رمز الخطأ int للأسلوب C

لإجراء الاختبارات ، استخدمنا مكتبة Google المرجعية البسيطة . أجرت بشكل متكرر كل اختبار في دورة. بعد ذلك ، سأصف كيف حدث كل شيء. يمكن للقراء غير الصبورين القفز على الفور إلى النتائج .

كود الاختبار

مولد الأرقام العشوائية المعقدة لدينا:

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

وظائف الاختبار:

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

هذا كل شيء ، الآن يمكننا استخدام مكتبة 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();

بالنسبة لأولئك الذين يرغبون في لمس الجميل ، قمنا بنشر الرمز الكامل هنا .

النتائج


في وحدة التحكم ، نرى نتائج اختبار مختلفة ، اعتمادًا على خيارات

الترجمة: 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

الإصدار -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

(تم إطلاقه في 2015 MacBook Pro 2.5GHz i7)

النتائج مذهلة! انظر إلى الفجوة الهائلة بين سرعة الرمز مع استثناءات (إرجاع فارغ و errorCode). ومع تحسين المترجم ، هذه الفجوة أكبر!

مما لا شك فيه أن هذا اختبار عظيم. قد يقوم المترجم بقليل من تحسين التعليمات البرمجية للاختبارين 3 و 4 ، ولكن الفجوة كبيرة على أي حال. وبالتالي ، يسمح لنا هذا بتقدير النفقات العامة عند استخدام الاستثناءات.

بفضل نموذج التكلفة الصفرية ، في معظم تطبيقات كتل التجريب في C ++ ، لا يوجد حمل إضافي. لكن كتلة الالتقاط تعمل بشكل أبطأ بكثير. باستخدام مثالنا البسيط ، يمكننا أن نرى كيف يمكن أن يعمل الاستبعاد ببطء ورمي الاستثناءات. حتى على كومة مكالمات صغيرة! ومع زيادة عمق المكدس ، سيزيد الحمل الخطي. هذا هو السبب في أنه من المهم للغاية محاولة التقاط الاستثناء قدر الإمكان من الرمز الذي طرحه.

حسنًا ، اكتشفنا أن الاستثناءات تعمل ببطء. ثم ربما توقفه؟ ولكن ليس كل شيء بهذه البساطة.

لماذا يستمر الجميع في استخدام الاستثناءات؟


فوائد الاستثناءات موثقة جيدًا في التقرير الفني حول أداء C ++ (الفصل 5.4) :

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

مرة أخرى: الفكرة الرئيسية هي عدم القدرة على تجاهل ونسيان الاستثناء. هذا يجعل الاستثناءات أداة C ++

مضمنة قوية جدًا ، قادرة على استبدال المعالجة الصعبة من خلال رمز الخطأ الذي ورثناه من C. علاوة على ذلك ، الاستثناءات مفيدة جدًا في المواقف التي لا تتعلق مباشرة بالبرنامج ، على سبيل المثال ، "القرص الصلب ممتلئ "أو" تلف كبل الشبكة. " في مثل هذه الحالات ، تكون الاستثناءات مثالية.

ولكن ما هي أفضل طريقة للتعامل مع معالجة الأخطاء المتعلقة مباشرة بالبرنامج؟ على أي حال ، نحن بحاجة إلى آلية تشير بشكل لا لبس فيه للمطور إلى أنه يجب عليه التحقق من الخطأ ، وتزويده بمعلومات كافية عنه (إذا نشأت) ، وإرساله في شكل رسالة أو في شكل آخر. يبدو أننا نعود مرة أخرى إلى الاستثناءات المضمنة ، ولكن الآن سنتحدث عن حل بديل.

متوقع


بالإضافة إلى النفقات العامة ، فإن الاستثناءات لها عيب آخر: فهي تعمل بالتسلسل: يجب اكتشاف الاستثناء ومعالجته بمجرد طرحه ، فمن المستحيل تأجيله لوقت لاحق.

مالذي يمكننا فعله حيال هذا؟

اتضح أن هناك طريقة. جاء البروفيسور أندريه ألكسندرسكو بفصل خاص بالنسبة لنا يسمى المتوقع. يسمح لك بإنشاء كائن من الفئة T (إذا سار كل شيء على ما يرام) أو كائن من استثناء الفئة (في حالة حدوث خطأ). أي إما هذا أو ذاك. او او.
في الجوهر ، هذا عبارة عن غلاف فوق بنية البيانات ، والذي يسمى في C ++ الاتحاد.

لذلك أوضحنا فكرته على النحو التالي:

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() ...
};

سيكون التنفيذ الكامل بالتأكيد أكثر صعوبة. لكن الفكرة الرئيسية للتوقعات هي أنه في نفس الكائن (المتوقع & e) يمكننا تلقي بيانات للتشغيل المنتظم للبرنامج (الذي سيكون له النوع والتنسيق المعروف لنا: T & value) وبيانات الخطأ ( استثناء & السابق). لذلك ، من السهل جدًا التحقق مما وصل إلينا مرة أخرى (على سبيل المثال ، باستخدام طريقة hasError).

ومع ذلك ، الآن لن يجبرنا أحد على التعامل مع الاستثناء في هذه اللحظة. لن يكون لدينا مكالمة رمي () أو كتلة صيد. بدلاً من ذلك ، يمكننا الرجوع إلى كائن الاستثناء الخاص بنا على راحتك.

اختبار الأداء المتوقع


سنكتب اختبارات أداء مماثلة لفئتنا الجديدة:

// 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();

الطبول !!!

تصحيح -O0:

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

الإصدار -O2:

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

ليس سيئا! بالنسبة إلى std :: runtime_error دون تحسين ، تمكنا من تقليل وقت التشغيل من 1605 إلى 147 نانو ثانية. مع التحسين ، يبدو كل شيء أفضل: انخفاض من 1261 إلى 57.5 نانو ثانية. هذا أسرع بعشر مرات من -O0 وأكثر من 20 مرة أسرع من -O2.

لذا ، بالمقارنة مع الاستثناءات المضمنة ، يعمل الإصدار المتوقع بشكل أسرع عدة مرات ، كما يمنحنا آلية أكثر مرونة للتعامل مع الأخطاء. بالإضافة إلى ذلك ، تتمتع بالنقاء الدلالي وتزيل الحاجة للتضحية برسائل الخطأ وحرمان المستخدمين من ردود الفعل الجيدة.

استنتاج


الاستثناءات ليست شرًا مطلقًا. وأحيانًا يكون ذلك جيدًا على الإطلاق ، نظرًا لأنهم يعملون بشكل فعال للغاية لتحقيق غرضهم المقصود: في ظروف استثنائية. نبدأ فقط في مواجهة المشاكل عندما نستخدمها حيث تتوفر حلول أكثر فعالية.

أظهرت اختباراتنا ، على الرغم من أنها بسيطة للغاية ، أنه يمكنك تقليل النفقات العامة إلى حد كبير إذا لم تكتشف الاستثناءات (كتلة الالتقاط البطيئة) في الحالات التي يكفي فيها إرسال البيانات (باستخدام الإرجاع).

في هذه المقالة ، قدمنا ​​أيضًا باختصار الفئة المتوقعة وكيف يمكننا استخدامها لتسريع عملية معالجة الأخطاء. يُسهل برنامج "توقع" من متابعة تقدم البرنامج ، كما يتيح لنا أن نكون أكثر مرونة وأن نرسل رسائل إلى المستخدمين والمطورين.


All Articles