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

问:Java中的异常处理真的很慢吗?

传统观点以及大量谷歌结果表明,不应该将异常逻辑用于Java中的正常程序流。通常会给出两个原因,

  1. 它真的很慢——甚至比普通代码慢一个数量级(给出的原因各不相同),

而且

  1. 它很混乱,因为人们只希望在异常代码中处理错误。

这个问题是关于第一条的。

作为一个例子,这个页面将Java异常处理描述为“非常慢”,并将这种慢与异常消息字符串的创建联系起来——“这个字符串随后用于创建抛出的异常对象。这并不快。”文章Java中有效的异常处理说:“造成这种情况的原因是异常处理的对象创建方面,从而导致抛出异常本身就很慢”。另一个原因是堆栈跟踪生成减慢了它的速度。

我的测试(使用Java 1.6.0_07, Java HotSpot 10.0,在32位Linux上)表明异常处理并不比常规代码慢。我尝试在循环中运行一个方法来执行一些代码。在方法的最后,我使用一个布尔值来指示是返回还是。这样实际的处理过程是一样的。我尝试以不同的顺序运行这些方法,并平均我的测试时间,我认为这可能是JVM预热的结果。在我的所有测试中,投掷速度至少和返回速度一样快,如果不是更快的话(快了3.1%)。我完全接受我的测试是错误的可能性,但是在过去一两年的代码示例、测试比较或结果中,我没有看到任何东西表明Java中的异常处理实际上是缓慢的。

引导我走上这条路的是我需要使用的一个API,它将抛出异常作为正常控制逻辑的一部分。我想纠正它们的用法,但现在我可能做不到。我是否应该赞美他们的前瞻思维?

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

101407 次浏览

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

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

为什么异常回报率会比正常回报率慢呢?

只要不将堆栈跟踪输出到终端,将其保存到一个文件或类似的文件中,catch块就不会比其他代码块做更多的工作。所以,我无法想象为什么“throw new my_cool_error()”应该这么慢。

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

我认为第一篇文章提到遍历调用堆栈和创建堆栈跟踪是最昂贵的部分,虽然第二篇文章没有这样说,但我认为这是对象创建中最昂贵的部分。约翰·罗斯有他在一篇文章中描述了加速异常的不同技术。(预分配和重用异常,没有堆栈跟踪的异常,等等)

但我仍然认为这应该被认为是一种必要的邪恶,一种最后的手段。John这样做的原因是为了模拟JVM中(还)没有的其他语言的特性。你不应该养成对控制流使用异常的习惯。尤其是因为性能原因!正如您自己在第2条中提到的,这样做可能会掩盖代码中的严重错误,而且对于新程序员来说,维护起来会更加困难。

Java中的微基准测试出奇地难以正确(有人告诉过我),特别是在进入JIT领域时,因此我真的怀疑在现实生活中使用异常是否比“返回”更快。例如,我怀疑您在测试中有2到5个堆栈帧?现在假设您的代码将由JBoss部署的JSF组件调用。现在您可能有一个数页长的堆栈跟踪。

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

HotSpot非常能够删除系统生成的异常代码,只要它是内联的。但是,显式创建的异常和其他未删除的异常要花费大量时间来创建堆栈跟踪。重写fillInStackTrace以查看它如何影响性能。

我用JVM 1.5做了一些性能测试,使用异常至少慢了两倍。平均:一个非常小的方法的执行时间超过3倍(3倍)。一个必须捕获异常的小循环的自时间增加了2倍。

我在产品代码和微基准测试中也看到过类似的数字。

任何频繁调用的东西都应该明确地使用异常不< em > < / em >。每秒抛出数千个异常将导致巨大的瓶颈。

例如,使用“Integer.ParseInt(…)”在一个非常大的文本文件中找到所有错误的值——非常糟糕的想法。(我在产品代码中看到过这个实用工具方法杀了的性能)

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

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

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

Sun和其他人认识到,这可能是次优的,当然随着时间的推移,虚拟机会变得越来越快。还有另一种实现异常的方法,它使try本身闪电般快(实际上try本身根本不会发生任何事情——当类被VM加载时,需要发生的一切都已经完成了),并且它使throw不那么慢。我不知道哪个JVM使用了这种新的、更好的技术……

...但你是在用Java写代码,所以你的代码以后只能在一个特定系统的一个JVM上运行吗?因为如果它可以在任何其他平台或任何其他JVM版本(可能是任何其他供应商的)上运行,谁说他们也使用快速实现呢?速度快的要比速度慢的复杂得多,而且不容易在所有系统上实现。你想要便携吗?那就不要指望异常会很快。

