15

我正在使用以下代码来测试 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!");
        }
    }

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

        // using a try block
        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method1(i);
            } catch (Exception e) {

            }
        }

        l = System.currentTimeMillis() - l;
        System.out.println("method1 with try block took " + l + " ms, result was "
                + t.getValue());
    }
}

我的机器运行 64 位 Windows 7 和 64 位 JDK7。我得到以下结果:

method1 took 914 ms, result was 2
method1 with try block took 789 ms, result was 2

而且我已经多次运行代码,每次都得到几乎相同的结果。

更新:

这是在 MacBook Pro、Java 6 上运行测试十次的结果。Try-catch 使该方法更快,与在 Windows 上相同。

method1 took 895 ms, result was 2
method1 with try block took 783 ms, result was 2
--------------------------------------------------
method1 took 943 ms, result was 2
method1 with try block took 803 ms, result was 2
--------------------------------------------------
method1 took 867 ms, result was 2
method1 with try block took 745 ms, result was 2
--------------------------------------------------
method1 took 856 ms, result was 2
method1 with try block took 744 ms, result was 2
--------------------------------------------------
method1 took 862 ms, result was 2
method1 with try block took 744 ms, result was 2
--------------------------------------------------
method1 took 859 ms, result was 2
method1 with try block took 765 ms, result was 2
--------------------------------------------------
method1 took 937 ms, result was 2
method1 with try block took 767 ms, result was 2
--------------------------------------------------
method1 took 861 ms, result was 2
method1 with try block took 744 ms, result was 2
--------------------------------------------------
method1 took 858 ms, result was 2
method1 with try block took 744 ms, result was 2
--------------------------------------------------
method1 took 858 ms, result was 2
method1 with try block took 749 ms, result was 2
4

4 回答 4

19

当您在同一方法中有多个长时间运行的循环时,可能会触发整个方法的优化,而在第二个循环上会产生不可预测的结果。避免这种情况的一种方法是;

  • 给每个循环自己的方法
  • 多次运行测试以检查结果是否可重现
  • 运行测试 2 - 10 秒。

你会看到一些变化,有时结果是不确定的。即变化大于差异。

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

    public static void main(String[] args) {
        Test t = new Test();
        for (int i = 0; i < 5; i++) {
            testWithTryCatch(t);
            testWithoutTryCatch(t);
        }
    }

    private static void testWithoutTryCatch(Test t) {
        t.reset();
        long l = System.currentTimeMillis();
        for (int j = 0; j < 10; j++)
            for (int i = 1; i <= 100000000; i++)
                t.method1(i);

        l = System.currentTimeMillis() - l;
        System.out.println("without try/catch method1 took " + l + " ms, result was " + t.getValue());
    }

    private static void testWithTryCatch(Test t) {
        t.reset();
        long l = System.currentTimeMillis();
        for (int j = 0; j < 10; j++)
            for (int i = 1; i <= 100000000; i++)
                try {
                    t.method1(i);
                } catch (Exception ignored) {
                }

        l = System.currentTimeMillis() - l;
        System.out.println("with try/catch method1 took " + l + " ms, result was " + t.getValue());
    }
}

印刷

with try/catch method1 took 9723 ms, result was 2
without try/catch method1 took 9456 ms, result was 2
with try/catch method1 took 9672 ms, result was 2
without try/catch method1 took 9476 ms, result was 2
with try/catch method1 took 8375 ms, result was 2
without try/catch method1 took 8233 ms, result was 2
with try/catch method1 took 8337 ms, result was 2
without try/catch method1 took 8227 ms, result was 2
with try/catch method1 took 8163 ms, result was 2
without try/catch method1 took 8565 ms, result was 2

从这些结果来看,使用 try/catch 可能会稍微慢一些,但并非总是如此。

在 Windows 7、Xeon E5450 和 Java 7 更新 7 上运行。

于 2012-10-12T07:39:58.857 回答
5

我用 Caliper Microbenchmark 进行了尝试,我真的看不出有什么不同。

这是代码:

public class TryCatchBenchmark extends SimpleBenchmark {

    private int value;

    public void setUp() {
        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!");
        }
    }

    public void timeWithoutTryCatch(int reps) {
        for (int i = 1; i < reps; i++) {
            this.method1(i);
        }
    }

    public void timeWithTryCatch(int reps) {
        for (int i = 1; i < reps; i++) {
            try {
                this.method1(i);
            } catch (Exception ignore) {
            }
        }
    }

    public static void main(String[] args) {
        new Runner().run(TryCatchBenchmark.class.getName());
    }
}

结果如下:

0% 场景{vm=java, trial=0, benchmark=WithoutTryCatch} 8,23 ns; σ=0,03 ns @ 3 次试验
50% 场景{vm=java, trial=0, benchmark=WithTryCatch} 8,13 ns;σ=0,03 ns @ 3 次试验

      基准 ns 线性运行时
没有TryCatch 8,23 ===============================
   With TryCatch 8,13 ==============================

如果我交换函数的顺序(让它们以相反的顺序运行),结果是:

0% 场景{vm=java, trial=0, benchmark=WithTryCatch} 8,21 ns; σ=0,05 ns @ 3 次试验
50% 场景{vm=java, trial=0, benchmark=WithoutTryCatch} 8,14 ns;σ=0,03 ns @ 3 次试验

      基准 ns 线性运行时
   With TryCatch 8,21 ===============================
