jvm 《七》对Java HotSpot VM进行基准测试 or 为什么HotSpot不能让我的图形代码变得更快?

156 阅读6分钟

我写了一个简单的循环来计算一个简单的操作,

我要研究一下他为啥这么慢?

| | ```html public class Benchmark { public static void main(String[] arg) { long before = System.currentTimeMillis(); int sum = 0; for (int index = 0; index < 1010001000; index += 1) { sum += index; } long after = System.currentTimeMillis(); System.out.println("Elapsed time: " + Long.toString(after - before) + " milliseconds"); } }

| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

 

**HotSpot的工作原理:**

       它首先使用解释器运行程序。当它发现某些方法是“热”时 - 也就是说,执行了很多,要么因为它被调用很多,要么因为它包含循环很多的循环 - 它会发送该方法以进行编译。之后,将发生两件事之一,或者在下次调用该方法时,将调用编译版本(而不是解释版本),或者使用编译方法替换当前长时间运行的循环,同时仍在运行。后者被称为“堆栈替换”或OSR。

同时,如果你坚持使用/编写这样的微基准,你可以通过将main的主体移动到一个新方法并从main调用一次来给编译器编译代码的机会,然后调用它再次出现在计时器中,看看HotSpot的速度有多快。

另请参阅JavaOne 2002演示文稿 [S-1816如何不编写Microbenchmark](https://www.oracle.com/technetwork/java/hotspotfaq-138619.html#)

**[]()我正在尝试计算方法调用时间。我不希望有任何额外的工作,所以我使用一个空方法。但是当我使用HotSpot运行时,我得到的速度令人难以置信。这是我的代码:**

| ```html
 public class EmptyMethod {             public static void method() {             }             public static void runTest() {                 long before;                 long after;                 // First, figure out the time for an empty loop                 before = System.currentTimeMillis();                 for (int index = 0; index < 1*1000*1000; index += 1) {                 }                 after = System.currentTimeMillis();                 long loopTime = after - before;                 System.out.println("Loop time: " +                                    Long.toString(loopTime) +                                    " milliseconds");                 // Then time the method call in the loop                 before = System.currentTimeMillis();                 for (int index = 0; index < 1*1000*1000; index += 1) {                     method();                 }                 after = System.currentTimeMillis();                 long methodTime = after - before;                 System.out.println("Method time: " +                                    Long.toString(methodTime) +                                    " milliseconds");                 System.out.println("Method time - Loop time: " +                                    Long.toString(methodTime - loopTime) +                                    " milliseconds");             }             public static void main(String[] arg) {                 // Warm up the virtual machine, and time it                 runTest();                 runTest();                 runTest();             }         }
``` |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

 

空方法不计算在内。而且您还看到生成的代码对齐对象非常敏感。

对空方法的调用正在被内联,因此实际上没有时间调用。编译器在其调用站点将内联小方法。这减少了对小方法的调用的开销。这对于用于提供数据抽象的访问器方法特别有用。如果该方法实际为空,则内联将完全删除该调用。

代码生成到内存中并从那里执行。代码在内存中的布局方式对其执行方式有很大影响。在我的机器上的这个例子中,声称调用该方法的循环更好地对齐,因此比试图计算运行空循环所需多长时间的循环运行得更快,所以我得到负数 `methodTime-loopTime`。

**[]()好的,所以我会在方法的主体中放入一些随机代码,因此它不是空的,并且内联不能只删除它。这是我的新方法(并且调用站点更改为调用方法(17)):**

| ```html
            public static void method(int arg){                 int value = arg + 25;             } 
``` |
| ---------------------------------------------------------------------------------------------------------------- |

 

HotSpot编译器足够智能,不会为死变量生成代码。

在上面的方法中,从不使用局部变量,因此没有理由计算其值。因此,方法体再次为空,当代码被编译时(并且内联,因为我们删除了足够小的代码以便内联),它又变成了一个空方法。

对于那些不习惯处理优化编译器的人来说,这可能是令人惊讶的,因为他们可以非常聪明地发现和消除死代码。它们偶尔可能相当愚蠢,所以不要指望编译器对代码进行任意优化。

死代码消除也扩展到控制流程。如果编译器可以在测试中看到特定的“变量”实际上是常量,则可以选择不编译永远不会执行的分支的代码。这使得微基准测试“足够棘手”以实际计算您认为您的计时时间变得棘手。

死代码消除在实际代码中非常有用。并不是说人们故意写死代码; 但是,由于内联常常(例如,方法的实际参数)替换变量,使得某些控制流失效,编译器通常会发现死代码。

**[]()我正在尝试对对象分配和垃圾收集进行基准测试。所以我有上面的那个,但方法的主体是:**

| ```html
            public static void method(){                 Object o = new Object();             } 
``` |
| ------------------------------------------------------------------------------------------------------------ |

 

这是HotSpot存储管理器的最佳案例。你会得到不切实际的数字。

您正在分配不需要初始化的对象,并立即将它们放在地板上。(不,编译器不够聪明,无法优化分配。)真正的程序确实分配了相当数量的短期临时对象,但它们也比这个简单的测试程序更长时间地保留了一些对象。HotSpot存储管理器为保留更长时间的对象执行更多工作,因此请注意尝试将此类测试中的数字扩展到实际系统。

**[]()我有一个图形密集型或基于GUI的程序。为什么HotSpot不能让我的图形代码变得更快?**

图形程序将大量时间花在本机库中。

Java应用程序的整体性能取决于四个因素:

* 应用程序的设计
* 虚拟机执行Java字节码的速度
* 执行基本功能任务的库执行的速度(以本机代码表示)
* 底层硬件和操作系统的速度

虚拟机负责字节代码执行,存储分配,线程同步等。与虚拟机一起运行的是本机代码库,它们通过操作系统处理输入和输出,尤其是通过窗口系统的图形操作。在这些本机代码库中花费大量时间的程序将不会看到它们在HotSpot上的性能提高与花费大部分时间执行字节代码的程序一样多。

关于本机代码的这种观察适用于您恰好与应用程序一起使用的其他本​​机库或任何本机代码库。

**您对HotSpot或任何虚拟机进行基准测试的建议**

这里最好的答案是使用真实的应用程序进行基准测试,因为它们是唯一能够产生真正差异的应用程序。如果无法做到这一点,请使用标准SPEC基准测试,然后使用其他备受推崇的行业基准测试。应避免使用微量标记,或至少要谨慎使用。微基准测试由于优化效果而给出误导性答案是很常见的。