异常对Java性能有什么影响?


496

问题:Java中的异常处理是否真的很慢?

常规知识以及许多Google的研究结果都表明,不应将异常逻辑用于Java中的常规程序流程。通常有两个原因,

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

  1. 这很混乱,因为人们希望仅在特殊代码中处理错误。

这个问题是关于#1的。

例如,此页面将Java异常处理描述为“非常慢”,并将这种缓慢与异常消息字符串的创建相关联-“然后,此字符串用于创建抛出的异常对象。这不是很快。” Java中的有效异常处理文章说:“其原因是由于异常处理的对象创建方面,因此使固有的异常抛出速度变慢”。另一个原因是堆栈跟踪生成会减慢堆栈跟踪的速度。

我的测试(在32位Linux上使用Java 1.6.0_07,Java HotSpot 10.0)表明异常处理并不比常规代码慢。我尝试在执行一些代码的循环中运行方法。在方法的最后,我使用一个布尔值来指示是返回还是throw。这样,实际处理是相同的。我尝试以不同的顺序运行这些方法并平均测试时间,以为可能是JVM升温。在我所有的测试中,投掷速度至少与返回速度一样快,甚至不快(高达3.1%)。我完全可以接受我的测试错误的可能性,但是我并没有看到代码样本,测试比较或最近一两年中显示Java异常处理的结果的任何方式慢。

导致我走这条路的是我需要使用的将异常作为常规控制逻辑一部分的API。我想更正它们的用法,但现在可能无法。我是否必须赞扬他们的前瞻性思维?

在《即时编译中的高效Java异常处理》一文中,作者建议即使没有抛出异常,仅存在异常处理程序也足以阻止JIT编译器正确优化代码,从而减慢速度。我还没有检验过这个理论。


8
我知道您并不是在问2),但您应该真正认识到,使用异常进行程序流并不比使用GOTO好。有些人捍卫gotos,有些人会捍卫您在说什么,但是如果您问一个已经实施和维护了一段时间的人,他们会告诉您,两者都很难维护设计实践(并且可能会骂人认为自己足够聪明以决定使用它们的人的名字)。
比尔K

80
Bill声称使用异常进行程序流不比使用GOTO更好,也不主张使用条件和循环进行程序流不比使用GOTO更好。这是一条红鲱鱼。自我解释。异常可以并且有效地用于其他语言的程序流。例如,成语的Python代码会定期使用异常。我可以并且已经维护过以这种方式使用异常的代码(尽管不是Java),而且我认为它本质上没有任何问题。
mmalone

14
@mmalone对于Java的正常控制流使用Exceptions是一个坏主意,因为范式选择是通过这种方式完成的。阅读Bloch EJ2,他清楚地指出,(第57项)exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow,给出了为什么的完整而广泛的解释。他是编写 Java lib的人。因此,他是定义类的API合同的人。/同意Bill K关于这一点。

8
@OndraŽižka如果某些框架执行此操作(在非异常条件下使用Exception),则它会因设计缺陷而被破坏,从而破坏语言的Exception类协定。仅仅因为有些人编写糟糕的代码并不会减少它的糟糕程度。

8
除了stackoverflow.com的创建者外,其他人都没有错。软件开发的黄金法则永远不会使简单变得复杂和笨拙。他写道:“的确,当您进行良好的错误检查时,本来应该是简单的3行程序常常会发展到48行,但这就是生命,……”这是对纯洁的追求,而不是简单性。
sf_jeff 2015年

Answers:


345

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

Sun和其他公司认识到,这可能不是最佳选择,并且随着时间的流逝,虚拟机当然会越来越快。还有另一种实现异常的方法,它可以使尝试本身快如闪电(实际上一般而言,尝试都不会发生任何事情-当类由VM加载时,所有需要发生的事情都已经完成了),并且抛出异常的速度也不太慢。我不知道哪个JVM使用这项新的更好的技术...

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

在try块中执行的操作也有很大的不同。如果您打开一个try块,并且从不从该try块中调用任何方法,则try块将非常快,因为JIT实际上可以像简单的goto一样处理throw。如果抛出异常(它只需要跳转到catch处理程序),则既不需要保存堆栈状态,也不需要取消堆栈。但是,这不是您通常执行的操作。通常,您打开一个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放在同一个方法(method3)中,则结果不会那么糟糕,但这是我不依赖的特殊JIT优化。即使使用此优化,抛出也仍然很慢。所以我不知道您要在这里做什么,但是绝对有比使用try / catch / throw更好的方法。