在try块中所做的事情也有很大不同。如果你打开一个try块,并且从不从这个try块中调用任何方法,那么try块将会非常快,因为JIT实际上可以像处理一个简单的goto一样处理抛出。它既不需要保存堆栈状态,也不需要在抛出异常时展开堆栈(它只需要跳转到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块的减速太小,无法排除后台进程等混杂因素。但是catch block杀死了一切,让它慢了66倍!

正如我所说,如果将try/catch和throw都放在同一个方法(method3)中,结果不会那么糟糕,但这是我不依赖的特殊JIT优化。即使使用这种优化,抛出仍然非常慢。我不知道你们想做什么,但肯定有比try/catch/throw更好的方法。

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

对于一个包含10,000个字符串的列表,如果它们都是有效数字,那么parseInt()方法的速度是regex方法的四倍。但如果只有80%的字符串是有效的,则regex的速度是parseInt()的两倍。如果20%是有效的,这意味着异常在80%的时间内被抛出和捕获,则regex的速度大约是parseInt()的20倍。

我对结果感到惊讶,因为regex方法处理了两次有效字符串:一次用于匹配,另一次用于parseInt()。但是抛出和捕获异常完全弥补了这一点。这种情况在现实世界中不太可能经常发生,但如果发生了,您绝对不应该使用异常捕获技术。但如果您只是验证用户输入或类似的东西,务必使用parseInt()方法。

不知道这些主题是否相关,但我曾经想实现一个依赖于当前线程的堆栈跟踪的技巧:我想发现方法的名称,它触发了实例化类中的实例化(是的,这个想法很疯狂,我完全放弃了它)。所以我发现调用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

您可以看到,method3method4之间的唯一区别是它们抛出不同类型的异常。是的,method4仍然比method1method2慢,但差异是更容易接受的。

比较一下,假设是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次),性能上的差异应该是至关重要的。

不幸的是,我的回答太长了,不能在这里发表。因此,让我在这里总结一下,并建议您参考http://www.fuwjax.com/how-slow-are-java-exceptions/以获取具体细节。

这里真正的问题不是“与“从未失败的代码”相比,“将失败报告为异常”的速度有多慢?”,正如人们所接受的回答可能会让你相信的那样。相反,问题应该是“与其他方式报告的失败相比,‘作为异常报告的失败’有多慢?”通常,报告失败的另外两种方法是使用哨兵值或使用结果包装器。

哨兵值是在成功情况下返回一个类,在失败情况下返回另一个类的尝试。你几乎可以把它看作是返回一个异常而不是抛出一个异常。这需要一个与success对象共享的父类,然后执行“instanceof”检查和几个类型转换来获得成功或失败的信息。

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

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

最重要的是,异常是语言提供的一种指示方法可能失败的方式。没有其他方法可以仅从API判断哪些方法总是(大部分)工作,哪些方法报告失败。

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

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

供你参考,我扩展了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

前3个和Mecki的一样(我的笔记本电脑明显慢一些)。

method4和method3是一样的,除了它创建了一个new Integer(1)而不是throw new Exception()

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

Method6和method3很像,只是它会抛出一个预先创建的异常(一个实例变量),而不是创建一个新异常。

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

我改变了上面的@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

Java和c#中的异常性能还有待改进。

作为程序员,这迫使我们遵循“异常应该很少引起”的规则,仅仅是出于实际性能的考虑。

然而,作为计算机科学家,我们应该反抗这种有问题的状态。编写函数的人通常不知道它被调用的频率,也不知道成功或失败的可能性更大。只有调用方拥有此信息。试图避免异常会导致不清楚的API规则,在某些情况下,我们只有干净但缓慢的异常版本,在其他情况下,我们有快速但笨拙的返回值错误,还有一些情况下,我们最终两者都有。标准库实现者可能必须编写和维护两个版本的api,而调用者必须决定在每种情况下使用两个版本中的哪一个。

这里有点乱。如果异常具有更好的性能,我们就可以避免这些笨拙的习惯用法,并按照它们应该使用的方式使用异常……作为结构化错误返回工具。

我真的希望看到异常机制使用更接近返回值的技术来实现,这样我们的性能就能更接近返回值。因为这是我们在性能敏感代码中恢复的内容。

下面是一个比较异常性能和错误返回值性能的代码示例。

公共类test {

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倍。

这在我看来可能是在我们的异常实现中做出了错误的权衡。异常可以更快,可以避免代价高昂的跟踪遍历,也可以直接将异常转换为编译器支持的返回值检查。在此之前,当我们希望代码运行得更快时,我们不得不避免它们。

我已经扩展了@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() );
}

