هزینه‌ی پرتاب Exception در جاوا

  • 18/آبا/1394
  • سیّد

شاید براتون این سؤال پیش اومده باشه که هزینه‌ی پرتاب یه Exception چقدره؟ اینجا می‌خوایم این موضوع رو آزمایش کنیم.

اول با یه کد ساده شروع می‌کنیم. توی این کد، تابع f کارش اینه که یک میلیون بار یه Exception رو پرتاب می‌کنه. توی main هم این تابع رو ۳ بار صدا زدیم که JIT با خیال راحت warm بشه و کارش رو بکنه.


public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            f();
        }
    }

    private static void f() {
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                throw new Exception("a");
            }
            catch(Exception e) {
                
            }
        }
        long t2 = System.currentTimeMillis();
        System.out.println(t2 - t1);
    }
}

یه نمونه‌ی خروجی اجرای این کد:

434
397
404

چند بار هم اجرا کردم، تقریباً همین عددا رو گرفتم. یعنی برای ۱ میلیون بار پرتاب استثناء، ۴۰۰ میلی‌ثانیه زمان لازمه.

به این می‌گن micro-benchmark. نکته‌ی خیلی مهمی که در مورد این micro-benchmark ها وجود داره، اینه که یه چیز خاص رو در یه شرایط خاص تست می‌کنن. مهم اینه که توی برنامه‌ی اصلی شما، کدوم بخش داره بیشترین زمان رو می‌گیره، اون رو بهینه کنید. وگرنه هر چی تعداد استثناهای پرتاب شده رو کم کنید، هیچ فرقی برای کل برنامه‌تون نمی‌کنه! چون پرتاب استثناء، اتفاقی نیست که دائم توی برنامه‌ی شما بیافته و اصل زمان CPU رو این قضیه نمی‌گیره.
نکته‌ی دیگه اینه که باید خیلی حواستون رو توی micro-benchmark ها جمع کنید که اشتباه نکنید و اشتباه نتیجه‌گیری نکنید، چون خییییییییلی راحت می‌شه این کارو کرد! باورتون نمی‌شه؟ به ادامه‌ی داستان توجه کنید تا باورتون بشه!

توی مثال بالا نتیجه‌گیری کردیم «پرتاب هر استثناء به طور متوسط ۴۰۰/۱۰۰۰۰۰۰ میلی‌ثانیه زمان می‌برد»، و این که «پرتاب استثناء بسیار عملیات سنگینی است»، و حتی این که «JIT انقدر خنگ است که نمی‌تواند پرتاب استثنای الکی را از اجرای برنامه حذف کند!». :)

اما ما اشتباه کردیم! چرا؟ چون اصل هزینه‌ای که توی برنامه‌ی بالا داریم می‌پردازیم، برای پرتاب استثناء نیست، بلکه برای ساختن شیء Exception هست. برنامه رو جوری تغییر می‌دیم که اصلاً خبری از try-catch و throw توش نباشه، و فقط شیء Exception ساخته بشه:


public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            f();
        }
    }

    private static void f() {
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            new Exception("a");
        }
        long t2 = System.currentTimeMillis();
        System.out.println(t2 - t1);
    }
}

نمونه‌ی خروجی:

404
394
404

یه تست دیگه: برنامه‌ی اول رو جوری تغییر می‌دیم که شیء Exception فقط یک بار ساخته بشه و توی حلقه، یک میلیون بار پرتاب بشه:


public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            f();
        }
    }

    private static void f() {
        long t1 = System.currentTimeMillis();
        Exception exception = new Exception("a");
        for (int i = 0; i < 1000000; i++) {
            try {
                throw exception;
            }
            catch(Exception e) {
                
            }
        }
        long t2 = System.currentTimeMillis();
        System.out.println(t2 - t1);
    }
}

نمونه‌ی خروجی:

4
2
0

تعداد حلقه رو از یک میلیون کردم یک میلیارد:

4
3
0

پس نتیجه می‌گیریم پرتاب یه استثناء هییییییییییییچ هزینه‌ای توی جاوا نداره! اون چیزی که هزینه داره، ساختن شیئش هست. دلیلش هم فراخوانی تابع fillInStackTrace توی سازنده‌ی Throwable هست که یه تابع native هست و می‌ره stack trace رو براتون میاره، که نسبتاً کار سنگینیه.

فکر کردید تموم شد؟! نخیر! بازم مونده! نتیجه‌گیری ما باز هم اشتباه بود! یعنی یک میلیون بار پرتاب استثناء، با یک میلیارد بار برای CPU فرقی نداره؟! پس چه اتفاقی افتاده که اینطوری شده؟
ماجرا اینه که اینجا کامپایلر کد رو بهینه کرده. یعنی فهمیده که اون پرتاب استثناء هیچ اثری نداره و در نتیجه کدش رو کلاً حذف کرده.
حالا بیاین کامپایلر رو گول بزنیم و یه کاری کنیم نتونه تشخیص بده هیچ کاری انجام نمی‌شه!


public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            f();
        }
    }

    private static void f() {
        long t1 = System.currentTimeMillis();
        Exception exception = new Exception("a");
        for (int i = 0; i < 1000000; i++) {
            try {
                throw exception;
            }
            catch(Exception e) {
                if (e.getStackTrace()[0].getLineNumber() > System.currentTimeMillis())
                	System.out.println("test");
            }
        }
        long t2 = System.currentTimeMillis();
        System.out.println(t2 - t1);
    }
}

خروجی:

56
44
41

تغییر یک میلیون به ده میلیون:

433
419
406

نکته‌ی جالب اینه که JIT نتونسته این حالت رو تشخیص بده و حذفش کنه.

دقت کنید که توی این حالت، زمان اجرای System.currentTimeMillis هم به زمان اجرای کد اضافه می‌شه. برای حذف اون، می‌تونیم به جای System.currentTimeMillis، یه عدد ثابت بذاریم، مثلاً 100000 (عددتون باید بزرگ باشه که شرطش همیشه غلط بشه).
نمونه خروجی برای این حالت با یک میلیون:

37
26
24

برای ده میلیون:

242
226
202

خوب حالا می‌تونیم یه نتیجه‌گیری نهایی بکنیم! این که هزینه‌ی پرتاب یک میلیون Exception توی جاوا (حداقل روی سیستم من)، حدوداً ۲۰۰ میلی‌ثانیه هست.

اضافه کردن دیدگاه جدید

Plain text

  • تگ های HTML قابل قبول نمی باشد.
  • آدرس های وب و ایمیل به صورت اتوماتیک به لینک تبدیل می شوند.
  • خطوط و پاراگرافها به صورت اتوماتیک جدا سازی می شود.