7
很好的答案,但我想补充一点,据我所知,应该使用System.nanoTime()来衡量性能,而不是System.currentTimeMillis()。
西蒙·佛斯伯格

10
@SimonAndréForsberg nanoTime()需要Java 1.5,并且在用于编写上述代码的系统上只有Java 1.4可用。在实践中它也没有扮演重要角色。两者之间的唯一区别是,一个为纳秒,另一个为一毫秒,nanoTime并且不受时钟操作的影响(这无关紧要,除非您或系统进程在测试代码运行时恰好修改了系统时钟)。通常,您是对的,但是nanoTime当然是更好的选择。
麦基

2
确实应该注意,您的测试是极端情况。对于带有代码try块的代码,您的性能影响很小,但是没有throw。您的throw测试在通过的时间中50%抛出异常try。显然,这种情况并非罕见。将其降低到仅10%可以大大降低性能。这种测试的问题在于,它鼓励人们完全停止使用异常。使用异常进行例外情况处理,其性能比测试显示的要好得多。
Nate 2014年

1
@Nate首先,我非常清楚地说,这一切都取决于异常的实现方式。我只是在测试一个特定的实现,但是还有很多,Oracle可能会在每个版本中选择完全不同的实现。其次,如果例外仅是例外,它们通常是什么,当然影响会更小,这是如此明显,以至于我真的没有必要明确指出例外,因此我根本无法理解您的观点。第三,异常过度使用是不好的,每个人都同意,因此,谨慎使用它们是一件非常好的事情。
Mecki 2014年

4
@Glide抛出不干净return。它将方法留在身体中部某处,甚至可能在操作过程中(到目前为止仅完成了50%),并且该catch块可能向上20个堆栈帧(一个方法有一个try块,称为method1,调用method2,调用mehtod3,...,并且在操作中间的method20中引发异常)。堆栈必须向上展开20帧,所有未完成的操作都必须撤消(操作不能完成一半),CPU寄存器必须处于干净状态。这全部消耗时间。
Mecki

255

仅供参考,我扩展了梅基所做的实验:

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

前3个与Mecki的相同(我的笔记本电脑明显较慢)。

method4与method3相同,除了它创建一个new Integer(1)而不是do throw new Exception()

方法5类似于方法3,不同之处在于它在new Exception()不抛出的情况下创建。

method6与method3相似,除了它抛出一个预先创建的异常(一个实例变量)而不是创建一个新的异常。

在Java中,引发异常的大部分开销是花费在收集堆栈跟踪信息上的时间,这是在创建异常对象时发生的。引发异常的实际成本虽然很大,但比创建异常的成本要低得多。


48
+1您的答案解决了核心问题-展开并跟踪堆栈所需的时间,其次是引发错误。我会选择这个作为最终答案。
工程师

8
很好 约70%的人创造了异常,约30%的人抛出了。好信息。
chaqke,2015年

1
@Basil-您应该能够从上述数字中得出答案。
热舔

1
这可能是特定于实现的。这些基准测试使用什么版本的Java?
托尔比约恩Ravn的安德森

3
我们可以指出,在标准代码中,创建和抛出异常的情况极少发生(我是说在运行时),如果不是这种情况,要么运行时条件非常糟糕,要么设计本身就是问题。在两种情况下,表演都不是问题...
Jean-BaptisteYunès17年

70

AlekseyShipilëv做了非常彻底的分析,他在各种条件组合下对Java异常进行了基准测试:

  • 新创建的例外与预先创建的例外
  • 堆栈跟踪已启用还是已禁用
  • 已请求堆栈跟踪vs从未请求
  • 在最高级别被捕获vs在每个级别被重新抛出vs在每个级别被链接/包装
  • 各种级别的Java调用堆栈深度
  • 没有内联优化vs极端内联vs默认设置
  • 用户定义的字段已读还是未读

他还将它们与在各种错误频率级别上检查错误代码的性能进行了比较。

结论(引自他的职务逐字记录)为:

  1. 真正异常的例外表现出色。如果您按设计使用它们,并且仅在绝大多数由常规代码处理的非异常情况中交流真正的特殊情况,那么使用异常将是性能上的优势。

  2. 异常的性能成本有两个主要组成部分:实例化Exception时的堆栈跟踪构造和抛出Exception期间的堆栈展开

  3. 在异常实例化时,堆栈跟踪构建成本与堆栈深度成正比。这已经很糟糕了,因为地球上谁知道这种投掷方法会被调用的堆栈深度?即使关闭堆栈跟踪生成和/或缓存异常,您也只能摆脱这部分性能成本。

  4. 堆栈展开成本取决于我们将异常处理程序放在已编译代码中附近的幸运程度。仔细地构造代码以避免深入的异常处理程序查找可能会帮助我们更加幸运。

  5. 如果我们要消除这两种影响,则异常的性能成本就是本地分支的性能成本。不管听起来多么漂亮,这并不意味着您应该将Exceptions用作通常的控制流,因为在这种情况下,您只能优化编译器!仅应在真正的例外情况下使用它们,在这种情况下,例外频率会摊销引发实际例外的可能不幸的成本。

  6. 乐观的经验法则似乎是10 ^ -4的频率,例外情况就足够例外了。当然,这取决于异常本身的重量级,异常处理程序中采取的确切操作等。

结果是,当不引发异常时,您无需支付任何费用,因此,当异常情况足够罕见时,异常处理的速度将比if每次使用都要快。完整的帖子非常值得一读。


41

不幸的是,我的回答太久了,无法在此处发布。因此,让我在这里进行总结,并向您介绍http://www.fuwjax.com/how-slow-are-java-exceptions/以获取详细信息。

这里真正的问题不是“将'失败报告为异常'相比'永不失败的代码'有多慢?” 因为您可能会相信已接受的回复。相反,问题应该是“与以其他方式报告的故障相比,'将故障报告为例外”有多慢?通常,报告失败的其他两种方法是使用哨兵值或结果包装器。

前哨值是在成功的情况下返回一个类的尝试,在失败的情况下返回另一个类的尝试。您可以将其视为返回异常而不是抛出异常。这需要与成功对象共享一个父类,然后执行“ instanceof”检查,并进行强制转换以获取成功或失败信息。

事实证明,冒着类型安全的风险,Sentinel值比异常值要快,但大约只有2倍。现在,这看起来很多,但是2倍仅能弥补实现差异的成本。实际上,该因素要低得多,因为我们失败的方法比本页面其他地方的示例代码中的一些算术运算符更有趣。

结果另一方面,包装纸根本不牺牲类型安全性。他们将成功和失败信息包装在一个类中。因此,它们为成功对象和失败对象提供了“ isSuccess()”和吸气剂,而不是“ instanceof”。但是,结果对象的速度大约慢了 2 比使用异常。事实证明,每次创建一个新的包装器对象比有时候抛出一个异常要昂贵得多。

最重要的是,异常是提供的表示方法可能失败的方式的语言。没有其他方法可以仅通过API来确定预期哪些方法将始终(大部分)正常工作,哪些预期将报告故障。

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

就是说,我想指出,影响性能的两种最常见的方式是创建不必要的对象和嵌套循环。如果您可以选择创建例外还是不创建例外,请不要创建例外。如果您可以选择有时候创建一个例外还是一直创建另一个对象,那么请创建例外。


5
我决定测试这三种实现的长期性能,而不是对照测试实现的性能,而这种控制实现无需报告即可检查故障。该过程的失败率约为4%。测试的迭代针对一种策略调用该过程10000次。每个策略经过1000次测试,最后900次用于生成统计信息。这是平均时间,以纳米为单位:控制338异常429结果348前哨345
Fuwjax 2011年

2
只是为了好玩,我在异常测试中禁用了fillInStackTrace。现在是现在的时间:控制347异常351结果364前哨355
Fuwjax 2011年

Fuwjax,除非我丢失了某些内容(并且我承认我只阅读了您的SO帖子,而不是您的博客帖子),否则似乎您上面的两条评论与您的​​帖子矛盾。我认为较低的数字在您的基准测试中会更好,对吧?在这种情况下,在启用fillInStackTrace的情况下生成异常(这是默认行为和通常的行为),导致性能比您描述的其他两种技术慢。我是否缺少任何内容,或者您​​实际上发表评论以驳斥您的帖子?
Felix GV 2014年

@Fuwjax-避免您在此处出现“艰难而艰难的选择”的方法是预先分配一个表示“成功”的对象。通常,也可以为常见故障情况预分配对象。然后只有在极少数情况下传回其他详细信息的情况下,才会创建一个新对象。(这是等效于整数“错误代码”的OO,再加上一个单独的调用来获取最后一个错误的详细信息-这种技术已经存在了几十年。)
ToolmakerSteve14年

@Fuwjax那么抛出异常不会通过您的帐户创建对象吗?不确定我是否理解这种推理。无论您引发异常还是返回结果对象,都在创建对象。从这个意义上说,结果对象并不比抛出异常慢。
Matthias 2014年

20

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

在Java 7+中,我们可以使用Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace)。但是对于Java6,请参阅我对这个问题的回答

// 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);
    }
}

有趣,谢谢。以下是缺少的类声明:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
Greg

在开始时您写了,With Java 7+, we can use但是后来您写了,Output with Java 1.6.0_45,所以这是Java 6或7的结果吗?
WBAR '19

1
Java 7中的@WBAR,我们只需要使用Throwable具有boolean writableStackTracearg 的构造函数即可。但这在Java 6及更低版本中不存在。这就是为什么我为Java 6及以下版本提供了自定义实现的原因。因此,以上代码适用于Java 6及以下版本。请仔细阅读第二段的第一行。
manikanta

@manikanta“恕我直言,我们不能仅使用返回类型来设计成功和错误流程的清晰API。”-我们可以,如果我们使用Optionals / Results /也许与许多语言一样。
Hejazzman

@Hejazzman,我同意。但是OptionalJava来得有点晚。在此之前,我们还使用了带有成功/错误标志的包装对象。但这似乎有点骇人听闻,对我来说并不自然。
manikanta

8

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

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

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


您使用了哪个JVM?sun-jdk 6还是那么慢吗?
Benedikt Waldvogel

在提交答案之前,我将其挖出并在JDK 1.6u10下再次运行,这些是我发布的结果。
艾伦·摩尔

这是非常非常有用的!谢谢。对于我通常的用例,我确实需要解析用户输入(使用类似的东西Integer.ParseInt()),并且我希望大多数情况下用户输入都是正确的,因此对于我的用例来说,似乎偶尔遇到异常击中是可行的方法。
markvgti 2012年

8

我认为第一篇文章将遍历调用堆栈和创建堆栈跟踪的行为视为昂贵的部分,而第二篇文章没有说,这是对象创建中最昂贵的部分。约翰·罗斯(John Rose)有一篇文章,他描述了加快异常处理的各种技术。(预分配和重用异常,没有堆栈跟踪的异常等)

但是,尽管如此,我认为这仅是必要的邪恶,不得已。John这样做的原因是模拟JVM中尚未提供的其他语言的功能。您不应该养成使用异常进行控制流的习惯。尤其是出于性能原因!正如您自己在#2中提到的那样,您冒着用这种方式掩盖代码中严重错误的风险,而对于新程序员而言,维护起来将更加困难。

令人惊讶的是,Java中的微基准很难正确设置(有人告诉我),尤其是当您进入JIT领域时,因此我真的怀疑使用异常在现实生活中比“返回”更快。例如,我怀疑您的测试中有2至5个堆栈帧?现在想象一下,您的代码将被JBoss部署的JSF组件调用。现在,您可能会有一个长达几页的堆栈跟踪。

也许您可以发布测试代码?


7

不知道这些主题是否相关,但是我曾经想依靠当前线程的堆栈跟踪来实现一个技巧:我想发现方法的名称,该方法在实例化的类中触发了实例化(是的,这个想法很疯狂,我完全放弃了)。所以我发现,调用Thread.currentThread().getStackTrace()极其缓慢的(由于本机dumpThreads供内部使用的方法)。

因此,Java Throwable相应地具有本机方法fillInStackTrace。我认为catch前面描述的杀手级块以某种方式触发了该方法的执行。

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

在Scala中,使用ControlThrowable,在JVM中编译了一些功能部件,该功能部件通过以下方式扩展Throwable和覆盖它fillInStackTrace

override def fillInStackTrace(): Throwable = this

所以我修改了上面的测试(周期数量减少了十个,我的机器慢了一点:):

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

您会看到,method3和之间的唯一区别method4是它们引发了不同种类的异常。叶氏,method4仍慢于method1method2,但不同的是远更容易接受。


6

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

我已经在生产代码和微型基准测试中看到了相似的数字。

绝对不要将异常用于任何经常被调用的东西。每秒抛出数千个异常将导致巨大的瓶颈。

例如,使用“ Integer.ParseInt(...)”在一个非常大的文本文件中查找所有错误值,这是一个非常糟糕的主意。(我看到这个工具方法生产代码的性能)

使用异常在用户GUI表单上报告错误的值,从性能的角度来看可能不是很糟糕。

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


让我提出一些很好的建议:如果您需要某种形式的数字,而不是使用Integer.valueOf(String),则应考虑使用正则表达式匹配器。您可以预编译和重用模式,因此使匹配器便宜。但是,在GUI表单上,具有isValid / validate / checkField或您拥有的内容可能更清楚。另外,在Java 8中,我们具有可选的monad,因此请考虑使用它们。(答案是9岁,但仍然!:p)
HaakonLøtveit17年