输出与Java 1.6.0_45,在Core i7, 8GB RAM:

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,只使用返回类型来获得成功和成功;错误流。在没有stacktrace的情况下抛出异常的方法比普通异常快4-5倍。

编辑:notacktracethrowable .java 由于@Greg

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

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

  • 新创建的异常vs预先创建的异常
  • 启用与禁用堆栈跟踪
  • 请求的堆栈跟踪vs从未请求的堆栈跟踪
  • 在顶层捕获vs在每一层重新抛出vs在每一层被链接/包裹
  • 不同级别的Java调用堆栈深度
  • 无内联优化vs极端内联vs默认设置
  • 用户定义字段读与不读

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

结论(逐字摘自他的帖子)如下:

  1. 真正异常的异常表现得非常漂亮。如果你按照设计使用它们,并且只在常规代码处理的绝大多数非异常情况中传递真正的异常情况,那么使用异常是性能上的胜利。

  2. 异常的性能代价有两个主要组成部分:异常实例化时堆栈跟踪构造,异常抛出时堆栈展开

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

  4. 堆栈展开成本取决于我们在已编译代码中使异常处理程序更接近的幸运程度。小心地构造代码以避免深度异常处理程序查找可能会帮助我们变得更幸运。

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

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

结果是,当没有抛出异常时,您不会付出代价,因此当异常条件足够罕见时,异常处理比每次都使用if要快。这篇文章的全文非常值得一读。

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

许多类都有字符串到值的转换器(扫描器/解析器),也有受人尊敬和知名的库;)

通常有形式

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

使用这段代码,程序员没有异常成本。BUT具有相当高的代价的正则表达式ALWAYS与小的代价异常有时。

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

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

没有分析堆栈跟踪等,我相信在你的讲座后相当快。

不要害怕例外情况

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

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

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

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)而变慢。

通过重写应用程序控制流,避免异常(返回错误为return),提高应用程序的性能10 -100倍,这取决于你抛出它们的频率))

使用附带的代码,在JDK 15上,@Mecki测试用例得到了完全不同的结果。这基本上是在5个循环中运行代码,第一个循环稍微短一些,给VM一些时间来热身。

结果:

Loop 1 10000 cycles
method1 took 1 ms, result was 2
method2 took 0 ms, result was 2
method3 took 22 ms, result was 2
method4 took 22 ms, result was 2
method5 took 24 ms, result was 2
Loop 2 10000000 cycles
method1 took 39 ms, result was 2
method2 took 39 ms, result was 2
method3 took 1558 ms, result was 2
method4 took 1640 ms, result was 2
method5 took 1717 ms, result was 2
Loop 3 10000000 cycles
method1 took 49 ms, result was 2
method2 took 48 ms, result was 2
method3 took 126 ms, result was 2
method4 took 88 ms, result was 2
method5 took 87 ms, result was 2
Loop 4 10000000 cycles
method1 took 34 ms, result was 2
method2 took 34 ms, result was 2
method3 took 33 ms, result was 2
method4 took 98 ms, result was 2
method5 took 58 ms, result was 2
Loop 5 10000000 cycles
method1 took 34 ms, result was 2
method2 took 33 ms, result was 2
method3 took 33 ms, result was 2
method4 took 48 ms, result was 2
method5 took 49 ms, result was 2
package hs.jfx.eventstream.api;


public class Snippet {
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();
}
}


private static final NoStackTraceRuntimeException E = new NoStackTraceRuntimeException();


// This one will regularly throw one
public void method3(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 E;
}
}


// 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) {
for(int k = 0; k < 5; k++) {
int cycles = 10000000;
if(k == 0) {
cycles = 10000;
try {
Thread.sleep(500);
}
catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("Loop " + (k + 1) + " " + cycles + " cycles");
int i;
long l;
Snippet t = new Snippet();


l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; 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 < cycles; 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 < cycles; i++) {
try {
t.method3(i);
} catch (NoStackTraceRuntimeException e) {
// always comes here
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method3 took " + l + " ms, result was " + t.getValue()
);




l = System.currentTimeMillis();
t.reset();
for (i = 1; i < cycles; i++) {
try {
t.method4(i);
} catch (NoStackTraceThrowable e) {
// always comes 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 < cycles; i++) {
try {
t.method5(i);
} catch (RuntimeException e) {
// always comes here
}
}
l = System.currentTimeMillis() - l;
System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}
}


public static class NoStackTraceRuntimeException extends RuntimeException {
public NoStackTraceRuntimeException() {
super("my special throwable", null, false, false);
}
}


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