การใช้บล็อกแบบลองดักจับมีราคาแพงแม้ว่าจะไม่มีข้อยกเว้นเกิดขึ้นหรือไม่


189

เรารู้ว่ามันแพงในการจับยกเว้น แต่มันก็มีราคาแพงที่จะใช้บล็อก try-catch ใน Java แม้ว่าข้อยกเว้นจะไม่ถูกโยนทิ้ง?

ฉันพบคำถาม / คำตอบ Stack Overflow เหตุใดการลองบล็อกจึงมีราคาแพง แต่มันเป็น.NET


30
คำถามนี้ไม่มีประเด็นจริงๆ ลอง .. จับมีวัตถุประสงค์เฉพาะมาก หากคุณต้องการคุณจำเป็นต้องใช้ ไม่ว่าในกรณีใดจุดลองโดยไม่มีการจับคืออะไร?
JohnFx

49
try { /* do stuff */ } finally { /* make sure to release resources */ }ถูกกฎหมายและมีประโยชน์
A4L

4
ค่าใช้จ่ายนั้นจะต้องถูกนำมาเปรียบเทียบกับผลประโยชน์ มันไม่ได้โดดเดี่ยว ไม่ว่าในกรณีใดราคาแพงจะเป็นค่าสัมพัทธ์และจนกว่าคุณจะรู้ว่าคุณไม่สามารถทำได้มันก็สมเหตุสมผลที่จะใช้วิธีการที่ชัดเจนที่สุดแทนที่จะไม่ทำอะไรเพราะมันอาจช่วยให้คุณประหยัดได้หนึ่งหรือสองมิลลิวินาทีในหนึ่งชั่วโมง การทำงานของโปรแกรม
Joel

4
ฉันหวังว่านี่จะไม่นำไปสู่สถานการณ์การพิมพ์ "
Let's

6
@SAFX: ด้วย Java7 คุณสามารถกำจัดfinallyบล็อกโดยใช้try-with-resources
a_horse_with_no_name

คำตอบ:


201

tryแทบไม่มีค่าใช้จ่ายเลย แทนการทำงานของการตั้งค่าtryat runtime ข้อมูลเมตาของรหัสถูกจัดโครงสร้างในเวลาคอมไพล์เช่นเมื่อมีการโยนข้อยกเว้นตอนนี้จะเป็นการดำเนินการที่ค่อนข้างแพงในการเดินขึ้นสแต็กและดูว่ามีtryบล็อกใด ๆข้อยกเว้น จากมุมมองของคนธรรมดาก็tryอาจเป็นอิสระ จริงๆแล้วมันเป็นการยกเว้นที่ทำให้คุณต้องเสียค่าใช้จ่าย แต่ถ้าคุณไม่ได้รับการยกเว้นหลายร้อยหรือหลายพันข้อยกเว้นคุณยังคงไม่สังเกตเห็นค่าใช้จ่าย


tryมีค่าใช้จ่ายเล็กน้อยที่เกี่ยวข้อง Java ไม่สามารถทำการเพิ่มประสิทธิภาพบางอย่างในรหัสในtryบล็อกที่มันจะทำอย่างอื่น ตัวอย่างเช่น Java มักจะจัดเรียงคำสั่งในวิธีการเพื่อให้ทำงานได้เร็วขึ้น - แต่ Java ยังต้องการรับประกันว่าหากมีการโยนข้อยกเว้นการดำเนินการของวิธีการจะถูกตรวจสอบราวกับคำสั่งของมันตามที่เขียนไว้ในซอร์สโค้ด ในการสั่งซื้อถึงบางบรรทัด

เพราะในtryบล็อกสามารถโยนข้อยกเว้น (ที่บรรทัดใด ๆ ในบล็อกลอง! ข้อยกเว้นบางอย่างจะถูกโยนแบบอะซิงโครนัสเช่นโดยการเรียกstopใช้เธรด (ซึ่งเลิกใช้แล้ว) และแม้แต่นอกเหนือจากนั้น OutOfMemoryError สามารถถูกจับได้และโค้ดจะยังคงทำงานหลังจากนั้นในวิธีเดียวกันมันเป็นเรื่องยากที่จะให้เหตุผลเกี่ยวกับการปรับให้เหมาะสมที่สามารถทำได้ดังนั้นจึงมีโอกาสน้อยที่จะเกิดขึ้น (ใครบางคนจะต้องเขียนโปรแกรมคอมไพเลอร์เพื่อทำเหตุผลและรับประกันความถูกต้อง ฯลฯ มันจะเป็นความเจ็บปวดครั้งใหญ่สำหรับบางสิ่งบางอย่างที่หมายถึง 'ยอดเยี่ยม') แต่ในทางปฏิบัติคุณจะไม่สังเกตเห็นสิ่งนี้


2
ข้อยกเว้นบางอย่างถูกส่งออกไปแบบอะซิงโครนัสพวกมันไม่ได้เป็นแบบอะซิงโครนัสแต่ถูกโยนในจุดที่ปลอดภัย และส่วนนี้ลองมีค่าใช้จ่ายเล็กน้อยที่เกี่ยวข้อง Java ไม่สามารถทำการเพิ่มประสิทธิภาพบางอย่างในรหัสในลองบล็อกที่มันจะทำจะต้องมีการอ้างอิงที่ร้ายแรง ในบางจุดรหัสมีโอกาสมากที่จะอยู่ในลอง / จับบล็อก อาจเป็นเรื่องจริงที่การลอง / จับบล็อกจะเป็นการยากที่จะถูกแทรกและสร้างตาข่ายที่เหมาะสมสำหรับผลลัพธ์ แต่ส่วนที่ w / การจัดเรียงใหม่นั้นไม่ชัดเจน
bestsss

2
ไม่try...finallyบล็อกโดยไม่ต้องcatchนอกจากนี้ยังป้องกันการเพิ่มประสิทธิภาพบางอย่าง?
dajood

5
@Patashu "จริงๆแล้วมันมีข้อยกเว้นที่ทำให้คุณเสียค่าใช้จ่าย" โดยทางเทคนิคแล้วการโยนข้อยกเว้นนั้นไม่แพงเลย การยกตัวอย่างExceptionวัตถุเป็นสิ่งที่ใช้เวลาส่วนใหญ่
Austin D

72

มาวัดกันใช่ไหม

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

ในคอมพิวเตอร์ของฉันสิ่งนี้จะพิมพ์สิ่งที่ชอบ:

try     0.598 ns
no try  0.601 ns

อย่างน้อยในตัวอย่างเล็ก ๆ น้อย ๆ นี้คำสั่งลองไม่มีผลกระทบที่วัดได้ต่อประสิทธิภาพ อย่าลังเลที่จะวัดสิ่งที่ซับซ้อนมากขึ้น

โดยทั่วไปฉันขอแนะนำว่าอย่ากังวลเกี่ยวกับค่าใช้จ่ายของการสร้างภาษาจนกว่าคุณจะมีหลักฐานของปัญหาประสิทธิภาพที่แท้จริงในโค้ดของคุณ หรืออย่างที่ Donald Knuth กล่าวไว้: "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากฐานของความชั่วทั้งหมด"


4
ในขณะที่พยายาม / ไม่มีความพยายามมีแนวโน้มที่จะเหมือนกันใน JVM ส่วนใหญ่ microbenchmark มีข้อบกพร่องอย่างมาก
bestsss

2
ค่อนข้างกี่ระดับ: คุณหมายถึงผลลัพธ์ถูกคำนวณในเวลาไม่ถึง 1ns ใช่ไหม รหัสที่คอมไพล์แล้วจะลบทั้งการลอง / จับและการวนซ้ำทั้งหมด (การรวมหมายเลขจาก 1 ถึง n เป็นผลรวมของความก้าวหน้าทางเลขคณิตเล็กน้อย) แม้ว่าโค้ดจะมีลอง / ในที่สุดคอมไพเลอร์ก็สามารถพิสูจน์ได้ แต่ก็ไม่มีอะไรที่จะต้องโยนทิ้งไป รหัสนามธรรมมีไซต์การโทรเพียง 2 ไซต์เท่านั้นและจะมีการโคลนและอินไลน์ มีกรณีอื่น ๆ มีเพียงแค่มองขึ้นบางบทความใน microbenchmark และคุณตัดสินใจที่จะเขียน microbenchmark เสมอตรวจสอบที่เกิดการชุมนุม
bestsss

3
เวลาที่รายงานคือการวนซ้ำตามการวนซ้ำ การวัดจะใช้เฉพาะเมื่อมีเวลาที่ผ่านไปทั้งหมด> 0.1 วินาที (หรือซ้ำอีก 2 พันล้านครั้งซึ่งไม่ใช่กรณีที่นี่) ฉันพบว่าคุณยืนยันว่าลูปถูกลบออกไปโดยสิ้นเชิงยากที่จะเชื่อ - เพราะถ้า การวนซ้ำถูกลบออกใช้เวลาดำเนินการอะไร 0.1 วินาที
meriton

... และแน่นอนตาม-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblyลูปและการเพิ่มในนั้นมีอยู่ในรหัสพื้นเมืองที่สร้างขึ้น และไม่วิธีการที่เป็นนามธรรมไม่ได้ถูกแทรกไว้เนื่องจากผู้โทรของพวกเขาไม่เพียงรวบรวมเวลา (สมมุติเพราะมันไม่ได้เรียกเวลาพอ)
meriton

ฉันจะเขียน micro-benchmark ที่ถูกต้องใน Java ได้อย่างไร: stackoverflow.com/questions/504103/…
Vadzim

47

try/ catchอาจมีผลกระทบต่อประสิทธิภาพ นี่เป็นเพราะมันป้องกันไม่ให้ JVM ทำการเพิ่มประสิทธิภาพบางอย่าง Joshua Bloch ใน "Effective Java" กล่าวว่า:

•การวางโค้ดในบล็อก try-catch จะยับยั้งการปรับแต่งบางอย่างที่การนำ JVM มาใช้อาจทำได้


25
"ช่วยป้องกัน JVM ไม่ทำการเพิ่มประสิทธิภาพบางอย่าง" ... ? คุณสามารถอธิบายรายละเอียดได้ทั้งหมดหรือไม่
Kraken

5
@ รหัส Kraken ที่อยู่ในบล็อคการลอง (มักจะ? เสมอ?) ไม่สามารถสั่งซื้อใหม่ได้โดยใช้โค้ดด้านนอกของบล็อคการลองเป็นตัวอย่างหนึ่ง
Patashu

3
โปรดทราบว่าคำถามคือ "มีราคาแพง" ไม่ใช่ว่า "มีผลกระทบต่อประสิทธิภาพ" หรือไม่
mikołak

3
เพิ่มข้อความที่ตัดตอนมาจาก Effective Javaและนั่นเป็นพระคัมภีร์ของ java แน่นอน ข้อความที่ตัดตอนมาไม่ได้บอกอะไรเลยนอกเสียจากว่าจะมีการอ้างอิง จวนรหัสใด ๆ ใน java อยู่ภายในลอง / ในที่สุดในบางระดับ
bestsss

29

ใช่ดังที่คนอื่น ๆ บอกว่าtryบล็อกขัดขวางการเพิ่มประสิทธิภาพบางอย่างของ{}ตัวละครที่อยู่รอบ ๆ โดยเฉพาะอย่างยิ่งเครื่องมือเพิ่มประสิทธิภาพจะต้องสมมติว่ามีข้อยกเว้นเกิดขึ้น ณ จุดใด ๆ ในบล็อกดังนั้นจึงไม่มีความมั่นใจว่าข้อความจะถูกดำเนินการ

ตัวอย่างเช่น:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

โดยไม่ต้องtryมีค่าคำนวณเพื่อกำหนดให้xจะถูกบันทึกไว้ว่าเป็น "subexpression ทั่วไป" yและนำกลับมากำหนดให้ แต่เนื่องจากtryไม่มีการรับประกันว่าการแสดงออกครั้งแรกจะได้รับการประเมินดังนั้นการแสดงออกจะต้องคำนวณใหม่ นี่ไม่ใช่เรื่องใหญ่ในรหัส "เส้นตรง" แต่อาจมีความสำคัญในการวนซ้ำ

อย่างไรก็ตามควรสังเกตว่าสิ่งนี้ใช้ได้กับรหัส JITCed เท่านั้น javac ทำการเพิ่มประสิทธิภาพเพียงเล็กน้อยและไม่มีค่าใช้จ่ายใด ๆ กับล่าม bytecode เพื่อเข้า / ออกtryบล็อก (ไม่มีรหัสไบต์สร้างขึ้นเพื่อทำเครื่องหมายขอบเขตบล็อก)

และสำหรับ bestsss:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

เอาท์พุท:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

javap เอาท์พุท:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

ไม่ใช่ "GOTO"


ไม่มีโค้ดไบต์ที่สร้างขึ้นเพื่อทำเครื่องหมายขอบเขตบล็อกซึ่งไม่จำเป็น - มันต้องให้ GOTO ออกจากบล็อกมิฉะนั้นมันจะตกลงไปในcatch/finallyเฟรม
bestsss

@ bests - แม้ว่า GOTO จะถูกสร้างขึ้น (ซึ่งไม่ได้กำหนด) ค่าใช้จ่ายของมันคือ miniscule และมันก็ยังห่างไกลจากการเป็น "เครื่องหมาย" สำหรับขอบเขตบล็อก - GOTO สามารถสร้างได้สำหรับการสร้างจำนวนมาก
Hot Licks

ฉันไม่เคยเอ่ยถึงค่าใช้จ่าย แต่ไม่มีไบต์ที่สร้างขึ้นเป็นคำสั่งที่ผิด นั่นคือทั้งหมดที่ ที่จริงแล้วไม่มีการบล็อกในไบต์รหัสเฟรมไม่เท่ากับบล็อก
bestsss

จะไม่มี GOTO หากความพยายามตรงเข้าไปในที่สุดและมีสถานการณ์อื่น ๆ ที่ไม่มี GOTO ประเด็นก็คือไม่มีอะไรในคำสั่งของ "ป้อนลอง" / "ออกลอง" bytecodes
Hot Licks

จะไม่มี GOTO หากความพยายามตรงไปสู่ที่สุด - เท็จ! ไม่มีfinallyในรหัสไบต์มันtry/catch(Throwable any){...; throw any;}และมันมีคำสั่งจับ w / a เฟรมและ Throwable ที่จะต้องกำหนด (ไม่ใช่โมฆะ) และอื่น ๆ ทำไมคุณถึงพยายามโต้แย้งเกี่ยวกับหัวข้อนี้คุณสามารถตรวจสอบไบต์อย่างน้อยบางส่วนได้? แนวทางปัจจุบันสำหรับ impl ในที่สุดก็คือการคัดลอกบล็อกและหลีกเลี่ยงส่วนข้าม (impl ก่อนหน้า) แต่ไบต์จะต้องคัดลอกขึ้นอยู่กับจำนวนจุดทางออกที่มี
bestsss

8

เพื่อทำความเข้าใจว่าเหตุใดการเพิ่มประสิทธิภาพจึงไม่สามารถทำได้จึงเป็นประโยชน์ที่จะเข้าใจกลไกพื้นฐาน ตัวอย่างรวบรัดที่สุดที่ฉันสามารถหาได้นำมาใช้ในมาโคร C ที่: http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

คอมไพเลอร์มักมีปัญหาในการพิจารณาว่าการกระโดดสามารถแปลเป็น X, Y และ Z ได้หรือไม่ดังนั้นพวกเขาจึงข้ามการเพิ่มประสิทธิภาพที่พวกเขาไม่สามารถรับประกันได้ว่าจะปลอดภัย แต่การใช้งานนั้นค่อนข้างเบา


4
มาโคร C เหล่านี้ที่คุณพบสำหรับการลอง / จับไม่เทียบเท่ากับ Java หรือการใช้งาน C # ซึ่งปล่อย 0 คำแนะนำเวลาทำงาน
Patashu

การใช้งานจาวานั้นกว้างขวางเกินกว่าที่จะรวมได้ทั้งหมดนี่เป็นการใช้งานที่ง่ายขึ้นเพื่อจุดประสงค์ในการทำความเข้าใจแนวคิดพื้นฐานของวิธีการยกเว้นที่สามารถนำไปใช้ได้ การบอกว่ามันส่งเสียง 0 คำสั่งเวลาทำงานนั้นทำให้เข้าใจผิด ตัวอย่างเช่น classcastexception ง่าย ๆ ขยาย runtimeexception ซึ่งขยายข้อยกเว้นที่ขยายได้ซึ่งเกี่ยวข้องกับ: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/ ...... นั่นก็เหมือนกับการบอกว่าสวิตช์ตัวพิมพ์ใน C คือ ฟรีหากใช้เพียง 1 เคสก็ยังมีโอเวอร์เฮดเริ่มต้นเล็ก ๆ
technosaurus

1
@Patashu บิตที่คอมไพล์แล้วทั้งหมดเหล่านั้นยังคงต้องโหลดเมื่อเริ่มต้นไม่ว่าจะใช้หรือไม่ก็ตาม ไม่มีทางที่จะรู้ได้ว่าจะมีข้อยกเว้นหน่วยความจำไม่เพียงพอระหว่างรันไทม์ในเวลารวบรวม - นั่นคือสาเหตุที่พวกเขาถูกเรียกว่าข้อยกเว้นรันไทม์ - ไม่เช่นนั้นพวกเขาก็จะรวบรวมคำเตือน / ข้อผิดพลาดของคอมไพเลอร์ รหัสทั้งหมดในการจัดการพวกเขาจะรวมอยู่ในรหัสรวบรวมและมีค่าใช้จ่ายเริ่มต้น
technosaurus

2
ฉันไม่สามารถพูดเกี่ยวกับ C. ใน C # และ Java ลองใช้งานได้โดยการเพิ่มข้อมูลเมตาไม่ใช่รหัส เมื่อป้อนบล็อกแบบลองจะไม่มีการดำเนินการใด ๆ เพื่อระบุสิ่งนี้ - เมื่อมีการโยนข้อยกเว้นสแต็กจะคลายลงและข้อมูลเมตาจะตรวจสอบตัวจัดการของประเภทข้อยกเว้นนั้น (แพง)
Patashu

1
ใช่ฉันได้ใช้ตัวแปลภาษาจาวา & static bytecode คอมไพเลอร์และทำงานกับ JITC (สำหรับ IBM iSeries) และฉันสามารถบอกคุณได้ว่าไม่มีสิ่งใดที่ "ทำเครื่องหมาย" การเข้า / ออกของช่วงลองใน bytecodes แต่แทนที่จะ ช่วงจะถูกระบุในตารางแยกต่างหาก ล่ามไม่ทำอะไรเป็นพิเศษสำหรับช่วงลอง (จนกว่าจะมีการยกข้อยกเว้น) JITC (หรือคอมไพเลอร์ bytecode คงที่) จะต้องตระหนักถึงขอบเขตที่จะระงับการเพิ่มประสิทธิภาพตามที่ระบุไว้ก่อนหน้านี้
Hot Licks

8

อีก microbenchmark ( แหล่งที่มา )

ฉันสร้างการทดสอบที่ฉันวัดรุ่นรหัสลองจับและไม่ลองจับตามเปอร์เซ็นต์ข้อยกเว้น 10% เปอร์เซ็นต์หมายความว่า 10% ของกรณีทดสอบมีการหารด้วยศูนย์กรณี ในสถานการณ์หนึ่งจะได้รับการจัดการโดยบล็อก try-catch ในอีกสถานการณ์หนึ่งโดยผู้ดำเนินการตามเงื่อนไข นี่คือตารางผลลัพธ์ของฉัน:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
ร้อยละ ผลลัพธ์ (ลอง / ถ้า, ns)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

ซึ่งบอกว่าไม่มีความแตกต่างอย่างมีนัยสำคัญระหว่างกรณีใด ๆ เหล่านี้


3

ฉันพบว่าการจับ NullPointException ค่อนข้างแพง สำหรับการปฏิบัติการ 1.2k เวลานั้นคือ 200ms และ 12ms เมื่อฉันพกมันแบบเดียวกับif(object==null)ที่ฉันได้รับการปรับปรุง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.