企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### [微基准测试](https://lingcoder.gitee.io/onjava8/#/book/16-Validating-Your-Code?id=%e5%be%ae%e5%9f%ba%e5%87%86%e6%b5%8b%e8%af%95) 写一个计时工具类从而比较不同代码块的执行速度是具有吸引力的。看上去这会产生一些有用的数据。比如,这里有一个简单的**Timer**类,可以用以下两种方式使用它: 1. 创建一个**Timer**对象,执行一些操作然后调用**Timer**的**duration()**方法产生以毫秒为单位的运行时间。 2. 向静态的**duration()**方法中传入**Runnable**。任何符合**Runnable**接口的类都有一个函数式方法 \*\*run()\*\*,该方法没有入参,且没有返回。 ~~~ // onjava/Timer.java package onjava; import static java.util.concurrent.TimeUnit.*; public class Timer { private long start = System.nanoTime(); public long duration() { return NANOSECONDS.toMillis(System.nanoTime() - start); } public static long duration(Runnable test) { Timer timer = new Timer(); test.run(); return timer.duration(); } } ~~~ 这是一个很直接的计时方式。难道我们不能只运行一些代码然后看它的运行时长吗? 有许多因素会影响你的结果,即使是生成提示符也会造成计时的混乱。这里举一个看上去天真的例子,它使用了 标准的 Java**Arrays**库(后面会详细介绍): ~~~ // validating/BadMicroBenchmark.java // {ExcludeFromTravisCI} import java.util.*; import onjava.Timer; public class BadMicroBenchmark { static final int SIZE = 250_000_000; public static void main(String[] args) { try { // For machines with insufficient memory long[] la = new long[SIZE]; System.out.println("setAll: " + Timer.duration(() -> Arrays.setAll(la, n -> n))); System.out.println("parallelSetAll: " + Timer.duration(() -> Arrays.parallelSetAll(la, n -> n))); } catch (OutOfMemoryError e) { System.out.println("Insufficient memory"); System.exit(0); } } } /* Output setAll: 272 parallelSetAll: 301 ~~~ **main()**方法的主体包含在**try**语句块中,因为一台机器用光内存后会导致构建停止。 对于一个长度为 250,000,000 的**long**型(仅仅差一点就会让大部分机器内存溢出)数组,我们比较了**Arrays.setAll()**和**Arrays.parallelSetAll()**的性能。这个并行的版本会尝试使用多个处理器加快完成任务(尽管我在这一节谈到了一些并行的概念,但是在[并发编程](https://lingcoder.gitee.io/onjava8/#/./24-Concurrent-Programming)章节我们才会详细讨论这些 )。然而非并行的版本似乎运行得更快,尽管在不同的机器上结果可能不同。 **BadMicroBenchmark.java**中的每一步操作都是独立的,但是如果你的操作依赖于同一资源,那么并行版本运行的速度会骤降,因为不同的进程会竞争相同的那个资源。 ~~~ // validating/BadMicroBenchmark2.java // Relying on a common resource import java.util.*; import onjava.Timer; public class BadMicroBenchmark2 { static final int SIZE = 5_000_000; public static void main(String[] args) { long[] la = new long[SIZE]; Random r = new Random(); System.out.println("parallelSetAll: " + Timer.duration(() -> Arrays.parallelSetAll(la, n -> r.nextLong()))); System.out.println("setAll: " + Timer.duration(() -> Arrays.setAll(la, n -> r.nextLong()))); SplittableRandom sr = new SplittableRandom(); System.out.println("parallelSetAll: " + Timer.duration(() -> Arrays.parallelSetAll(la, n -> sr.nextLong()))); System.out.println("setAll: " + Timer.duration(() -> Arrays.setAll(la, n -> sr.nextLong()))); } } /* Output parallelSetAll: 1147 setAll: 174 parallelSetAll: 86 setAll: 39 ~~~ **SplittableRandom**是为并行算法设计的,它当然看起来比普通的**Random**在**parallelSetAll()**中运行得更快。 但是看上去还是比非并发的**setAll()**运行时间更长,有点难以置信(也许是真的,但我们不能通过一个坏的微基准测试得到这个结论)。 这只考虑了微基准测试的问题。Java 虚拟机 Hotspot 也非常影响性能。如果你在测试前没有通过运行代码给 JVM 预热,那么你就会得到“冷”的结果,不能反映出代码在 JVM 预热之后的运行速度(假如你运行的应用没有在预热的 JVM 上运行,你就可能得不到所预期的性能,甚至可能减缓速度)。 优化器有时可以检测出你创建了没有使用的东西,或者是部分代码的运行结果对程序没有影响。如果它优化掉你的测试,那么你可能得到不好的结果。 一个良好的微基准测试系统能自动地弥补像这样的问题(和很多其他的问题)从而产生合理的结果,但是创建这么一套系统是非常棘手,需要深入的知识。