برنامه نویسی

اندازه باینری و استثناها – انجمن DEV

در این سری از مقالات در مورد اندازه های باینری، قبلاً در مورد آن صحبت کردیم default کلمه کلیدی و گفت که default کلمه کلیدی عملکرد خاصی ایجاد می کند noexcept، هر زمان که بتواند.

چیست noexcept?

این noexcept specifier مشخص می کند که آیا یک تابع می تواند یک استثنا ایجاد کند noexcept یا noexcept(expression == true) مشخص کننده، پس نمی تواند استثناء ایجاد کند. اگر همچنان به طور صریح یا از طریق تابع دیگری که فراخوانی می کند پرتاب کند، برنامه فراخوانی می کند std::terminate بلافاصله. مستقیما.

آیا استثنائات نادیده گرفته شده هزینه دارند؟

وقتی در مورد استثناها یاد می گیریم، اغلب در مورد جدول زیر به ما آموزش داده می شود.

هزینه های عملیاتی در چرخه های CPU

ما در مورد هزینه قابل توجه عملیات CPU برای پرتاب و گرفتن استثناهای C++ آشنا می شویم. همانطور که می بینید، هزینه ها حتی در مقایسه با خواندن از رم اصلی بسیار زیاد است، بدون اینکه به خواندن از حافظه نهان یا حتی تماس گرفتن اشاره کنیم. virtual تابع.

با این حال، ما اغلب می‌توانیم این هزینه را برای انتزاع استثناها بپردازیم، زیرا آن فشار سرعت را نداریم یا به دلیل اینکه سایر بخش‌های برنامه بسیار کندتر هستند و کندی نسبی استثناها ناچیز است. یا ما به سادگی در نظر می گیریم که وقتی یک استثنا پرتاب می شود، چیزی قبلاً اشتباه شده است و جریمه سرعت قابل قبول است.

اغلب به ما می گویند که استثنائات زمانی که پرتاب نمی شوند، هزینه صفر دارند.

این کاملا درست نیست.

هزینه CPU تنها هزینه نیست.

اگر از استثناها استفاده کنید باینری شما رشد خواهد کرد. بدیهی است که این ربطی به تعداد استثناهای واقعی پرتاب و گرفتار ندارد.

رسیدگی به استثنائات به مقداری سربار نیاز دارد و سطح جزئیات موجود می تواند بسیار زیاد باشد. اگر بخواهیم آن را تا حد امکان ساده کنیم، می‌توان گفت که کامپایلر باید در باینری ذخیره کند که چه نوع استثناهایی را می‌توان توسط بخش‌های مختلف کد ایجاد کرد و چگونه باید با آن‌ها رفتار کرد. جزئیات دقیق پیاده سازی خارج از محدوده ما است.

اگر یک قطعه کد به هیچ وجه نمی تواند استثناء ایجاد کند، پس چنین اطلاعاتی نباید ذخیره شود. حداقل این تئوری است، حالا ببینیم این چقدر در عمل درست است.

بیایید استثناها را کاملاً خاموش کنیم

بیایید با ممنوع کردن کامل استثناها در کدمان شروع کنیم.

اگر کامپایل کنیم، با پرچم -fno-exceptions، می توانیم به کامپایلر بگوییم که به هیچ وجه استثنا اجازه ندهد. این نه تنها به این معنی نیست که رسیدگی به استثناها اتفاق نمی افتد و در صورت وجود استثنا، برنامه خاتمه می یابد، بلکه به این معنی است که شما مجاز به نوشتن هیچ کد رسیدگی به استثنا نیستید.

در عین حال، شما مجاز به استفاده از کدهای خارجی هستید که پرتاب می کند، اما در صورت پرتاب شدن خطا، برنامه شما خاتمه می یابد. اگر می خواهید آن را تست کنید، با شماره تماس بگیرید at(size_t) روش بر روی الف vector. این یکی از معدود روش‌هایی در کتابخانه استاندارد است که پرتاب می‌کند. at(size_t) کرانه ها را بررسی می کند و نمونه ای از آن را پرتاب می کند std::out_of_range استثنا در مواردی که سعی می کنید به چیزی فراتر از محدودیت های یک ظرف دسترسی پیدا کنید.

کلاسی با توابع ویژه پیش فرض یا خالی

بیایید ببینیم اگر استثناها را برای برخی از کدهایی که در چند هفته گذشته نوشته‌ایم خاموش کنیم، چه اتفاقی می‌افتد. ابتدا، استثناهای یک کد ساده را که در آن اندازه‌های باینری کلاس‌ها را با آن آزمایش می‌کردیم، تبدیل کردم defaulted توابع ویژه.

نسخه اندازه باینری
توابع ویژه پیش فرض با استثنا -O0 116,302
توابع ویژه پیش فرض بدون استثنا -O0 116,302
توابع ویژه پیش فرض با استثنا -O3 116270
توابع ویژه پیش فرض بدون استثنا -O3 116270
توابع ویژه پیش فرض با استثنا -Os 116270
توابع ویژه پیش فرض بدون استثنا -Os 116270

همانطور که می بینید، ما اصلاً چیزی به دست نیاوردیم. زمانی که ما فقط کلاسی با توابع ویژه پیش‌فرض داشتیم، حذف پشتیبانی از استثناها چیزی را تغییر نداد. همانطور که قبلا نوشتم، default اجرای یک تابع خاص صرفاً تولید اجسام خالی (یا ساده ترین) برای توابع ویژه نیست. همچنین اضافه می کند noexcept جایی که ممکن است در مورد ما، مطمئناً ممکن است.

با در نظر گرفتن این موضوع، بیایید آزمایش خود را با یک قطعه کد ساده مشابه اجرا کنیم. در این مورد، توابع عضو نیستند defaulted، اما یک پیاده سازی خالی دارند و هستند نه 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.

اگر در مورد اندازه باینری بیشتر از چیزی که به خوانندگان کد خود ارسال می کنید نگران هستید، اندازه گیری را فراموش نکنید، زیرا کامپایلرها کامل نیستند و ممکن است کاری را که شما انتظار دارید انجام ندهند.

عمیق تر وصل شوید

اگر این مقاله را دوست داشتید، لطفا

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا