اندازه باینری و استثناها – انجمن DEV
در این سری از مقالات در مورد اندازه های باینری، قبلاً در مورد آن صحبت کردیم default
کلمه کلیدی و گفت که default
کلمه کلیدی عملکرد خاصی ایجاد می کند noexcept
، هر زمان که بتواند.
چیست noexcept
?
این noexcept
specifier مشخص می کند که آیا یک تابع می تواند یک استثنا ایجاد کند noexcept
یا noexcept(expression == true)
مشخص کننده، پس نمی تواند استثناء ایجاد کند. اگر همچنان به طور صریح یا از طریق تابع دیگری که فراخوانی می کند پرتاب کند، برنامه فراخوانی می کند std::terminate
بلافاصله. مستقیما.
آیا استثنائات نادیده گرفته شده هزینه دارند؟
وقتی در مورد استثناها یاد می گیریم، اغلب در مورد جدول زیر به ما آموزش داده می شود.
ما در مورد هزینه قابل توجه عملیات CPU برای پرتاب و گرفتن استثناهای C++ آشنا می شویم. همانطور که می بینید، هزینه ها حتی در مقایسه با خواندن از رم اصلی بسیار زیاد است، بدون اینکه به خواندن از حافظه نهان یا حتی تماس گرفتن اشاره کنیم. virtual
تابع.
با این حال، ما اغلب میتوانیم این هزینه را برای انتزاع استثناها بپردازیم، زیرا آن فشار سرعت را نداریم یا به دلیل اینکه سایر بخشهای برنامه بسیار کندتر هستند و کندی نسبی استثناها ناچیز است. یا ما به سادگی در نظر می گیریم که وقتی یک استثنا پرتاب می شود، چیزی قبلاً اشتباه شده است و جریمه سرعت قابل قبول است.
اغلب به ما می گویند که استثنائات زمانی که پرتاب نمی شوند، هزینه صفر دارند.
این کاملا درست نیست.
هزینه CPU تنها هزینه نیست.
اگر از استثناها استفاده کنید باینری شما رشد خواهد کرد. بدیهی است که این ربطی به تعداد استثناهای واقعی پرتاب و گرفتار ندارد.
رسیدگی به استثنائات به مقداری سربار نیاز دارد و سطح جزئیات موجود می تواند بسیار زیاد باشد. اگر بخواهیم آن را تا حد امکان ساده کنیم، میتوان گفت که کامپایلر باید در باینری ذخیره کند که چه نوع استثناهایی را میتوان توسط بخشهای مختلف کد ایجاد کرد و چگونه باید با آنها رفتار کرد. جزئیات دقیق پیاده سازی خارج از محدوده ما است.
اگر یک قطعه کد به هیچ وجه نمی تواند استثناء ایجاد کند، پس چنین اطلاعاتی نباید ذخیره شود. حداقل این تئوری است، حالا ببینیم این چقدر در عمل درست است.
بیایید استثناها را کاملاً خاموش کنیم
بیایید با ممنوع کردن کامل استثناها در کدمان شروع کنیم.
اگر کامپایل کنیم، با پرچم -fno-exceptions
، می توانیم به کامپایلر بگوییم که به هیچ وجه استثنا اجازه ندهد. این نه تنها به این معنی نیست که رسیدگی به استثناها اتفاق نمی افتد و در صورت وجود استثنا، برنامه خاتمه می یابد، بلکه به این معنی است که شما مجاز به نوشتن هیچ کد رسیدگی به استثنا نیستید.
در عین حال، شما مجاز به استفاده از کدهای خارجی هستید که پرتاب می کند، اما در صورت پرتاب شدن خطا، برنامه شما خاتمه می یابد. اگر می خواهید آن را تست کنید، با شماره تماس بگیرید at(size_t)
روش بر روی الف vector
. این یکی از معدود روشهایی در کتابخانه استاندارد است که پرتاب میکند. at(size_t)
کرانه ها را بررسی می کند و نمونه ای از آن را پرتاب می کند std::out_of_range
استثنا در مواردی که سعی می کنید به چیزی فراتر از محدودیت های یک ظرف دسترسی پیدا کنید.
کلاسی با توابع ویژه پیش فرض یا خالی
بیایید ببینیم اگر استثناها را برای برخی از کدهایی که در چند هفته گذشته نوشتهایم خاموش کنیم، چه اتفاقی میافتد. ابتدا، استثناهای یک کد ساده را که در آن اندازههای باینری کلاسها را با آن آزمایش میکردیم، تبدیل کردم default
ed توابع ویژه.
نسخه | اندازه باینری |
---|---|
توابع ویژه پیش فرض با استثنا -O0 | 116,302 |
توابع ویژه پیش فرض بدون استثنا -O0 | 116,302 |
توابع ویژه پیش فرض با استثنا -O3 | 116270 |
توابع ویژه پیش فرض بدون استثنا -O3 | 116270 |
توابع ویژه پیش فرض با استثنا -Os | 116270 |
توابع ویژه پیش فرض بدون استثنا -Os | 116270 |
همانطور که می بینید، ما اصلاً چیزی به دست نیاوردیم. زمانی که ما فقط کلاسی با توابع ویژه پیشفرض داشتیم، حذف پشتیبانی از استثناها چیزی را تغییر نداد. همانطور که قبلا نوشتم، default
اجرای یک تابع خاص صرفاً تولید اجسام خالی (یا ساده ترین) برای توابع ویژه نیست. همچنین اضافه می کند noexcept
جایی که ممکن است در مورد ما، مطمئناً ممکن است.
با در نظر گرفتن این موضوع، بیایید آزمایش خود را با یک قطعه کد ساده مشابه اجرا کنیم. در این مورد، توابع عضو نیستند default
ed، اما یک پیاده سازی خالی دارند و هستند نه noexcept
.
نسخه | اندازه باینری |
---|---|
توابع ویژه خالی با استثنا -O0 | 33,983 |
توابع ویژه خالی بدون استثنا -O0 | 33,823 |
توابع ویژه خالی با استثنا -O3 | 16,879 |
توابع ویژه خالی بدون استثنا -O3 | 16,879 |
توابع ویژه را با استثناهای -Os خالی کنید | 16,879 |
توابع ویژه خالی بدون استثنا -Os | 16,879 |
در این مورد، -fno-exceptions
کمی کمک کرد، اما فقط بدون بهینه سازی. با فعال بودن بهینهسازی، چیزی به دست نیاوردیم، زیرا کامپایلر به تنهایی و بدون اشاره به اندازه کافی هوشمند است. اما همیشه نمی تواند همه این نوع اطلاعات را استنباط کند.
برای نشان دادن آن، به مثال پیچیده تری نیاز داریم.
الگوی دکوراتور
بیایید نگاهی به الگوی دکوراتوری که اخیراً در مورد آن صحبت کردیم بیندازیم. اکنون با تمام سطوح مختلف بهینهسازی که امتحان کردیم، تفاوت میبینیم.
نسخه | اندازه باینری |
---|---|
الگوی دکوراسیون مدرن با استثنا -O0 | 76,481 |
الگوی دکوراسیون مدرن بدون استثنا -O0 | 58,881 |
الگوی دکوراسیون مدرن با استثنا -O3 | 35729 |
الگوی دکوراسیون مدرن بدون استثنا -O3 | 35,457 |
الگوی دکوراسیون مدرن با استثناء -Os | 36,257 |
الگوی دکوراسیون مدرن بدون استثنا -Os | 36193 |
ما تفاوتی را می بینیم، اما به غیر از این تفاوت قابل توجهی نیست -O0
سطح بهینه سازی اگر اجرای کلاسیک الگوی دکوراتور را بررسی کنیم، تفاوت حتی کمتر می شود. در واقع، اگر با آن کامپایل کنیم، کاملاً ناپدید می شود -O3
.
الگوی ناظر
از آنجایی که هیچ تفاوت قابل توجهی ندیدیم، بیایید ادامه دهیم و الگوی مشاهده گر را امتحان کنیم. یک بار دیگر می بینیم مقداری تفاوت برای اجرای کلاسیک
نسخه | اندازه باینری |
---|---|
الگوی ناظر کلاسیک با استثنا -O0 | 86,081 |
الگوی ناظر کلاسیک بدون استثنا -O0 | 84769 |
الگوی ناظر کلاسیک با استثنا -O3 | 36,385 |
الگوی ناظر کلاسیک بدون استثنا -O3 | 36,225 |
الگوی ناظر کلاسیک با استثنا -Os | 37,569 |
الگوی ناظر کلاسیک بدون استثنا -Os | 37265 |
در این مورد، تفاوت هر دو با اجرای مدرن و کلاسیک پابرجاست!
نسخه | اندازه باینری |
---|---|
الگوی ناظر مدرن با استثنا -O0 | 84689 |
الگوی ناظر مدرن بدون استثنا -O0 | 83,345 |
الگوی ناظر مدرن با استثنا -O3 | 35,137 |
الگوی ناظر مدرن بدون استثنا -O3 | 34,977 |
الگوی ناظر مدرن با استثنا -Os | 36,305 |
الگوی ناظر مدرن بدون استثنا -Os | 36,017 |
تفاوت زیاد نیست، اما وجود دارد. بیایید نگاهی به مونتاژ بیندازیم تا ببینیم آیا می توانیم چیز جالبی را ببینیم.
به عنوان یادآوری، اگر با استفاده از کامپایل clang
با -S
پرچم می توانید کد اسمبلی را دریافت کنید. همانطور که در بالا دیدیم، وقتی بدون کامپایل میکنید -fno-exceptions
شما یک مجموعه کمی بزرگتر دریافت می کنید. من قصد ندارم تمام تغییرات را لیست کنم، پیشنهاد می کنم اگر علاقه دارید خودتان آن را امتحان کنید. در اینجا چند گزیده وجود دارد که فقط با استثنا می توانید در نسخه پیدا کنید.
Lfunc_begin0:
.cfi_startproc
.cfi_personality 155, ___gxx_personality_v0
.cfi_lsda 16, Lexception0
// ...
Lfunc_begin0:
.cfi_startproc
.cfi_personality 155, ___gxx_personality_v0
.cfi_lsda 16, Lexception0
; %bb.0:
stp x20, x19, [sp, #-32]! ; 16-byte Folded Spill
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16
sub sp, sp, #528
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
.cfi_offset w19, -24
.cfi_offset w20, -32
Lloh0:
adrp x8, __Z15propertyChangedRK6PersonNS_11StateChangeE@PAGE
Lloh1:
add x9, x8, __Z15propertyChangedRK6PersonNS_11StateChangeE@PAGEOFF
Lloh2:
adrp x8, __ZZ4mainEN3$_08__invokeERK6PersonNS0_11StateChangeE@PAGE
// few hundred lines
LBB2_33:
ldur x0, [x29, #-128]
bl __ZdlPv
mov w0, #0
add sp, sp, #528
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
ldp x20, x19, [sp], #32 ; 16-byte Folded Reload
ret
// another lengthy section
Lfunc_end0:
.cfi_endproc
.section __TEXT,__gcc_except_tab
.p2align 2
GCC_except_table2:
Lexception0:
.byte 255 ; @LPStart Encoding = omit
.byte 255 ; @TType Encoding = omit
.byte 1 ; Call site Encoding = uleb128
.uleb128 Lcst_end0-Lcst_begin0
Lcst_begin0:
.uleb128 Ltmp0-Lfunc_begin0 ; >> Call Site 1 <<
.uleb128 Ltmp5-Ltmp0 ; Call between Ltmp0 and Ltmp5
.uleb128 Ltmp21-Lfunc_begin0 ; jumps to Ltmp21
.byte 0 ; On action: cleanup
.uleb128 Ltmp6-Lfunc_begin0 ; >> Call Site 2 <<
.uleb128 Ltmp7-Ltmp6 ; Call between Ltmp6 and Ltmp7
.uleb128 Ltmp8-Lfunc_begin0 ; jumps to Ltmp8
.byte 0 ; On action: cleanup
.uleb128 Ltmp9-Lfunc_begin0 ; >> Call Site 3 <<
.uleb128 Ltmp10-Ltmp9 ; Call between Ltmp9 and Ltmp10
.uleb128 Ltmp21-Lfunc_begin0 ; jumps to Ltmp21
.byte 0 ; On action: cleanup
.uleb128 Ltmp11-Lfunc_begin0 ; >> Call Site 4 <<
.uleb128 Ltmp12-Ltmp11 ; Call between Ltmp11 and Ltmp12
.uleb128 Ltmp13-Lfunc_begin0 ; jumps to Ltmp13
.byte 0 ; On action: cleanup
.uleb128 Ltmp14-Lfunc_begin0 ; >> Call Site 5 <<
.uleb128 Ltmp15-Ltmp14 ; Call between Ltmp14 and Ltmp15
.uleb128 Ltmp21-Lfunc_begin0 ; jumps to Ltmp21
.byte 0 ; On action: cleanup
.uleb128 Ltmp16-Lfunc_begin0 ; >> Call Site 6 <<
.uleb128 Ltmp17-Ltmp16 ; Call between Ltmp16 and Ltmp17
.uleb128 Ltmp18-Lfunc_begin0 ; jumps to Ltmp18
.byte 0 ; On action: cleanup
.uleb128 Ltmp19-Lfunc_begin0 ; >> Call Site 7 <<
.uleb128 Ltmp20-Ltmp19 ; Call between Ltmp19 and Ltmp20
.uleb128 Ltmp21-Lfunc_begin0 ; jumps to Ltmp21
.byte 0 ; On action: cleanup
.uleb128 Ltmp20-Lfunc_begin0 ; >> Call Site 8 <<
.uleb128 Lfunc_end0-Ltmp20 ; Call between Ltmp20 and Lfunc_end0
.byte 0 ; has no landing pad
.byte 0 ; On action: cleanup
Lcst_end0:
.p2align 2
حتی اگر تک تک اطلاعات را درک نکنیم، می توانیم ببینیم بجز یا استثنا در چندین مکان در کد ظاهر می شود. ما در اینجا بخش هایی از جداول استثنا را می بینیم.
یا حداقل چیزی جز هر چیزی که می توانیم بسازیم
حالا که دیدیم چطور -fno-exceptions
باینری ما را تحت تأثیر قرار دهید، بیایید ببینیم چه اتفاقی میافتد وقتی نمیتوانید استثناها را خاموش کنید، اما همچنان میخواهید تعداد استثناها را کاهش دهید، بیایید از noexcept
مشخص کننده
بیایید با ناظر مدرن کار کنیم. در قدم اول تمام کلاس ها را گرفتم و اضافه کردم noexcept
به همه سازنده های ارائه شده توسط کاربر.
نسخه | اندازه باینری |
---|---|
الگوی ناظر مدرن -O0 | 84689 |
الگوی ناظر مدرن به جز سازنده -O0 | 83,345 |
الگوی ناظر مدرن -O3 | 35,137 |
الگوی ناظر مدرن به جز سازنده -O3 | 34,977 |
الگوی ناظر مدرن -Os | 36,305 |
الگوی ناظر مدرن به جز سازنده ها -Os | 36,017 |
بنابراین همانطور که می بینید هیچ چیز تغییر نکرده است. سپس شروع کردم به اضافه کردن همه جا. می دانستم که می تواند در کد تولید مضر باشد، اما می خواستم ببینم آیا می توانم تفاوت بین نسخه های ساخته شده با -fno-exceptions
و بدون آن با استفاده از noexcept
به طور گسترده
نسخه | اندازه باینری |
---|---|
الگوی ناظر مدرن -O0 | 84689 |
الگوی ناظر مدرن به جز همه جا -O0 | 84785 |
الگوی ناظر مدرن -O3 | 35,137 |
الگوی ناظر مدرن به جز همه جا -O3 | 35,425 |
الگوی ناظر مدرن -Os | 36,305 |
الگوی ناظر مدرن به جز همه جا -Os | 36577 |
در کمال وحشت من، اندازه باینری بزرگتر از آن چیزی شد که هیچ نداشت noexcept
!
مدت زیادی تلاش کردم تا بفهمم چه اتفاقی افتاده است. مدتی فکر می کردم که اعداد یا فیلمنامه ام را به هم ریخته ام. اما نه، اعداد درست هستند، فیلمنامه کار خودش را می کند.
وقتی به کد اسمبلی نگاه کردم، متوجه شدم که از main.s
، همه کدهای مربوط به استثنا ناپدید شدند، اما person.h
بسیار بزرگتر شد منظورم این است که از 23 کیلوبایت به 29 کیلوبایت افزایش یافته است.
وقتی آن را جستجو کردم، چنین جداول استثنایی را پیدا کردم:
Lexception0:
.byte 255 ; @LPStart Encoding = omit
.byte 155 ; @TType Encoding = indirect pcrel sdata4
.uleb128 Lttbase0-Lttbaseref0
// ... it's much longer
بعد از مدتی جستجو، یک RFC در llvm.org پیدا کردم. معلوم می شود که در واقع، باینری باید کاهش یابد و در GCC کاهش می یابد. اما یک اشکال در llvm وجود دارد و کامپایلر کدی را برای باز کردن پشته و مدیریت استثنا منتشر می کند، در حالی که می تواند و باید خاتمه یابد.
این به این معنی نیست noexcept
همیشه باینری شما را بزرگتر می کند clang
، به این معنی است که ممکن است به مزایایی که انتظار دارید نرسید و باید اندازه گیری کنید.
نتیجه
امروز در مورد استثناها و نحوه تأثیر آنها باینری شما صحبت کردیم. حتی اگر استثنائات اغلب در صورت عدم پرتاب، هزینه صفر در نظر گرفته شوند، این درست نیست. در زندگی ناهار رایگان وجود ندارد و در این صورت باید برای یک باینری بزرگتر هزینه کنید.
اگر اصلاً از استثناها استفاده نمی کنید و می خواهید از شر سربار خلاص شوید، باید به کامپایلر بگویید. اگر اصلاً از استثناها استفاده نمی کنید، می توانید آنها را خاموش کنید، اگر فقط می خواهید ارتباط برقرار کنید که برخی از توابع نمی توانند پرتاب کنند، باید از آنها استفاده کنید. noexcept
.
اگر در مورد اندازه باینری بیشتر از چیزی که به خوانندگان کد خود ارسال می کنید نگران هستید، اندازه گیری را فراموش نکنید، زیرا کامپایلرها کامل نیستند و ممکن است کاری را که شما انتظار دارید انجام ندهند.
عمیق تر وصل شوید
اگر این مقاله را دوست داشتید، لطفا