4

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

作为程序员,这仅出于实际性能原因,就迫使我们遵守“不应该引起异常的规则”。

但是,作为计算机科学家,我们应该反对这种有问题的状态。编写功能的人通常不知道调用该功能的频率,也不知道成功或失败的可能性更高。仅呼叫者具有此信息。试图避免异常会导致API异常,在某些情况下,我们只有干净但缓慢的异常版本,在其他情况下,我们有快速但笨拙的返回值错误,而在其他情况下,我们最终都以两种方式出现。库实现者可能必须编写和维护两个版本的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倍。

这向我暗示,也许在我们的异常实现中做出了错误的权衡。通过避免代价高昂的跟踪,或将它们直接转换为编译器支持的返回值检查,异常可以更快。除非它们这样做,否则当我们希望我们的代码快速运行时,我们总是避免使用它们。


3

只要全部内联,HotSpot就能删除系统生成的异常的异常代码。但是,显式创建的异常和其他未清除的异常会花费大量时间来创建堆栈跟踪。重写fillInStackTrace以了解这如何影响性能。


2

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

我想这并不能真正回答问题。我可以想象,在较早的Java版本(<1.4)中,缓慢地抛出异常的“传统”观点是正确的。创建异常需要VM创建整个堆栈跟踪。从那以后,VM发生了很多变化,以加快处理速度,这很可能是一个需要改进的地方。


1
定义“正常程序流”会很好。关于使用检查异常作为业务流程故障以及将非检查异常用于不可恢复的故障,已经有很多著作,因此从某种意义上讲,业务逻辑故障仍可以视为正常流程。
Spencer Kormos

2
@Spencer K:顾名思义,异常意味着发现了一种异常情况(文件消失了,网络突然关闭了……)。这意味着这种情况是意外的。如果预计会发生这种情况,我不会对此使用异常。
麦基

2
@Mecki:对。我最近与某人进行了讨论……他们正在编写一个验证框架,并在验证失败的情况下抛出异常。我认为这是一个坏主意,因为这很普遍。我宁愿看到该方法返回ValidationResult。
user38051

2
就控制流而言,异常类似于a breakreturnnot goto
2013年

3
有大量的编程范例。无论您说的是什么,都不可能有一个“正常流”。基本上,异常机制只是一种快速离开当前帧并展开堆栈直到特定点的方法。“例外”一词与其“意外”性质无关。一个简单的例子:当某些情况沿路由方式发生时,从Web应用程序“抛出” 404很自然。为什么不采用例外实现该逻辑?什么是反模式?
化身为

2

只需将Integer.parseInt与以下方法进行比较,该方法将在无法解析的数据的情况下返回默认值,而不是引发Exception:

  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次),性能上的差异就至关重要。


2

关于异常性能的好文章是:

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/ 替换它,breakboolean从方法调用中返回标志,如C中的标志)可提高应用程序的性能10x-100x ,取决于您扔它们的频率))


0

我更改了上面的@Mecki的答案,以使method1在调用方法中返回一个布尔值和一个检查,因为您不能仅将Exception替换为空。经过两次运行,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

0

一个例外是为处理在运行时意外情况而已。

使用异常代替可以在编译时完成的简单验证将使验证延迟到执行时间。这反过来会降低程序的效率。

引发异常而不是使用简单的if..else验证也将使代码的编写和维护变得复杂。


-3

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

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

通常有形式

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

异常名称仅是示例,通常未经检查(运行时),因此引发声明只是我的照片

有时存在第二种形式:

public static Example Parse(String input, Example defaultValue)

永不扔

当第二个ins不可用时(或程序员阅读的文档太少,仅使用第一个),请使用正则表达式编写此类代码。正则表达式很酷,在政治上是正确的,例如:

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

有了这些代码,程序员无需付出任何代价。但是总是有相当高的正则表达式开销,而有时却只有很小的例外开销。

我几乎总是在这种情况下使用

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

无需分析stacktrace等,我相信您的演讲后速度相当快。

不要害怕异常


-5

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

只要不将堆栈跟踪信息打印到终端上,将其保存到文件或类似文件中,catch块就不会比其他代码块执行更多的工作。因此,我无法想象为什么“ throw new my_cool_error()”应该这么慢。

很好的问题,我期待有关此主题的更多信息!


17
异常必须捕获​​有关堆栈跟踪的信息,即使实际上并没有使用它也是如此。
乔恩·斯基特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.