没有TryCatch 8,14 ==============================

我会说它们基本上是一样的。

于 2012-10-12T08:49:59.627 回答
2

我做了一些实验。

首先,我完全确认了 OP 的发现。即使删除第一个循环,或者将异常更改为一些完全不相关的异常,try catch,只要您不通过重新抛出异常来添加分支,确实会使代码更快。如果确实必须捕获异常,则代码仍然更快(例如,如果您使循环从 0 而不是 1 开始)。

我的“解释”是 JIT 是狂野的优化机器,有时它们的性能比其他时候更好,如果没有在 JIT 级别进行非常具体的研究,您通常无法理解。有很多可能的事情可以改变(例如使用寄存器)。

这是在与 C# JIT 非常相似的案例中发现的全局。

在任何情况下,Java 都针对 try-catch 进行了优化。由于总是存在异常的可能性,因此添加 try-catch 并不会真正添加太多分支,因此发现第二个循环比第一个循环长也就不足为奇了。

于 2012-10-12T06:33:59.360 回答
1

To avoid any hidden optimization or cache that could be performed by the JVM and the OS, I first developed two seed java programs, TryBlock and NoTryBlock, where their difference is use a try block or not. These two seed programs will be used to generate different programs to disallow JVM or OS to do hidden optimization. In each test, a new java program will be generated and compiled, and I repeated the test 10 times.

Based on my experiment, running without try block takes 9779.3 ms in average while running with try block takes 9775.9ms: a 3.4ms (or 0.035%) difference in their average running time, which can be viewed as noise. This indicates that using a void try block (by void, I mean other than null-pointer exception there exists no possible exceptions) or not seems not have impact on running time.

The test is running on same linux machine (cpu 2392MHz) and under java version "1.6.0_24".

Below is my script for generating testing programs based on the seed programs:

for i in `seq 1 10`; do
  echo "NoTryBlock$i"
  cp NoTryBlock.java NoTryBlock$i.java
  find . -name "NoTryBlock$i.java" -print | xargs sed -i "s/NoTryBlock/NoTryBlock$i/g";
  javac NoTryBlock$i.java;
  java NoTryBlock$i
  rm NoTryBlock$i.* -f;
done

for i in `seq 1 10`; do
  echo "TryBlock$i"
  cp TryBlock.java TryBlock$i.java
  find . -name "TryBlock$i.java" -print | xargs sed -i "s/TryBlock/TryBlock$i/g";
  javac TryBlock$i.java;
  java TryBlock$i
  rm TryBlock$i.* -f;
done

Here are the seed programs, first is the NoTryBlock.java

import java.util.*;
import java.lang.*;

public class NoTryBlock {
    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!");
        }
    }

    public static void main(String[] args) {
        int i, j;
        long l;
        NoTryBlock t = new NoTryBlock();

        // using a try block
        l = System.currentTimeMillis();
        t.reset();
        for (j = 1; j < 10; ++j) {
          for (i = 1; i < 100000000; i++) {
              t.method1(i);
          }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 with try block took " + l + " ms, result was "
                + t.getValue());
    }
}

the second is the TryBlock.java, which uses a try-block on the method function call:

import java.util.*;
import java.lang.*;

public class TryBlock {
    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!");
        }
    }

    public static void main(String[] args) {
        int i, j;
        long l;
        TryBlock t = new TryBlock();

        // using a try block
        l = System.currentTimeMillis();
        t.reset();
        for (j = 1; j < 10; ++j) {
          for (i = 1; i < 100000000; i++) {
            try {
              t.method1(i);
            } catch (Exception e) {
            }
          }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 with try block took " + l + " ms, result was "
                + t.getValue());
    }
}

Below is the diff of my two seed programs, and you can see except the class name, the try block is their only difference:

$ diff TryBlock.java NoTryBlock.java
4c4
<     public class TryBlock {
---
>     public class NoTryBlock {
27c27
<             TryBlock t = new TryBlock();
---
>             NoTryBlock t = new NoTryBlock();
34d33
<                 try {
36,37d34
<                 } catch (Exception e) {
<                 }
42c39
<                 "method1 with try block took " + l + " ms, result was "
---
>                 "method1 without try block took " + l + " ms, result was "

Below is the output:

method1 without try block took,9732,ms, result was 2
method1 without try block took,9756,ms, result was 2
method1 without try block took,9845,ms, result was 2
method1 without try block took,9794,ms, result was 2
method1 without try block took,9758,ms, result was 2
method1 without try block took,9733,ms, result was 2
method1 without try block took,9763,ms, result was 2
method1 without try block took,9893,ms, result was 2
method1 without try block took,9761,ms, result was 2
method1 without try block took,9758,ms, result was 2

method1 with try block took,9776,ms, result was 2
method1 with try block took,9751,ms, result was 2
method1 with try block took,9767,ms, result was 2
method1 with try block took,9726,ms, result was 2
method1 with try block took,9779,ms, result was 2
method1 with try block took,9797,ms, result was 2
method1 with try block took,9845,ms, result was 2
method1 with try block took,9784,ms, result was 2
method1 with try block took,9787,ms, result was 2
method1 with try block took,9747,ms, result was 2
于 2013-06-12T03:31:45.057 回答