首页 文章

Java异常有多慢?

提问于
浏览
422

问题:Java中的异常处理实际上是否很慢?

传统观念以及许多谷歌搜索结果表明,不应将特殊逻辑用于Java中的正常程序流程 . 通常有两个原因,

  • 它真的很慢 - 甚至比常规代码慢一个数量级(给出的原因各不相同),

  • 它很混乱,因为人们只希望在特殊代码中处理错误 .

这个问题是关于#1 .

例如,this page将Java异常处理描述为"very slow",并将缓慢与异常消息字符串的创建联系起来 - "this string is then used in creating the exception object that is thrown. This is not fast."文章Effective Exception Handling in Java表示"the reason for this is due to the object creation aspect of exception handling, which thereby makes throwing exceptions inherently slow" . 另一个原因是堆栈跟踪生成减慢了它的速度 .

我的测试(使用Java 1.6.0_07,Java HotSpot 10.0,在32位Linux上)表明异常处理并不比常规代码慢 . 我尝试在循环中运行一个执行一些代码的方法 . 在方法结束时,我使用布尔值来指示是返回还是抛出 . 这样实际处理是一样的 . 我尝试以不同的顺序运行方法并平均我的测试时间,认为它可能是JVM升温 . 在我的所有测试中,投掷至少与返回一样快,如果不是更快(最多快3.1%) . 我对我的测试错误的可能性持开放态度,但我没有看到代码示例,测试比较或过去一两年中显示Java中的异常处理的结果慢 .

引导我走这条道路的是我需要使用的API,它将异常作为正常控制逻辑的一部分 . 我想用它们来纠正它们,但现在我可能无法做到 . 相反,我是否必须赞美他们的前瞻性思维?

在文章Efficient Java exception handling in just-in-time compilation中,作者建议单独存在异常处理程序,即使没有抛出异常,也足以阻止JIT编译器正确优化代码,从而减慢它的速度 . 我还没有测试过这个理论 .

17 回答

  • 3

    仅供参考,我延长了Mecki所做的实验:

    method1 took 1733 ms, result was 2
    method2 took 1248 ms, result was 2
    method3 took 83997 ms, result was 2
    method4 took 1692 ms, result was 2
    method5 took 60946 ms, result was 2
    method6 took 25746 ms, result was 2
    

    前三个与Mecki相同(我的笔记本电脑显然比较慢) .

    method4与method3相同,只是它创建 new Integer(1) 而不是 throw new Exception() .

    method5类似于method3,除了它创建 new Exception() 而不抛出它 .

    method6与method3类似,不同之处在于它抛出预先创建的异常(实例变量)而不是创建新异常 .

    在Java中,抛出异常的大部分费用是收集堆栈跟踪所花费的时间,这是在创建异常对象时发生的 . 抛出异常的实际成本虽然很大,但远远低于创建异常的成本 .

  • 0

    即使抛出异常并不慢,但为正常的程序流抛出异常仍然是个坏主意 . 使用这种方式它类似于GOTO ......

    我想这并没有真正回答这个问题 . 我认为在早期的java版本(<1.4)中,抛出异常的“常规”智慧是正确的 . 创建异常需要VM创建整个堆栈跟踪 . 从那以后,在VM中发生了很多变化,以加快速度,这可能是一个已经改进的领域 .

  • -5

    关于异常性能的好帖子是:

    https://shipilev.net/blog/2014/exceptional-performance/

    实例化和重用现有的,使用堆栈跟踪和不使用等:

    Benchmark                            Mode   Samples         Mean   Mean error  Units
    
    dynamicException                     avgt        25     1901.196       14.572  ns/op
    dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
    dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
    dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
    dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
    dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op
    
    plain                                avgt        25        1.259        0.002  ns/op
    staticException                      avgt        25        1.510        0.001  ns/op
    staticException_NoStack              avgt        25        1.514        0.003  ns/op
    staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
    staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
    staticException_UsedData             avgt        25        4.159        0.007  ns/op
    staticException_UsedStack            avgt        25       25.144        0.186  ns/op
    

    根据堆栈跟踪的深度:

    Benchmark        Mode   Samples         Mean   Mean error  Units
    
    exception_0000   avgt        25     1959.068       30.783  ns/op
    exception_0001   avgt        25     1945.958       12.104  ns/op
    exception_0002   avgt        25     2063.575       47.708  ns/op
    exception_0004   avgt        25     2211.882       29.417  ns/op
    exception_0008   avgt        25     2472.729       57.336  ns/op
    exception_0016   avgt        25     2950.847       29.863  ns/op
    exception_0032   avgt        25     4416.548       50.340  ns/op
    exception_0064   avgt        25     6845.140       40.114  ns/op
    exception_0128   avgt        25    11774.758       54.299  ns/op
    exception_0256   avgt        25    21617.526      101.379  ns/op
    exception_0512   avgt        25    42780.434      144.594  ns/op
    exception_1024   avgt        25    82839.358      291.434  ns/op
    

    有关其他详细信息(包括来自JIT的x64汇编程序),请阅读原始博文 .

    这意味着Hibernate / Spring / etc-EE-shit很慢,因为异常(xD)和重写应用程序控制流程远离异常(用 continure / break 替换它,并像方法调用中的C一样返回 boolean 标志)提高了应用程序的性能10x-100x,取决于你抛出它们的频率))

  • 3

    我对异常速度与以编程方式检查数据的看法 .

    许多类都有String to value转换器(扫描器/解析器),受人尊敬和知名的库;)

    通常有形式

    class Example {
    public static Example Parse(String input) throws AnyRuntimeParsigException
    ...
    }
    

    异常名称只是示例,通常是未选中的(运行时),因此抛出声明只是我的图片

    有时存在第二种形式:

    public static Example Parse(String input, Example defaultValue)
    

    从不投掷

    当第二个不可用时(或者程序员读取的文档太少而且只使用第一个),用正则表达式编写这样的代码 . 正则表达很酷,政治正确等等:

    Xxxxx.regex(".....pattern", src);
    if(ImTotallySure)
    {
      Example v = Example.Parse(src);
    }
    

    使用此代码程序员不需要例外 . 但是正常表达式的成本非常高,总是与有时的小成本例外相比 .

    我几乎总是在这样的环境中使用

    try { parse } catch(ParsingException ) // concrete exception from javadoc
    {
    }
    

    没有分析堆栈跟踪等,我相信你的讲座速度很快 .

    不要害怕例外

  • 0

    不幸的是,我的回答太长了,无法在此发布 . 所以,让我在这里总结一下,并向您推荐http://www.fuwjax.com/how-slow-are-java-exceptions/的细节 .

    这里真正的问题不是“与'永不失败的代码'相比,”故障报告为异常的速度有多慢?“因为公认的回应可能让你相信 . 相反,问题应该是“与其他方式报告的故障相比,故障报告为异常的速度有多慢?”通常,报告失败的另外两种方法是使用sentinel值或使用结果包装器 .

    Sentinel值是尝试在成功的情况下返回一个类,而另一个在失败的情况下返回 . 您可以将其视为返回异常而不是抛出异常 . 这需要一个带有成功对象的共享父类,然后执行“instanceof”检查和几次转换以获取成功或失败信息 .

    事实证明,在存在类型安全风险的情况下,Sentinel值比异常更快,但只有大约2倍 . 现在,这可能看起来很多,但2x只涵盖了实施的成本区别 . 在实践中,因为我们的可能失败的方法比本页其他地方的示例代码中的一些算术运算符更有趣,因此该因子要低得多 .

    结果另一方面,包装器根本不会牺牲类型安全性 . 它们将成功和失败信息包装在一个类中 . 因此,它们不是"instanceof",而是为成功和失败对象提供"isSuccess()"和getter . 但是,结果对象大约是使用异常的2x slower . 事实证明,每次创建一个新的包装器对象比有时抛出异常要昂贵得多 .

    最重要的是,异常是指示方法可能失败的语言 . 没有其他方法可以告诉API,预期哪些方法总是(大部分)工作,哪些方法会报告失败 .

    例外比哨兵更安全,比结果对象更快,并且比任何一个都更不令人惊讶 . 我并不是建议使用try / catch替换if / else,但异常是报告失败的正确方法,即使在业务逻辑中也是如此 .

    也就是说,我想指出,我遇到的两种最常见的影响性能的方法是创建不必要的对象和嵌套循环 . 如果您可以在创建例外或不创建例外之间做出选择,请不要创建例外 . 如果您可以选择有时创建异常或始终创建另一个对象,则创建异常 .

  • 6

    我已经使用JVM 1.5进行了一些性能测试,并且使用异常至少慢了2倍 . 平均而言:一个非常小的方法的执行时间超过三倍(3x),但有例外 . 一个必须捕获异常的微小循环看到自我时间增加了2倍 .

    我在 生产环境 代码和微基准测试中看到了类似的数字 .

    绝对应该用于经常调用的任何事情 . 每秒抛出数以千计的异常将导致巨大的瓶颈 .

    例如,使用"Integer.ParseInt(...)"查找非常大的文本文件中的所有错误值 - 非常糟糕 . (我已经看到这个实用程序方法在 生产环境 代码上杀死性能)

    使用异常来报告用户GUI表单上的错误值,从性能角度来看可能并不那么糟糕 .

    无论它是否是一个好的设计实践,我都会遵守规则:如果错误是正常/预期的,那么使用返回值 . 如果不正常,请使用例外 . 例如:读取用户输入,错误值是正常的 - 使用错误代码 . 将值传递给内部实用程序函数时,应通过调用代码来过滤错误值 - 使用异常 .

  • 6

    为什么异常应该比正常回报慢?

    只要您不将堆栈跟踪打印到终端,将其保存到文件或类似文件中,catch-block就不会比其他代码块做更多的工作 . 所以,我无法想象为什么“抛出新的my_cool_error()”应该那么慢 .

    好问题,我期待有关这个主题的进一步信息!

  • 7

    HotSpot能够删除系统生成的异常的异常代码,只要它全部内联即可 . 但是,显式创建的异常和其他未删除的异常会花费大量时间来创建堆栈跟踪 . 覆盖 fillInStackTrace 以查看这会如何影响性能 .

  • 53

    我扩展了@Mecki@incarnate给出的答案,没有堆栈填充Java .

    使用Java 7,我们可以使用 Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace) . 但对于Java6,请参阅my answer for this question

    // This one will regularly throw one
    public void method4(int i) throws NoStackTraceThrowable {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new NoStackTraceThrowable();
        }
    }
    
    // This one will regularly throw one
    public void method5(int i) throws NoStackTraceRuntimeException {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new NoStackTraceRuntimeException();
        }
    }
    
    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();
    
        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method4(i);
            } catch (NoStackTraceThrowable e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );
    
    
        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method5(i);
            } catch (RuntimeException e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
    }
    

    在Core i7,8GB RAM上使用Java 1.6.0_45输出:

    method1 took 883 ms, result was 2
    method2 took 882 ms, result was 2
    method3 took 32270 ms, result was 2 // throws Exception
    method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
    method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException
    

    因此,与抛出异常的方法相比,返回值的方法仍然更快 . 恕我直言,我们无法为成功和错误流程使用返回类型设计一个明确的API . 抛出异常而没有堆栈跟踪的方法比正常异常快4-5倍 .

    编辑:NoStackTraceThrowable.java谢谢@Greg

    public class NoStackTraceThrowable extends Throwable { 
        public NoStackTraceThrowable() { 
            super("my special throwable", null, false, false);
        }
    }
    
  • 37

    Java和C#中的异常性能还有很多不足之处 .

    作为程序员,这迫使我们遵守“不经常引起例外”的规则,仅仅出于实际的性能原因 .

    然而,作为计算机科学家,我们应该反对这个有问题的国家 . 创作函数的人通常不知道调用它的频率,或者更有可能成功或失败 . 只有来电者才有此信息 . 试图避免异常导致API idom不清楚,在某些情况下我们只有干净但缓慢的异常版本,而在其他情况下,我们有快速但笨重的返回值错误,而在其他情况下,我们最终都会 . 库实现者可能必须编写和维护两个版本的API,并且调用者必须决定在每种情况下使用哪两个版本 .

    这有点乱 . 如果异常具有更好的性能,我们可以避免使用这些笨重的习语并使用异常,因为它们本身就是用作...作为结构化错误返回工具 .

    我真的很想看到异常使用更接近返回值的技术实现的机制,因此我们可以使性能更接近返回值..因为这是我们在性能敏感代码中所回复的 .

    下面是一个代码示例,它将异常性能与错误返回值性能进行比较 .

    公共类TestIt {

    int value;
    
    
    public int getValue() {
        return value;
    }
    
    public void reset() {
        value = 0;
    }
    
    public boolean baseline_null(boolean shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            return shouldfail;
        } else {
            return baseline_null(shouldfail,recurse_depth-1);
        }
    }
    
    public boolean retval_error(boolean shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                return false;
            } else {
                return true;
            }
        } else {
            boolean nested_error = retval_error(shouldfail,recurse_depth-1);
            if (nested_error) {
                return true;
            } else {
                return false;
            }
        }
    }
    
    public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                throw new Exception();
            }
        } else {
            exception_error(shouldfail,recurse_depth-1);
        }
    
    }
    
    public static void main(String[] args) {
        int i;
        long l;
        TestIt t = new TestIt();
        int failures;
    
        int ITERATION_COUNT = 100000000;
    
    
        // (0) baseline null workload
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            
    
                failures = 0;
                long start_time = System.currentTimeMillis();
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                    t.baseline_null(shoulderror,recurse_depth);
                }
                long elapsed_time = System.currentTimeMillis() - start_time;
                System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                        recurse_depth, exception_freq, failures,elapsed_time);
            }
        }
    
    
        // (1) retval_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            
    
                failures = 0;
                long start_time = System.currentTimeMillis();
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                    if (!t.retval_error(shoulderror,recurse_depth)) {
                        failures++;
                    }
                }
                long elapsed_time = System.currentTimeMillis() - start_time;
                System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                        recurse_depth, exception_freq, failures,elapsed_time);
            }
        }
    
        // (2) exception_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            
    
                failures = 0;
                long start_time = System.currentTimeMillis();
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                    try {
                        t.exception_error(shoulderror,recurse_depth);
                    } catch (Exception e) {
                        failures++;
                    }
                }
                long elapsed_time = System.currentTimeMillis() - start_time;
                System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                        recurse_depth, exception_freq, failures,elapsed_time);              
            }
        }
    }
    

    }

    以下是结果:

    baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
    baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
    baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
    baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
    baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
    baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
    baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
    baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
    baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
    baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
    baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
    baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
    baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
    baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
    baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
    retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
    retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
    retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
    retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
    retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
    retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
    retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
    retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
    retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
    retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
    retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
    retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
    retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
    retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
    retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
    exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
    exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
    exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
    exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
    exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
    exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
    exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
    exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
    exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
    exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
    exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
    exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
    exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
    exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
    exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms
    

    检查和传播返回值确实会增加一些成本与基线空调用相比,并且该成本与调用深度成比例 . 在调用链深度为8时,错误返回值检查版本比未检查返回值的基线版本慢约27% .

    相比之下,异常性能不是调用深度的函数,而是异常频率的函数 . 然而,作为例外频率增加的降级更加剧烈 . 错误频率仅为25%,代码运行速度慢24小时 . 在错误频率为100%时,异常版本几乎慢100倍 .

    这告诉我,在我们的异常实现中可能正在做出错误的权衡 . 例外情况可能更快,无论是通过避免昂贵的跟踪行走,还是通过直接将它们转换为编译器支持的返回值检查 . 在他们这样做之前,当我们希望我们的代码快速运行时,我们就会避免使用它们 .

  • 17

    Don 't know if these topics relate, but I once wanted to implement one trick relying on current thread' s堆栈跟踪:我想发现方法的名称,它触发了实例化类中的实例化(是的,这个想法很疯狂,我完全放弃了) . 所以我发现调用 Thread.currentThread().getStackTrace() 是缓慢的(由于它在内部使用的原生 dumpThreads 方法) .

    所以Java Throwable ,相应地,有一个原生方法 fillInStackTrace . 我认为前面描述的杀手 catch 块以某种方式触发了这种方法的执行 .

    但是让我告诉你另一个故事......

    在Scala中,一些功能特性在JVM中使用 ControlThrowable 编译,它扩展 Throwable 并以下列方式覆盖其 fillInStackTrace

    override def fillInStackTrace(): Throwable = this
    

    所以我调整了上面的测试(周期数量减少了10,我的机器有点慢:):

    class ControlException extends ControlThrowable
    
    class T {
      var value = 0
    
      def reset = {
        value = 0
      }
    
      def method1(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0xfffffff) == 1000000000) {
          println("You'll never see this!")
        }
      }
    
      def method2(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0xfffffff) == 1000000000) {
          throw new Exception()
        }
      }
    
      def method3(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0x1) == 1) {
          throw new Exception()
        }
      }
    
      def method4(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0x1) == 1) {
          throw new ControlException()
        }
      }
    }
    
    class Main {
      var l = System.currentTimeMillis
      val t = new T
      for (i <- 1 to 10000000)
        t.method1(i)
      l = System.currentTimeMillis - l
      println("method1 took " + l + " ms, result was " + t.value)
    
      t.reset
      l = System.currentTimeMillis
      for (i <- 1 to 10000000) try {
        t.method2(i)
      } catch {
        case _ => println("You'll never see this")
      }
      l = System.currentTimeMillis - l
      println("method2 took " + l + " ms, result was " + t.value)
    
      t.reset
      l = System.currentTimeMillis
      for (i <- 1 to 10000000) try {
        t.method4(i)
      } catch {
        case _ => // do nothing
      }
      l = System.currentTimeMillis - l
      println("method4 took " + l + " ms, result was " + t.value)
    
      t.reset
      l = System.currentTimeMillis
      for (i <- 1 to 10000000) try {
        t.method3(i)
      } catch {
        case _ => // do nothing
      }
      l = System.currentTimeMillis - l
      println("method3 took " + l + " ms, result was " + t.value)
    
    }
    

    所以,结果是:

    method1 took 146 ms, result was 2
    method2 took 159 ms, result was 2
    method4 took 1551 ms, result was 2
    method3 took 42492 ms, result was 2
    

    你看, method3method4 之间的唯一区别是它们会抛出不同类型的异常 . Yeap, method4 仍然比 method1method2 慢,但差别更可接受 .

  • 2

    我认为第一篇文章是指遍历调用堆栈并创建堆栈跟踪作为昂贵部分的行为,虽然第二篇文章没有说明,但我认为这是对象创建中最昂贵的部分 . 约翰罗斯有an article where he describes different techniques for speeding up exceptions . (预分配和重用异常,没有堆栈跟踪的异常等)

    但仍然 - 我认为这应该只被视为必要的邪恶,最后的手段 . John这样做的原因是模拟JVM中尚未提供的其他语言的功能 . 你不应该养成使用控制流异常的习惯 . 特别是出于性能原因!正如您自己在#2中提到的那样,您可能会以这种方式掩盖代码中的严重错误,并且对于新程序员来说维护起来会更困难 .

    Java中的Microbenchmarks非常难以正确(我被告知),特别是当你进入JIT领域时,所以我真的怀疑使用异常比现实生活中的“返回”更快 . 例如,我怀疑你的测试中有2到5个堆栈帧?现在假设您的代码将由JBoss部署的JSF组件调用 . 现在您可能有几页长的堆栈跟踪 .

    也许你可以发布你的测试代码?

  • 3

    AlekseyShipilëv做了very thorough analysis,他在各种条件组合下对Java异常进行了基准测试:

    • 新创建的例外与预先创建的例外

    • 启用堆栈跟踪与禁用

    • 请求堆栈跟踪vs从未请求过

    • 在每个级别上都处于最高级别,而不是每个级别的链接/包裹

    • 各种级别的Java调用堆栈深度

    • 没有内联优化与极端内联与默认设置

    • 用户定义的字段读取与未读取

    他也比较了他们在各种错误频率级别检查错误代码的性能 .

    结论(从他的帖子中逐字引用)是:

    • Truly exceptional exceptions are beautifully performant. 如果您按照设计使用它们,并且只在常规代码处理的绝大多数非例外情况中传达真正例外情况,那么使用例外就是性能获胜 .

    • 异常的性能成本有两个主要组件:实例化异常时 stack trace construction 和异常抛出期间 stack unwinding .

    • Stack trace construction costs are proportional to stack depth 在异常实例化时 . 这已经很糟糕了,因为地球上的谁知道这种投掷方法的堆栈深度?即使您关闭堆栈跟踪生成和/或缓存异常,您也只能摆脱这部分性能成本 .

    • Stack unwinding costs depend on how lucky we are with bringing the exception handler closer in the compiled code. 仔细构建代码以避免深度异常处理程序查找可能有助于我们更幸运 .

    • Should we eliminate both effects, the performance cost of exceptions is that of the local branch. 无论听起来多么美丽,这并不意味着你应该使用Exceptions作为通常的控制流程,因为在那种情况下 you are at the mercy of optimizing compiler! 你应该只在真正例外的情况下使用它们,其中异常频率 amortizes 可能不幸的成本提高了实际例外 .

    • 乐观的经验法则似乎是 10^-4 异常的频率非常特殊 . 当然,这取决于异常本身的重量级,异常处理程序中采取的确切操作等 .

    结果是,当一个例外不支付成本时,所以当异常条件足够罕见时,异常处理比每次使用_76172更快 . 完整的帖子非常值得一读 .

  • 225

    只需比较一下让我们说Integer.parseInt到下面的方法,它只是在不可解析数据的情况下返回默认值而不是抛出异常:

    public static int parseUnsignedInt(String s, int defaultValue) {
        final int strLength = s.length();
        if (strLength == 0)
          return defaultValue;
        int value = 0;
        for (int i=strLength-1; i>=0; i--) {
          int c = s.charAt(i);
          if (c > 47 && c < 58) {
            c -= 48;
            for (int j=strLength-i; j!=1; j--)
              c *= 10;
            value += c;
          } else {
            return defaultValue;
          }
        }
        return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
      }
    

    只要将两种方法应用于“有效”数据,它们都将以大致相同的速率工作(即使Integer.parseInt设法处理更复杂的数据) . 但是,只要您尝试解析无效数据(例如,解析“abc”1.000.000次),性能差异就应该是必不可少的 .

  • 7

    我更改了@Mecki上面的答案,让method1返回一个布尔值并检查调用方法,因为你不能只用一个替换异常 . 两次运行后,method1仍然是最快的或者与method2一样快 .

    这是代码的快照:

    // Calculates without exception
    public boolean method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        return ((i & 0xFFFFFFF) == 1000000000);
    
    }
    ....
       for (i = 1; i < 100000000; i++) {
                if (t.method1(i)) {
                    System.out.println("Will never be true!");
                }
        }
    

    和结果:

    运行1

    method1 took 841 ms, result was 2
    method2 took 841 ms, result was 2
    method3 took 85058 ms, result was 2
    

    跑2

    method1 took 821 ms, result was 2
    method2 took 838 ms, result was 2
    method3 took 85929 ms, result was 2
    
  • -3

    这取决于如何实施例外 . 最简单的方法是使用setjmp和longjmp . 这意味着CPU的所有寄存器都被写入堆栈(已经需要一些时间),并且可能需要创建一些其他数据......所有这些都已经发生在try语句中 . throw语句需要展开堆栈并恢复所有寄存器的值(以及VM中可能的其他值) . 所以try和throw同样很慢,而且速度很慢,但是如果没有抛出异常,退出try块在大多数情况下都不会花费任何时间(因为如果方法存在,所有内容都会被放到堆栈中自动清理) .

    Sun和其他人认识到,这可能不是最理想的,当然VM随着时间的推移变得越来越快 . 还有另一种实现异常的方法,这使得try本身闪电般快速(实际上没有任何事情发生在一般的尝试 - 当VM加载类时,所有需要发生的事情已经完成)并且它使得抛出的速度不是很慢 . 我不知道哪个JVM使用这种新的,更好的技术......

    ...但是你是用Java编写的,所以稍后你的代码只在一个特定系统上的一个JVM上运行?因为它可能在任何其他平台或任何其他JVM版本(可能是任何其他供应商)上运行,谁说他们也使用快速实现?快速的比复杂的更复杂,并且在所有系统上都不容易实现 . 你想保持便携?然后不要依赖快速的异常 .

    它在try块中的作用也有很大的不同 . 如果你打开一个try块并且从不在这个try块中调用任何方法,那么try块将是超快的,因为JIT可以实际上像一个简单的goto一样处理throw . 如果抛出异常(它只需要跳转到catch处理程序),它既不需要保存堆栈状态也不需要展开堆栈 . 但是,这不是你通常做的 . 通常你打开一个try块,然后调用一个可能引发异常的方法,对吧?即使你只是在你的方法中使用try块,这将是什么样的方法,不会调用任何其他方法?它只是计算一个数字吗?那么你需要什么例外呢?有更多优雅的方法来规范程序流程 . 对于除了简单的数学之外的其他任何事情,你将不得不调用外部方法,这已经破坏了本地try块的优势 .

    请参阅以下测试码:

    public class Test {
        int value;
    
    
        public int getValue() {
            return value;
        }
    
        public void reset() {
            value = 0;
        }
    
        // Calculates without exception
        public void method1(int i) {
            value = ((value + i) / i) << 1;
            // Will never be true
            if ((i & 0xFFFFFFF) == 1000000000) {
                System.out.println("You'll never see this!");
            }
        }
    
        // Could in theory throw one, but never will
        public void method2(int i) throws Exception {
            value = ((value + i) / i) << 1;
            // Will never be true
            if ((i & 0xFFFFFFF) == 1000000000) {
                throw new Exception();
            }
        }
    
        // This one will regularly throw one
        public void method3(int i) throws Exception {
            value = ((value + i) / i) << 1;
            // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
            // an AND operation between two integers. The size of the number plays
            // no role. AND on 32 BIT always ANDs all 32 bits
            if ((i & 0x1) == 1) {
                throw new Exception();
            }
        }
    
        public static void main(String[] args) {
            int i;
            long l;
            Test t = new Test();
    
            l = System.currentTimeMillis();
            t.reset();
            for (i = 1; i < 100000000; i++) {
                t.method1(i);
            }
            l = System.currentTimeMillis() - l;
            System.out.println(
                "method1 took " + l + " ms, result was " + t.getValue()
            );
    
            l = System.currentTimeMillis();
            t.reset();
            for (i = 1; i < 100000000; i++) {
                try {
                    t.method2(i);
                } catch (Exception e) {
                    System.out.println("You'll never see this!");
                }
            }
            l = System.currentTimeMillis() - l;
            System.out.println(
                "method2 took " + l + " ms, result was " + t.getValue()
            );
    
            l = System.currentTimeMillis();
            t.reset();
            for (i = 1; i < 100000000; i++) {
                try {
                    t.method3(i);
                } catch (Exception e) {
                    // Do nothing here, as we will get here
                }
            }
            l = System.currentTimeMillis() - l;
            System.out.println(
                "method3 took " + l + " ms, result was " + t.getValue()
            );
        }
    }
    

    结果:

    method1 took 972 ms, result was 2
    method2 took 1003 ms, result was 2
    method3 took 66716 ms, result was 2
    

    try块的减速太小,无法排除后台进程等混杂因素 . 但是拦截块杀死了所有东西并使它慢了66倍!

    正如我所说,如果你把try / catch和all all放在同一个方法(method3)中,结果就不会那么糟糕,但这是一个我不会依赖的特殊JIT优化 . 即使使用这种优化,投掷仍然很慢 . 所以我不知道你在这里要做什么,但肯定有一种比使用try / catch / throw更好的方法 .

  • 313

    前一段时间我写了一个类来测试使用两种方法将字符串转换为int的相对性能:(1)调用Integer.parseInt()并捕获异常,或(2)将字符串与正则表达式匹配并调用parseInt()只有匹配成功 . 我以最有效的方式使用正则表达式(即,在循环之前创建Pattern和Matcher对象),并且我没有打印或保存异常中的堆栈跟踪 .

    对于一万个字符串的列表,如果它们都是有效数字,则parseInt()方法的速度是正则表达式方法的四倍 . 但如果只有80%的字符串有效,那么正则表达式的速度是parseInt()的两倍 . 如果20%是有效的,意味着异常被抛出并且有80%的时间被捕获,那么正则表达式的速度大约是parseInt()的20倍 .

    考虑到正则表达式方法两次处理有效字符串,我对结果感到惊讶:一次是匹配,另一次是parseInt() . 但抛出和捕获异常超过了弥补 . 在现实世界中,这种情况不太可能经常发生,但如果确实如此,你绝对不应该使用异常捕获技术 . 但是,如果您只是验证用户输入或类似的东西,请务必使用parseInt()方法 .

相关问题